diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 000000000..5d56b3594 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,41 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the firefly-5.0.0 branch + push: + branches: [ main ] + pull_request: + branches: [ main ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + strategy: + matrix: + java: [8, 11, 17, 21] + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Install dependencies Ubuntu + run: sudo apt-get update && sudo apt-get install openssl + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + java-version: '${{ matrix.java }}' + distribution: 'temurin' + + - name: Build with Maven + run: mvn -B clean package diff --git a/.gitignore b/.gitignore index 0910ed97c..3b4776dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,14 @@ .settings *.class .DS_Store +*.iml +*.iws +*.ipr +.idea +out +target +firefly-common/logs/ +firefly/temp/ +logs/ +temp/ +/.java-version \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..f907521b9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +arch: arm64 +os: linux +language: java +jdk: + - openjdk8 + - openjdk11 + - openjdk16 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /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/README.md b/README.md new file mode 100644 index 000000000..fe011397a --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# What is Firefly? +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/hypercube1024/firefly/maven.yml)](https://github.com/hypercube1024/firefly) +[![Maven Central](https://img.shields.io/maven-central/v/com.fireflysource/firefly-net)](https://search.maven.org/artifact/com.fireflysource/firefly-net/) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +Firefly framework is an asynchronous Java web framework. It helps you create a web application ***Easy*** and ***Quickly***. +It provides asynchronous HTTP, Websocket, TCP Server/Client, and many other useful components for developing +web applications, protocol servers, etc. That means you can easy deploy your web without any other java web containers, +in short, it's containerless. Using Kotlin coroutines, Firefly is truly asynchronous and highly scalable. It taps into +the fullest potential of hardware. Use the power of non-blocking development without the callback nightmare. + +Firefly core provides functionality for things like: + +- HTTP server and client +- WebSocket server and client +- HTTP, Socks proxy +- HTTP Gateway +- TCP server and client +- UDP server and client + +# Event driven +The Firefly APIs are largely event-driven. It means that when things happen in Firefly that you are interested in, +Firefly will call you by sending you events. + +Some example events are: +- some data has arrived on a socket +- an HTTP server has received a request + +Firefly handles a lot of concurrencies using just a small number of threads, so ***don't block Firefly thread***, you +must manage blocking call in the standalone thread pool. + +With a conventional blocking API the calling thread might block when: +- Thread.sleep() +- Waiting on a Lock +- Waiting on a mutex or monitor +- Doing a long-lived database operation and waiting for a result +- Call blocking I/O APIs + +In all the above cases, when your thread is waiting for a result it can’t do anything else - it’s effectively useless. + +It means that if you want a lot of concurrencies using blocking APIs, then you need a lot of threads to prevent your +application grinding to a halt. + +Threads have overhead regarding the memory they require (e.g. for their stack) and in context switching. + +For the levels of concurrency required in many modern applications, a blocking approach just doesn’t scale. + +# Quick start +Add maven dependency in your pom.xml. +```xml + + + com.fireflysource + firefly-net + 5.0.2 + + + + com.fireflysource + firefly-slf4j + 5.0.2 + + +``` + +Add log configuration file "firefly-log.xml" to the classpath. +```xml + + + + firefly-system + INFO + logs + + +``` + +HTTP server and client example: +```kotlin +fun main() { + `$`.httpServer() + .router().get("/").handler { ctx -> ctx.end("Hello http! ") } + .listen("localhost", 8090) + + `$`.httpClient().get("http://localhost:8090/").submit() + .thenAccept { response -> println(response.stringBody) } +} +``` + +WebSocket server and client example: +```kotlin +fun main() { + `$`.httpServer().websocket("/websocket/hello") + .onServerMessageAsync { frame, _ -> onMessage(frame) } + .onAcceptAsync { connection -> sendMessage("Server", connection) } + .listen("localhost", 8090) + + val url = "ws://localhost:8090" + `$`.httpClient().websocket("$url/websocket/hello") + .onClientMessageAsync { frame, _ -> onMessage(frame) } + .connectAsync { connection -> sendMessage("Client", connection) } +} + +private suspend fun sendMessage(data: String, connection: WebSocketConnection) = connection.useAwait { + (1..10).forEach { + connection.sendText("WebSocket ${data}. count: $it, time: ${Date()}") + delay(1000) + } +} + +private fun onMessage(frame: Frame) { + if (frame is TextFrame) { + println(frame.payloadAsUTF8) + } +} +``` + +TCP server and client example: +```kotlin +fun main() { + `$`.tcpServer().onAcceptAsync { connection -> + launch { writeLoop("Server", connection) } + launch { readLoop(connection) } + }.listen("localhost", 8090) + + `$`.tcpClient().connectAsync("localhost", 8090) { connection -> + launch { writeLoop("Client", connection) } + launch { readLoop(connection) } + } +} + +private suspend fun readLoop(connection: TcpConnection) = connection.useAwait { + while (true) { + try { + val buffer = connection.read().await() + println(BufferUtils.toString(buffer)) + } catch (e: Exception) { + println("Connection closed.") + break + } + } +} + +private suspend fun writeLoop(data: String, connection: TcpConnection) = connection.useAwait { + (1..10).forEach { + connection.write(toBuffer("TCP ${data}. count: $it, time: ${Date()}")) + delay(1000) + } +} +``` + +# Contact information +- E-mail: qptkk@163.com +- QQ Group: 126079579 +- Wechat: AlvinQiu diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 000000000..efda59f25 --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,7 @@ +$env:HTTP_PROXY="http://127.0.0.1:7890" +$env:HTTPS_PROXY="http://127.0.0.1:7890" +$response = Invoke-RestMethod 'https://www.google.com' -Method 'GET' +if ($response.Length -gt 0) { + Write-Output "set proxy success" +} +mvn clean deploy -Prelease \ No newline at end of file diff --git a/firefly-benchmark/.classpath b/firefly-benchmark/.classpath deleted file mode 100644 index 75e6fcbdc..000000000 --- a/firefly-benchmark/.classpath +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/firefly-benchmark/.gitignore b/firefly-benchmark/.gitignore deleted file mode 100644 index 8ba548b51..000000000 --- a/firefly-benchmark/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/classes diff --git a/firefly-benchmark/.project b/firefly-benchmark/.project deleted file mode 100644 index 4dcc3dff1..000000000 --- a/firefly-benchmark/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - firefly-benchmark - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - diff --git a/firefly-benchmark/.settings/org.eclipse.jdt.core.prefs b/firefly-benchmark/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index f06e90485..000000000 --- a/firefly-benchmark/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,12 +0,0 @@ -#Wed Feb 15 21:18:47 CST 2012 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 diff --git a/firefly-benchmark/lib/firefly-common.jar b/firefly-benchmark/lib/firefly-common.jar deleted file mode 100644 index 6196fe5f3..000000000 Binary files a/firefly-benchmark/lib/firefly-common.jar and /dev/null differ diff --git a/firefly-benchmark/lib/firefly-nettool.jar b/firefly-benchmark/lib/firefly-nettool.jar deleted file mode 100644 index 1348631ea..000000000 Binary files a/firefly-benchmark/lib/firefly-nettool.jar and /dev/null differ diff --git a/firefly-benchmark/lib/firefly-template.jar b/firefly-benchmark/lib/firefly-template.jar deleted file mode 100644 index 4698b7f19..000000000 Binary files a/firefly-benchmark/lib/firefly-template.jar and /dev/null differ diff --git a/firefly-benchmark/lib/firefly.jar b/firefly-benchmark/lib/firefly.jar deleted file mode 100644 index 01d8cb762..000000000 Binary files a/firefly-benchmark/lib/firefly.jar and /dev/null differ diff --git a/firefly-benchmark/lib/servlet-api-2.5.jar b/firefly-benchmark/lib/servlet-api-2.5.jar deleted file mode 100644 index 46ae9ecf6..000000000 Binary files a/firefly-benchmark/lib/servlet-api-2.5.jar and /dev/null differ diff --git a/firefly-benchmark/page/favicon.ico b/firefly-benchmark/page/favicon.ico deleted file mode 100644 index a51a76e33..000000000 Binary files a/firefly-benchmark/page/favicon.ico and /dev/null differ diff --git a/firefly-benchmark/page/index.html b/firefly-benchmark/page/index.html deleted file mode 100644 index 747b641bc..000000000 --- a/firefly-benchmark/page/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - -Welcome firefly! -测试页面 - - \ No newline at end of file diff --git a/firefly-benchmark/page/template/_compiled_view/_index.class b/firefly-benchmark/page/template/_compiled_view/_index.class deleted file mode 100644 index 320ee759b..000000000 Binary files a/firefly-benchmark/page/template/_compiled_view/_index.class and /dev/null differ diff --git a/firefly-benchmark/page/template/_compiled_view/_index.java b/firefly-benchmark/page/template/_compiled_view/_index.java deleted file mode 100644 index 8cda24eb0..000000000 --- a/firefly-benchmark/page/template/_compiled_view/_index.java +++ /dev/null @@ -1,28 +0,0 @@ -import java.io.OutputStream; -import com.firefly.template.support.ObjectNavigator; -import com.firefly.template.Model; -import com.firefly.template.view.AbstractView; -import com.firefly.template.TemplateFactory; -import com.firefly.template.FunctionRegistry; - -public class _index extends AbstractView { - - public _index(TemplateFactory templateFactory){this.templateFactory = templateFactory;} - - @Override - protected void main(Model model, OutputStream out) throws Throwable { - ObjectNavigator objNav = ObjectNavigator.getInstance(); - out.write(_TEXT_0); - out.write(objNav.getValue(model ,"hello").getBytes("UTF-8")); - out.write(_TEXT_1); - out.write(objNav.getValue(model ,"info[0]").getBytes("UTF-8")); - out.write(_TEXT_2); - out.write(objNav.getValue(model ,"info[1]").getBytes("UTF-8")); - out.write(_TEXT_3); - } - - private final byte[] _TEXT_0 = new byte[]{60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 104, 116, 109, 108, 62, 60, 104, 116, 109, 108, 62, 60, 104, 101, 97, 100, 62, 60, 116, 105, 116, 108, 101, 62}; - private final byte[] _TEXT_1 = new byte[]{32, -26, -75, -117, -24, -81, -107, -28, -72, -128, -28, -72, -117, -23, -95, -75, -23, -99, -94, 60, 47, 116, 105, 116, 108, 101, 62, 60, 115, 116, 121, 108, 101, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 99, 115, 115, 34, 62, 46, 116, 105, 116, 108, 101, 123, 111, 118, 101, 114, 102, 108, 111, 119, 58, 32, 104, 105, 100, 100, 101, 110, 59, 116, 101, 120, 116, 45, 97, 108, 105, 103, 110, 58, 32, 99, 101, 110, 116, 101, 114, 59, 125, 46, 99, 111, 110, 116, 101, 110, 116, 32, 123, 119, 105, 100, 116, 104, 58, 32, 53, 48, 101, 109, 59, 111, 118, 101, 114, 102, 108, 111, 119, 58, 32, 104, 105, 100, 100, 101, 110, 59, 109, 97, 114, 103, 105, 110, 58, 32, 48, 32, 97, 117, 116, 111, 59, 125, 60, 47, 115, 116, 121, 108, 101, 62, 60, 47, 104, 101, 97, 100, 62, 60, 98, 111, 100, 121, 62}; - private final byte[] _TEXT_2 = new byte[]{44}; - private final byte[] _TEXT_3 = new byte[]{60, 104, 49, 32, 99, 108, 97, 115, 115, 61, 34, 116, 105, 116, 108, 101, 34, 62, -23, -105, -78, -26, -102, -121, -26, -105, -74, -25, -102, -124, -23, -127, -112, -26, -125, -77, 60, 47, 104, 49, 62, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 34, 99, 111, 110, 116, 101, 110, 116, 34, 62, -26, -106, -121, 47, -29, -128, -108, -28, -65, -124, -25, -67, -105, -26, -106, -81, -29, -128, -107, -27, -68, -105, -62, -73, -28, -68, -118, -62, -73, -27, -123, -117, -23, -121, -116, -27, -120, -85, -23, -121, -116, -29, -128, -128, 32, 32, -24, -81, -111, 47, -23, -103, -120, -26, -73, -111, -24, -76, -92, -26, -120, -111, -28, -72, -128, -25, -101, -76, -28, -68, -127, -25, -101, -68, -27, -71, -72, -25, -90, -113, -17, -68, -116, -27, -115, -76, -26, -124, -97, -27, -120, -80, -24, -65, -103, -23, -99, -98, -27, -72, -72, -23, -127, -91, -24, -65, -100, -17, -68, -116, -27, -82, -125, -27, -113, -120, -27, -90, -126, -26, -83, -92, -25, -119, -71, -26, -82, -118, -17, -68, -116, -27, -113, -81, -26, -100, -101, -24, -128, -116, -28, -72, -115, -27, -113, -81, -27, -115, -77, -17, -68, -116, -27, -121, -96, -28, -71, -114, -26, -105, -96, -26, -77, -107, -24, -114, -73, -27, -66, -105, -29, -128, -126, -26, -82, -118, -28, -72, -115, -25, -97, -91, -27, -71, -72, -25, -90, -113, -27, -80, -79, -27, -100, -88, -26, -120, -111, -24, -70, -85, -24, -66, -71, -17, -68, -116, -27, -100, -88, -26, -120, -111, -24, -125, -116, -27, -112, -114, -17, -68, -116, -27, -82, -125, -26, -126, -124, -25, -124, -74, -26, -105, -96, -27, -93, -80, -17, -68, -116, -28, -72, -115, -28, -70, -117, -27, -68, -96, -26, -119, -84, -29, -128, -126, -27, -114, -97, -26, -99, -91, -17, -68, -116, -26, -120, -111, -27, -127, -102, -25, -102, -124, -27, -73, -91, -28, -67, -100, -17, -68, -116, -27, -70, -90, -24, -65, -121, -25, -102, -124, -26, -105, -74, -26, -105, -91, -17, -68, -116, -28, -72, -114, -27, -111, -88, -27, -101, -76, -28, -70, -70, -25, -102, -124, -27, -110, -116, -24, -80, -112, -27, -123, -79, -27, -92, -124, -30, -128, -108, -30, -128, -108, -27, -71, -72, -25, -90, -113, -27, -80, -79, -27, -100, -88, -27, -123, -74, -28, -72, -83, -29, -128, -126, -26, -105, -91, -27, -92, -115, -28, -72, -128, -26, -105, -91, -17, -68, -116, -27, -71, -76, -27, -92, -115, -28, -72, -128, -27, -71, -76, -17, -68, -116, -26, -75, -127, -27, -71, -76, -28, -68, -68, -26, -80, -76, -17, -68, -116, -27, -113, -86, -26, -100, -119, -27, -120, -80, -28, -70, -122, -27, -101, -98, -23, -90, -106, -26, -105, -74, -26, -119, -115, -23, -95, -65, -26, -126, -97, -17, -68, -102, -24, -65, -103, -27, -80, -79, -26, -104, -81, -27, -71, -72, -25, -90, -113, -17, -68, -116, -27, -71, -72, -25, -90, -113, -26, -100, -84, -26, -99, -91, -27, -80, -79, -28, -72, -128, -25, -101, -76, -28, -72, -114, -26, -120, -111, -25, -101, -72, -28, -68, -76, -25, -101, -72, -23, -102, -113, -17, -68, -127, -29, -128, -128, -29, -128, -128, -28, -70, -70, -17, -68, -116, -27, -82, -98, -24, -76, -88, -28, -72, -118, -27, -120, -122, -28, -72, -70, -28, -72, -92, -25, -79, -69, -17, -68, -102, -25, -84, -84, -28, -72, -128, -25, -79, -69, -26, -124, -97, -24, -89, -119, -24, -121, -86, -27, -73, -79, -26, -104, -81, -27, -128, -70, -26, -99, -125, -28, -70, -70, -17, -68, -116, -25, -84, -84, -28, -70, -116, -25, -79, -69, -27, -120, -103, -26, -124, -97, -24, -89, -119, -24, -121, -86, -27, -73, -79, -26, -104, -81, -27, -128, -70, -27, -118, -95, -28, -70, -70, -29, -128, -126, -27, -128, -70, -26, -99, -125, -28, -70, -70, -26, -99, -98, -28, -70, -70, -27, -65, -89, -27, -92, -87, -17, -68, -116, -26, -128, -69, -26, -104, -81, -26, -128, -88, -27, -92, -87, -27, -80, -92, -28, -70, -70, -17, -68, -116, -24, -82, -92, -28, -72, -70, -26, -119, -128, -26, -100, -119, -25, -102, -124, -28, -70, -70, -30, -128, -108, -30, -128, -108, -27, -124, -65, -27, -91, -77, -29, -128, -127, -27, -113, -116, -28, -70, -78, -29, -128, -127, -27, -112, -116, -28, -70, -117, -28, -71, -125, -24, -121, -77, -28, -70, -70, -26, -80, -111, -30, -128, -108, -30, -128, -108, -23, -125, -67, -27, -100, -88, -26, -97, -112, -28, -70, -101, -26, -106, -71, -23, -99, -94, -27, -81, -71, -28, -72, -115, -24, -75, -73, -28, -69, -106, -29, -128, -127, -28, -70, -113, -26, -84, -96, -28, -69, -106, -17, -68, -116, -23, -128, -96, -26, -120, -112, -28, -69, -106, -25, -108, -97, -26, -76, -69, -25, -102, -124, -28, -72, -115, -27, -71, -72, -17, -68, -116, -24, -111, -84, -23, -128, -127, -28, -69, -106, -25, -102, -124, -28, -72, -86, -28, -70, -70, -27, -119, -115, -25, -88, -117, -29, -128, -126, -27, -128, -70, -27, -118, -95, -28, -70, -70, -27, -120, -103, -25, -69, -113, -27, -113, -105, -25, -99, -128, -27, -113, -90, -27, -92, -106, -28, -72, -128, -25, -89, -115, -26, -101, -76, -23, -85, -104, -25, -102, -124, -27, -94, -125, -25, -107, -116, -17, -68, -116, -28, -69, -92, -28, -70, -70, -25, -66, -95, -26, -123, -107, -25, -102, -124, -25, -105, -101, -24, -117, -90, -17, -68, -102, -26, -105, -96, -26, -77, -107, -27, -127, -65, -24, -65, -104, -28, -70, -113, -26, -84, -96, -25, -108, -97, -26, -76, -69, -29, -128, -127, -28, -70, -113, -26, -84, -96, -28, -70, -70, -26, -80, -111, -25, -102, -124, -26, -73, -79, -26, -125, -123, -27, -114, -102, -24, -80, -118, -29, -128, -126, -26, -120, -111, -26, -101, -76, -27, -125, -113, -27, -128, -70, -27, -118, -95, -28, -70, -70, -29, -128, -126, -26, -120, -111, -26, -105, -94, -28, -72, -115, -26, -79, -126, -27, -118, -97, -27, -112, -115, -17, -68, -116, -28, -71, -97, -28, -72, -115, -25, -101, -68, -27, -120, -87, -25, -90, -124, -17, -68, -116, -26, -120, -111, -26, -124, -97, -27, -120, -80, -27, -71, -72, -25, -90, -113, -25, -102, -124, -26, -104, -81, -24, -125, -67, -27, -92, -97, -27, -127, -102, -28, -72, -128, -25, -126, -71, -26, -100, -119, -25, -101, -118, -27, -92, -124, -25, -102, -124, -28, -70, -117, -26, -125, -123, -29, -128, -126, -29, -128, -128, -29, -128, -128, -28, -70, -70, -27, -100, -88, -27, -66, -120, -27, -92, -102, -26, -106, -71, -23, -99, -94, -28, -66, -99, -24, -75, -106, -27, -92, -89, -24, -121, -86, -25, -124, -74, -17, -68, -116, -28, -66, -99, -24, -75, -106, -27, -92, -87, -26, -80, -108, -25, -118, -74, -27, -122, -75, -29, -128, -126, -27, -81, -110, -27, -122, -73, -29, -128, -127, -23, -104, -76, -23, -101, -88, -17, -68, -116, -28, -70, -70, -27, -81, -71, -26, -83, -92, -26, -105, -96, -24, -125, -67, -28, -72, -70, -27, -118, -101, -17, -68, -116, -27, -113, -86, -26, -100, -119, -25, -83, -119, -27, -66, -123, -27, -92, -89, -24, -121, -86, -25, -124, -74, -25, -118, -74, -26, -128, -127, -25, -102, -124, -27, -91, -67, -24, -67, -84, -17, -68, -116, -25, -69, -89, -25, -69, -83, -25, -108, -97, -26, -76, -69, -29, -128, -126, -23, -101, -123, -27, -70, -109, -25, -119, -71, -24, -128, -127, -28, -70, -70, -24, -81, -76, -27, -66, -105, -27, -91, -67, -17, -68, -102, -30, -128, -100, -27, -81, -110, -27, -122, -73, -29, -128, -127, -23, -104, -76, -23, -101, -88, -30, -128, -108, -30, -128, -108, -27, -66, -120, -27, -91, -67, -17, -68, -127, -24, -65, -103, -28, -70, -101, -24, -65, -121, -27, -112, -114, -27, -80, -122, -27, -121, -70, -25, -114, -80, -27, -92, -86, -23, -104, -77, -17, -68, -116, -27, -80, -122, -28, -68, -102, -26, -102, -106, -26, -76, -117, -26, -76, -117, -17, -68, -127, -30, -128, -99, -29, -128, -128, -29, -128, -128, -27, -81, -69, -26, -119, -66, -27, -71, -72, -25, -90, -113, -30, -128, -108, -30, -128, -108, -27, -66, -82, -27, -90, -103, -24, -128, -116, -25, -90, -69, -27, -91, -121, -29, -128, -126, -25, -108, -97, -26, -76, -69, -28, -72, -83, -28, -72, -128, -27, -111, -77, -24, -65, -67, -26, -79, -126, -27, -118, -97, -26, -120, -112, -27, -112, -115, -27, -80, -79, -17, -68, -116, -25, -69, -109, -26, -98, -100, -27, -66, -110, -27, -118, -77, -26, -105, -96, -25, -101, -118, -29, -128, -126, -27, -70, -108, -24, -81, -91, -24, -128, -127, -24, -128, -127, -27, -82, -98, -27, -82, -98, -27, -100, -80, -25, -108, -97, -26, -76, -69, -17, -68, -116, -26, -114, -91, -27, -113, -105, -27, -92, -89, -24, -121, -86, -25, -124, -74, -26, -100, -84, -24, -70, -85, -25, -102, -124, -23, -90, -120, -24, -75, -96, -29, -128, -126, -26, -120, -111, -28, -69, -84, -27, -72, -72, -27, -72, -72, -27, -100, -88, -23, -127, -91, -24, -65, -100, -25, -102, -124, -26, -97, -112, -28, -72, -86, -27, -100, -80, -26, -106, -71, -27, -81, -69, -26, -119, -66, -27, -71, -72, -25, -90, -113, -17, -68, -116, -26, -120, -111, -28, -69, -84, -27, -116, -122, -27, -65, -103, -24, -65, -67, -23, -128, -112, -24, -67, -84, -25, -98, -84, -27, -115, -77, -23, -128, -99, -25, -102, -124, -27, -123, -119, -26, -99, -97, -29, -128, -126, -27, -123, -74, -27, -82, -98, -27, -71, -72, -25, -90, -113, -27, -80, -79, -27, -100, -88, -24, -70, -85, -26, -105, -127, -17, -68, -116, -27, -100, -88, -26, -105, -91, -27, -72, -72, -25, -108, -97, -26, -76, -69, -28, -71, -117, -28, -72, -83, -30, -128, -108, -30, -128, -108, -24, -66, -66, -27, -120, -80, -27, -118, -101, -26, -119, -128, -24, -125, -67, -27, -113, -118, -25, -102, -124, -25, -101, -82, -26, -96, -121, -17, -68, -116, -25, -89, -80, -27, -65, -125, -25, -102, -124, -27, -73, -91, -28, -67, -100, -17, -68, -116, -27, -82, -74, -27, -70, -83, -25, -102, -124, -27, -110, -116, -24, -80, -112, -17, -68, -116, -28, -70, -78, -28, -70, -70, -25, -102, -124, -27, -82, -119, -27, -123, -88, -26, -105, -96, -26, -127, -103, -29, -128, -126, -28, -72, -115, -24, -65, -121, -17, -68, -116, -26, -80, -72, -28, -71, -123, -25, -102, -124, -29, -128, -127, -28, -72, -128, -26, -120, -112, -28, -72, -115, -27, -113, -104, -25, -102, -124, -27, -71, -72, -25, -90, -113, -26, -104, -81, -26, -78, -95, -26, -100, -119, -25, -102, -124, -17, -68, -116, -27, -71, -72, -25, -90, -113, -26, -100, -119, -26, -105, -74, -28, -72, -118, -27, -115, -121, -17, -68, -116, -24, -65, -121, -28, -72, -128, -26, -82, -75, -26, -105, -74, -23, -105, -76, -27, -113, -120, -28, -68, -102, -28, -72, -117, -23, -103, -115, -29, -128, -126, -28, -72, -117, -23, -103, -115, -26, -105, -74, -26, -120, -106, -27, -71, -77, -25, -88, -77, -26, -120, -106, -26, -128, -91, -23, -86, -92, -17, -68, -116, -25, -108, -102, -24, -121, -77, -28, -68, -102, -28, -72, -91, -23, -121, -115, -24, -73, -116, -28, -68, -92, -29, -128, -126, -27, -81, -71, -24, -65, -103, -25, -89, -115, -27, -115, -121, -23, -103, -115, -27, -113, -86, -23, -100, -128, -24, -128, -112, -27, -65, -125, -27, -70, -90, -24, -65, -121, -17, -68, -116, -27, -91, -67, -27, -125, -113, -27, -65, -115, -24, -128, -112, -26, -127, -74, -27, -118, -93, -27, -92, -87, -26, -80, -108, -26, -120, -106, -27, -111, -67, -28, -72, -83, -26, -77, -88, -27, -82, -102, -25, -102, -124, -27, -123, -74, -28, -69, -106, -28, -72, -115, -26, -75, -117, -28, -72, -128, -26, -96, -73, -29, -128, -126, -26, -100, -128, -27, -112, -114, -17, -68, -116, -28, -72, -76, -25, -69, -120, -26, -105, -74, -28, -67, -96, -26, -100, -119, -26, -99, -125, -24, -81, -76, -17, -68, -102, -30, -128, -100, -26, -120, -111, -26, -104, -81, -27, -71, -72, -25, -90, -113, -25, -102, -124, -29, -128, -126, -30, -128, -99, -29, -128, -128, -29, -128, -128, -27, -67, -109, -26, -100, -119, -28, -70, -70, -23, -105, -82, -24, -112, -89, -28, -68, -81, -25, -70, -77, -26, -104, -81, -27, -112, -90, -27, -71, -72, -25, -90, -113, -26, -105, -74, -17, -68, -116, -28, -69, -106, -27, -101, -98, -25, -83, -108, -24, -81, -76, -17, -68, -116, -28, -69, -106, -27, -71, -72, -25, -90, -113, -26, -104, -81, -27, -101, -96, -28, -72, -70, -26, -78, -95, -26, -100, -119, -26, -105, -74, -23, -105, -76, -24, -128, -125, -24, -103, -111, -24, -65, -103, -28, -72, -128, -25, -126, -71, -29, -128, -126, -28, -69, -106, -25, -102, -124, -27, -71, -72, -25, -90, -113, -27, -100, -88, -28, -70, -114, -27, -73, -91, -28, -67, -100, -17, -68, -116, -27, -100, -88, -28, -70, -114, -27, -120, -101, -28, -67, -100, -29, -128, -126, -27, -71, -72, -25, -90, -113, -27, -112, -124, -27, -68, -113, -27, -112, -124, -26, -96, -73, -17, -68, -116, -28, -71, -97, -27, -113, -81, -27, -120, -110, -27, -120, -122, -23, -104, -74, -26, -82, -75, -17, -68, -116, -25, -118, -71, -27, -90, -126, -26, -104, -68, -27, -92, -100, -26, -105, -74, -23, -105, -76, -28, -72, -128, -26, -96, -73, -29, -128, -126, -27, -90, -126, -26, -98, -100, -28, -72, -128, -27, -120, -121, -23, -125, -67, -26, -116, -119, -27, -92, -89, -24, -121, -86, -25, -124, -74, -24, -89, -124, -27, -82, -102, -25, -102, -124, -23, -126, -93, -26, -96, -73, -17, -68, -102, -23, -128, -126, -26, -105, -74, -29, -128, -127, -23, -128, -126, -27, -70, -90, -29, -128, -127, -26, -105, -96, -27, -65, -89, -29, -128, -127, -26, -105, -96, -24, -103, -111, -17, -68, -116, -23, -126, -93, -28, -71, -120, -17, -68, -116, -27, -113, -81, -28, -69, -91, -24, -82, -92, -28, -72, -70, -17, -68, -116, -28, -70, -70, -27, -71, -72, -25, -90, -113, -27, -100, -80, -27, -70, -90, -24, -65, -121, -28, -70, -122, -28, -72, -128, -25, -108, -97, -29, -128, -126, 60, 47, 100, 105, 118, 62, 60, 47, 98, 111, 100, 121, 62, 60, 47, 104, 116, 109, 108, 62}; -} \ No newline at end of file diff --git a/firefly-benchmark/page/template/index.html b/firefly-benchmark/page/template/index.html deleted file mode 100644 index 9dd824e21..000000000 --- a/firefly-benchmark/page/template/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - -${hello} 测试一下页面 - - - -${info[0]},${info[1]} -

闲暇时的遐想

-
-文/〔俄罗斯〕弗·伊·克里别里 -  译/陈淑贤 -我一直企盼幸福,却感到这非常遥远,它又如此特殊,可望而不可即,几乎无法获得。殊不知幸福就在我身边,在我背后,它悄然无声,不事张扬。原来,我做的工作,度过的时日,与周围人的和谐共处——幸福就在其中。日复一日,年复一年,流年似水,只有到了回首时才顿悟:这就是幸福,幸福本来就一直与我相伴相随! -  人,实质上分为两类:第一类感觉自己是债权人,第二类则感觉自己是债务人。债权人杞人忧天,总是怨天尤人,认为所有的人——儿女、双亲、同事乃至人民——都在某些方面对不起他、亏欠他,造成他生活的不幸,葬送他的个人前程。债务人则经受着另外一种更高的境界,令人羡慕的痛苦:无法偿还亏欠生活、亏欠人民的深情厚谊。我更像债务人。我既不求功名,也不盼利禄,我感到幸福的是能够做一点有益处的事情。 -  人在很多方面依赖大自然,依赖天气状况。寒冷、阴雨,人对此无能为力,只有等待大自然状态的好转,继续生活。雅库特老人说得好:“寒冷、阴雨——很好!这些过后将出现太阳,将会暖洋洋!” -  寻找幸福——微妙而离奇。生活中一味追求功成名就,结果徒劳无益。应该老老实实地生活,接受大自然本身的馈赠。我们常常在遥远的某个地方寻找幸福,我们匆忙追逐转瞬即逝的光束。其实幸福就在身旁,在日常生活之中——达到力所能及的目标,称心的工作,家庭的和谐,亲人的安全无恙。不过,永久的、一成不变的幸福是没有的,幸福有时上升,过一段时间又会下降。下降时或平稳或急骤,甚至会严重跌伤。对这种升降只需耐心度过,好像忍耐恶劣天气或命中注定的其他不测一样。最后,临终时你有权说:“我是幸福的。” -  当有人问萧伯纳是否幸福时,他回答说,他幸福是因为没有时间考虑这一点。他的幸福在于工作,在于创作。幸福各式各样,也可划分阶段,犹如昼夜时间一样。如果一切都按大自然规定的那样:适时、适度、无忧、无虑,那么,可以认为,人幸福地度过了一生。 -
- - diff --git a/firefly-benchmark/src/com/firefly/benchmark/Bootstrap.java b/firefly-benchmark/src/com/firefly/benchmark/Bootstrap.java deleted file mode 100644 index 309c58d97..000000000 --- a/firefly-benchmark/src/com/firefly/benchmark/Bootstrap.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firefly.benchmark; - -import java.io.File; - -import com.firefly.server.ServerBootstrap; - -public class Bootstrap { - - public static void main(String[] args) throws Throwable { - String projectHome = new File(Bootstrap.class.getResource("/").toURI()).getParent(); - String serverHome = new File(projectHome, "/page").getAbsolutePath(); - ServerBootstrap.start(serverHome, "localhost", 6655); - } - -} diff --git a/firefly-benchmark/src/com/firefly/benchmark/controller/IndexController.java b/firefly-benchmark/src/com/firefly/benchmark/controller/IndexController.java deleted file mode 100644 index a3218f613..000000000 --- a/firefly-benchmark/src/com/firefly/benchmark/controller/IndexController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.benchmark.controller; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.annotation.Controller; -import com.firefly.annotation.PathVariable; -import com.firefly.annotation.RequestMapping; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.view.TemplateView; - -@Controller -public class IndexController { - @RequestMapping(value = "/index") - public View index(HttpServletRequest request, HttpServletResponse response) { - request.setAttribute("info", new String[]{"hello firefly", "test"}); - return new TemplateView("/index.html"); - } - - @RequestMapping(value = "/document/?/?") - public View document(HttpServletRequest request, @PathVariable String[] args) { - request.setAttribute("info", args); - return new TemplateView("/index.html"); - } -} diff --git a/firefly-benchmark/src/firefly-log.properties b/firefly-benchmark/src/firefly-log.properties deleted file mode 100644 index 17c026e41..000000000 --- a/firefly-benchmark/src/firefly-log.properties +++ /dev/null @@ -1,2 +0,0 @@ -firefly-system=INFO,/Users/qiupengtao/develop/logs -firefly-access=INFO,/Users/qiupengtao/develop/logs \ No newline at end of file diff --git a/firefly-benchmark/src/firefly.xml b/firefly-benchmark/src/firefly.xml deleted file mode 100644 index 7641913bd..000000000 --- a/firefly-benchmark/src/firefly.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/firefly-common/pom.xml b/firefly-common/pom.xml index e0122fe01..2f6ccd092 100644 --- a/firefly-common/pom.xml +++ b/firefly-common/pom.xml @@ -1,108 +1,72 @@ - - 4.0.0 + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 - com.firefly - firefly-common - 1.0-SNAPSHOT - jar + firefly-common + jar - firefly-common - http://maven.apache.org - - - ${project.artifactId} - install - - - src/main/resources - true - - - - - true - src/test/resources - - - - - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - UTF-8 - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - UTF-8 - - - - - - - com.firefly - firefly-common - - INFO - D:/log/ - - - - mac - - INFO - /Users/qiupengtao/develop/logs/ - - - - macdebug - - DEBUG - /Users/qiupengtao/develop/logs/ - - - - windebug - - DEBUG - D:/log/ - - - + firefly-common + http://www.fireflysource.com - - + - junit - junit - 4.8.1 - test + org.slf4j + slf4j-api - org.hamcrest - hamcrest-all - 1.1 - test + org.javassist + javassist - - - - - 3rdRepo - 3rd party - http://localhost:7777/nexus-webapp/content/repositories/thirdparty - - - dev - Snapshots - http://localhost:7777/nexus-webapp/content/repositories/snapshots - - + + org.jctools + jctools-core + + + + + firefly-common + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.xml + **/*.properties + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/test/resources + false + + **/*.xml + **/*.properties + + + + diff --git a/firefly-common/src/main/java/com/firefly/utils/ConvertUtils.java b/firefly-common/src/main/java/com/firefly/utils/ConvertUtils.java deleted file mode 100644 index 9c9aec3eb..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/ConvertUtils.java +++ /dev/null @@ -1,276 +0,0 @@ -package com.firefly.utils; - -import java.lang.reflect.Array; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Queue; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ConcurrentNavigableMap; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.LinkedBlockingDeque; - -import com.firefly.utils.collection.IdentityHashMap; - -abstract public class ConvertUtils { - private static final IdentityHashMap, ParseValue> map = new IdentityHashMap, ParseValue>(); - private static final Map map2 = new HashMap(); - - static { - ParseValue p = new ParseValue() { - @Override - public Object parse(String value) { - return Integer.parseInt(value); - } - }; - map.put(int.class, p); - map.put(Integer.class, p); - map2.put("byte", p); - map2.put("java.lang.Byte", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return Long.parseLong(value); - } - }; - map.put(long.class, p); - map.put(Long.class, p); - map2.put("long", p); - map2.put("java.lang.Long", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return Double.parseDouble(value); - } - }; - map.put(double.class, p); - map.put(Double.class, p); - map2.put("double", p); - map2.put("java.lang.Double", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return Float.parseFloat(value); - } - }; - map.put(float.class, p); - map.put(Float.class, p); - map2.put("float", p); - map2.put("java.lang.Float", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return Boolean.parseBoolean(value); - } - }; - map.put(boolean.class, p); - map.put(Boolean.class, p); - map2.put("boolean", p); - map2.put("java.lang.Boolean", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return Short.parseShort(value); - } - }; - map.put(short.class, p); - map.put(Short.class, p); - map2.put("short", p); - map2.put("java.lang.Short", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return Byte.parseByte(value); - } - }; - map.put(byte.class, p); - map.put(Byte.class, p); - map2.put("byte", p); - map2.put("java.lang.Byte", p); - - p = new ParseValue() { - @Override - public Object parse(String value) { - return value; - } - }; - map.put(String.class, p); - map2.put("java.lang.String", p); - } - - interface ParseValue { - Object parse(String value); - } - - @SuppressWarnings("unchecked") - public static T convert(String value, Class c) { - Object ret = null; - ParseValue p = c == null ? null : map.get(c); - if (p != null) - ret = p.parse(value); - else { - if (VerifyUtils.isInteger(value)) { - ret = Integer.parseInt(value); - } else if (VerifyUtils.isLong(value)) { - ret = Long.parseLong(value); - } else if (VerifyUtils.isDouble(value)) { - ret = Double.parseDouble(value); - } else if (VerifyUtils.isFloat(value)) { - ret = Float.parseFloat(value); - } else - ret = value; - } - return (T) ret; - } - - @SuppressWarnings("unchecked") - public static T convert(String value, String argsType) { - Object ret = null; - ParseValue p = argsType == null ? null : map2.get(argsType); - if (p != null) - ret = p.parse(value); - else { - if (VerifyUtils.isInteger(value)) { - ret = Integer.parseInt(value); - } else if (VerifyUtils.isLong(value)) { - ret = Long.parseLong(value); - } else if (VerifyUtils.isDouble(value)) { - ret = Double.parseDouble(value); - } else if (VerifyUtils.isFloat(value)) { - ret = Float.parseFloat(value); - } else - ret = value; - } - return (T) ret; - } - - /** - * 把集合转换为指定类型的数组 - * - * @param collection - * @param type - * @return - */ - public static Object convert(Collection collection, Class arrayType) { - int size = collection.size(); - // Allocate a new Array - Iterator iterator = collection.iterator(); - Class componentType = null; - - if (arrayType == null) { - componentType = Object.class; - } else { - if (!arrayType.isArray()) - throw new IllegalArgumentException("type is not a array"); - - componentType = arrayType.getComponentType(); - // log.debug("componentType = " + componentType.getName()); - } - Object newArray = Array.newInstance(componentType, size); - - // Convert and set each element in the new Array - for (int i = 0; i < size; i++) { - Object element = iterator.next(); - // log.debug("element value [{}], type [{}]", element, element - // .getClass().getName()); - Array.set(newArray, i, element); - } - - return newArray; - } - - /** - * 根据类型自动返回一个集合 - * - * @param clazz - * @return - */ - @SuppressWarnings("rawtypes") - public static Collection getCollectionObj(Class clazz) { - if (clazz.isInterface()) { - if (clazz.isAssignableFrom(List.class)) - return new ArrayList(); - else if (clazz.isAssignableFrom(Set.class)) - return new HashSet(); - else if (clazz.isAssignableFrom(Queue.class)) - return new ArrayDeque(); - else if (clazz.isAssignableFrom(SortedSet.class)) - return new TreeSet(); - else if (clazz.isAssignableFrom(BlockingQueue.class)) - return new LinkedBlockingDeque(); - else - return null; - } else { - Collection collection = null; - try { - collection = (Collection) clazz.newInstance(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - return collection; - } - } - - @SuppressWarnings("rawtypes") - public static Map getMapObj(Class clazz) { - if (clazz.isInterface()) { - if (clazz.isAssignableFrom(Map.class)) - return new HashMap(); - else if (clazz.isAssignableFrom(ConcurrentMap.class)) - return new ConcurrentHashMap(); - else if (clazz.isAssignableFrom(SortedMap.class)) - return new TreeMap(); - else if (clazz.isAssignableFrom(NavigableMap.class)) - return new TreeMap(); - else if (clazz.isAssignableFrom(ConcurrentNavigableMap.class)) - return new ConcurrentSkipListMap(); - else - return null; - } else { - Map map = null; - try { - map = (Map) clazz.newInstance(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - return map; - } - } - - public static Enumeration enumeration(Collection col) { - final Iterator it = col.iterator(); - return new Enumeration() { - public boolean hasMoreElements() { - return it.hasNext(); - } - - public T nextElement() { - return it.next(); - } - }; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/RandomUtils.java b/firefly-common/src/main/java/com/firefly/utils/RandomUtils.java deleted file mode 100644 index d1958f361..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/RandomUtils.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.firefly.utils; - -abstract public class RandomUtils { - public static final String ALL_CHAR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - /** - * 生成min(包括)到max(包括)范围的随机数 - * - * @param min - * 随机数最小值 - * @param max - * 随机数最大值 - * @return min(包括)到max(包括)范围的随机数 - */ - public static long random(long min, long max) { - return Math.round(ThreadLocalRandom.current().nextDouble() - * (max - min) + min); - } - - /** - * 返回一个随机区段,例如:100:1:32:200:16:30,返回0的概率为100/(100+1+32+200+16+30) - * - * @param conf - * 区段配置字符串 - * @return 随机区段下标 - */ - public static int randomSegment(String conf) { - String[] tmp = StringUtils.split(conf, ":"); - int[] probability = new int[tmp.length]; - for (int i = 0; i < probability.length; i++) - probability[i] = Integer.parseInt(tmp[i].trim()); - - return randomSegment(probability); - } - - /** - * 返回一个随机区段 - * - * @param probability - * 区段概率值 - * @return 区段下标 - */ - public static int randomSegment(int[] probability) { - int total = 0; - for (int i = 0; i < probability.length; i++) { - total += probability[i]; - probability[i] = total; - } - int rand = (int) random(0, total - 1); - for (int i = 0; i < probability.length; i++) { - if (rand < probability[i]) { - return i; - } - } - return -1; - } - - /** - * 生成随机字符串 - * - * @param length - * 生成字符串的长度 - * @return 指定长度的随机字符串 - */ - public static String randomString(int length) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < length; i++) { - int index = (int) random(0, ALL_CHAR.length() - 1); - sb.append(ALL_CHAR.charAt(index)); - } - return sb.toString(); - } - - public static void main(String[] args) { - String conf = "100:1:32:200:16:30"; - System.out.println(randomSegment(conf)); - - System.out.println(random(0, 5)); - System.out.println(randomString(16)); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/ReflectUtils.java b/firefly-common/src/main/java/com/firefly/utils/ReflectUtils.java deleted file mode 100644 index 5234a88b7..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/ReflectUtils.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.firefly.utils; - -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public abstract class ReflectUtils { - - public static String getPropertyNameBySetterMethod(Method method) { - String methodName = method.getName(); - String propertyName = Character.toLowerCase(methodName.charAt(3)) - + methodName.substring(4); - return propertyName; - } - - public static interface BeanMethodFilter { - boolean accept(String propertyName, Method method); - } - - public static Map getSetterMethods(Class clazz) { - return getSetterMethods(clazz, null); - } - - public static Map getSetterMethods(Class clazz, - BeanMethodFilter filter) { - Map beanSetMethod = new HashMap(); - Method[] methods = clazz.getMethods(); - - for (Method method : methods) { - - if (!method.getName().startsWith("set") - || Modifier.isStatic(method.getModifiers()) - || !method.getReturnType().equals(Void.TYPE) - || method.getParameterTypes().length != 1) { - continue; - } - String propertyName = getPropertyNameBySetterMethod(method); - method.setAccessible(true); - - if (filter == null || filter.accept(propertyName, method)) - beanSetMethod.put(propertyName, method); - } - return beanSetMethod; - } - - /** - * 获取所有接口名称 - * - * @param c - * @return - */ - public static String[] getInterfaceNames(Class c) { - Class[] interfaces = c.getInterfaces(); - List names = new ArrayList(); - for (Class i : interfaces) { - names.add(i.getName()); - } - return names.toArray(new String[0]); - } - - public static Method getGetterMethod(Class clazz, String p) { - Method ret = null; - Method[] methods = clazz.getMethods(); - for (int i = 0; i < methods.length; i++) { - Method method = methods[i]; - method.setAccessible(true); - String methodName = method.getName(); - - if (method.getName().length() < 3) continue; - if (Modifier.isStatic(method.getModifiers())) continue; - if (Modifier.isAbstract(method.getModifiers())) continue; - if (method.getName().equals("getClass")) continue; - if (!method.getName().startsWith("is") && !method.getName().startsWith("get")) continue; - if (method.getParameterTypes().length != 0) continue; - if (method.getReturnType() == void.class) continue; - - if (methodName.charAt(0) == 'g') { // 取get方法的返回值 - if (methodName.length() < 4 || !Character.isUpperCase(methodName.charAt(3))) { - continue; - } - - String propertyName = Character.toLowerCase(methodName - .charAt(3)) + methodName.substring(4); - - if (propertyName.equals(p)) - return method; - - } else { // 取is方法的返回值 - if (methodName.length() < 3 || !Character.isUpperCase(methodName.charAt(2))) { - continue; - } - - String propertyName = Character.toLowerCase(methodName - .charAt(2)) + methodName.substring(3); - - if (propertyName.equals(p)) - return method; - } - } - - return ret; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/StringUtils.java b/firefly-common/src/main/java/com/firefly/utils/StringUtils.java deleted file mode 100644 index 01a9edc11..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/StringUtils.java +++ /dev/null @@ -1,575 +0,0 @@ -package com.firefly.utils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class StringUtils { - - public static final String EMPTY = ""; - public static final String[] EMPTY_STRING_ARRAY = new String[0]; - - // Splitting - // ----------------------------------------------------------------------- - /** - *

- * Splits the provided text into an array, using whitespace as the - * separator. Whitespace is defined by {@link Character#isWhitespace(char)}. - *

- * - *

- * The separator is not included in the returned String array. Adjacent - * separators are treated as one separator. For more control over the split - * use the StrTokenizer class. - *

- * - *

- * A null input String returns null. - *

- * - *
-	 * StringUtils.split(null)       = null
-	 * StringUtils.split("")         = []
-	 * StringUtils.split("abc def")  = ["abc", "def"]
-	 * StringUtils.split("abc  def") = ["abc", "def"]
-	 * StringUtils.split(" abc ")    = ["abc"]
-	 * 
- * - * @param str - * the String to parse, may be null - * @return an array of parsed Strings, null if null String - * input - */ - public static String[] split(String str) { - return split(str, null, -1); - } - - /** - *

- * Splits the provided text into an array, separators specified. This is an - * alternative to using StringTokenizer. - *

- * - *

- * The separator is not included in the returned String array. Adjacent - * separators are treated as one separator. For more control over the split - * use the StrTokenizer class. - *

- * - *

- * A null input String returns null. A - * null separatorChars splits on whitespace. - *

- * - *
-	 * StringUtils.split(null, *)         = null
-	 * StringUtils.split("", *)           = []
-	 * StringUtils.split("abc def", null) = ["abc", "def"]
-	 * StringUtils.split("abc def", " ")  = ["abc", "def"]
-	 * StringUtils.split("abc  def", " ") = ["abc", "def"]
-	 * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
-	 * 
- * - * @param str - * the String to parse, may be null - * @param separatorChars - * the characters used as the delimiters, null - * splits on whitespace - * @return an array of parsed Strings, null if null String - * input - */ - public static String[] split(String str, String separatorChars) { - return splitWorker(str, separatorChars, -1, false); - } - - /** - *

- * Splits the provided text into an array, separator specified. This is an - * alternative to using StringTokenizer. - *

- * - *

- * The separator is not included in the returned String array. Adjacent - * separators are treated as one separator. For more control over the split - * use the StrTokenizer class. - *

- * - *

- * A null input String returns null. - *

- * - *
-	 * StringUtils.split(null, *)         = null
-	 * StringUtils.split("", *)           = []
-	 * StringUtils.split("a.b.c", '.')    = ["a", "b", "c"]
-	 * StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
-	 * StringUtils.split("a:b:c", '.')    = ["a:b:c"]
-	 * StringUtils.split("a b c", ' ')    = ["a", "b", "c"]
-	 * 
- * - * @param str - * the String to parse, may be null - * @param separatorChar - * the character used as the delimiter - * @return an array of parsed Strings, null if null String - * input - * @since 2.0 - */ - public static String[] split(String str, char separatorChar) { - return splitWorker(str, separatorChar, false); - } - - /** - *

- * Splits the provided text into an array with a maximum length, separators - * specified. - *

- * - *

- * The separator is not included in the returned String array. Adjacent - * separators are treated as one separator. - *

- * - *

- * A null input String returns null. A - * null separatorChars splits on whitespace. - *

- * - *

- * If more than max delimited substrings are found, the last - * returned string includes all characters after the first - * max - 1 returned strings (including separator characters). - *

- * - *
-	 * StringUtils.split(null, *, *)            = null
-	 * StringUtils.split("", *, *)              = []
-	 * StringUtils.split("ab de fg", null, 0)   = ["ab", "cd", "ef"]
-	 * StringUtils.split("ab   de fg", null, 0) = ["ab", "cd", "ef"]
-	 * StringUtils.split("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
-	 * StringUtils.split("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
-	 * 
- * - * @param str - * the String to parse, may be null - * @param separatorChars - * the characters used as the delimiters, null - * splits on whitespace - * @param max - * the maximum number of elements to include in the array. A zero - * or negative value implies no limit - * @return an array of parsed Strings, null if null String - * input - */ - public static String[] split(String str, String separatorChars, int max) { - return splitWorker(str, separatorChars, max, false); - } - - /** - * Performs the logic for the split and - * splitPreserveAllTokens methods that return a maximum array - * length. - * - * @param str - * the String to parse, may be null - * @param separatorChars - * the separate character - * @param max - * the maximum number of elements to include in the array. A zero - * or negative value implies no limit. - * @param preserveAllTokens - * if true, adjacent separators are treated as empty - * token separators; if false, adjacent separators - * are treated as one separator. - * @return an array of parsed Strings, null if null String - * input - */ - private static String[] splitWorker(String str, String separatorChars, - int max, boolean preserveAllTokens) { - // Performance tuned for 2.0 (JDK1.4) - // Direct code is quicker than StringTokenizer. - // Also, StringTokenizer uses isSpace() not isWhitespace() - - if (str == null) { - return null; - } - int len = str.length(); - if (len == 0) { - return EMPTY_STRING_ARRAY; - } - List list = new ArrayList(); - int sizePlus1 = 1; - int i = 0, start = 0; - boolean match = false; - boolean lastMatch = false; - if (separatorChars == null) { - // Null separator means use whitespace - while (i < len) { - if (Character.isWhitespace(str.charAt(i))) { - if (match || preserveAllTokens) { - lastMatch = true; - if (sizePlus1++ == max) { - i = len; - lastMatch = false; - } - list.add(str.substring(start, i)); - match = false; - } - start = ++i; - continue; - } - lastMatch = false; - match = true; - i++; - } - } else if (separatorChars.length() == 1) { - // Optimise 1 character case - char sep = separatorChars.charAt(0); - while (i < len) { - if (str.charAt(i) == sep) { - if (match || preserveAllTokens) { - lastMatch = true; - if (sizePlus1++ == max) { - i = len; - lastMatch = false; - } - list.add(str.substring(start, i)); - match = false; - } - start = ++i; - continue; - } - lastMatch = false; - match = true; - i++; - } - } else { - // standard case - while (i < len) { - if (separatorChars.indexOf(str.charAt(i)) >= 0) { - if (match || preserveAllTokens) { - lastMatch = true; - if (sizePlus1++ == max) { - i = len; - lastMatch = false; - } - list.add(str.substring(start, i)); - match = false; - } - start = ++i; - continue; - } - lastMatch = false; - match = true; - i++; - } - } - if (match || (preserveAllTokens && lastMatch)) { - list.add(str.substring(start, i)); - } - return (String[]) list.toArray(EMPTY_STRING_ARRAY); - } - - /** - * Performs the logic for the split and - * splitPreserveAllTokens methods that do not return a maximum - * array length. - * - * @param str - * the String to parse, may be null - * @param separatorChar - * the separate character - * @param preserveAllTokens - * if true, adjacent separators are treated as empty - * token separators; if false, adjacent separators - * are treated as one separator. - * @return an array of parsed Strings, null if null String - * input - */ - private static String[] splitWorker(String str, char separatorChar, - boolean preserveAllTokens) { - // Performance tuned for 2.0 (JDK1.4) - - if (str == null) { - return null; - } - int len = str.length(); - if (len == 0) { - return EMPTY_STRING_ARRAY; - } - List list = new ArrayList(); - int i = 0, start = 0; - boolean match = false; - boolean lastMatch = false; - while (i < len) { - if (str.charAt(i) == separatorChar) { - if (match || preserveAllTokens) { - list.add(str.substring(start, i)); - match = false; - lastMatch = true; - } - start = ++i; - continue; - } - lastMatch = false; - match = true; - i++; - } - if (match || (preserveAllTokens && lastMatch)) { - list.add(str.substring(start, i)); - } - return list.toArray(EMPTY_STRING_ARRAY); - } - - /** - *

Splits the provided text into an array, separator string specified.

- * - *

The separator(s) will not be included in the returned String array. - * Adjacent separators are treated as one separator.

- * - *

A null input String returns null. - * A null separator splits on whitespace.

- * - *
-     * StringUtils.splitByWholeSeparator(null, *)               = null
-     * StringUtils.splitByWholeSeparator("", *)                 = []
-     * StringUtils.splitByWholeSeparator("ab de fg", null)      = ["ab", "de", "fg"]
-     * StringUtils.splitByWholeSeparator("ab   de fg", null)    = ["ab", "de", "fg"]
-     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":")       = ["ab", "cd", "ef"]
-     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]
-     * 
- * - * @param str the String to parse, may be null - * @param separator String containing the String to be used as a delimiter, - * null splits on whitespace - * @return an array of parsed Strings, null if null String was input - */ - public static String[] splitByWholeSeparator(String str, String separator) { - return splitByWholeSeparatorWorker( str, separator, -1, false ) ; - } - - /** - *

Splits the provided text into an array, separator string specified. - * Returns a maximum of max substrings.

- * - *

The separator(s) will not be included in the returned String array. - * Adjacent separators are treated as one separator.

- * - *

A null input String returns null. - * A null separator splits on whitespace.

- * - *
-     * StringUtils.splitByWholeSeparator(null, *, *)               = null
-     * StringUtils.splitByWholeSeparator("", *, *)                 = []
-     * StringUtils.splitByWholeSeparator("ab de fg", null, 0)      = ["ab", "de", "fg"]
-     * StringUtils.splitByWholeSeparator("ab   de fg", null, 0)    = ["ab", "de", "fg"]
-     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":", 2)       = ["ab", "cd:ef"]
-     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 5) = ["ab", "cd", "ef"]
-     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 2) = ["ab", "cd-!-ef"]
-     * 
- * - * @param str the String to parse, may be null - * @param separator String containing the String to be used as a delimiter, - * null splits on whitespace - * @param max the maximum number of elements to include in the returned - * array. A zero or negative value implies no limit. - * @return an array of parsed Strings, null if null String was input - */ - public static String[] splitByWholeSeparator( String str, String separator, int max ) { - return splitByWholeSeparatorWorker(str, separator, max, false); - } - - /** - * Performs the logic for the splitByWholeSeparatorPreserveAllTokens methods. - * - * @param str the String to parse, may be null - * @param separator String containing the String to be used as a delimiter, - * null splits on whitespace - * @param max the maximum number of elements to include in the returned - * array. A zero or negative value implies no limit. - * @param preserveAllTokens if true, adjacent separators are - * treated as empty token separators; if false, adjacent - * separators are treated as one separator. - * @return an array of parsed Strings, null if null String input - * @since 2.4 - */ - private static String[] splitByWholeSeparatorWorker(String str, String separator, int max, - boolean preserveAllTokens) - { - if (str == null) { - return null; - } - - int len = str.length(); - - if (len == 0) { - return EMPTY_STRING_ARRAY; - } - - if ((separator == null) || (EMPTY.equals(separator))) { - // Split on whitespace. - return splitWorker(str, null, max, preserveAllTokens); - } - - int separatorLength = separator.length(); - - ArrayList substrings = new ArrayList(); - int numberOfSubstrings = 0; - int beg = 0; - int end = 0; - while (end < len) { - end = str.indexOf(separator, beg); - - if (end > -1) { - if (end > beg) { - numberOfSubstrings += 1; - - if (numberOfSubstrings == max) { - end = len; - substrings.add(str.substring(beg)); - } else { - // The following is OK, because String.substring( beg, end ) excludes - // the character at the position 'end'. -// System.out.println("sub " + beg + "|" + end +"|" + str.substring(beg, end)); - substrings.add(str.substring(beg, end)); - - // Set the starting point for the next search. - // The following is equivalent to beg = end + (separatorLength - 1) + 1, - // which is the right calculation: - beg = end + separatorLength; - } - } else { - // We found a consecutive occurrence of the separator, so skip it. - if (preserveAllTokens) { - numberOfSubstrings += 1; - if (numberOfSubstrings == max) { - end = len; - substrings.add(str.substring(beg)); - } else { - substrings.add(EMPTY); - } - } - beg = end + separatorLength; - } - } else { - // String.substring( beg ) goes from 'beg' to the end of the String. -// System.out.println("sub~~ " + beg + "|" + end +"|" + str.substring(beg)); - String t = str.substring(beg); - if(!t.equals(EMPTY)) - substrings.add(str.substring(beg)); - end = len; - } - } - - return substrings.toArray(EMPTY_STRING_ARRAY); - } - - public static boolean hasText(String str) { - return hasText((CharSequence) str); - } - - public static boolean hasText(CharSequence str) { - if (!hasLength(str)) { - return false; - } - int strLen = str.length(); - for (int i = 0; i < strLen; i++) { - if (!Character.isWhitespace(str.charAt(i))) { - return true; - } - } - return false; - } - - public static boolean hasLength(CharSequence str) { - return (str != null && str.length() > 0); - } - - public static boolean hasLength(String str) { - return hasLength((CharSequence) str); - } - - /** - * 将字符串中特定模式的字符转换成map中对应的值 - * - * @param s - * 需要转换的字符串 - * @param map - * 转换所需的键值对集合 - * @return 转换后的字符串 - */ - public static String replace(String s, Map map) { - StringBuilder ret = new StringBuilder((int)(s.length() * 1.5)); - int cursor = 0; - for (int start, end; (start = s.indexOf("${", cursor)) != -1 - && (end = s.indexOf("}", start)) != -1;) { - ret.append(s.substring(cursor, start)).append( - map.get(s.substring(start + 2, end))); - cursor = end + 1; - } - ret.append(s.substring(cursor, s.length())); - return ret.toString(); - } - - public static String replace(String s, Object...objs) { - if(objs == null || objs.length == 0) - return s; - if(s.indexOf("{}") == -1) - return s; - - StringBuilder ret = new StringBuilder((int)(s.length() * 1.5)); - int cursor = 0; - int index = 0; - for(int start; (start = s.indexOf("{}", cursor)) != -1 ;) { - ret.append(s.substring(cursor, start)); - if(index < objs.length) - ret.append(objs[index]); - else - ret.append("{}"); - cursor = start + 2; - index++; - } - ret.append(s.substring(cursor, s.length())); - return ret.toString(); - } - - public static String escapeXML(String str) { - if (str == null) - return ""; - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < str.length(); ++i) { - char c = str.charAt(i); - switch (c) { - case '\u00FF': - case '\u0024': - break; - case '&': - sb.append("&"); - break; - case '<': - sb.append("<"); - break; - case '>': - sb.append(">"); - break; - case '\"': - sb.append("""); - break; - case '\'': - sb.append("'"); - break; - default: - if (c >= '\u0000' && c <= '\u001F') - break; - if (c >= '\uE000' && c <= '\uF8FF') - break; - if (c >= '\uFFF0' && c <= '\uFFFF') - break; - sb.append(c); - break; - } - } - return sb.toString(); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/ThreadLocalBoolean.java b/firefly-common/src/main/java/com/firefly/utils/ThreadLocalBoolean.java deleted file mode 100644 index a6deeb92d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/ThreadLocalBoolean.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.firefly.utils; - -public class ThreadLocalBoolean extends ThreadLocal { - private final boolean defaultValue; - - public ThreadLocalBoolean() { - this(false); - } - - public ThreadLocalBoolean(boolean defaultValue) { - this.defaultValue = defaultValue; - } - - @Override - protected Boolean initialValue() { - return defaultValue ? Boolean.TRUE : Boolean.FALSE; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/ThreadLocalRandom.java b/firefly-common/src/main/java/com/firefly/utils/ThreadLocalRandom.java deleted file mode 100644 index ba9188c52..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/ThreadLocalRandom.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat 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. - */ - -/* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at - * http://creativecommons.org/licenses/publicdomain - */ - -package com.firefly.utils; - -import java.util.Random; - -/** - * A random number generator isolated to the current thread. Like the - * global {@link java.util.Random} generator used by the {@link - * java.lang.Math} class, a {@code ThreadLocalRandom} is initialized - * with an internally generated seed that may not otherwise be - * modified. When applicable, use of {@code ThreadLocalRandom} rather - * than shared {@code Random} objects in concurrent programs will - * typically encounter much less overhead and contention. Use of - * {@code ThreadLocalRandom} is particularly appropriate when multiple - * tasks use random numbers in parallel in thread pools. - * - *

Usages of this class should typically be of the form: - * {@code ThreadLocalRandom.current().nextX(...)} (where - * {@code X} is {@code Int}, {@code Long}, etc). - * When all usages are of this form, it is never possible to - * accidently share a {@code ThreadLocalRandom} across multiple threads. - * - *

This class also provides additional commonly used bounded random - * generation methods. - * - * @since 1.7 - * @author Doug Lea - */ -public final class ThreadLocalRandom extends Random { - // same constants as Random, but must be redeclared because private - private final static long multiplier = 0x5DEECE66DL; - private final static long addend = 0xBL; - private final static long mask = (1L << 48) - 1; - - /** - * The random seed. We can't use super.seed. - */ - private long rnd; - - /** - * Initialization flag to permit the first and only allowed call - * to setSeed (inside Random constructor) to succeed. We can't - * allow others since it would cause setting seed in one part of a - * program to unintentionally impact other usages by the thread. - */ - private boolean initialized; - - // Padding to help avoid memory contention among seed updates in - // different TLRs in the common case that they are located near - // each other. - @SuppressWarnings("unused") - private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7; - - /** - * The actual ThreadLocal - */ - private static final ThreadLocal localRandom = - new ThreadLocal() { - @Override - protected ThreadLocalRandom initialValue() { - return new ThreadLocalRandom(); - } - }; - - - /** - * Constructor called only by localRandom.initialValue. - * We rely on the fact that the superclass no-arg constructor - * invokes setSeed exactly once to initialize. - */ - public ThreadLocalRandom() { - super(); - } - - /** - * Returns the current thread's {@code ThreadLocalRandom}. - * - * @return the current thread's {@code ThreadLocalRandom} - */ - public static ThreadLocalRandom current() { - return localRandom.get(); - } - - /** - * Throws {@code UnsupportedOperationException}. Setting seeds in - * this generator is not supported. - * - * @throws UnsupportedOperationException always - */ - @Override - public void setSeed(long seed) { - if (initialized) { - throw new UnsupportedOperationException(); - } - initialized = true; - rnd = (seed ^ multiplier) & mask; - } - - @Override - protected int next(int bits) { - rnd = rnd * multiplier + addend & mask; - return (int) (rnd >>> 48-bits); - } - - private static final long serialVersionUID = -5851777807851030925L; -} diff --git a/firefly-common/src/main/java/com/firefly/utils/VerifyUtils.java b/firefly-common/src/main/java/com/firefly/utils/VerifyUtils.java deleted file mode 100644 index 2ee04516f..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/VerifyUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.firefly.utils; - -abstract public class VerifyUtils { - - public static boolean isNumeric(String str) { - if (isEmpty(str)) - return false; - - char first = str.charAt(0); - int i = first == '-' ? 1 : 0; - for (; i < str.length(); i++) { - if (isDigit(str.charAt(i)) == false) { - return false; - } - } - return true; - } - - public static boolean isInteger(String str) { - if (isEmpty(str)) - return false; - - char first = str.charAt(0); - int i = first == '-' ? 1 : 0; - for (; i < str.length(); i++) { - if (isDigit(str.charAt(i)) == false) { - return false; - } - } - - Long t = Long.parseLong(str); - return t <= Integer.MAX_VALUE && t >= Integer.MIN_VALUE; - } - - public static boolean isLong(String str) { - if (isEmpty(str)) - return false; - - char first = str.charAt(0); - char end = str.charAt(str.length() - 1); - boolean j = end == 'l' || end == 'L'; - int i = first == '-' ? 1 : 0; - int len = j ? str.length() - 1 : str.length(); - for (; i < len; i++) { - if (isDigit(str.charAt(i)) == false) { - return false; - } - } - - if(!j) { - Long t = Long.parseLong(str); - return t > Integer.MAX_VALUE || t < Integer.MIN_VALUE; - } else { - return true; - } - } - - public static boolean isFloat(String str) { - if (isEmpty(str)) - return false; - - char end = str.charAt(str.length() - 1); - if(!(end == 'f' || end == 'F' )) - return false; - - int point = 0; - int i = str.charAt(0) == '-' ? 1 : 0; - for (; i < str.length() - 1; i++) { - char c = str.charAt(i); - if(c == '.') { - point++; - } else if (VerifyUtils.isDigit(c) == false) { - return false; - } - } - return point == 1 || point == 0; - } - - public static boolean isDouble(String str) { - if (isEmpty(str)) - return false; - - int point = 0; - int i = str.charAt(0) == '-' ? 1 : 0; - for (; i < str.length(); i++) { - char c = str.charAt(i); - if(c == '.') { - point++; - } else if (isDigit(c) == false) { - return false; - } - } - - return point == 1; - } - - public static boolean isDigit(char ch) { - return ch >= '0' && ch <= '9'; - } - - public static boolean isNotEmpty(Long o) { - return o != null && StringUtils.hasText(o.toString()); - } - - public static boolean isNotEmpty(Integer o) { - return o != null && StringUtils.hasText(o.toString()); - } - - public static boolean isNotEmpty(String o) { - return StringUtils.hasText(o); - } - - public static boolean isEmpty(Long o) { - return !isNotEmpty(o); - } - - public static boolean isEmpty(Integer o) { - return !isNotEmpty(o); - } - - public static boolean isEmpty(String o) { - return !isNotEmpty(o); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/codec/Base64.java b/firefly-common/src/main/java/com/firefly/utils/codec/Base64.java deleted file mode 100644 index 60ccc8c12..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/codec/Base64.java +++ /dev/null @@ -1,575 +0,0 @@ -package com.firefly.utils.codec; - -import java.util.Arrays; - -/** A very fast and memory efficient class to encode and decode to and from BASE64 in full accordance - * with RFC 2045.

- * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10 times faster - * on small arrays (10 - 1000 bytes) and 2-3 times as fast on larger arrays (10000 - 1000000 bytes) - * compared to sun.misc.Encoder()/Decoder().

- * - * On byte arrays the encoder is about 20% faster than Jakarta Commons Base64 Codec for encode and - * about 50% faster for decoding large arrays. This implementation is about twice as fast on very small - * arrays (< 30 bytes). If source/destination is a String this - * version is about three times as fast due to the fact that the Commons Codec result has to be recoded - * to a String from byte[], which is very expensive.

- * - * This encode/decode algorithm doesn't create any temporary arrays as many other codecs do, it only - * allocates the resulting array. This produces less garbage and it is possible to handle arrays twice - * as large as algorithms that create a temporary array. (E.g. Jakarta Commons Codec). It is unknown - * whether Sun's sun.misc.Encoder()/Decoder() produce temporary arrays but since performance - * is quite low it probably does.

- * - * The encoder produces the same output as the Sun one except that the Sun's encoder appends - * a trailing line separator if the last character isn't a pad. Unclear why but it only adds to the - * length and is probably a side effect. Both are in conformance with RFC 2045 though.
- * Commons codec seem to always att a trailing line separator.

- * - * Note! - * The encode/decode method pairs (types) come in three versions with the exact same algorithm and - * thus a lot of code redundancy. This is to not create any temporary arrays for transcoding to/from different - * format types. The methods not used can simply be commented out.

- * - * There is also a "fast" version of all decode methods that works the same way as the normal ones, but - * har a few demands on the decoded input. Normally though, these fast verions should be used if the source if - * the input is known and it hasn't bee tampered with.

- * - * If you find the code useful or you find a bug, please send me a note at base64 @ miginfocom . com. - * - * Licence (BSD): - * ============== - * - * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. - * Neither the name of the MiG InfoCom AB nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - * @version 2.2 - * @author Mikael Grev - * Date: 2004-aug-02 - * Time: 11:31:11 - */ - -public class Base64 -{ - private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); - private static final int[] IA = new int[256]; - static { - Arrays.fill(IA, -1); - for (int i = 0, iS = CA.length; i < iS; i++) - IA[CA[i]] = i; - IA['='] = 0; - } - - // **************************************************************************************** - // * char[] version - // **************************************************************************************** - - /** Encodes a raw byte array into a BASE64 char[] representation i accordance with RFC 2045. - * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. - * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
- * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a - * little faster. - * @return A BASE64 encoded array. Never null. - */ - public final static char[] encodeToChar(byte[] sArr, boolean lineSep) - { - // Check special case - int sLen = sArr != null ? sArr.length : 0; - if (sLen == 0) - return new char[0]; - - int eLen = (sLen / 3) * 3; // Length of even 24-bits. - int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count - int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array - char[] dArr = new char[dLen]; - - // Encode even 24-bits - for (int s = 0, d = 0, cc = 0; s < eLen;) { - // Copy next three bytes into lower 24 bits of int, paying attension to sign. - int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); - - // Encode the int into four chars - dArr[d++] = CA[(i >>> 18) & 0x3f]; - dArr[d++] = CA[(i >>> 12) & 0x3f]; - dArr[d++] = CA[(i >>> 6) & 0x3f]; - dArr[d++] = CA[i & 0x3f]; - - // Add optional line separator - if (lineSep && ++cc == 19 && d < dLen - 2) { - dArr[d++] = '\r'; - dArr[d++] = '\n'; - cc = 0; - } - } - - // Pad and encode last bits if source isn't even 24 bits. - int left = sLen - eLen; // 0 - 2. - if (left > 0) { - // Prepare the int - int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); - - // Set last four chars - dArr[dLen - 4] = CA[i >> 12]; - dArr[dLen - 3] = CA[(i >>> 6) & 0x3f]; - dArr[dLen - 2] = left == 2 ? CA[i & 0x3f] : '='; - dArr[dLen - 1] = '='; - } - return dArr; - } - - /** Decodes a BASE64 encoded char array. All illegal characters will be ignored and can handle both arrays with - * and without line separators. - * @param sArr The source array. null or length 0 will return an empty array. - * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters - * (including '=') isn't divideable by 4. (I.e. definitely corrupted). - */ - public final static byte[] decode(char[] sArr) - { - // Check special case - int sLen = sArr != null ? sArr.length : 0; - if (sLen == 0) - return new byte[0]; - - // Count illegal characters (including '\r', '\n') to know what size the returned array will be, - // so we don't have to reallocate & copy it later. - int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) - for (int i = 0; i < sLen; i++) // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. - if (IA[sArr[i]] < 0) - sepCnt++; - - // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. - if ((sLen - sepCnt) % 4 != 0) - return null; - - int pad = 0; - for (int i = sLen; i > 1 && IA[sArr[--i]] <= 0;) - if (sArr[i] == '=') - pad++; - - int len = ((sLen - sepCnt) * 6 >> 3) - pad; - - byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - for (int s = 0, d = 0; d < len;) { - // Assemble three bytes into an int from four "valid" characters. - int i = 0; - for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. - int c = IA[sArr[s++]]; - if (c >= 0) - i |= c << (18 - j * 6); - else - j--; - } - // Add the bytes - dArr[d++] = (byte) (i >> 16); - if (d < len) { - dArr[d++]= (byte) (i >> 8); - if (d < len) - dArr[d++] = (byte) i; - } - } - return dArr; - } - - /** Decodes a BASE64 encoded char array that is known to be resonably well formatted. The method is about twice as - * fast as {@link #decode(char[])}. The preconditions are:
- * + The array must have a line length of 76 chars OR no line separators at all (one line).
- * + Line separator must be "\r\n", as specified in RFC 2045 - * + The array must not contain illegal characters within the encoded string
- * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
- * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. - * @return The decoded array of bytes. May be of length 0. - */ - public final static byte[] decodeFast(char[] sArr) - { - // Check special case - int sLen = sArr.length; - if (sLen == 0) - return new byte[0]; - - int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[sArr[sIx]] < 0) - sIx++; - - // Trim illegal chars from end - while (eIx > 0 && IA[sArr[eIx]] < 0) - eIx--; - - // get the padding count (=) (0, 1 or 2) - int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. - int cCnt = eIx - sIx + 1; // Content count including possible separators - int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; - - int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes - byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) - i |= IA[sArr[sIx++]] << (18 - j * 6); - - for (int r = 16; d < len; r -= 8) - dArr[d++] = (byte) (i >> r); - } - - return dArr; - } - - // **************************************************************************************** - // * byte[] version - // **************************************************************************************** - - /** Encodes a raw byte array into a BASE64 byte[] representation i accordance with RFC 2045. - * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. - * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
- * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a - * little faster. - * @return A BASE64 encoded array. Never null. - */ - public final static byte[] encodeToByte(byte[] sArr, boolean lineSep) - { - // Check special case - int sLen = sArr != null ? sArr.length : 0; - if (sLen == 0) - return new byte[0]; - - int eLen = (sLen / 3) * 3; // Length of even 24-bits. - int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count - int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array - byte[] dArr = new byte[dLen]; - - // Encode even 24-bits - for (int s = 0, d = 0, cc = 0; s < eLen;) { - // Copy next three bytes into lower 24 bits of int, paying attension to sign. - int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); - - // Encode the int into four chars - dArr[d++] = (byte) CA[(i >>> 18) & 0x3f]; - dArr[d++] = (byte) CA[(i >>> 12) & 0x3f]; - dArr[d++] = (byte) CA[(i >>> 6) & 0x3f]; - dArr[d++] = (byte) CA[i & 0x3f]; - - // Add optional line separator - if (lineSep && ++cc == 19 && d < dLen - 2) { - dArr[d++] = '\r'; - dArr[d++] = '\n'; - cc = 0; - } - } - - // Pad and encode last bits if source isn't an even 24 bits. - int left = sLen - eLen; // 0 - 2. - if (left > 0) { - // Prepare the int - int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); - - // Set last four chars - dArr[dLen - 4] = (byte) CA[i >> 12]; - dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f]; - dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '='; - dArr[dLen - 1] = '='; - } - return dArr; - } - - /** Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with - * and without line separators. - * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. - * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters - * (including '=') isn't divideable by 4. (I.e. definitely corrupted). - */ - public final static byte[] decode(byte[] sArr) - { - // Check special case - int sLen = sArr.length; - - // Count illegal characters (including '\r', '\n') to know what size the returned array will be, - // so we don't have to reallocate & copy it later. - int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) - for (int i = 0; i < sLen; i++) // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. - if (IA[sArr[i] & 0xff] < 0) - sepCnt++; - - // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. - if ((sLen - sepCnt) % 4 != 0) - return null; - - int pad = 0; - for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) - if (sArr[i] == '=') - pad++; - - int len = ((sLen - sepCnt) * 6 >> 3) - pad; - - byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - for (int s = 0, d = 0; d < len;) { - // Assemble three bytes into an int from four "valid" characters. - int i = 0; - for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. - int c = IA[sArr[s++] & 0xff]; - if (c >= 0) - i |= c << (18 - j * 6); - else - j--; - } - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - if (d < len) { - dArr[d++]= (byte) (i >> 8); - if (d < len) - dArr[d++] = (byte) i; - } - } - - return dArr; - } - - - /** Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as - * fast as {@link #decode(byte[])}. The preconditions are:
- * + The array must have a line length of 76 chars OR no line separators at all (one line).
- * + Line separator must be "\r\n", as specified in RFC 2045 - * + The array must not contain illegal characters within the encoded string
- * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
- * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. - * @return The decoded array of bytes. May be of length 0. - */ - public final static byte[] decodeFast(byte[] sArr) - { - // Check special case - int sLen = sArr.length; - if (sLen == 0) - return new byte[0]; - - int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) - sIx++; - - // Trim illegal chars from end - while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) - eIx--; - - // get the padding count (=) (0, 1 or 2) - int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. - int cCnt = eIx - sIx + 1; // Content count including possible separators - int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; - - int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes - byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) - i |= IA[sArr[sIx++]] << (18 - j * 6); - - for (int r = 16; d < len; r -= 8) - dArr[d++] = (byte) (i >> r); - } - - return dArr; - } - - // **************************************************************************************** - // * String version - // **************************************************************************************** - - /** Encodes a raw byte array into a BASE64 String representation i accordance with RFC 2045. - * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. - * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
- * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a - * little faster. - * @return A BASE64 encoded array. Never null. - */ - public final static String encodeToString(byte[] sArr, boolean lineSep) - { - // Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower. - return new String(encodeToChar(sArr, lineSep)); - } - - /** Decodes a BASE64 encoded String. All illegal characters will be ignored and can handle both strings with - * and without line separators.
- * Note! It can be up to about 2x the speed to call decode(str.toCharArray()) instead. That - * will create a temporary array though. This version will use str.charAt(i) to iterate the string. - * @param str The source string. null or length 0 will return an empty array. - * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters - * (including '=') isn't divideable by 4. (I.e. definitely corrupted). - */ - public final static byte[] decode(String str) - { - // Check special case - int sLen = str != null ? str.length() : 0; - if (sLen == 0) - return new byte[0]; - - // Count illegal characters (including '\r', '\n') to know what size the returned array will be, - // so we don't have to reallocate & copy it later. - int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) - for (int i = 0; i < sLen; i++) // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. - if (IA[str.charAt(i)] < 0) - sepCnt++; - - // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. - if ((sLen - sepCnt) % 4 != 0) - return null; - - // Count '=' at end - int pad = 0; - for (int i = sLen; i > 1 && IA[str.charAt(--i)] <= 0;) - if (str.charAt(i) == '=') - pad++; - - int len = ((sLen - sepCnt) * 6 >> 3) - pad; - - byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - for (int s = 0, d = 0; d < len;) { - // Assemble three bytes into an int from four "valid" characters. - int i = 0; - for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. - int c = IA[str.charAt(s++)]; - if (c >= 0) - i |= c << (18 - j * 6); - else - j--; - } - // Add the bytes - dArr[d++] = (byte) (i >> 16); - if (d < len) { - dArr[d++]= (byte) (i >> 8); - if (d < len) - dArr[d++] = (byte) i; - } - } - return dArr; - } - - /** Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as - * fast as {@link #decode(String)}. The preconditions are:
- * + The array must have a line length of 76 chars OR no line separators at all (one line).
- * + Line separator must be "\r\n", as specified in RFC 2045 - * + The array must not contain illegal characters within the encoded string
- * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
- * @param s The source string. Length 0 will return an empty array. null will throw an exception. - * @return The decoded array of bytes. May be of length 0. - */ - public final static byte[] decodeFast(String s) - { - // Check special case - int sLen = s.length(); - if (sLen == 0) - return new byte[0]; - - int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[s.charAt(sIx) & 0xff] < 0) - sIx++; - - // Trim illegal chars from end - while (eIx > 0 && IA[s.charAt(eIx) & 0xff] < 0) - eIx--; - - // get the padding count (=) (0, 1 or 2) - int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0; // Count '=' at end. - int cCnt = eIx - sIx + 1; // Content count including possible separators - int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0; - - int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes - byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[s.charAt(sIx++)] << 18 | IA[s.charAt(sIx++)] << 12 | IA[s.charAt(sIx++)] << 6 | IA[s.charAt(sIx++)]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) - i |= IA[s.charAt(sIx++)] << (18 - j * 6); - - for (int r = 16; d < len; r -= 8) - dArr[d++] = (byte) (i >> r); - } - - return dArr; - } -} \ No newline at end of file diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/AtomicFieldUpdaterUtil.java b/firefly-common/src/main/java/com/firefly/utils/collection/AtomicFieldUpdaterUtil.java deleted file mode 100644 index 633f5ffd3..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/AtomicFieldUpdaterUtil.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2009 Red Hat, Inc. - * - * Red Hat 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 com.firefly.utils.collection; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; - -/** - * @author The Netty Project - * @author Trustin Lee - * @version $Rev: 2080 $, $Date: 2010-01-26 18:04:19 +0900 (Tue, 26 Jan 2010) $ - */ -class AtomicFieldUpdaterUtil { - - private static final boolean AVAILABLE; - - static final class Node { - volatile Node next; - Node() { - super(); - } - } - - static { - boolean available = false; - try { - AtomicReferenceFieldUpdater tmp = - AtomicReferenceFieldUpdater.newUpdater( - Node.class, Node.class, "next"); - - // Test if AtomicReferenceFieldUpdater is really working. - Node testNode = new Node(); - tmp.set(testNode, testNode); - if (testNode.next != testNode) { - // Not set as expected - fall back to the safe mode. - throw new Exception(); - } - available = true; - } catch (Throwable t) { - // Running in a restricted environment with a security manager. - } - AVAILABLE = available; - } - - static AtomicReferenceFieldUpdater newRefUpdater(Class tclass, Class vclass, String fieldName) { - if (AVAILABLE) { - return AtomicReferenceFieldUpdater.newUpdater(tclass, vclass, fieldName); - } else { - return null; - } - } - - static AtomicIntegerFieldUpdater newIntUpdater(Class tclass, String fieldName) { - if (AVAILABLE) { - return AtomicIntegerFieldUpdater.newUpdater(tclass, fieldName); - } else { - return null; - } - } - - static boolean isAvailable() { - return AVAILABLE; - } - - private AtomicFieldUpdaterUtil() { - // Unused - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/ConcurrentLRUHashMap.java b/firefly-common/src/main/java/com/firefly/utils/collection/ConcurrentLRUHashMap.java deleted file mode 100644 index 84c6044f2..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/ConcurrentLRUHashMap.java +++ /dev/null @@ -1,1314 +0,0 @@ -package com.firefly.utils.collection; - -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.*; -import java.util.*; -import java.io.Serializable; -import java.io.IOException; - -public class ConcurrentLRUHashMap extends AbstractMap implements - ConcurrentMap, Serializable { - - /* - * The basic strategy is to subdivide the table among Segments, each of - * which itself is a concurrently readable hash table. - */ - - /* ---------------- Constants -------------- */ - private static final long serialVersionUID = -5031526786765467550L; - - /** - * Segement默认最大数 - */ - static final int DEFAULT_SEGEMENT_MAX_CAPACITY = 10000; - - /** - * The default load factor for this table, used when not otherwise specified - * in a constructor. - */ - static final float DEFAULT_LOAD_FACTOR = 0.75f; - - /** - * The default concurrency level for this table, used when not otherwise - * specified in a constructor. - */ - static final int DEFAULT_CONCURRENCY_LEVEL = 16; - - /** - * The maximum capacity, used if a higher value is implicitly specified by - * either of the constructors with arguments. MUST be a power of two <= - * 1<<30 to ensure that entries are indexable using ints. - */ - static final int MAXIMUM_CAPACITY = 1 << 30; - - /** - * The maximum number of segments to allow; used to bound constructor - * arguments. - */ - static final int MAX_SEGMENTS = 1 << 16; // slightly conservative - - /** - * Number of unsynchronized retries in size and containsValue methods before - * resorting to locking. This is used to avoid unbounded retries if tables - * undergo continuous modification which would make it impossible to obtain - * an accurate result. - */ - static final int RETRIES_BEFORE_LOCK = 2; - - /* ---------------- Fields -------------- */ - - /** - * Mask value for indexing into segments. The upper bits of a key's hash - * code are used to choose the segment. - */ - final int segmentMask; - - /** - * Shift value for indexing within segments. - */ - final int segmentShift; - - /** - * The segments, each of which is a specialized hash table - */ - final Segment[] segments; - - transient Set keySet; - transient Set> entrySet; - transient Collection values; - - /* ---------------- Small Utilities -------------- */ - - /** - * Applies a supplemental hash function to a given hashCode, which defends - * against poor quality hash functions. This is critical because - * ConcurrentHashMap uses power-of-two length hash tables, that otherwise - * encounter collisions for hashCodes that do not differ in lower or upper - * bits. - */ - private static int hash(int h) { - // Spread bits to regularize both segment and index locations, - // using variant of single-word Wang/Jenkins hash. - h += (h << 15) ^ 0xffffcd7d; - h ^= (h >>> 10); - h += (h << 3); - h ^= (h >>> 6); - h += (h << 2) + (h << 14); - return h ^ (h >>> 16); - } - - /** - * Returns the segment that should be used for key with given hash - * - * @param hash - * the hash code for the key - * @return the segment - */ - final Segment segmentFor(int hash) { - return segments[(hash >>> segmentShift) & segmentMask]; - } - - /* ---------------- Inner Classes -------------- */ - - /** - * 修改原HashEntry, - */ - static final class HashEntry { - final K key; - final int hash; - volatile V value; - - /** - * hash链指针 - */ - final HashEntry next; - - /** - * 双向链表的下一个节点 - */ - HashEntry linkNext; - - /** - * 双向链表的上一个节点 - */ - HashEntry linkPrev; - - /** - * 死亡标记 - */ - AtomicBoolean dead; - - HashEntry(K key, int hash, HashEntry next, V value) { - this.key = key; - this.hash = hash; - this.next = next; - this.value = value; - dead = new AtomicBoolean(false); - } - - @SuppressWarnings("unchecked") - static final HashEntry[] newArray(int i) { - return new HashEntry[i]; - } - } - - /** - * 基于原Segment修改,内部实现一个双向列表 - */ - static final class Segment extends ReentrantLock implements - Serializable { - /* - * Segments maintain a table of entry lists that are ALWAYS kept in a - * consistent state, so can be read without locking. Next fields of - * nodes are immutable (final). All list additions are performed at the - * front of each bin. This makes it easy to check changes, and also fast - * to traverse. When nodes would otherwise be changed, new nodes are - * created to replace them. This works well for hash tables since the - * bin lists tend to be short. (The average length is less than two for - * the default load factor threshold.) - * - * Read operations can thus proceed without locking, but rely on - * selected uses of volatiles to ensure that completed write operations - * performed by other threads are noticed. For most purposes, the - * "count" field, tracking the number of elements, serves as that - * volatile variable ensuring visibility. This is convenient because - * this field needs to be read in many read operations anyway: - * - * - All (unsynchronized) read operations must first read the "count" - * field, and should not look at table entries if it is 0. - * - * - All (synchronized) write operations should write to the "count" - * field after structurally changing any bin. The operations must not - * take any action that could even momentarily cause a concurrent read - * operation to see inconsistent data. This is made easier by the nature - * of the read operations in Map. For example, no operation can reveal - * that the table has grown but the threshold has not yet been updated, - * so there are no atomicity requirements for this with respect to - * reads. - * - * As a guide, all critical volatile reads and writes to the count field - * are marked in code comments. - */ - - private static final long serialVersionUID = 2249069246763182397L; - - /** - * The number of elements in this segment's region. - */ - transient volatile int count; - - /** - * Number of updates that alter the size of the table. This is used - * during bulk-read methods to make sure they see a consistent snapshot: - * If modCounts change during a traversal of segments computing size or - * checking containsValue, then we might have an inconsistent view of - * state so (usually) must retry. - */ - transient int modCount; - - /** - * The table is rehashed when its size exceeds this threshold. (The - * value of this field is always (int)(capacity * - * loadFactor).) - */ - transient int threshold; - - /** - * The per-segment table. - */ - transient volatile HashEntry[] table; - - /** - * The load factor for the hash table. Even though this value is same - * for all segments, it is replicated to avoid needing links to outer - * object. - * - * @serial - */ - final float loadFactor; - - /** - * 头节点 - */ - transient final HashEntry header; - - /** - * Segement最大容量 - */ - final int maxCapacity; - - /** - * map回调 - */ - transient LRUMapEventListener listener = new LRUMapEventListener() { - @Override - public void eliminated(Object key, Object value) { - } - - @Override - public Object getNull(Object key) { - return null; - } - }; - - Segment(int maxCapacity, float lf, LRUMapEventListener listener) { - this.maxCapacity = maxCapacity; - loadFactor = lf; - setTable(HashEntry. newArray(maxCapacity)); - header = new HashEntry(null, -1, null, null); - header.linkNext = header; - header.linkPrev = header; - - if(listener != null) - this.listener = listener; - } - - @SuppressWarnings("unchecked") - static final Segment[] newArray(int i) { - return new Segment[i]; - } - - /** - * Sets table to new HashEntry array. Call only while holding lock or in - * constructor. - */ - void setTable(HashEntry[] newTable) { - threshold = (int) (newTable.length * loadFactor); - table = newTable; - } - - /** - * Returns properly casted first entry of bin for given hash. - */ - HashEntry getFirst(int hash) { - HashEntry[] tab = table; - return tab[hash & (tab.length - 1)]; - } - - /** - * Reads value field of an entry under lock. Called if value field ever - * appears to be null. This is possible only if a compiler happens to - * reorder a HashEntry initialization with its table assignment, which - * is legal under memory model but is not known to ever occur. - */ - @SuppressWarnings("unchecked") - V readValueUnderLock(HashEntry e) { - lock(); - try { - return e.value == null ? (V) listener.getNull(e.key) : e.value; - } finally { - unlock(); - } - } - - /* Specialized implementations of map methods */ - - @SuppressWarnings("unchecked") - V get(Object key, int hash) { - if (count != 0) { // read-volatile - HashEntry e = getFirst(hash); - while (e != null) { - if (e.hash == hash && key.equals(e.key)) { - V v = e.value; - // 将节点移动到头节点之前 - lock(); - try { - moveNodeToHeader(e); - } finally { - unlock(); - } - if (v != null) - return v; - return readValueUnderLock(e); // recheck - } - e = e.next; - } - } - return (V) listener.getNull(key); - } - - /** - * 将节点移动到头节点之前 - * - * @param entry - */ - void moveNodeToHeader(HashEntry entry) { - // 先移除,然后插入到头节点的前面 - removeNode(entry); - addBefore(entry, header); - } - - /** - * 将第一个参数代表的节点插入到第二个参数代表的节点之前 - * - * @param newEntry - * 需要插入的节点 - * @param entry - * 被插入的节点 - */ - void addBefore(HashEntry newEntry, HashEntry entry) { - newEntry.linkNext = entry; - newEntry.linkPrev = entry.linkPrev; - entry.linkPrev.linkNext = newEntry; - entry.linkPrev = newEntry; - } - - /** - * 从双向链中删除该Entry - * - * @param entry - */ - void removeNode(HashEntry entry) { - entry.linkPrev.linkNext = entry.linkNext; - entry.linkNext.linkPrev = entry.linkPrev; - } - - boolean containsKey(Object key, int hash) { - if (count != 0) { // read-volatile - HashEntry e = getFirst(hash); - while (e != null) { - if (e.hash == hash && key.equals(e.key)) { - lock(); - try { - moveNodeToHeader(e); - } finally { - unlock(); - } - return true; - } - e = e.next; - } - } - return false; - } - - boolean containsValue(Object value) { - if (count != 0) { // read-volatile - HashEntry[] tab = table; - int len = tab.length; - for (int i = 0; i < len; i++) { - for (HashEntry e = tab[i]; e != null; e = e.next) { - V v = e.value; - if (v == null) // recheck - v = readValueUnderLock(e); - if (value.equals(v)) { - lock(); - try { - moveNodeToHeader(e); - } finally { - unlock(); - } - return true; - } - } - } - } - return false; - } - - boolean replace(K key, int hash, V oldValue, V newValue) { - lock(); - try { - HashEntry e = getFirst(hash); - while (e != null && (e.hash != hash || !key.equals(e.key))) - e = e.next; - - boolean replaced = false; - if (e != null && oldValue.equals(e.value)) { - replaced = true; - e.value = newValue; - moveNodeToHeader(e); - } - return replaced; - } finally { - unlock(); - } - } - - V replace(K key, int hash, V newValue) { - lock(); - try { - HashEntry e = getFirst(hash); - while (e != null && (e.hash != hash || !key.equals(e.key))) - e = e.next; - - V oldValue = null; - if (e != null) { - oldValue = e.value; - e.value = newValue; - moveNodeToHeader(e); - } - return oldValue; - } finally { - unlock(); - } - } - - V put(K key, int hash, V value, boolean onlyIfAbsent) { - lock(); - try { - int c = count; - if (c++ > threshold) // ensure capacity - rehash(); - HashEntry[] tab = table; - int index = hash & (tab.length - 1); - HashEntry first = tab[index]; - HashEntry e = first; - while (e != null && (e.hash != hash || !key.equals(e.key))) - e = e.next; - - V oldValue = null; - if (e != null) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - moveNodeToHeader(e); - } - } else { - oldValue = null; - ++modCount; - HashEntry newEntry = new HashEntry(key, hash, - first, value); - tab[index] = newEntry; - count = c; // write-volatile - // 添加到双向链 - addBefore(newEntry, header); - // 判断是否达到最大值 - removeEldestEntry(); - } - return oldValue; - } finally { - unlock(); - } - } - - void rehash() { - HashEntry[] oldTable = table; - int oldCapacity = oldTable.length; - if (oldCapacity >= MAXIMUM_CAPACITY) - return; - - /* - * Reclassify nodes in each list to new Map. Because we are using - * power-of-two expansion, the elements from each bin must either - * stay at same index, or move with a power of two offset. We - * eliminate unnecessary node creation by catching cases where old - * nodes can be reused because their next fields won't change. - * Statistically, at the default threshold, only about one-sixth of - * them need cloning when a table doubles. The nodes they replace - * will be garbage collectable as soon as they are no longer - * referenced by any reader thread that may be in the midst of - * traversing table right now. - */ - - HashEntry[] newTable = HashEntry.newArray(oldCapacity << 1); - threshold = (int) (newTable.length * loadFactor); - int sizeMask = newTable.length - 1; - for (int i = 0; i < oldCapacity; i++) { - // We need to guarantee that any existing reads of old Map can - // proceed. So we cannot yet null out each bin. - HashEntry e = oldTable[i]; - - if (e != null) { - HashEntry next = e.next; - int idx = e.hash & sizeMask; - - // Single node on list - if (next == null) - newTable[idx] = e; - - else { - // Reuse trailing consecutive sequence at same slot - HashEntry lastRun = e; - int lastIdx = idx; - for (HashEntry last = next; last != null; last = last.next) { - int k = last.hash & sizeMask; - if (k != lastIdx) { - lastIdx = k; - lastRun = last; - } - } - newTable[lastIdx] = lastRun; - - // Clone all remaining nodes - for (HashEntry p = e; p != lastRun; p = p.next) { - int k = p.hash & sizeMask; - HashEntry n = newTable[k]; - HashEntry newEntry = new HashEntry( - p.key, p.hash, n, p.value); - - newEntry.linkNext = p.linkNext; - newEntry.linkPrev = p.linkPrev; - newTable[k] = newEntry; - } - } - } - } - table = newTable; - } - - /** - * Remove; match on key only if value null, else match both. - */ - V remove(Object key, int hash, Object value) { - lock(); - try { - int c = count - 1; - HashEntry[] tab = table; - int index = hash & (tab.length - 1); - HashEntry first = tab[index]; - HashEntry e = first; - while (e != null && (e.hash != hash || !key.equals(e.key))) - e = e.next; - - V oldValue = null; - if (e != null) { - V v = e.value; - if (value == null || value.equals(v)) { - oldValue = v; - // All entries following removed node can stay - // in list, but all preceding ones need to be - // cloned. - ++modCount; - HashEntry newFirst = e.next; - for (HashEntry p = first; p != e; p = p.next) { - newFirst = new HashEntry(p.key, p.hash, - newFirst, p.value); - newFirst.linkNext = p.linkNext; - newFirst.linkPrev = p.linkPrev; - } - tab[index] = newFirst; - count = c; // write-volatile - // 移除节点 - removeNode(e); - } - } - return oldValue; - } finally { - unlock(); - } - } - - /** - * 移除最旧元素 - */ - void removeEldestEntry() { - if (count > this.maxCapacity) { - HashEntry eldest = header.linkNext; - listener.eliminated(eldest.key, eldest.value); - remove(eldest.key, eldest.hash, null); - } - } - - void clear() { - if (count != 0) { - lock(); - try { - HashEntry[] tab = table; - for (int i = 0; i < tab.length; i++) - tab[i] = null; - ++modCount; - count = 0; // write-volatile - } finally { - unlock(); - } - } - } - } - - /** - * 使用指定参数,创建一个ConcurrentLRUHashMap - * - * @param segementCapacity - * Segement最大容量 - * @param loadFactor - * 加载因子 - * @param concurrencyLevel - * 并发级别 - * @param listener - * map事件回调 - */ - public ConcurrentLRUHashMap(int segementCapacity, float loadFactor, - int concurrencyLevel, LRUMapEventListener listener) { - if (!(loadFactor > 0) || segementCapacity < 0 || concurrencyLevel <= 0) - throw new IllegalArgumentException(); - - if (concurrencyLevel > MAX_SEGMENTS) - concurrencyLevel = MAX_SEGMENTS; - - // Find power-of-two sizes best matching arguments - int sshift = 0; - int ssize = 1; - while (ssize < concurrencyLevel) { - ++sshift; - ssize <<= 1; - } - segmentShift = 32 - sshift; - segmentMask = ssize - 1; - this.segments = Segment.newArray(ssize); - - for (int i = 0; i < this.segments.length; ++i) - this.segments[i] = new Segment(segementCapacity, loadFactor, listener); - } - - /** - * 使用指定参数,创建一个ConcurrentLRUHashMap - * - * @param segementCapacity - * Segement最大容量 - * @param loadFactor - * 加载因子 - */ - public ConcurrentLRUHashMap(int segementCapacity, float loadFactor) { - this(segementCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL, null); - } - - /** - * 使用指定参数,创建一个ConcurrentLRUHashMap - * - * @param segementCapacity - * Segement最大容量 - */ - public ConcurrentLRUHashMap(int segementCapacity) { - this(segementCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, null); - } - - /** - * 使用默认参数,创建一个ConcurrentLRUHashMap,存放元素最大数默认为16W, 加载因子为0.75,并发级别16 - */ - public ConcurrentLRUHashMap() { - this(DEFAULT_SEGEMENT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, null); - } - - /** - * Returns true if this map contains no key-value mappings. - * - * @return true if this map contains no key-value mappings - */ - public boolean isEmpty() { - final Segment[] segments = this.segments; - /* - * We keep track of per-segment modCounts to avoid ABA problems in which - * an element in one segment was added and in another removed during - * traversal, in which case the table was never actually empty at any - * point. Note the similar use of modCounts in the size() and - * containsValue() methods, which are the only other methods also - * susceptible to ABA problems. - */ - int[] mc = new int[segments.length]; - int mcsum = 0; - for (int i = 0; i < segments.length; ++i) { - if (segments[i].count != 0) - return false; - else - mcsum += mc[i] = segments[i].modCount; - } - // If mcsum happens to be zero, then we know we got a snapshot - // before any modifications at all were made. This is - // probably common enough to bother tracking. - if (mcsum != 0) { - for (int i = 0; i < segments.length; ++i) { - if (segments[i].count != 0 || mc[i] != segments[i].modCount) - return false; - } - } - return true; - } - - /** - * Returns the number of key-value mappings in this map. If the map contains - * more than Integer.MAX_VALUE elements, returns - * Integer.MAX_VALUE. - * - * @return the number of key-value mappings in this map - */ - public int size() { - final Segment[] segments = this.segments; - long sum = 0; - long check = 0; - int[] mc = new int[segments.length]; - // Try a few times to get accurate count. On failure due to - // continuous async changes in table, resort to locking. - for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { - check = 0; - sum = 0; - int mcsum = 0; - for (int i = 0; i < segments.length; ++i) { - sum += segments[i].count; - mcsum += mc[i] = segments[i].modCount; - } - if (mcsum != 0) { - for (int i = 0; i < segments.length; ++i) { - check += segments[i].count; - if (mc[i] != segments[i].modCount) { - check = -1; // force retry - break; - } - } - } - if (check == sum) - break; - } - if (check != sum) { // Resort to locking all segments - sum = 0; - for (int i = 0; i < segments.length; ++i) - segments[i].lock(); - for (int i = 0; i < segments.length; ++i) - sum += segments[i].count; - for (int i = 0; i < segments.length; ++i) - segments[i].unlock(); - } - if (sum > Integer.MAX_VALUE) - return Integer.MAX_VALUE; - else - return (int) sum; - - } - - /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - * - *

- * More formally, if this map contains a mapping from a key {@code k} to a - * value {@code v} such that {@code key.equals(k)}, then this method returns - * {@code v}; otherwise it returns {@code null}. (There can be at most one - * such mapping.) - * - * @throws NullPointerException - * if the specified key is null - */ - public V get(Object key) { - int hash = hash(key.hashCode()); - return segmentFor(hash).get(key, hash); - } - - /** - * Tests if the specified object is a key in this table. - * - * @param key - * possible key - * @return true if and only if the specified object is a key in - * this table, as determined by the equals method; - * false otherwise. - * @throws NullPointerException - * if the specified key is null - */ - public boolean containsKey(Object key) { - int hash = hash(key.hashCode()); - return segmentFor(hash).containsKey(key, hash); - } - - /** - * Returns true if this map maps one or more keys to the specified - * value. Note: This method requires a full internal traversal of the hash - * table, and so is much slower than method containsKey. - * - * @param value - * value whose presence in this map is to be tested - * @return true if this map maps one or more keys to the specified - * value - * @throws NullPointerException - * if the specified value is null - */ - public boolean containsValue(Object value) { - if (value == null) - throw new NullPointerException(); - - // See explanation of modCount use above - - final Segment[] segments = this.segments; - int[] mc = new int[segments.length]; - - // Try a few times without locking - for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { - int mcsum = 0; - for (int i = 0; i < segments.length; ++i) { - mcsum += mc[i] = segments[i].modCount; - if (segments[i].containsValue(value)) - return true; - } - boolean cleanSweep = true; - if (mcsum != 0) { - for (int i = 0; i < segments.length; ++i) { - if (mc[i] != segments[i].modCount) { - cleanSweep = false; - break; - } - } - } - if (cleanSweep) - return false; - } - // Resort to locking all segments - for (int i = 0; i < segments.length; ++i) - segments[i].lock(); - boolean found = false; - try { - for (int i = 0; i < segments.length; ++i) { - if (segments[i].containsValue(value)) { - found = true; - break; - } - } - } finally { - for (int i = 0; i < segments.length; ++i) - segments[i].unlock(); - } - return found; - } - - /** - * Legacy method testing if some key maps into the specified value in this - * table. This method is identical in functionality to - * {@link #containsValue}, and exists solely to ensure full compatibility - * with class {@link java.util.Hashtable}, which supported this method prior - * to introduction of the Java Collections framework. - * - * @param value - * a value to search for - * @return true if and only if some key maps to the value - * argument in this table as determined by the equals - * method; false otherwise - * @throws NullPointerException - * if the specified value is null - */ - public boolean contains(Object value) { - return containsValue(value); - } - - /** - * Put一个键值,加Map锁 - */ - public V put(K key, V value) { - if (value == null) - throw new NullPointerException(); - int hash = hash(key.hashCode()); - return segmentFor(hash).put(key, hash, value, false); - } - - /** - * Put一个键值,如果该Key不存在的话 - */ - public V putIfAbsent(K key, V value) { - if (value == null) - throw new NullPointerException(); - int hash = hash(key.hashCode()); - return segmentFor(hash).put(key, hash, value, true); - } - - /** - * Copies all of the mappings from the specified map to this one. These - * mappings replace any mappings that this map had for any of the keys - * currently in the specified map. - * - * @param m - * mappings to be stored in this map - */ - public void putAll(Map m) { - for (Map.Entry e : m.entrySet()) - put(e.getKey(), e.getValue()); - } - - /** - * Removes the key (and its corresponding value) from this map. This method - * does nothing if the key is not in the map. - * - * @param key - * the key that needs to be removed - * @return the previous value associated with key, or null - * if there was no mapping for key - * @throws NullPointerException - * if the specified key is null - */ - public V remove(Object key) { - int hash = hash(key.hashCode()); - return segmentFor(hash).remove(key, hash, null); - } - - /** - * {@inheritDoc} - * - * @throws NullPointerException - * if the specified key is null - */ - public boolean remove(Object key, Object value) { - int hash = hash(key.hashCode()); - if (value == null) - return false; - return segmentFor(hash).remove(key, hash, value) != null; - } - - /** - * {@inheritDoc} - * - * @throws NullPointerException - * if any of the arguments are null - */ - public boolean replace(K key, V oldValue, V newValue) { - if (oldValue == null || newValue == null) - throw new NullPointerException(); - int hash = hash(key.hashCode()); - return segmentFor(hash).replace(key, hash, oldValue, newValue); - } - - /** - * {@inheritDoc} - * - * @return the previous value associated with the specified key, or - * null if there was no mapping for the key - * @throws NullPointerException - * if the specified key or value is null - */ - public V replace(K key, V value) { - if (value == null) - throw new NullPointerException(); - int hash = hash(key.hashCode()); - return segmentFor(hash).replace(key, hash, value); - } - - /** - * Removes all of the mappings from this map. - */ - public void clear() { - for (int i = 0; i < segments.length; ++i) - segments[i].clear(); - } - - /** - * Returns a {@link Set} view of the keys contained in this map. The set is - * backed by the map, so changes to the map are reflected in the set, and - * vice-versa. The set supports element removal, which removes the - * corresponding mapping from this map, via the Iterator.remove, - * Set.remove, removeAll, retainAll, and - * clear operations. It does not support the add or - * addAll operations. - * - *

- * The view's iterator is a "weakly consistent" iterator that will - * never throw {@link ConcurrentModificationException}, and guarantees to - * traverse elements as they existed upon construction of the iterator, and - * may (but is not guaranteed to) reflect any modifications subsequent to - * construction. - */ - public Set keySet() { - Set ks = keySet; - return (ks != null) ? ks : (keySet = new KeySet()); - } - - /** - * Returns a {@link Collection} view of the values contained in this map. - * The collection is backed by the map, so changes to the map are reflected - * in the collection, and vice-versa. The collection supports element - * removal, which removes the corresponding mapping from this map, via the - * Iterator.remove, Collection.remove, removeAll, - * retainAll, and clear operations. It does not support - * the add or addAll operations. - * - *

- * The view's iterator is a "weakly consistent" iterator that will - * never throw {@link ConcurrentModificationException}, and guarantees to - * traverse elements as they existed upon construction of the iterator, and - * may (but is not guaranteed to) reflect any modifications subsequent to - * construction. - */ - public Collection values() { - Collection vs = values; - return (vs != null) ? vs : (values = new Values()); - } - - /** - * Returns a {@link Set} view of the mappings contained in this map. The set - * is backed by the map, so changes to the map are reflected in the set, and - * vice-versa. The set supports element removal, which removes the - * corresponding mapping from the map, via the Iterator.remove, - * Set.remove, removeAll, retainAll, and - * clear operations. It does not support the add or - * addAll operations. - * - *

- * The view's iterator is a "weakly consistent" iterator that will - * never throw {@link ConcurrentModificationException}, and guarantees to - * traverse elements as they existed upon construction of the iterator, and - * may (but is not guaranteed to) reflect any modifications subsequent to - * construction. - */ - public Set> entrySet() { - Set> es = entrySet; - return (es != null) ? es : (entrySet = new EntrySet()); - } - - /** - * Returns an enumeration of the keys in this table. - * - * @return an enumeration of the keys in this table - * @see #keySet() - */ - public Enumeration keys() { - return new KeyIterator(); - } - - /** - * Returns an enumeration of the values in this table. - * - * @return an enumeration of the values in this table - * @see #values() - */ - public Enumeration elements() { - return new ValueIterator(); - } - - /* ---------------- Iterator Support -------------- */ - - abstract class HashIterator { - int nextSegmentIndex; - int nextTableIndex; - HashEntry[] currentTable; - HashEntry nextEntry; - HashEntry lastReturned; - - HashIterator() { - nextSegmentIndex = segments.length - 1; - nextTableIndex = -1; - advance(); - } - - public boolean hasMoreElements() { - return hasNext(); - } - - final void advance() { - if (nextEntry != null && (nextEntry = nextEntry.next) != null) - return; - - while (nextTableIndex >= 0) { - if ((nextEntry = currentTable[nextTableIndex--]) != null) - return; - } - - while (nextSegmentIndex >= 0) { - Segment seg = segments[nextSegmentIndex--]; - if (seg.count != 0) { - currentTable = seg.table; - for (int j = currentTable.length - 1; j >= 0; --j) { - if ((nextEntry = currentTable[j]) != null) { - nextTableIndex = j - 1; - return; - } - } - } - } - } - - public boolean hasNext() { - return nextEntry != null; - } - - HashEntry nextEntry() { - if (nextEntry == null) - throw new NoSuchElementException(); - lastReturned = nextEntry; - advance(); - return lastReturned; - } - - public void remove() { - if (lastReturned == null) - throw new IllegalStateException(); - ConcurrentLRUHashMap.this.remove(lastReturned.key); - lastReturned = null; - } - } - - final class KeyIterator extends HashIterator implements Iterator, - Enumeration { - public K next() { - return super.nextEntry().key; - } - - public K nextElement() { - return super.nextEntry().key; - } - } - - final class ValueIterator extends HashIterator implements Iterator, - Enumeration { - public V next() { - return super.nextEntry().value; - } - - public V nextElement() { - return super.nextEntry().value; - } - } - - /** - * Custom Entry class used by EntryIterator.next(), that relays setValue - * changes to the underlying map. - */ - final class WriteThroughEntry extends AbstractMap.SimpleEntry { - /** - * - */ - private static final long serialVersionUID = -2545938966452012894L; - - WriteThroughEntry(K k, V v) { - super(k, v); - } - - /** - * Set our entry's value and write through to the map. The value to - * return is somewhat arbitrary here. Since a WriteThroughEntry does not - * necessarily track asynchronous changes, the most recent "previous" - * value could be different from what we return (or could even have been - * removed in which case the put will re-establish). We do not and - * cannot guarantee more. - */ - public V setValue(V value) { - if (value == null) - throw new NullPointerException(); - V v = super.setValue(value); - ConcurrentLRUHashMap.this.put(getKey(), value); - return v; - } - } - - final class EntryIterator extends HashIterator implements - Iterator> { - public Map.Entry next() { - HashEntry e = super.nextEntry(); - return new WriteThroughEntry(e.key, e.value); - } - } - - final class KeySet extends AbstractSet { - public Iterator iterator() { - return new KeyIterator(); - } - - public int size() { - return ConcurrentLRUHashMap.this.size(); - } - - public boolean contains(Object o) { - return ConcurrentLRUHashMap.this.containsKey(o); - } - - public boolean remove(Object o) { - return ConcurrentLRUHashMap.this.remove(o) != null; - } - - public void clear() { - ConcurrentLRUHashMap.this.clear(); - } - } - - final class Values extends AbstractCollection { - public Iterator iterator() { - return new ValueIterator(); - } - - public int size() { - return ConcurrentLRUHashMap.this.size(); - } - - public boolean contains(Object o) { - return ConcurrentLRUHashMap.this.containsValue(o); - } - - public void clear() { - ConcurrentLRUHashMap.this.clear(); - } - } - - final class EntrySet extends AbstractSet> { - public Iterator> iterator() { - return new EntryIterator(); - } - - public boolean contains(Object o) { - if (!(o instanceof Map.Entry)) - return false; - Map.Entry e = (Map.Entry) o; - V v = ConcurrentLRUHashMap.this.get(e.getKey()); - return v != null && v.equals(e.getValue()); - } - - public boolean remove(Object o) { - if (!(o instanceof Map.Entry)) - return false; - Map.Entry e = (Map.Entry) o; - return ConcurrentLRUHashMap.this.remove(e.getKey(), e.getValue()); - } - - public int size() { - return ConcurrentLRUHashMap.this.size(); - } - - public void clear() { - ConcurrentLRUHashMap.this.clear(); - } - } - - /* ---------------- Serialization Support -------------- */ - - /** - * Save the state of the ConcurrentHashMap instance to a stream - * (i.e., serialize it). - * - * @param s - * the stream - * @serialData the key (Object) and value (Object) for each key-value - * mapping, followed by a null pair. The key-value mappings are - * emitted in no particular order. - */ - private void writeObject(java.io.ObjectOutputStream s) throws IOException { - s.defaultWriteObject(); - - for (int k = 0; k < segments.length; ++k) { - Segment seg = segments[k]; - seg.lock(); - try { - HashEntry[] tab = seg.table; - for (int i = 0; i < tab.length; ++i) { - for (HashEntry e = tab[i]; e != null; e = e.next) { - s.writeObject(e.key); - s.writeObject(e.value); - } - } - } finally { - seg.unlock(); - } - } - s.writeObject(null); - s.writeObject(null); - } - - /** - * Reconstitute the ConcurrentHashMap instance from a stream (i.e., - * deserialize it). - * - * @param s - * the stream - */ - @SuppressWarnings("unchecked") - private void readObject(java.io.ObjectInputStream s) throws IOException, - ClassNotFoundException { - s.defaultReadObject(); - - // Initialize each segment to be minimally sized, and let grow. - for (int i = 0; i < segments.length; ++i) { - segments[i].setTable(new HashEntry[1]); - } - - // Read the keys and values, and put the mappings in the table - for (;;) { - K key = (K) s.readObject(); - V value = (V) s.readObject(); - if (key == null) - break; - put(key, value); - } - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/HashedArrayTree.java b/firefly-common/src/main/java/com/firefly/utils/collection/HashedArrayTree.java deleted file mode 100644 index a448c5f22..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/HashedArrayTree.java +++ /dev/null @@ -1,392 +0,0 @@ -package com.firefly.utils.collection; - -/*************************************************************************** - * File: HashedArrayTree.java - * Author: Keith Schwarz (htiek@cs.stanford.edu) - * - * An implementation of the List abstraction backed by a hashed array tree - * (HAT), a data structure supporting amortized O(1) lookup, append, and - * last-element removal. In this sense it is akin to a standard dynamic - * array implementation. However, a hashed array tree also has the advantage - * that its memory overhead is only O(sqrt(n)) rather than the typical O(n) - * found in dynamic arrays and their variants. - * - * Internally, the hashed array tree is implemented as an array of pointers - * that optionally point to an array of elements. The topmost array and each - * element always have the same size, which is always a power of two. In - * this sense, the hashed array tree is essentially a two-dimensional array - * of elements. However, the advantage of the hashed array tree is that the - * topmost array pointers are all initially null and only filled in when space - * is needed. This means that the maximum overhead of the structure is the - * size of the topmost array, plus the number of unused elements in the current - * block. Here is one sample HAT: - * - * [ ] [ ] [ ] [ ] - * | | | - * v v v - * [0] [4] [8] - * [1] [5] [9] - * [2] [6] [ ] - * [3] [7] [ ] - * - * Here, the topmost array of pointers has three pointers in use, each of which - * point to an array of the corresponding number of elements. - * - * Whenever an element is added to a hashed array tree, one of three cases - * must hold: - * - * 1. There is extra space at the end of the final subarray (for example, in - * the top picture). In that case, the element is added to that position. - * 2. The final subarray is full, but space for another subarray exists in the - * topmost array. In that case, a new array is allocated and the element - * is added to that array. - * 3. The final subarray is full and no new arrays remain open. In that case, - * since the topmost array has size 2^n and each array has size 2^n, there - * must be a total of 2^(2n) elements in the hashed array tree. We - * next double the size of the topmost array to 2^(n + 1), then allocate - * 2^(n - 1) subarrays of size 2^(n + 1) elements for a total of 2^(2n) - * elements' worth of space. The elements from the old HAT are then copied - * over, a new array is allocated for the new element, and it is added as - * the first element of that array. - * - * Let's now talk about the performance and memory usage of this structure. - * First, we note that we can perform lookups in O(1), assuming that the - * machine is transdichotomous (meaning that a single machine word can hold - * the size of any array). This can be done by breaking the input index in - * half, then using the first half to choose which array to look into (the - * "hashed" part of HAT) and the second half to choose which index to select. - * This trick is similar to the trick used to represent a two-dimensional - * array using a linear structure. - * - * Next, let's think about how much time it takes to do an append operation. - * Each append when space remains takes O(1) to look up the proper position - * in the hashed array tree for writing, but appends can be much more expensive - * when the HAT needs to be resized. Fortunately, this does not happen very - * often. Whenever the HAT doubles in size, its capacity grows from 2^(2n) to - * 2^(2n + 2), meaning that four times as many elements must be inserted before - * the next copy operation. If we define a potential function as twice the - * number of filled-in elements in the HAT above half capacity (i.e. the - * number of elements in the arrays in the second half), then we can prove an - * amortized O(1) time for append. Consider any series of appends. If the - * append does not expand the HAT, then it takes O(1) time and increases the - * potential by 1/2. If the append does expand the HAT, and the HAT's topmost - * array has size 2^n, then there must be 2^(n-1) elements in the latter half - * of the HAT, so the potential is 2^(2n). The time required to move each - * of the 2^(2n) elements is 2^(2n), and in the new HAT there are no elements - * in the latter half of the array. Consequently, the new potential is zero. - * The actual time required to perform the append is thus 2^2n + O(1), and - * the decrease in potential is -2^2n, so the amortized cost is O(1) as - * expected. - * - * Last, let's talk about the cost to do a remove. This is similar to the - * append case - we delete the last element of the last array, removing the - * array from the topmost array if it becomes empty. We also compact the - * HAT if it becomes too sparse by shrinking from a HAT of size 2^(2n + 2) to - * a HAT of size 2^(2n) if the HAT becomes one-eighth full. A similar - * potential method can be used to show that this operation runs in amortized - * O(1). - * - * Finally, let's consider the memory overhead of the HAT. For any HAT of - * topmost array size 8 or more, since the HAT is always at least one-eighth - * full, there must be at least one full array. This array has size equal to - * the size of the topmost array (call this k), and so if every array were to - * be filled in to capacity, there would be a total of k arrays of size k, - * for a total of k^2 elements. Of this capacity, we know that at least an - * eighth of them are filled in, so k^2 must be at most 8n, and so - * k = O(sqrt(n)). To finish the analysis, the overhead of the structure is - * at most the overhead of this top-level array, plus potentially k - 1 - * unused elements in some array. This is a total of O(k) = O(sqrt(n)) - * overhead, which is what we originally desired. - */ -import java.util.*; // For AbstractList - -@SuppressWarnings("unchecked") -public final class HashedArrayTree extends AbstractList { - /* To simplify the implementation, we enforce that the size of the topmost - * array never drops below 2. This prevents weirdness when we try to - * allocate 2^(n-1) arrays during a doubling and find that n = 0. - */ - private static final int kMinArraySize = 2; - - /* The topmost array of elements; initially of size two. */ - private T[][] mArrays = (T[][]) new Object[kMinArraySize][]; - - /* Number of elements, initially zero since the HAT is created empty. */ - private int mSize = 0; - - /* A constant containing lg2 of the topmost array size. This enables some - * cute bit-twiddling tricks to improve efficiency. - */ - private int mLgSize = 1; - - /** - * Returns the number of elements in the HashedArrayTree. - * - * @return The number of elements in the HashedArrayTree. - */ - @Override - public int size() { - return mSize; - } - - /** - * Adds a new element to the HashedArrayTree. - * - * @param elem The element to add. - * @return true - */ - @Override - public boolean add(T elem) { - /* First, check if we're completely out of space. If so, do a resize - * to ensure we do indeed have room. - */ - if (size() == mArrays.length * mArrays.length) - grow(); - - /* Compute the (arr, index) pair for the next position. The next - * position is at the location indicated by size(), but we know that - * space exists from the previous call. - */ - final int offset = computeOffset(size()); - final int index = computeIndex(size()); - - /* Check if an array exists here. If not, make one up. */ - if (mArrays[offset] == null) - mArrays[offset] = (T[]) new Object[mArrays.length]; - - /* Write the element to its location. */ - mArrays[offset][index] = elem; - - /* Update the element count. */ - ++mSize; - - /* Per the Collections contract, return true to signal a successful - * add. - */ - return true; - } - - /** - * Sets the element at the specified position to the indicated value. - * If the index is out of bounds, throws an IndexOutOfBounds exception. - * - * @param index The index at which to set the value. - * @param elem The element to store at that position. - * @return The value initially at that location. - * @throws IndexOutOfBoundsException If index is invalid. - */ - @Override - public T set(int index, T elem) { - /* Find out where to look. */ - final int offset = computeOffset(index); - final int arrIndex = computeIndex(index); - - /* Cache the value there and write the new one. */ - T result = mArrays[offset][arrIndex]; - mArrays[offset][arrIndex] = elem; - - /* Hand back the old value. */ - return result; - } - - /** - * Returns the value of the element at the specified position. - * - * @param index The index at which to query. - * @return The value of the element at that position. - * @throws IndexOutOfBoundsException If the index is invalid. - */ - @Override - public T get(int index) { - /* Check that this is a valid index. */ - if (index < 0 || index >= size()) - throw new IndexOutOfBoundsException("Index " + index + ", size " + size()); - - /* Look up the element. */ - return mArrays[computeOffset(index)][computeIndex(index)]; - } - - /** - * Adds the specified element at the position just before the specified - * index. - * - * @param index The index just before which to insert. - * @param elem The value to insert - * @throws IndexOutOfBoundsException if the index is invalid. - */ - @Override - public void add(int index, T elem) { - /* Confirm the validity of the index. */ - if (index < 0 || index >= size()) - throw new IndexOutOfBoundsException("Index " + index + ", size " + size()); - - /* Add a dummy element to ensure that everything resizes correctly. - * There's no reason to repeat the logic. - */ - add(null); - - /* Next, we need to shuffle down every element that appears after - * the inserted element. We'll do this using our own public interface. - */ - for (int i = size(); i > index; ++i) - set(i, get(i - 1)); - - /* Finally, write the element. */ - set(index, elem); - } - - /** - * Removes the element at the specified position from the HashedArrayTree. - * - * @param index The index of the element to remove. - * @return The value of the element at that position. - * @throws IndexOutOfBoundsException If the index is invalid. - */ - @Override - public T remove(int index) { - /* Cache the value at the indicated position; this also does the bounds - * check. - */ - T result = get(index); - - /* Use a naive shuffle-down algorithm to reposition elements after - * the removed one. - */ - for (int i = index + 1; i < size(); ++i) - set(i - 1, get(i)); - - /* Clobber the last element to play nicely with the garbage collector. */ - set(size() - 1, null); - - /* Decrement our size. */ - --mSize; - - /* If we are now at 1/8 total capacity, shrink the structure. */ - if (size() * 8 <= mArrays.length * mArrays.length) - shrink(); - /* Otherwise, if the size is now an even multiple of the array size, - * we can drop the very last array. This is the array whose offset - * is one after the end of the elements. - */ - else if (size() % mArrays.length == 0) - mArrays[computeOffset(size())] = null; - - return result; - } - - /** - * Given an index, returns the offset into the master array at which the - * element with that index can be found. - * - * @return The index into the topmost array where the given element can - * be found. - */ - private int computeOffset(int index) { - /* This can be computed by dividing the index by the index by the - * topmost array. However, if we want to be very clever, we can do - * this efficiently by bit-shifting downard by the lg2 of the size - * of the topmost array. - */ - return index >> mLgSize; - } - - /** - * Given an index, returns the offset into the appropriate subarray in - * which the element with that index can be found. - * - * @return The index into the subarray array where the given element can - * be found. - */ - private int computeIndex(int index) { - /* This can be computed by modding the index by the index by the - * topmost array. But we can do this more efficiently with a different - * tactic. Since the array size is a perfect power of two, it must - * look like this: - * - * 00..010..0 - * - * Subtracting one yields - * - * 00..001..1 - * - * ANDing this with the index produces the value we're looking for. - */ - return index & (mArrays.length - 1); - } - - /** - * Grows the internal representation by doubling the size of the topmost - * array and copying the appropriate number of elements over. - */ - private void grow() { - /* Double the size of the topmost array. */ - T[][] newArrays = (T[][]) new Object[mArrays.length * 2][]; - - /* The new arrays each have size 2^(n + 1). We need 2^(n - 1) of them - * to hold the old elements. Allocate those here and copy everything - * over. - */ - for (int i = 0; i < mArrays.length; i += 2) { - /* Allocate the array. */ - newArrays[i / 2] = (T[]) new Object[newArrays.length]; - - /* Use System.arraycopy to move everything over. */ - System.arraycopy(mArrays[i], 0, newArrays[i / 2], 0, mArrays.length); - System.arraycopy(mArrays[i + 1], 0, newArrays[i / 2], mArrays.length, mArrays.length); - - /* Null out the old arrays to be nice to the GC during this - * potentially stressful time. - */ - mArrays[i] = mArrays[i + 1] = null; - } - - /* Switch out this new array for the old. */ - mArrays = newArrays; - - /* Bump up lg2 of the size. */ - ++mLgSize; - } - - /** - * Decreases the size of the HAT by shrinking into a better fit. - */ - private void shrink() { - /* If the size of the topmost array is at its minimum, don't do - * anything. This doesn't change the asymptotic memory usage because - * we only do this for small arrays. - */ - if (mArrays.length == kMinArraySize) return; - - /* Otherwise, we currently have 2^(2n) / 8 = 2^(2n - 3) elements. - * We're about to shrink into a grid of 2^(2n - 2) elements, and so - * we'll fill in half of the elements. - */ - T[][] newArrays = (T[][]) new Object[mArrays.length / 2][]; - - /* Copy everything over. We'll need half as many arrays as before. */ - for (int i = 0; i < newArrays.length / 2; ++i) { - /* Create the arrays. */ - newArrays[i] = (T[]) new Object[newArrays.length]; - - /* Move everything into it. If this is an odd array, it comes - * from the upper half of the old array; otherwise it comes from - * the lower half. - */ - System.arraycopy(mArrays[i / 2], (i % 2 == 0)? 0 : newArrays.length, - newArrays[i], 0, newArrays.length); - - /* Play nice with the GC. If this is an odd-numbered array, we - * just copied over everything we needed and can clear out the - * old array. - */ - if (i % 2 == 1) - mArrays[i / 2] = null; - } - - /* Copy the arrays over. */ - mArrays = newArrays; - - /* Drop the lg2 of the size. */ - --mLgSize; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/IdentityHashMap.java b/firefly-common/src/main/java/com/firefly/utils/collection/IdentityHashMap.java deleted file mode 100644 index 0b9d5dd65..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/IdentityHashMap.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.firefly.utils.collection; - -@SuppressWarnings("unchecked") -public class IdentityHashMap { - - public static final int DEFAULT_TABLE_SIZE = 1024; - - private final Entry[] buckets; - private final int indexMask; - - public IdentityHashMap() { - this(DEFAULT_TABLE_SIZE); - } - - public IdentityHashMap(int tableSize) { - this.indexMask = tableSize - 1; - this.buckets = new Entry[tableSize]; - } - - public final V get(K key) { - final int hash = System.identityHashCode(key); - final int bucket = hash & indexMask; - - for (Entry entry = buckets[bucket]; entry != null; entry = entry.next) { - if (key == entry.key) { - return (V) entry.value; - } - } - - return null; - } - - public boolean put(K key, V value) { - final int hash = System.identityHashCode(key); - final int bucket = hash & indexMask; - - for (Entry entry = buckets[bucket]; entry != null; entry = entry.next) { - if (key == entry.key) { - return true; - } - } - - Entry entry = new Entry(key, value, hash, buckets[bucket]); - buckets[bucket] = entry; // 并发是处理时会可能导致缓存丢失,但不影响正确性 - - return false; - } - - public int size() { - int size = 0; - for (int i = 0; i < buckets.length; ++i) { - for (Entry entry = buckets[i]; entry != null; entry = entry.next) { - size++; - } - } - return size; - } - - protected static final class Entry { - - public final int hashCode; - public final K key; - public final V value; - - public final Entry next; - - public Entry(K key, V value, int hash, Entry next) { - this.key = key; - this.value = value; - this.next = next; - this.hashCode = hash; - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/LRUMapEventListener.java b/firefly-common/src/main/java/com/firefly/utils/collection/LRUMapEventListener.java deleted file mode 100644 index cbc249e89..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/LRUMapEventListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.utils.collection; - -public interface LRUMapEventListener { - void eliminated(Object key, Object value); - - Object getNull(Object key); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/LinkedTransferQueue.java b/firefly-common/src/main/java/com/firefly/utils/collection/LinkedTransferQueue.java deleted file mode 100644 index 4f1578acb..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/LinkedTransferQueue.java +++ /dev/null @@ -1,1352 +0,0 @@ -/* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -package com.firefly.utils.collection; - -import java.util.AbstractQueue; -import java.util.Collection; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Queue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; - -import com.firefly.utils.ThreadLocalRandom; - -/** - * An unbounded {@link TransferQueue} based on linked nodes. - * This queue orders elements FIFO (first-in-first-out) with respect - * to any given producer. The head of the queue is that - * element that has been on the queue the longest time for some - * producer. The tail of the queue is that element that has - * been on the queue the shortest time for some producer. - * - *

Beware that, unlike in most collections, the {@code size} method - * is NOT a constant-time operation. Because of the - * asynchronous nature of these queues, determining the current number - * of elements requires a traversal of the elements, and so may report - * inaccurate results if this collection is modified during traversal. - * Additionally, the bulk operations {@code addAll}, - * {@code removeAll}, {@code retainAll}, {@code containsAll}, - * {@code equals}, and {@code toArray} are not guaranteed - * to be performed atomically. For example, an iterator operating - * concurrently with an {@code addAll} operation might view only some - * of the added elements. - * - *

This class and its iterator implement all of the - * optional methods of the {@link Collection} and {@link - * Iterator} interfaces. - * - *

Memory consistency effects: As with other concurrent - * collections, actions in a thread prior to placing an object into a - * {@code LinkedTransferQueue} - * happen-before - * actions subsequent to the access or removal of that element from - * the {@code LinkedTransferQueue} in another thread. - * - *

This class is a member of the - * - * Java Collections Framework. - * - * @since 1.7 - * @author Doug Lea - * @param the type of elements held in this collection - */ -public class LinkedTransferQueue extends AbstractQueue - implements TransferQueue, java.io.Serializable { - private static final long serialVersionUID = -3223113410248163686L; - - /* - * *** Overview of Dual Queues with Slack *** - * - * Dual Queues, introduced by Scherer and Scott - * (http://www.cs.rice.edu/~wns1/papers/2004-DISC-DDS.pdf) are - * (linked) queues in which nodes may represent either data or - * requests. When a thread tries to enqueue a data node, but - * encounters a request node, it instead "matches" and removes it; - * and vice versa for enqueuing requests. Blocking Dual Queues - * arrange that threads enqueuing unmatched requests block until - * other threads provide the match. Dual Synchronous Queues (see - * Scherer, Lea, & Scott - * http://www.cs.rochester.edu/u/scott/papers/2009_Scherer_CACM_SSQ.pdf) - * additionally arrange that threads enqueuing unmatched data also - * block. Dual Transfer Queues support all of these modes, as - * dictated by callers. - * - * A FIFO dual queue may be implemented using a variation of the - * Michael & Scott (M&S) lock-free queue algorithm - * (http://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf). - * It maintains two pointer fields, "head", pointing to a - * (matched) node that in turn points to the first actual - * (unmatched) queue node (or null if empty); and "tail" that - * points to the last node on the queue (or again null if - * empty). For example, here is a possible queue with four data - * elements: - * - * head tail - * | | - * v v - * M -> U -> U -> U -> U - * - * The M&S queue algorithm is known to be prone to scalability and - * overhead limitations when maintaining (via CAS) these head and - * tail pointers. This has led to the development of - * contention-reducing variants such as elimination arrays (see - * Moir et al http://portal.acm.org/citation.cfm?id=1074013) and - * optimistic back pointers (see Ladan-Mozes & Shavit - * http://people.csail.mit.edu/edya/publications/OptimisticFIFOQueue-journal.pdf). - * However, the nature of dual queues enables a simpler tactic for - * improving M&S-style implementations when dual-ness is needed. - * - * In a dual queue, each node must atomically maintain its match - * status. While there are other possible variants, we implement - * this here as: for a data-mode node, matching entails CASing an - * "item" field from a non-null data value to null upon match, and - * vice-versa for request nodes, CASing from null to a data - * value. (Note that the linearization properties of this style of - * queue are easy to verify -- elements are made available by - * linking, and unavailable by matching.) Compared to plain M&S - * queues, this property of dual queues requires one additional - * successful atomic operation per enq/deq pair. But it also - * enables lower cost variants of queue maintenance mechanics. (A - * variation of this idea applies even for non-dual queues that - * support deletion of interior elements, such as - * j.u.c.ConcurrentLinkedQueue.) - * - * Once a node is matched, its match status can never again - * change. We may thus arrange that the linked list of them - * contain a prefix of zero or more matched nodes, followed by a - * suffix of zero or more unmatched nodes. (Note that we allow - * both the prefix and suffix to be zero length, which in turn - * means that we do not use a dummy header.) If we were not - * concerned with either time or space efficiency, we could - * correctly perform enqueue and dequeue operations by traversing - * from a pointer to the initial node; CASing the item of the - * first unmatched node on match and CASing the next field of the - * trailing node on appends. (Plus some special-casing when - * initially empty). While this would be a terrible idea in - * itself, it does have the benefit of not requiring ANY atomic - * updates on head/tail fields. - * - * We introduce here an approach that lies between the extremes of - * never versus always updating queue (head and tail) pointers. - * This offers a tradeoff between sometimes requiring extra - * traversal steps to locate the first and/or last unmatched - * nodes, versus the reduced overhead and contention of fewer - * updates to queue pointers. For example, a possible snapshot of - * a queue is: - * - * head tail - * | | - * v v - * M -> M -> U -> U -> U -> U - * - * The best value for this "slack" (the targeted maximum distance - * between the value of "head" and the first unmatched node, and - * similarly for "tail") is an empirical matter. We have found - * that using very small constants in the range of 1-3 work best - * over a range of platforms. Larger values introduce increasing - * costs of cache misses and risks of long traversal chains, while - * smaller values increase CAS contention and overhead. - * - * Dual queues with slack differ from plain M&S dual queues by - * virtue of only sometimes updating head or tail pointers when - * matching, appending, or even traversing nodes; in order to - * maintain a targeted slack. The idea of "sometimes" may be - * operationalized in several ways. The simplest is to use a - * per-operation counter incremented on each traversal step, and - * to try (via CAS) to update the associated queue pointer - * whenever the count exceeds a threshold. Another, that requires - * more overhead, is to use random number generators to update - * with a given probability per traversal step. - * - * In any strategy along these lines, because CASes updating - * fields may fail, the actual slack may exceed targeted - * slack. However, they may be retried at any time to maintain - * targets. Even when using very small slack values, this - * approach works well for dual queues because it allows all - * operations up to the point of matching or appending an item - * (hence potentially allowing progress by another thread) to be - * read-only, thus not introducing any further contention. As - * described below, we implement this by performing slack - * maintenance retries only after these points. - * - * As an accompaniment to such techniques, traversal overhead can - * be further reduced without increasing contention of head - * pointer updates: Threads may sometimes shortcut the "next" link - * path from the current "head" node to be closer to the currently - * known first unmatched node, and similarly for tail. Again, this - * may be triggered with using thresholds or randomization. - * - * These ideas must be further extended to avoid unbounded amounts - * of costly-to-reclaim garbage caused by the sequential "next" - * links of nodes starting at old forgotten head nodes: As first - * described in detail by Boehm - * (http://portal.acm.org/citation.cfm?doid=503272.503282) if a GC - * delays noticing that any arbitrarily old node has become - * garbage, all newer dead nodes will also be unreclaimed. - * (Similar issues arise in non-GC environments.) To cope with - * this in our implementation, upon CASing to advance the head - * pointer, we set the "next" link of the previous head to point - * only to itself; thus limiting the length of connected dead lists. - * (We also take similar care to wipe out possibly garbage - * retaining values held in other Node fields.) However, doing so - * adds some further complexity to traversal: If any "next" - * pointer links to itself, it indicates that the current thread - * has lagged behind a head-update, and so the traversal must - * continue from the "head". Traversals trying to find the - * current tail starting from "tail" may also encounter - * self-links, in which case they also continue at "head". - * - * It is tempting in slack-based scheme to not even use CAS for - * updates (similarly to Ladan-Mozes & Shavit). However, this - * cannot be done for head updates under the above link-forgetting - * mechanics because an update may leave head at a detached node. - * And while direct writes are possible for tail updates, they - * increase the risk of long retraversals, and hence long garbage - * chains, which can be much more costly than is worthwhile - * considering that the cost difference of performing a CAS vs - * write is smaller when they are not triggered on each operation - * (especially considering that writes and CASes equally require - * additional GC bookkeeping ("write barriers") that are sometimes - * more costly than the writes themselves because of contention). - * - * *** Overview of implementation *** - * - * We use a threshold-based approach to updates, with a slack - * threshold of two -- that is, we update head/tail when the - * current pointer appears to be two or more steps away from the - * first/last node. The slack value is hard-wired: a path greater - * than one is naturally implemented by checking equality of - * traversal pointers except when the list has only one element, - * in which case we keep slack threshold at one. Avoiding tracking - * explicit counts across method calls slightly simplifies an - * already-messy implementation. Using randomization would - * probably work better if there were a low-quality dirt-cheap - * per-thread one available, but even ThreadLocalRandom is too - * heavy for these purposes. - * - * With such a small slack threshold value, it is not worthwhile - * to augment this with path short-circuiting (i.e., unsplicing - * interior nodes) except in the case of cancellation/removal (see - * below). - * - * We allow both the head and tail fields to be null before any - * nodes are enqueued; initializing upon first append. This - * simplifies some other logic, as well as providing more - * efficient explicit control paths instead of letting JVMs insert - * implicit NullPointerExceptions when they are null. While not - * currently fully implemented, we also leave open the possibility - * of re-nulling these fields when empty (which is complicated to - * arrange, for little benefit.) - * - * All enqueue/dequeue operations are handled by the single method - * "xfer" with parameters indicating whether to act as some form - * of offer, put, poll, take, or transfer (each possibly with - * timeout). The relative complexity of using one monolithic - * method outweighs the code bulk and maintenance problems of - * using separate methods for each case. - * - * Operation consists of up to three phases. The first is - * implemented within method xfer, the second in tryAppend, and - * the third in method awaitMatch. - * - * 1. Try to match an existing node - * - * Starting at head, skip already-matched nodes until finding - * an unmatched node of opposite mode, if one exists, in which - * case matching it and returning, also if necessary updating - * head to one past the matched node (or the node itself if the - * list has no other unmatched nodes). If the CAS misses, then - * a loop retries advancing head by two steps until either - * success or the slack is at most two. By requiring that each - * attempt advances head by two (if applicable), we ensure that - * the slack does not grow without bound. Traversals also check - * if the initial head is now off-list, in which case they - * start at the new head. - * - * If no candidates are found and the call was untimed - * poll/offer, (argument "how" is NOW) return. - * - * 2. Try to append a new node (method tryAppend) - * - * Starting at current tail pointer, find the actual last node - * and try to append a new node (or if head was null, establish - * the first node). Nodes can be appended only if their - * predecessors are either already matched or are of the same - * mode. If we detect otherwise, then a new node with opposite - * mode must have been appended during traversal, so we must - * restart at phase 1. The traversal and update steps are - * otherwise similar to phase 1: Retrying upon CAS misses and - * checking for staleness. In particular, if a self-link is - * encountered, then we can safely jump to a node on the list - * by continuing the traversal at current head. - * - * On successful append, if the call was ASYNC, return. - * - * 3. Await match or cancellation (method awaitMatch) - * - * Wait for another thread to match node; instead cancelling if - * the current thread was interrupted or the wait timed out. On - * multiprocessors, we use front-of-queue spinning: If a node - * appears to be the first unmatched node in the queue, it - * spins a bit before blocking. In either case, before blocking - * it tries to unsplice any nodes between the current "head" - * and the first unmatched node. - * - * Front-of-queue spinning vastly improves performance of - * heavily contended queues. And so long as it is relatively - * brief and "quiet", spinning does not much impact performance - * of less-contended queues. During spins threads check their - * interrupt status and generate a thread-local random number - * to decide to occasionally perform a Thread.yield. While - * yield has underdefined specs, we assume that it might help, - * and will not hurt, in limiting impact of spinning on busy - * systems. We also use smaller (1/2) spins for nodes that are - * not known to be front but whose predecessors have not - * blocked -- these "chained" spins avoid artifacts of - * front-of-queue rules which otherwise lead to alternating - * nodes spinning vs blocking. Further, front threads that - * represent phase changes (from data to request node or vice - * versa) compared to their predecessors receive additional - * chained spins, reflecting longer paths typically required to - * unblock threads during phase changes. - * - * - * ** Unlinking removed interior nodes ** - * - * In addition to minimizing garbage retention via self-linking - * described above, we also unlink removed interior nodes. These - * may arise due to timed out or interrupted waits, or calls to - * remove(x) or Iterator.remove. Normally, given a node that was - * at one time known to be the predecessor of some node s that is - * to be removed, we can unsplice s by CASing the next field of - * its predecessor if it still points to s (otherwise s must - * already have been removed or is now offlist). But there are two - * situations in which we cannot guarantee to make node s - * unreachable in this way: (1) If s is the trailing node of list - * (i.e., with null next), then it is pinned as the target node - * for appends, so can only be removed later after other nodes are - * appended. (2) We cannot necessarily unlink s given a - * predecessor node that is matched (including the case of being - * cancelled): the predecessor may already be unspliced, in which - * case some previous reachable node may still point to s. - * (For further explanation see Herlihy & Shavit "The Art of - * Multiprocessor Programming" chapter 9). Although, in both - * cases, we can rule out the need for further action if either s - * or its predecessor are (or can be made to be) at, or fall off - * from, the head of list. - * - * Without taking these into account, it would be possible for an - * unbounded number of supposedly removed nodes to remain - * reachable. Situations leading to such buildup are uncommon but - * can occur in practice; for example when a series of short timed - * calls to poll repeatedly time out but never otherwise fall off - * the list because of an untimed call to take at the front of the - * queue. - * - * When these cases arise, rather than always retraversing the - * entire list to find an actual predecessor to unlink (which - * won't help for case (1) anyway), we record a conservative - * estimate of possible unsplice failures (in "sweepVotes"). - * We trigger a full sweep when the estimate exceeds a threshold - * ("SWEEP_THRESHOLD") indicating the maximum number of estimated - * removal failures to tolerate before sweeping through, unlinking - * cancelled nodes that were not unlinked upon initial removal. - * We perform sweeps by the thread hitting threshold (rather than - * background threads or by spreading work to other threads) - * because in the main contexts in which removal occurs, the - * caller is already timed-out, cancelled, or performing a - * potentially O(n) operation (e.g. remove(x)), none of which are - * time-critical enough to warrant the overhead that alternatives - * would impose on other threads. - * - * Because the sweepVotes estimate is conservative, and because - * nodes become unlinked "naturally" as they fall off the head of - * the queue, and because we allow votes to accumulate even while - * sweeps are in progress, there are typically significantly fewer - * such nodes than estimated. Choice of a threshold value - * balances the likelihood of wasted effort and contention, versus - * providing a worst-case bound on retention of interior nodes in - * quiescent queues. The value defined below was chosen - * empirically to balance these under various timeout scenarios. - * - * Note that we cannot self-link unlinked interior nodes during - * sweeps. However, the associated garbage chains terminate when - * some successor ultimately falls off the head of the list and is - * self-linked. - */ - - /** True if on multiprocessor */ - private static final boolean MP = - Runtime.getRuntime().availableProcessors() > 1; - - /** - * The number of times to spin (with randomly interspersed calls - * to Thread.yield) on multiprocessor before blocking when a node - * is apparently the first waiter in the queue. See above for - * explanation. Must be a power of two. The value is empirically - * derived -- it works pretty well across a variety of processors, - * numbers of CPUs, and OSes. - */ - private static final int FRONT_SPINS = 1 << 7; - - /** - * The number of times to spin before blocking when a node is - * preceded by another node that is apparently spinning. Also - * serves as an increment to FRONT_SPINS on phase changes, and as - * base average frequency for yielding during spins. Must be a - * power of two. - */ - private static final int CHAINED_SPINS = FRONT_SPINS >>> 1; - - /** - * The maximum number of estimated removal failures (sweepVotes) - * to tolerate before sweeping through the queue unlinking - * cancelled nodes that were not unlinked upon initial - * removal. See above for explanation. The value must be at least - * two to avoid useless sweeps when removing trailing nodes. - */ - static final int SWEEP_THRESHOLD = 32; - - /** - * Queue nodes. Uses Object, not E, for items to allow forgetting - * them after use. Relies heavily on Unsafe mechanics to minimize - * unnecessary ordering constraints: Writes that are intrinsically - * ordered wrt other accesses or CASes use simple relaxed forms. - */ - static final class Node { - final boolean isData; // false if this is a request node - volatile Object item; // initially non-null if isData; CASed to match - volatile Node next; - volatile Thread waiter; // null until waiting - - // CAS methods for fields - final boolean casNext(Node cmp, Node val) { - return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); - } - - final boolean casItem(Object cmp, Object val) { - // assert cmp == null || cmp.getClass() != Node.class; - return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); - } - - /** - * Constructs a new node. Uses relaxed write because item can - * only be seen after publication via casNext. - */ - Node(Object item, boolean isData) { - UNSAFE.putObject(this, itemOffset, item); // relaxed write - this.isData = isData; - } - - /** - * Links node to itself to avoid garbage retention. Called - * only after CASing head field, so uses relaxed write. - */ - final void forgetNext() { - UNSAFE.putObject(this, nextOffset, this); - } - - /** - * Sets item to self and waiter to null, to avoid garbage - * retention after matching or cancelling. Uses relaxed writes - * because order is already constrained in the only calling - * contexts: item is forgotten only after volatile/atomic - * mechanics that extract items. Similarly, clearing waiter - * follows either CAS or return from park (if ever parked; - * else we don't care). - */ - final void forgetContents() { - UNSAFE.putObject(this, itemOffset, this); - UNSAFE.putObject(this, waiterOffset, null); - } - - /** - * Returns true if this node has been matched, including the - * case of artificial matches due to cancellation. - */ - final boolean isMatched() { - Object x = item; - return (x == this) || ((x == null) == isData); - } - - /** - * Returns true if this is an unmatched request node. - */ - final boolean isUnmatchedRequest() { - return !isData && item == null; - } - - /** - * Returns true if a node with the given mode cannot be - * appended to this node because this node is unmatched and - * has opposite data mode. - */ - final boolean cannotPrecede(boolean haveData) { - boolean d = isData; - Object x; - return d != haveData && (x = item) != this && (x != null) == d; - } - - /** - * Tries to artificially match a data node -- used by remove. - */ - final boolean tryMatchData() { - // assert isData; - Object x = item; - if (x != null && x != this && casItem(x, null)) { - LockSupport.unpark(waiter); - return true; - } - return false; - } - - // Unsafe mechanics - private static final sun.misc.Unsafe UNSAFE; - private static final long itemOffset; - private static final long nextOffset; - private static final long waiterOffset; - static { - try { - UNSAFE = getUnsafe(); - Class k = Node.class; - itemOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("item")); - nextOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("next")); - waiterOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("waiter")); - } catch (Exception e) { - throw new Error(e); - } - } - } - - /** head of the queue; null until first enqueue */ - transient volatile Node head; - - /** tail of the queue; null until first append */ - private transient volatile Node tail; - - /** The number of apparent failures to unsplice removed nodes */ - private transient volatile int sweepVotes; - - // CAS methods for fields - private boolean casTail(Node cmp, Node val) { - return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val); - } - - private boolean casHead(Node cmp, Node val) { - return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val); - } - - private boolean casSweepVotes(int cmp, int val) { - return UNSAFE.compareAndSwapInt(this, sweepVotesOffset, cmp, val); - } - - /* - * Possible values for "how" argument in xfer method. - */ - private static final int NOW = 0; // for untimed poll, tryTransfer - private static final int ASYNC = 1; // for offer, put, add - private static final int SYNC = 2; // for transfer, take - private static final int TIMED = 3; // for timed poll, tryTransfer - - @SuppressWarnings("unchecked") - static E cast(Object item) { - // assert item == null || item.getClass() != Node.class; - return (E) item; - } - - /** - * Implements all queuing methods. See above for explanation. - * - * @param e the item or null for take - * @param haveData true if this is a put, else a take - * @param how NOW, ASYNC, SYNC, or TIMED - * @param nanos timeout in nanosecs, used only if mode is TIMED - * @return an item if matched, else e - * @throws NullPointerException if haveData mode but e is null - */ - private E xfer(E e, boolean haveData, int how, long nanos) { - if (haveData && (e == null)) - throw new NullPointerException(); - Node s = null; // the node to append, if needed - - retry: - for (;;) { // restart on append race - - for (Node h = head, p = h; p != null;) { // find & match first node - boolean isData = p.isData; - Object item = p.item; - if (item != p && (item != null) == isData) { // unmatched - if (isData == haveData) // can't match - break; - if (p.casItem(item, e)) { // match - for (Node q = p; q != h;) { - Node n = q.next; // update by 2 unless singleton - if (head == h && casHead(h, n == null ? q : n)) { - h.forgetNext(); - break; - } // advance and retry - if ((h = head) == null || - (q = h.next) == null || !q.isMatched()) - break; // unless slack < 2 - } - LockSupport.unpark(p.waiter); - return LinkedTransferQueue.cast(item); - } - } - Node n = p.next; - p = (p != n) ? n : (h = head); // Use head if p offlist - } - - if (how != NOW) { // No matches available - if (s == null) - s = new Node(e, haveData); - Node pred = tryAppend(s, haveData); - if (pred == null) - continue retry; // lost race vs opposite mode - if (how != ASYNC) - return awaitMatch(s, pred, e, (how == TIMED), nanos); - } - return e; // not waiting - } - } - - /** - * Tries to append node s as tail. - * - * @param s the node to append - * @param haveData true if appending in data mode - * @return null on failure due to losing race with append in - * different mode, else s's predecessor, or s itself if no - * predecessor - */ - private Node tryAppend(Node s, boolean haveData) { - for (Node t = tail, p = t;;) { // move p to last node and append - Node n, u; // temps for reads of next & tail - if (p == null && (p = head) == null) { - if (casHead(null, s)) - return s; // initialize - } - else if (p.cannotPrecede(haveData)) - return null; // lost race vs opposite mode - else if ((n = p.next) != null) // not last; keep traversing - p = p != t && t != (u = tail) ? (t = u) : // stale tail - (p != n) ? n : null; // restart if off list - else if (!p.casNext(null, s)) - p = p.next; // re-read on CAS failure - else { - if (p != t) { // update if slack now >= 2 - while ((tail != t || !casTail(t, s)) && - (t = tail) != null && - (s = t.next) != null && // advance and retry - (s = s.next) != null && s != t); - } - return p; - } - } - } - - /** - * Spins/yields/blocks until node s is matched or caller gives up. - * - * @param s the waiting node - * @param pred the predecessor of s, or s itself if it has no - * predecessor, or null if unknown (the null case does not occur - * in any current calls but may in possible future extensions) - * @param e the comparison value for checking match - * @param timed if true, wait only until timeout elapses - * @param nanos timeout in nanosecs, used only if timed is true - * @return matched item, or e if unmatched on interrupt or timeout - */ - private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) { - long lastTime = timed ? System.nanoTime() : 0L; - Thread w = Thread.currentThread(); - int spins = -1; // initialized after first item and cancel checks - ThreadLocalRandom randomYields = null; // bound if needed - - for (;;) { - Object item = s.item; - if (item != e) { // matched - // assert item != s; - s.forgetContents(); // avoid garbage - return LinkedTransferQueue.cast(item); - } - if ((w.isInterrupted() || (timed && nanos <= 0)) && - s.casItem(e, s)) { // cancel - unsplice(pred, s); - return e; - } - - if (spins < 0) { // establish spins at/near front - if ((spins = spinsFor(pred, s.isData)) > 0) - randomYields = ThreadLocalRandom.current(); - } - else if (spins > 0) { // spin - --spins; - if (randomYields.nextInt(CHAINED_SPINS) == 0) - Thread.yield(); // occasionally yield - } - else if (s.waiter == null) { - s.waiter = w; // request unpark then recheck - } - else if (timed) { - long now = System.nanoTime(); - if ((nanos -= now - lastTime) > 0) - LockSupport.parkNanos(this, nanos); - lastTime = now; - } - else { - LockSupport.park(this); - } - } - } - - /** - * Returns spin/yield value for a node with given predecessor and - * data mode. See above for explanation. - */ - private static int spinsFor(Node pred, boolean haveData) { - if (MP && pred != null) { - if (pred.isData != haveData) // phase change - return FRONT_SPINS + CHAINED_SPINS; - if (pred.isMatched()) // probably at front - return FRONT_SPINS; - if (pred.waiter == null) // pred apparently spinning - return CHAINED_SPINS; - } - return 0; - } - - /* -------------- Traversal methods -------------- */ - - /** - * Returns the successor of p, or the head node if p.next has been - * linked to self, which will only be true if traversing with a - * stale pointer that is now off the list. - */ - final Node succ(Node p) { - Node next = p.next; - return (p == next) ? head : next; - } - - /** - * Returns the first unmatched node of the given mode, or null if - * none. Used by methods isEmpty, hasWaitingConsumer. - */ - private Node firstOfMode(boolean isData) { - for (Node p = head; p != null; p = succ(p)) { - if (!p.isMatched()) - return (p.isData == isData) ? p : null; - } - return null; - } - - /** - * Returns the item in the first unmatched node with isData; or - * null if none. Used by peek. - */ - private E firstDataItem() { - for (Node p = head; p != null; p = succ(p)) { - Object item = p.item; - if (p.isData) { - if (item != null && item != p) - return LinkedTransferQueue.cast(item); - } - else if (item == null) - return null; - } - return null; - } - - /** - * Traverses and counts unmatched nodes of the given mode. - * Used by methods size and getWaitingConsumerCount. - */ - private int countOfMode(boolean data) { - int count = 0; - for (Node p = head; p != null; ) { - if (!p.isMatched()) { - if (p.isData != data) - return 0; - if (++count == Integer.MAX_VALUE) // saturated - break; - } - Node n = p.next; - if (n != p) - p = n; - else { - count = 0; - p = head; - } - } - return count; - } - - final class Itr implements Iterator { - private Node nextNode; // next node to return item for - private E nextItem; // the corresponding item - private Node lastRet; // last returned node, to support remove - private Node lastPred; // predecessor to unlink lastRet - - /** - * Moves to next node after prev, or first node if prev null. - */ - private void advance(Node prev) { - /* - * To track and avoid buildup of deleted nodes in the face - * of calls to both Queue.remove and Itr.remove, we must - * include variants of unsplice and sweep upon each - * advance: Upon Itr.remove, we may need to catch up links - * from lastPred, and upon other removes, we might need to - * skip ahead from stale nodes and unsplice deleted ones - * found while advancing. - */ - - Node r, b; // reset lastPred upon possible deletion of lastRet - if ((r = lastRet) != null && !r.isMatched()) - lastPred = r; // next lastPred is old lastRet - else if ((b = lastPred) == null || b.isMatched()) - lastPred = null; // at start of list - else { - Node s, n; // help with removal of lastPred.next - while ((s = b.next) != null && - s != b && s.isMatched() && - (n = s.next) != null && n != s) - b.casNext(s, n); - } - - this.lastRet = prev; - - for (Node p = prev, s, n;;) { - s = (p == null) ? head : p.next; - if (s == null) - break; - else if (s == p) { - p = null; - continue; - } - Object item = s.item; - if (s.isData) { - if (item != null && item != s) { - nextItem = LinkedTransferQueue.cast(item); - nextNode = s; - return; - } - } - else if (item == null) - break; - // assert s.isMatched(); - if (p == null) - p = s; - else if ((n = s.next) == null) - break; - else if (s == n) - p = null; - else - p.casNext(s, n); - } - nextNode = null; - nextItem = null; - } - - Itr() { - advance(null); - } - - public final boolean hasNext() { - return nextNode != null; - } - - public final E next() { - Node p = nextNode; - if (p == null) throw new NoSuchElementException(); - E e = nextItem; - advance(p); - return e; - } - - public final void remove() { - final Node lastRet = this.lastRet; - if (lastRet == null) - throw new IllegalStateException(); - this.lastRet = null; - if (lastRet.tryMatchData()) - unsplice(lastPred, lastRet); - } - } - - /* -------------- Removal methods -------------- */ - - /** - * Unsplices (now or later) the given deleted/cancelled node with - * the given predecessor. - * - * @param pred a node that was at one time known to be the - * predecessor of s, or null or s itself if s is/was at head - * @param s the node to be unspliced - */ - final void unsplice(Node pred, Node s) { - s.forgetContents(); // forget unneeded fields - /* - * See above for rationale. Briefly: if pred still points to - * s, try to unlink s. If s cannot be unlinked, because it is - * trailing node or pred might be unlinked, and neither pred - * nor s are head or offlist, add to sweepVotes, and if enough - * votes have accumulated, sweep. - */ - if (pred != null && pred != s && pred.next == s) { - Node n = s.next; - if (n == null || - (n != s && pred.casNext(s, n) && pred.isMatched())) { - for (;;) { // check if at, or could be, head - Node h = head; - if (h == pred || h == s || h == null) - return; // at head or list empty - if (!h.isMatched()) - break; - Node hn = h.next; - if (hn == null) - return; // now empty - if (hn != h && casHead(h, hn)) - h.forgetNext(); // advance head - } - if (pred.next != pred && s.next != s) { // recheck if offlist - for (;;) { // sweep now if enough votes - int v = sweepVotes; - if (v < SWEEP_THRESHOLD) { - if (casSweepVotes(v, v + 1)) - break; - } - else if (casSweepVotes(v, 0)) { - sweep(); - break; - } - } - } - } - } - } - - /** - * Unlinks matched (typically cancelled) nodes encountered in a - * traversal from head. - */ - private void sweep() { - for (Node p = head, s, n; p != null && (s = p.next) != null; ) { - if (!s.isMatched()) - // Unmatched nodes are never self-linked - p = s; - else if ((n = s.next) == null) // trailing node is pinned - break; - else if (s == n) // stale - // No need to also check for p == s, since that implies s == n - p = head; - else - p.casNext(s, n); - } - } - - /** - * Main implementation of remove(Object) - */ - private boolean findAndRemove(Object e) { - if (e != null) { - for (Node pred = null, p = head; p != null; ) { - Object item = p.item; - if (p.isData) { - if (item != null && item != p && e.equals(item) && - p.tryMatchData()) { - unsplice(pred, p); - return true; - } - } - else if (item == null) - break; - pred = p; - if ((p = p.next) == pred) { // stale - pred = null; - p = head; - } - } - } - return false; - } - - - /** - * Creates an initially empty {@code LinkedTransferQueue}. - */ - public LinkedTransferQueue() { - } - - /** - * Creates a {@code LinkedTransferQueue} - * initially containing the elements of the given collection, - * added in traversal order of the collection's iterator. - * - * @param c the collection of elements to initially contain - * @throws NullPointerException if the specified collection or any - * of its elements are null - */ - public LinkedTransferQueue(Collection c) { - this(); - addAll(c); - } - - /** - * Inserts the specified element at the tail of this queue. - * As the queue is unbounded, this method will never block. - * - * @throws NullPointerException if the specified element is null - */ - public void put(E e) { - xfer(e, true, ASYNC, 0); - } - - /** - * Inserts the specified element at the tail of this queue. - * As the queue is unbounded, this method will never block or - * return {@code false}. - * - * @return {@code true} (as specified by - * {@link java.util.concurrent.BlockingQueue#offer(Object,long,TimeUnit) - * BlockingQueue.offer}) - * @throws NullPointerException if the specified element is null - */ - public boolean offer(E e, long timeout, TimeUnit unit) { - xfer(e, true, ASYNC, 0); - return true; - } - - /** - * Inserts the specified element at the tail of this queue. - * As the queue is unbounded, this method will never return {@code false}. - * - * @return {@code true} (as specified by {@link Queue#offer}) - * @throws NullPointerException if the specified element is null - */ - public boolean offer(E e) { - xfer(e, true, ASYNC, 0); - return true; - } - - /** - * Inserts the specified element at the tail of this queue. - * As the queue is unbounded, this method will never throw - * {@link IllegalStateException} or return {@code false}. - * - * @return {@code true} (as specified by {@link Collection#add}) - * @throws NullPointerException if the specified element is null - */ - public boolean add(E e) { - xfer(e, true, ASYNC, 0); - return true; - } - - /** - * Transfers the element to a waiting consumer immediately, if possible. - * - *

More precisely, transfers the specified element immediately - * if there exists a consumer already waiting to receive it (in - * {@link #take} or timed {@link #poll(long,TimeUnit) poll}), - * otherwise returning {@code false} without enqueuing the element. - * - * @throws NullPointerException if the specified element is null - */ - public boolean tryTransfer(E e) { - return xfer(e, true, NOW, 0) == null; - } - - /** - * Transfers the element to a consumer, waiting if necessary to do so. - * - *

More precisely, transfers the specified element immediately - * if there exists a consumer already waiting to receive it (in - * {@link #take} or timed {@link #poll(long,TimeUnit) poll}), - * else inserts the specified element at the tail of this queue - * and waits until the element is received by a consumer. - * - * @throws NullPointerException if the specified element is null - */ - public void transfer(E e) throws InterruptedException { - if (xfer(e, true, SYNC, 0) != null) { - Thread.interrupted(); // failure possible only due to interrupt - throw new InterruptedException(); - } - } - - /** - * Transfers the element to a consumer if it is possible to do so - * before the timeout elapses. - * - *

More precisely, transfers the specified element immediately - * if there exists a consumer already waiting to receive it (in - * {@link #take} or timed {@link #poll(long,TimeUnit) poll}), - * else inserts the specified element at the tail of this queue - * and waits until the element is received by a consumer, - * returning {@code false} if the specified wait time elapses - * before the element can be transferred. - * - * @throws NullPointerException if the specified element is null - */ - public boolean tryTransfer(E e, long timeout, TimeUnit unit) - throws InterruptedException { - if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null) - return true; - if (!Thread.interrupted()) - return false; - throw new InterruptedException(); - } - - public E take() throws InterruptedException { - E e = xfer(null, false, SYNC, 0); - if (e != null) - return e; - Thread.interrupted(); - throw new InterruptedException(); - } - - public E poll(long timeout, TimeUnit unit) throws InterruptedException { - E e = xfer(null, false, TIMED, unit.toNanos(timeout)); - if (e != null || !Thread.interrupted()) - return e; - throw new InterruptedException(); - } - - public E poll() { - return xfer(null, false, NOW, 0); - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws IllegalArgumentException {@inheritDoc} - */ - public int drainTo(Collection c) { - if (c == null) - throw new NullPointerException(); - if (c == this) - throw new IllegalArgumentException(); - int n = 0; - for (E e; (e = poll()) != null;) { - c.add(e); - ++n; - } - return n; - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws IllegalArgumentException {@inheritDoc} - */ - public int drainTo(Collection c, int maxElements) { - if (c == null) - throw new NullPointerException(); - if (c == this) - throw new IllegalArgumentException(); - int n = 0; - for (E e; n < maxElements && (e = poll()) != null;) { - c.add(e); - ++n; - } - return n; - } - - /** - * Returns an iterator over the elements in this queue in proper sequence. - * The elements will be returned in order from first (head) to last (tail). - * - *

The returned iterator is a "weakly consistent" iterator that - * will never throw {@link java.util.ConcurrentModificationException - * ConcurrentModificationException}, and guarantees to traverse - * elements as they existed upon construction of the iterator, and - * may (but is not guaranteed to) reflect any modifications - * subsequent to construction. - * - * @return an iterator over the elements in this queue in proper sequence - */ - public Iterator iterator() { - return new Itr(); - } - - public E peek() { - return firstDataItem(); - } - - /** - * Returns {@code true} if this queue contains no elements. - * - * @return {@code true} if this queue contains no elements - */ - public boolean isEmpty() { - for (Node p = head; p != null; p = succ(p)) { - if (!p.isMatched()) - return !p.isData; - } - return true; - } - - public boolean hasWaitingConsumer() { - return firstOfMode(false) != null; - } - - /** - * Returns the number of elements in this queue. If this queue - * contains more than {@code Integer.MAX_VALUE} elements, returns - * {@code Integer.MAX_VALUE}. - * - *

Beware that, unlike in most collections, this method is - * NOT a constant-time operation. Because of the - * asynchronous nature of these queues, determining the current - * number of elements requires an O(n) traversal. - * - * @return the number of elements in this queue - */ - public int size() { - return countOfMode(true); - } - - public int getWaitingConsumerCount() { - return countOfMode(false); - } - - /** - * Removes a single instance of the specified element from this queue, - * if it is present. More formally, removes an element {@code e} such - * that {@code o.equals(e)}, if this queue contains one or more such - * elements. - * Returns {@code true} if this queue contained the specified element - * (or equivalently, if this queue changed as a result of the call). - * - * @param o element to be removed from this queue, if present - * @return {@code true} if this queue changed as a result of the call - */ - public boolean remove(Object o) { - return findAndRemove(o); - } - - /** - * Returns {@code true} if this queue contains the specified element. - * More formally, returns {@code true} if and only if this queue contains - * at least one element {@code e} such that {@code o.equals(e)}. - * - * @param o object to be checked for containment in this queue - * @return {@code true} if this queue contains the specified element - */ - public boolean contains(Object o) { - if (o == null) return false; - for (Node p = head; p != null; p = succ(p)) { - Object item = p.item; - if (p.isData) { - if (item != null && item != p && o.equals(item)) - return true; - } - else if (item == null) - break; - } - return false; - } - - /** - * Always returns {@code Integer.MAX_VALUE} because a - * {@code LinkedTransferQueue} is not capacity constrained. - * - * @return {@code Integer.MAX_VALUE} (as specified by - * {@link java.util.concurrent.BlockingQueue#remainingCapacity() - * BlockingQueue.remainingCapacity}) - */ - public int remainingCapacity() { - return Integer.MAX_VALUE; - } - - /** - * Saves the state to a stream (that is, serializes it). - * - * @serialData All of the elements (each an {@code E}) in - * the proper order, followed by a null - * @param s the stream - */ - private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException { - s.defaultWriteObject(); - for (E e : this) - s.writeObject(e); - // Use trailing null as sentinel - s.writeObject(null); - } - - /** - * Reconstitutes the Queue instance from a stream (that is, - * deserializes it). - * - * @param s the stream - */ - private void readObject(java.io.ObjectInputStream s) - throws java.io.IOException, ClassNotFoundException { - s.defaultReadObject(); - for (;;) { - @SuppressWarnings("unchecked") - E item = (E) s.readObject(); - if (item == null) - break; - else - offer(item); - } - } - - // Unsafe mechanics - - private static final sun.misc.Unsafe UNSAFE; - private static final long headOffset; - private static final long tailOffset; - private static final long sweepVotesOffset; - static { - try { - UNSAFE = getUnsafe(); - Class k = LinkedTransferQueue.class; - headOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("head")); - tailOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("tail")); - sweepVotesOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("sweepVotes")); - } catch (Exception e) { - throw new Error(e); - } - } - - /** - * Returns a sun.misc.Unsafe. Suitable for use in a 3rd party package. - * Replace with a simple call to Unsafe.getUnsafe when integrating - * into a jdk. - * - * @return a sun.misc.Unsafe - */ - static sun.misc.Unsafe getUnsafe() { - try { - return sun.misc.Unsafe.getUnsafe(); - } catch (SecurityException se) { - try { - return java.security.AccessController.doPrivileged - (new java.security - .PrivilegedExceptionAction() { - public sun.misc.Unsafe run() throws Exception { - java.lang.reflect.Field f = sun.misc - .Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - return (sun.misc.Unsafe) f.get(null); - }}); - } catch (java.security.PrivilegedActionException e) { - throw new RuntimeException("Could not initialize intrinsics", - e.getCause()); - } - } - } - -} \ No newline at end of file diff --git a/firefly-common/src/main/java/com/firefly/utils/collection/TransferQueue.java b/firefly-common/src/main/java/com/firefly/utils/collection/TransferQueue.java deleted file mode 100644 index 802a0df49..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/collection/TransferQueue.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -package com.firefly.utils.collection; -import java.util.concurrent.*; - -/** - * A {@link BlockingQueue} in which producers may wait for consumers - * to receive elements. A {@code TransferQueue} may be useful for - * example in message passing applications in which producers - * sometimes (using method {@link #transfer}) await receipt of - * elements by consumers invoking {@code take} or {@code poll}, while - * at other times enqueue elements (via method {@code put}) without - * waiting for receipt. - * {@linkplain #tryTransfer(Object) Non-blocking} and - * {@linkplain #tryTransfer(Object,long,TimeUnit) time-out} versions of - * {@code tryTransfer} are also available. - * A {@code TransferQueue} may also be queried, via {@link - * #hasWaitingConsumer}, whether there are any threads waiting for - * items, which is a converse analogy to a {@code peek} operation. - * - *

Like other blocking queues, a {@code TransferQueue} may be - * capacity bounded. If so, an attempted transfer operation may - * initially block waiting for available space, and/or subsequently - * block waiting for reception by a consumer. Note that in a queue - * with zero capacity, such as {@link SynchronousQueue}, {@code put} - * and {@code transfer} are effectively synonymous. - * - *

This interface is a member of the - * - * Java Collections Framework. - * - * @since 1.7 - * @author Doug Lea - * @param the type of elements held in this collection - */ -public interface TransferQueue extends BlockingQueue { - /** - * Transfers the element to a waiting consumer immediately, if possible. - * - *

More precisely, transfers the specified element immediately - * if there exists a consumer already waiting to receive it (in - * {@link #take} or timed {@link #poll(long,TimeUnit) poll}), - * otherwise returning {@code false} without enqueuing the element. - * - * @param e the element to transfer - * @return {@code true} if the element was transferred, else - * {@code false} - * @throws ClassCastException if the class of the specified element - * prevents it from being added to this queue - * @throws NullPointerException if the specified element is null - * @throws IllegalArgumentException if some property of the specified - * element prevents it from being added to this queue - */ - boolean tryTransfer(E e); - - /** - * Transfers the element to a consumer, waiting if necessary to do so. - * - *

More precisely, transfers the specified element immediately - * if there exists a consumer already waiting to receive it (in - * {@link #take} or timed {@link #poll(long,TimeUnit) poll}), - * else waits until the element is received by a consumer. - * - * @param e the element to transfer - * @throws InterruptedException if interrupted while waiting, - * in which case the element is not left enqueued - * @throws ClassCastException if the class of the specified element - * prevents it from being added to this queue - * @throws NullPointerException if the specified element is null - * @throws IllegalArgumentException if some property of the specified - * element prevents it from being added to this queue - */ - void transfer(E e) throws InterruptedException; - - /** - * Transfers the element to a consumer if it is possible to do so - * before the timeout elapses. - * - *

More precisely, transfers the specified element immediately - * if there exists a consumer already waiting to receive it (in - * {@link #take} or timed {@link #poll(long,TimeUnit) poll}), - * else waits until the element is received by a consumer, - * returning {@code false} if the specified wait time elapses - * before the element can be transferred. - * - * @param e the element to transfer - * @param timeout how long to wait before giving up, in units of - * {@code unit} - * @param unit a {@code TimeUnit} determining how to interpret the - * {@code timeout} parameter - * @return {@code true} if successful, or {@code false} if - * the specified waiting time elapses before completion, - * in which case the element is not left enqueued - * @throws InterruptedException if interrupted while waiting, - * in which case the element is not left enqueued - * @throws ClassCastException if the class of the specified element - * prevents it from being added to this queue - * @throws NullPointerException if the specified element is null - * @throws IllegalArgumentException if some property of the specified - * element prevents it from being added to this queue - */ - boolean tryTransfer(E e, long timeout, TimeUnit unit) - throws InterruptedException; - - /** - * Returns {@code true} if there is at least one consumer waiting - * to receive an element via {@link #take} or - * timed {@link #poll(long,TimeUnit) poll}. - * The return value represents a momentary state of affairs. - * - * @return {@code true} if there is at least one waiting consumer - */ - boolean hasWaitingConsumer(); - - /** - * Returns an estimate of the number of consumers waiting to - * receive elements via {@link #take} or timed - * {@link #poll(long,TimeUnit) poll}. The return value is an - * approximation of a momentary state of affairs, that may be - * inaccurate if consumers have completed or given up waiting. - * The value may be useful for monitoring and heuristics, but - * not for synchronization control. Implementations of this - * method are likely to be noticeably slower than those for - * {@link #hasWaitingConsumer}. - * - * @return the number of consumers waiting to receive elements - */ - int getWaitingConsumerCount(); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/dom/DefaultDom.java b/firefly-common/src/main/java/com/firefly/utils/dom/DefaultDom.java deleted file mode 100644 index 097f0d2d8..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/dom/DefaultDom.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.firefly.utils.dom; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.w3c.dom.CharacterData; -import org.w3c.dom.Comment; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.EntityReference; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -public class DefaultDom implements Dom { - - private DocumentBuilderFactory dbf; - private DocumentBuilder db; - - public DefaultDom() { - // 得到dom解析器工厂实例 - dbf = DocumentBuilderFactory.newInstance(); - try { - // 得到dom解析器 - db = dbf.newDocumentBuilder(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } - } - - @Override - public Document getDocument(String file) { - Document doc = null; - try { - InputStream is = DefaultDom.class.getResourceAsStream("/" + file); - doc = db.parse(is); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return doc; - } - - @Override - public Element getRoot(Document doc) { - return doc.getDocumentElement(); - } - - @Override - public List elements(Element e) { - return elements(e, null); - } - - @Override - public List elements(Element e, String name) { - List eList = new ArrayList(); - - NodeList nodeList = e.getChildNodes(); - for (int i = 0; i < nodeList.getLength(); ++i) { - Node node = nodeList.item(i); - if (node.getNodeType() == Node.ELEMENT_NODE) { - if (name != null) { - if (node.getNodeName().equals(name)) - eList.add((Element) node); - } else { - eList.add((Element) node); - } - } - } - return eList; - } - - @Override - public Element element(Element e, String name) { - NodeList element = e.getElementsByTagName(name); - if (element != null && e.getNodeType() == Node.ELEMENT_NODE) { - return (Element) element.item(0); - } - return null; - } - - @Override - public String getTextValue(Element valueEle) { - if (valueEle != null) { - StringBuilder sb = new StringBuilder(); - NodeList nl = valueEle.getChildNodes(); - for (int i = 0; i < nl.getLength(); i++) { - Node item = nl.item(i); - if ((item instanceof CharacterData && !(item instanceof Comment)) - || item instanceof EntityReference) { - sb.append(item.getNodeValue()); - } - } - return sb.toString().trim(); - } - return null; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/dom/Dom.java b/firefly-common/src/main/java/com/firefly/utils/dom/Dom.java deleted file mode 100644 index 8407b5006..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/dom/Dom.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.firefly.utils.dom; - -import java.util.List; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -public interface Dom { - - /** - * 根据文件读取文档对象 - * @Date 2011-3-3 - * @param file - * @return 文档对象 - */ - public abstract Document getDocument(String file); - - /** - * 取得根节点 - * @param doc - * @return - */ - public abstract Element getRoot(Document doc); - - /** - * 取得所有子元素 - * @param e - * @return - */ - public abstract List elements(Element e); - - /** - * 根据元素名取得子元素列表 - * @param e - * @param name - * @return - */ - public abstract List elements(Element e, String name); - - /** - * 获取元素 - * @param e - * @param name - * @return - */ - public abstract Element element(Element e, String name); - - /** - * 获取元素值 - * @param valueEle - * @return - */ - public abstract String getTextValue(Element valueEle); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/io/BufferedRandomAccessFile.java b/firefly-common/src/main/java/com/firefly/utils/io/BufferedRandomAccessFile.java deleted file mode 100644 index 87a1539bf..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/io/BufferedRandomAccessFile.java +++ /dev/null @@ -1,307 +0,0 @@ -package com.firefly.utils.io; - -import java.io.RandomAccessFile; -import java.io.File; -import java.io.IOException; -import java.io.FileNotFoundException; - -public class BufferedRandomAccessFile extends RandomAccessFile { - - private static final int DEFAULT_BUFFER_BIT_LEN = 10; - - protected byte buf[]; - protected int bufbitlen; - protected int bufsize; - protected long bufmask; - protected boolean bufdirty; - protected int bufusedsize; - protected long curpos; - - protected long bufstartpos; - protected long bufendpos; - protected long fileendpos; - - protected boolean append; - protected String filename; - protected long initfilelen; - - public BufferedRandomAccessFile(String name) throws IOException { - this(name, "r", DEFAULT_BUFFER_BIT_LEN); - } - - public BufferedRandomAccessFile(File file) throws IOException, - FileNotFoundException { - this(file.getPath(), "r", DEFAULT_BUFFER_BIT_LEN); - } - - public BufferedRandomAccessFile(String name, int bufbitlen) - throws IOException { - this(name, "r", bufbitlen); - } - - public BufferedRandomAccessFile(File file, int bufbitlen) - throws IOException, FileNotFoundException { - this(file.getPath(), "r", bufbitlen); - } - - public BufferedRandomAccessFile(String name, String mode) - throws IOException { - this(name, mode, DEFAULT_BUFFER_BIT_LEN); - } - - public BufferedRandomAccessFile(File file, String mode) throws IOException, - FileNotFoundException { - this(file.getPath(), mode, DEFAULT_BUFFER_BIT_LEN); - } - - public BufferedRandomAccessFile(String name, String mode, int bufbitlen) - throws IOException { - super(name, mode); - init(name, mode, bufbitlen); - } - - public BufferedRandomAccessFile(File file, String mode, int bufbitlen) - throws IOException, FileNotFoundException { - this(file.getPath(), mode, bufbitlen); - } - - private void init(String name, String mode, int bufbitlen) - throws IOException { - if (mode.equals("r") == true) { - append = false; - } else { - append = true; - } - - filename = name; - initfilelen = super.length(); - fileendpos = initfilelen - 1; - curpos = super.getFilePointer(); - - if (bufbitlen < 0) { - throw new IllegalArgumentException("bufbitlen size must >= 0"); - } - - this.bufbitlen = bufbitlen; - bufsize = 1 << bufbitlen; - buf = new byte[bufsize]; - bufmask = ~((long) bufsize - 1L); - bufdirty = false; - bufusedsize = 0; - bufstartpos = -1; - bufendpos = -1; - } - - private void flushbuf() throws IOException { - if (bufdirty == true) { - if (super.getFilePointer() != bufstartpos) { - super.seek(bufstartpos); - } - super.write(buf, 0, bufusedsize); - bufdirty = false; - } - } - - private int fillbuf() throws IOException { - super.seek(bufstartpos); - bufdirty = false; - return super.read(buf); - } - - public byte read(long pos) throws IOException { - if (pos < bufstartpos || pos > bufendpos) { - flushbuf(); - seek(pos); - - if ((pos < bufstartpos) || (pos > bufendpos)) { - throw new IOException(); - } - } - curpos = pos; - return buf[(int) (pos - bufstartpos)]; - } - - public boolean write(byte bw) throws IOException { - return write(bw, curpos); - } - - public boolean append(byte bw) throws IOException { - return write(bw, fileendpos + 1); - } - - public boolean write(byte bw, long pos) throws IOException { - - if ((pos >= bufstartpos) && (pos <= bufendpos)) { - buf[(int) (pos - bufstartpos)] = bw; - bufdirty = true; - - if (pos == fileendpos + 1) { // write pos is append pos - fileendpos++; - bufusedsize++; - } - } else { // write pos not in buf - seek(pos); - - if ((pos >= 0) && (pos <= fileendpos) && (fileendpos != 0)) { - buf[(int) (pos - bufstartpos)] = bw; - } else if (((pos == 0) && (fileendpos == 0)) - || (pos == fileendpos + 1)) { // write pos is append pos - buf[0] = bw; - fileendpos++; - bufusedsize = 1; - } else { - throw new IndexOutOfBoundsException(); - } - bufdirty = true; - } - curpos = pos; - return true; - } - - public void write(byte b[], int off, int len) throws IOException { - - long writeendpos = curpos + len - 1; - - if (writeendpos <= bufendpos) { // b[] in cur buf - System.arraycopy(b, off, buf, (int) (curpos - bufstartpos), len); - bufdirty = true; - bufusedsize = (int) (writeendpos - bufstartpos + 1); - } else { // b[] not in cur buf - super.seek(curpos); - super.write(b, off, len); - } - - if (writeendpos > fileendpos) - fileendpos = writeendpos; - - seek(writeendpos + 1); - } - - public int read(byte b[], int off, int len) throws IOException { - - long readendpos = curpos + len - 1; - - if (readendpos <= bufendpos && readendpos <= fileendpos) { - System.arraycopy(buf, (int) (curpos - bufstartpos), b, off, len); - } else { // read b[] size > buf[] - if (readendpos > fileendpos) { // read b[] part in file - len = (int) (this.length() - curpos + 1); - } - - super.seek(curpos); - len = super.read(b, off, len); - readendpos = curpos + len - 1; - } - seek(readendpos + 1); - return len; - } - - public void write(byte b[]) throws IOException { - write(b, 0, b.length); - } - - public int read(byte b[]) throws IOException { - return read(b, 0, b.length); - } - - public void seek(long pos) throws IOException { - - if ((pos < bufstartpos) || (pos > bufendpos)) { // seek pos not in buf - this.flushbuf(); - if ((pos >= 0) && (pos <= fileendpos) && (fileendpos != 0)) { - bufstartpos = pos & bufmask; - bufusedsize = fillbuf(); - - } else if (((pos == 0) && (fileendpos == 0)) - || (pos == fileendpos + 1)) { // seek pos is append pos - bufstartpos = pos; - bufusedsize = 0; - } - bufendpos = bufstartpos + bufsize - 1; - } - curpos = pos; - } - - public long length() throws IOException { - return this.max(fileendpos + 1, initfilelen); - } - - public void setLength(long newLength) throws IOException { - if (newLength > 0) { - fileendpos = newLength - 1; - } else { - fileendpos = 0; - } - super.setLength(newLength); - } - - public long getFilePointer() throws IOException { - return curpos; - } - - private long max(long a, long b) { - if (a > b) - return a; - return b; - } - - public void close() throws IOException { - this.flushbuf(); - super.close(); - } - - public static void main(String[] args) throws IOException { - long readfilelen = 0; - BufferedRandomAccessFile brafReadFile, brafWriteFile; - - brafReadFile = new BufferedRandomAccessFile( - "C:/Windows/Fonts/STKAITI.TTF"); - readfilelen = brafReadFile.initfilelen; - brafWriteFile = new BufferedRandomAccessFile("D:/STKAITI.001", "rw", 10); - - byte buf[] = new byte[1024]; - int readcount; - - long start = System.currentTimeMillis(); - - while ((readcount = brafReadFile.read(buf)) != -1) { - brafWriteFile.write(buf, 0, readcount); - } - - brafWriteFile.close(); - brafReadFile.close(); - - System.out.println("BufferedRandomAccessFile Copy & Write File: " - + brafReadFile.filename + " FileSize: " - + java.lang.Integer.toString((int) readfilelen >> 1024) - + " (KB) " + "Spend: " - + (double) (System.currentTimeMillis() - start) / 1000 + "(s)"); - - java.io.FileInputStream fdin = new java.io.FileInputStream( - "C:/Windows/Fonts/STKAITI.TTF"); - java.io.BufferedInputStream bis = new java.io.BufferedInputStream(fdin, - 1024); - java.io.DataInputStream dis = new java.io.DataInputStream(bis); - - java.io.FileOutputStream fdout = new java.io.FileOutputStream( - "D:/STKAITI.002"); - java.io.BufferedOutputStream bos = new java.io.BufferedOutputStream( - fdout, 1024); - java.io.DataOutputStream dos = new java.io.DataOutputStream(bos); - - start = System.currentTimeMillis(); - - for (int i = 0; i < readfilelen; i++) { - dos.write(dis.readByte()); - } - - dos.close(); - dis.close(); - - System.out.println("DataBufferedios Copy & Write File: " - + brafReadFile.filename + " FileSize: " - + java.lang.Integer.toString((int) readfilelen >> 1024) - + " (KB) " + "Spend: " - + (double) (System.currentTimeMillis() - start) / 1000 + "(s)"); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/io/FileUtils.java b/firefly-common/src/main/java/com/firefly/utils/io/FileUtils.java deleted file mode 100644 index 077fb55b1..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/io/FileUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.firefly.utils.io; - -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.LineNumberReader; -import java.nio.channels.FileChannel; - -abstract public class FileUtils { - - public static void recursiveDelete(File dir) { - dir.listFiles(new FileFilter() { - @Override - public boolean accept(File f) { - if (f.isDirectory()) - recursiveDelete(f); - else - f.delete(); - return false; - } - }); - dir.delete(); - } - - public static void read(File file, LineReaderHandler handler, String charset) - throws IOException { - LineNumberReader reader = new LineNumberReader(new InputStreamReader( - new FileInputStream(file), charset)); - try { - for (String line = null; (line = reader.readLine()) != null;) { - handler.readline(line, reader.getLineNumber()); - } - } finally { - if (reader != null) - reader.close(); - } - } - - public static long copy(File src, File dest) throws IOException { - return copy(src, dest, 0, src.length()); - } - - public static long copy(File src, File dest, long position, long count) - throws IOException { - FileInputStream in = new FileInputStream(src); - FileOutputStream out = new FileOutputStream(dest); - FileChannel inChannel = in.getChannel(); - FileChannel outChannel = out.getChannel(); - try { - return inChannel.transferTo(position, count, outChannel); - } finally { - if (inChannel != null) - inChannel.close(); - if (outChannel != null) - outChannel.close(); - if (in != null) - in.close(); - if (out != null) - out.close(); - } - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/io/IOUtils.java b/firefly-common/src/main/java/com/firefly/utils/io/IOUtils.java deleted file mode 100644 index f2b3fc189..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/io/IOUtils.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.firefly.utils.io; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; - -public class IOUtils { - - // Requires positive x - public static int stringSize(long x) { - long p = 10; - for (int i = 1; i < 19; i++) { - if (x < p) return i; - p = 10 * p; - } - return 19; - } - - public static void getChars(long i, int index, char[] buf) { - long q; - int r; - int charPos = index; - char sign = 0; - - if (i < 0) { - sign = '-'; - i = -i; - } - - // Get 2 digits/iteration using longs until quotient fits into an int - while (i > Integer.MAX_VALUE) { - q = i / 100; - // really: r = i - (q * 100); - r = (int) (i - ((q << 6) + (q << 5) + (q << 2))); - i = q; - buf[--charPos] = DigitOnes[r]; - buf[--charPos] = DigitTens[r]; - } - - // Get 2 digits/iteration using ints - int q2; - int i2 = (int) i; - while (i2 >= 65536) { - q2 = i2 / 100; - // really: r = i2 - (q * 100); - r = i2 - ((q2 << 6) + (q2 << 5) + (q2 << 2)); - i2 = q2; - buf[--charPos] = DigitOnes[r]; - buf[--charPos] = DigitTens[r]; - } - - // Fall thru to fast mode for smaller numbers - // assert(i2 <= 65536, i2); - for (;;) { - q2 = (i2 * 52429) >>> (16 + 3); - r = i2 - ((q2 << 3) + (q2 << 1)); // r = i2-(q2*10) ... - buf[--charPos] = digits[r]; - i2 = q2; - if (i2 == 0) break; - } - if (sign != 0) { - buf[--charPos] = sign; - } - } - - /** - * Places characters representing the integer i into the character array buf. The characters are placed into the - * buffer backwards starting with the least significant digit at the specified index (exclusive), and working - * backwards from there. Will fail if i == Integer.MIN_VALUE - */ - public static void getChars(int i, int index, char[] buf) { - int q, r; - int charPos = index; - char sign = 0; - - if (i < 0) { - sign = '-'; - i = -i; - } - - // Generate two digits per iteration - while (i >= 65536) { - q = i / 100; - // really: r = i - (q * 100); - r = i - ((q << 6) + (q << 5) + (q << 2)); - i = q; - buf[--charPos] = DigitOnes[r]; - buf[--charPos] = DigitTens[r]; - } - - // Fall thru to fast mode for smaller numbers - // assert(i <= 65536, i); - for (;;) { - q = (i * 52429) >>> (16 + 3); - r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ... - buf[--charPos] = digits[r]; - i = q; - if (i == 0) break; - } - if (sign != 0) { - buf[--charPos] = sign; - } - } - - public static void getChars(byte b, int index, char[] buf) { - int i = b; - int q, r; - int charPos = index; - char sign = 0; - - if (i < 0) { - sign = '-'; - i = -i; - } - - // Fall thru to fast mode for smaller numbers - // assert(i <= 65536, i); - for (;;) { - q = (i * 52429) >>> (16 + 3); - r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ... - buf[--charPos] = digits[r]; - i = q; - if (i == 0) break; - } - if (sign != 0) { - buf[--charPos] = sign; - } - } - - /** - * All possible chars for representing a number as a String - */ - final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; - - final static char[] DigitTens = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6', '6', '6', '6', '6', '6', - '6', '6', '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', }; - - final static char[] DigitOnes = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', }; - - final static int[] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE }; - - // Requires positive x - public static int stringSize(int x) { - for (int i = 0;; i++) - if (x <= sizeTable[i]) return i + 1; - } - - public static void decode(CharsetDecoder charsetDecoder, ByteBuffer byteBuf, CharBuffer charByte) { - try { - CoderResult cr = charsetDecoder.decode(byteBuf, charByte, true); - - if (!cr.isUnderflow()) { - cr.throwException(); - } - - cr = charsetDecoder.flush(charByte); - - if (!cr.isUnderflow()) { - cr.throwException(); - } - } catch (CharacterCodingException x) { - // Substitution is always enabled, - // so this shouldn't happen - // TODO - } - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/io/LineReaderHandler.java b/firefly-common/src/main/java/com/firefly/utils/io/LineReaderHandler.java deleted file mode 100644 index d0e488860..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/io/LineReaderHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.firefly.utils.io; - -public interface LineReaderHandler { - void readline(String text, int num); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/io/StringWriter.java b/firefly-common/src/main/java/com/firefly/utils/io/StringWriter.java deleted file mode 100644 index e910dd271..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/io/StringWriter.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.firefly.utils.io; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.Writer; -import java.lang.ref.SoftReference; - -public class StringWriter extends Writer { - - protected char buf[]; - protected int count; - protected final static ThreadLocal> bufLocal = new ThreadLocal>(); - public static final char[] NULL = "null".toCharArray(); - public static final char[] MIN_INT_VALUE = "-2147483648".toCharArray(); - public static final char[] MIN_LONG_VALUE = "-9223372036854775808" - .toCharArray(); - public static final char[] TRUE_VALUE = "true".toCharArray(); - public static final char[] FALSE_VALUE = "false".toCharArray(); - - public StringWriter() { - SoftReference ref = bufLocal.get(); - - if (ref != null) { - buf = ref.get(); - bufLocal.set(null); - } - - if (buf == null) - buf = new char[1024]; - } - - public StringWriter(int initialSize) { - if (initialSize < 0) { - throw new IllegalArgumentException("Negative initial size: " - + initialSize); - } - buf = new char[initialSize]; - } - - @Override - public void write(int c) { - int newcount = count + 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - buf[count] = (char) c; - count = newcount; - } - - @Override - public void write(char[] c) { - write(c, 0, c.length); - } - - @Override - public void write(char[] c, int off, int len) { - if (off < 0 || off > c.length || len < 0 || off + len > c.length - || off + len < 0) { - throw new IndexOutOfBoundsException(); - } else if (len == 0) { - return; - } - - int newcount = count + len; - if (newcount > buf.length) { - expandCapacity(newcount); - } - System.arraycopy(c, off, buf, count, len); - count = newcount; - } - - @Override - public void write(String str) { - write(str, 0, str.length()); - } - - @Override - public void write(String str, int off, int len) { - int newcount = count + len; - if (newcount > buf.length) { - expandCapacity(newcount); - } - str.getChars(off, off + len, buf, count); - count = newcount; - } - - @Override - public StringWriter append(CharSequence csq) { - String str = csq.toString(); - write(str, 0, str.length()); - return this; - } - - @Override - public StringWriter append(CharSequence csq, int start, int end) { - String str = csq.subSequence(start, end).toString(); - write(str, 0, str.length()); - return this; - } - - @Override - public StringWriter append(char c) { - write(c); - return this; - } - - @Override - public String toString() { - return new String(buf, 0, count); - } - - @Override - public void flush() { - } - - @Override - public void close() { - reset(); - bufLocal.set(new SoftReference(buf)); - } - - public void writeNull() { - write(NULL); - } - - public void writeBoolean(boolean b) { - if (b) - write(TRUE_VALUE); - else - write(FALSE_VALUE); - } - - public void write(char c) { - int newcount = count + 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - buf[count] = c; - count = newcount; - } - - public void writeChars(char... chs) { - write(chs, 0, chs.length); - } - - public void writeInt(int i) { - if (i == Integer.MIN_VALUE) { - write(MIN_INT_VALUE); - return; - } - int size = (i < 0) ? IOUtils.stringSize(-i) + 1 : IOUtils.stringSize(i); - int newcount = count + size; - - if (newcount > buf.length) { - expandCapacity(newcount); - } - - IOUtils.getChars(i, newcount, buf); - count = newcount; - } - - public void writeShort(short i) { - writeInt((int) i); - } - - public void writeByte(byte i) { - writeInt((int) i); - } - - public void writeLong(long i) { - if (i == Long.MIN_VALUE) { - write(MIN_LONG_VALUE); - return; - } - - int size = (i < 0) ? IOUtils.stringSize(-i) + 1 : IOUtils.stringSize(i); - - int newcount = count + size; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - IOUtils.getChars(i, newcount, buf); - count = newcount; - } - - public void writeTo(Writer out) throws IOException { - out.write(buf, 0, count); - } - - public void writeTo(OutputStream out, String charset) throws IOException { - byte[] bytes = new String(buf, 0, count).getBytes(charset); - out.write(bytes); - } - - public void reset() { - count = 0; - } - - public char[] toCharArray() { - char[] newValue = new char[count]; - System.arraycopy(buf, 0, newValue, 0, count); - return newValue; - } - - public int size() { - return count; - } - - protected void expandCapacity(int minimumCapacity) { - int newCapacity = (buf.length * 3) / 2 + 1; - - if (newCapacity < minimumCapacity) { - newCapacity = minimumCapacity; - } - char newValue[] = new char[newCapacity]; - System.arraycopy(buf, 0, newValue, 0, count); - buf = newValue; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/Json.java b/firefly-common/src/main/java/com/firefly/utils/json/Json.java deleted file mode 100644 index 3cbf46c39..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/Json.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.firefly.utils.json; - -import java.io.IOException; - -import com.firefly.utils.json.parser.ParserStateMachine; -import com.firefly.utils.json.serializer.SerialStateMachine; -import com.firefly.utils.json.support.JsonStringReader; -import com.firefly.utils.json.support.JsonStringWriter; - - -public abstract class Json { - public static String toJson(Object obj) { - String ret = null; - JsonStringWriter writer = new JsonStringWriter(); - try { - SerialStateMachine.toJson(obj, writer); - ret = writer.toString(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - writer.close(); - } - return ret; - } - - @SuppressWarnings("unchecked") - public static T toObject(String json, Class clazz) { - JsonStringReader reader = new JsonStringReader(json); - return (T) ParserStateMachine.toObject(reader, clazz); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/JsonStringSymbol.java b/firefly-common/src/main/java/com/firefly/utils/json/JsonStringSymbol.java deleted file mode 100644 index c71acd7ff..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/JsonStringSymbol.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.firefly.utils.json; - -public interface JsonStringSymbol { - char QUOTE = '"'; - char ARRAY_PRE = '['; - char ARRAY_SUF = ']'; - char OBJ_PRE = '{'; - char OBJ_SUF = '}'; - char SEPARATOR = ','; - char OBJ_SEPARATOR = ':'; - char[] EMPTY_ARRAY = "[]".toCharArray(); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/Parser.java b/firefly-common/src/main/java/com/firefly/utils/json/Parser.java deleted file mode 100644 index d7d3ba129..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/Parser.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.utils.json; - -import com.firefly.utils.json.support.JsonStringReader; - -public interface Parser { - Object convertTo(JsonStringReader reader, Class clazz); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/Serializer.java b/firefly-common/src/main/java/com/firefly/utils/json/Serializer.java deleted file mode 100644 index 20c1e6cd4..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/Serializer.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.firefly.utils.json; - -import java.io.IOException; - -import com.firefly.utils.json.support.JsonStringWriter; - -public interface Serializer { - void convertTo(JsonStringWriter writer, Object obj) throws IOException; -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/annotation/CircularReferenceCheck.java b/firefly-common/src/main/java/com/firefly/utils/json/annotation/CircularReferenceCheck.java deleted file mode 100644 index cc5353bd8..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/annotation/CircularReferenceCheck.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.utils.json.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface CircularReferenceCheck { - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/annotation/Transient.java b/firefly-common/src/main/java/com/firefly/utils/json/annotation/Transient.java deleted file mode 100644 index faf057b06..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/annotation/Transient.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.utils.json.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.METHOD, ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Transient { - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/compiler/DecodeCompiler.java b/firefly-common/src/main/java/com/firefly/utils/json/compiler/DecodeCompiler.java deleted file mode 100644 index 94e2d2407..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/compiler/DecodeCompiler.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.firefly.utils.json.compiler; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; - -import com.firefly.utils.json.annotation.Transient; -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.parser.CollectionParser; -import com.firefly.utils.json.parser.ComplexTypeParser; -import com.firefly.utils.json.parser.MapParser; -import com.firefly.utils.json.parser.ParserStateMachine; -import com.firefly.utils.json.support.FieldInvoke; -import com.firefly.utils.json.support.MethodInvoke; -import com.firefly.utils.json.support.ParserMetaInfo; - -public class DecodeCompiler { - private static final ParserMetaInfo[] EMPTY_ARRAY = new ParserMetaInfo[0]; - - public static ParserMetaInfo[] compile(Class clazz) { - ParserMetaInfo[] parserMetaInfos = null; - Set fieldSet = new TreeSet(); - for (Method method : clazz.getMethods()) { - method.setAccessible(true); - String methodName = method.getName(); - - if (method.getName().length() < 4) continue; - if (!method.getName().startsWith("set")) continue; - if (method.getParameterTypes().length != 1) continue; - if (Modifier.isStatic(method.getModifiers())) continue; - if (Modifier.isAbstract(method.getModifiers())) continue; - if (method.isAnnotationPresent(Transient.class)) continue; - - if (methodName.length() < 4 || !Character.isUpperCase(methodName.charAt(3))) - continue; - - String propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); - Field field = null; - try { - field = clazz.getDeclaredField(propertyName); - } catch (Throwable t) { - t.printStackTrace(); - } - - if (field != null - && (Modifier.isTransient(field.getModifiers()) - || field.isAnnotationPresent(Transient.class))) - continue; - - ParserMetaInfo parserMetaInfo = new ParserMetaInfo(); - parserMetaInfo.setPropertyNameString(propertyName); - parserMetaInfo.setPropertyInvoke(new MethodInvoke(method)); - Class type = method.getParameterTypes()[0]; - - if (Collection.class.isAssignableFrom(type)) { - Type[] types = method.getGenericParameterTypes(); - if(types.length != 1 || !(types[0] instanceof ParameterizedType)) - throw new JsonException("not support the " + method); - - ParameterizedType paramType = (ParameterizedType) types[0]; - Type[] types2 = paramType.getActualTypeArguments(); - if(types2.length != 1) - throw new JsonException("not support the " + method); - - Type elementType = types2[0]; - parserMetaInfo.setType(ComplexTypeParser.getImplClass(type)); - parserMetaInfo.setParser(new CollectionParser(elementType)); - } else if (Map.class.isAssignableFrom(type)) { // Map元信息构造 - Type[] types = method.getGenericParameterTypes(); - if(types.length != 1 || !(types[0] instanceof ParameterizedType)) - throw new JsonException("not support the " + method); - - ParameterizedType paramType = (ParameterizedType) types[0]; - Type[] types2 = paramType.getActualTypeArguments(); - if(types2.length != 2) - throw new JsonException("not support the " + method); - - Type key = types2[0]; - if (!((key instanceof Class) && key == String.class)) - throw new JsonException("not support the " + method); - - Type elementType = types2[1]; - parserMetaInfo.setType(ComplexTypeParser.getImplClass(type)); - parserMetaInfo.setParser(new MapParser(elementType)); - } else { // 获取对象、枚举或者数组Parser - parserMetaInfo.setType(type); - parserMetaInfo.setParser(ParserStateMachine.getParser(type)); - } - fieldSet.add(parserMetaInfo); - } - - for(Field field : clazz.getFields()) { // public字段反序列化构造 - if(Modifier.isTransient(field.getModifiers()) || field.isAnnotationPresent(Transient.class)) - continue; - - field.setAccessible(true); - - ParserMetaInfo parserMetaInfo = new ParserMetaInfo(); - parserMetaInfo.setPropertyNameString(field.getName()); - parserMetaInfo.setPropertyInvoke(new FieldInvoke(field)); - - Class type = field.getType(); - if (Collection.class.isAssignableFrom(type)) { - Type fieldType = field.getGenericType(); - if(!(fieldType instanceof ParameterizedType)) - throw new JsonException("not support the " + field); - - ParameterizedType paramType = (ParameterizedType)fieldType; - Type[] types2 = paramType.getActualTypeArguments(); - if(types2.length != 1) - throw new JsonException("not support the " + field); - - Type elementType = types2[0]; - parserMetaInfo.setType(ComplexTypeParser.getImplClass(type)); - parserMetaInfo.setParser(new CollectionParser(elementType)); - } else if (Map.class.isAssignableFrom(type)) { // Map元信息构造 - Type fieldType = field.getGenericType(); - if(!(fieldType instanceof ParameterizedType)) - throw new JsonException("not support the " + field); - - ParameterizedType paramType = (ParameterizedType) fieldType; - Type[] types2 = paramType.getActualTypeArguments(); - if(types2.length != 2) - throw new JsonException("not support the " + field); - - Type key = types2[0]; - if (!((key instanceof Class) && key == String.class)) - throw new JsonException("not support the " + field); - - Type elementType = types2[1]; - parserMetaInfo.setType(ComplexTypeParser.getImplClass(type)); - parserMetaInfo.setParser(new MapParser(elementType)); - } else { // 获取对象、枚举或者数组Parser - parserMetaInfo.setType(type); - parserMetaInfo.setParser(ParserStateMachine.getParser(type)); - } - fieldSet.add(parserMetaInfo); - } - - parserMetaInfos = fieldSet.toArray(EMPTY_ARRAY); - if(parserMetaInfos.length <= 0) - throw new JsonException("not support the " + clazz.getName()); - return parserMetaInfos; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/compiler/EncodeCompiler.java b/firefly-common/src/main/java/com/firefly/utils/json/compiler/EncodeCompiler.java deleted file mode 100644 index eea151298..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/compiler/EncodeCompiler.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.firefly.utils.json.compiler; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Set; -import java.util.TreeSet; - -import com.firefly.utils.json.annotation.Transient; -import com.firefly.utils.json.serializer.SerialStateMachine; -import com.firefly.utils.json.support.FieldInvoke; -import com.firefly.utils.json.support.MethodInvoke; -import com.firefly.utils.json.support.SerializerMetaInfo; - -public class EncodeCompiler { - - private static final SerializerMetaInfo[] EMPTY_ARRAY = new SerializerMetaInfo[0]; - - public static SerializerMetaInfo[] compile(Class clazz) { - SerializerMetaInfo[] serializerMetaInfos = null; - Set fieldSet = new TreeSet(); - - for (Method method : clazz.getMethods()) { - method.setAccessible(true); - String methodName = method.getName(); - - if (method.getName().length() < 3) continue; - if (Modifier.isStatic(method.getModifiers())) continue; - if (Modifier.isAbstract(method.getModifiers())) continue; - if (method.getName().equals("getClass")) continue; - if (!method.getName().startsWith("is") && !method.getName().startsWith("get")) continue; - if (method.getParameterTypes().length != 0) continue; - if (method.getReturnType() == void.class) continue; - if (method.isAnnotationPresent(Transient.class)) continue; - - String propertyName = null; - if (methodName.charAt(0) == 'g') { - if (methodName.length() < 4 || !Character.isUpperCase(methodName.charAt(3))) - continue; - - propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); - } else { - if (methodName.length() < 3 || !Character.isUpperCase(methodName.charAt(2))) - continue; - - propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3); - } - - Field field = null; - try { - field = clazz.getDeclaredField(propertyName); - } catch (Throwable t) { - t.printStackTrace(); - } - - if (field != null - && (Modifier.isTransient(field.getModifiers()) - || field.isAnnotationPresent(Transient.class))) - continue; - - Class fieldClazz = method.getReturnType(); - SerializerMetaInfo fieldMetaInfo = new SerializerMetaInfo(); - fieldMetaInfo.setPropertyName(propertyName, false); - fieldMetaInfo.setPropertyInvoke(new MethodInvoke(method)); - - fieldMetaInfo.setSerializer(SerialStateMachine.getSerializerInCompiling(fieldClazz)); - fieldSet.add(fieldMetaInfo); - } - - for(Field field : clazz.getFields()) { // public字段序列化构造 - if(Modifier.isTransient(field.getModifiers()) || field.isAnnotationPresent(Transient.class)) - continue; - - field.setAccessible(true); - SerializerMetaInfo fieldMetaInfo = new SerializerMetaInfo(); - fieldMetaInfo.setPropertyName(field.getName(), false); - fieldMetaInfo.setPropertyInvoke(new FieldInvoke(field)); - fieldMetaInfo.setSerializer(SerialStateMachine.getSerializerInCompiling(field.getType())); - fieldSet.add(fieldMetaInfo); - } - - serializerMetaInfos = fieldSet.toArray(EMPTY_ARRAY); - if(serializerMetaInfos.length > 0) { - serializerMetaInfos[0].setPropertyName(serializerMetaInfos[0].getPropertyNameString(), true); - } - return serializerMetaInfos; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/exception/JsonException.java b/firefly-common/src/main/java/com/firefly/utils/json/exception/JsonException.java deleted file mode 100644 index 9bb3b0d9f..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/exception/JsonException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.utils.json.exception; - -public class JsonException extends RuntimeException { - - private static final long serialVersionUID = -6018684860739376818L; - - public JsonException(String msg) { - super(msg); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/ArrayParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/ArrayParser.java deleted file mode 100644 index b8e2d3d82..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/ArrayParser.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.List; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.support.JsonStringReader; -import com.firefly.utils.json.support.ParserMetaInfo; - -public class ArrayParser implements Parser { - - private ParserMetaInfo elementMetaInfo; - - public ArrayParser(Class clazz) { - elementMetaInfo = new ParserMetaInfo(); - elementMetaInfo.setType(clazz); - elementMetaInfo.setParser(ParserStateMachine.getParser(clazz)); - } - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - reader.mark(); - if(reader.isNull()) - return null; - else - reader.reset(); - - if(!reader.isArray()) - throw new JsonException("json string is not array format"); - - // 判断空数组 - reader.mark(); - char c0 = reader.readAndSkipBlank(); - if(c0 == ']') - return Array.newInstance(clazz, 0); - else - reader.reset(); - - List obj = new ArrayList(); - - for(;;) { - obj.add(elementMetaInfo.getValue(reader)); - - char ch = reader.readAndSkipBlank(); - if(ch == ']') - return copyOf(obj); - - if(ch != ',') - throw new JsonException("missing ','"); - } - } - - public Object copyOf(List list) { - Object ret = Array.newInstance(elementMetaInfo.getType(), list.size()); - for (int i = 0; i < list.size(); i++) { - Array.set(ret, i, list.get(i)); - } - return ret; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/BooleanParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/BooleanParser.java deleted file mode 100644 index 7517f293c..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/BooleanParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class BooleanParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return reader.readBoolean(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/ByteArrayParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/ByteArrayParser.java deleted file mode 100644 index 833510182..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/ByteArrayParser.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.codec.Base64; -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class ByteArrayParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return Base64.decodeFast(reader.readChars()); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/CharArrayParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/CharArrayParser.java deleted file mode 100644 index beecd71df..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/CharArrayParser.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class CharArrayParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - String ret = reader.readString(); - return ret != null ? ret.toCharArray() : null; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/CharacterParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/CharacterParser.java deleted file mode 100644 index 540a0abba..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/CharacterParser.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class CharacterParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - char ret = 0; - String s = reader.readString(); - - if(s.length() > 0) - ret = s.charAt(0); - return ret; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/CollectionParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/CollectionParser.java deleted file mode 100644 index bdf7d5487..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/CollectionParser.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.lang.reflect.Type; -import java.util.Collection; - -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.support.JsonStringReader; - -public class CollectionParser extends ComplexTypeParser { - - public CollectionParser(Type elementType) { - super(elementType); - } - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Object convertTo(JsonStringReader reader, Class clazz) { - reader.mark(); - if(reader.isNull()) - return null; - else - reader.reset(); - - if(!reader.isArray()) - throw new JsonException("json string is not array format"); - - Collection obj = null; - try { - obj = (Collection)clazz.newInstance(); - } catch (Throwable e) { - e.printStackTrace(); - } - - // 判断空数组 - reader.mark(); - char c0 = reader.readAndSkipBlank(); - if(c0 == ']') - return obj; - else - reader.reset(); - - for(;;) { - obj.add(elementMetaInfo.getValue(reader)); - - char ch = reader.readAndSkipBlank(); - if(ch == ']') - return obj; - - if(ch != ',') - throw new JsonException("missing ','"); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/ComplexTypeParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/ComplexTypeParser.java deleted file mode 100644 index 356fa4104..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/ComplexTypeParser.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.lang.reflect.Array; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.support.ParserMetaInfo; - -public abstract class ComplexTypeParser implements Parser { - - protected ParserMetaInfo elementMetaInfo; - - protected ComplexTypeParser(Type elementType) { - if(elementType instanceof ParameterizedType) { // 集合元素是参数类型 - ParameterizedType pt = (ParameterizedType)elementType; - Class rawClass = (Class) (pt.getRawType()); - elementMetaInfo = new ParserMetaInfo(); - - if(Collection.class.isAssignableFrom(rawClass)) { - Type[] types2 = pt.getActualTypeArguments(); - if(types2.length != 1) - throw new JsonException("collection actual type args length not equals 1"); - - Type eleType = types2[0]; - elementMetaInfo.setType(getImplClass(rawClass)); - elementMetaInfo.setParser(new CollectionParser(eleType)); - } else if (Map.class.isAssignableFrom(rawClass)) { - Type[] types2 = pt.getActualTypeArguments(); - if(types2.length != 2) - throw new JsonException("map actual type args length not equals 2"); - - Type key = types2[0]; - if (!((key instanceof Class) && key == String.class)) - throw new JsonException("map key type not string"); - - Type eleType = types2[1]; - elementMetaInfo.setType(getImplClass(rawClass)); - elementMetaInfo.setParser(new MapParser(eleType)); - } else { - elementMetaInfo.setType(rawClass); - elementMetaInfo.setParser(ParserStateMachine.getParser(rawClass)); - } - } else if (elementType instanceof Class) { - Class eleClass = (Class) elementType; // 获取集合元素Parser - elementMetaInfo = new ParserMetaInfo(); - elementMetaInfo.setType(eleClass); - elementMetaInfo.setParser(ParserStateMachine.getParser(eleClass)); - } else if(elementType instanceof GenericArrayType) { - GenericArrayType t = (GenericArrayType)elementType; - Class eleType = (Class)t.getGenericComponentType(); - Object obj = Array.newInstance(eleType, 0); - Class rawClass = obj.getClass(); - - elementMetaInfo = new ParserMetaInfo(); - elementMetaInfo.setType(rawClass); - elementMetaInfo.setParser(ParserStateMachine.getParser(rawClass)); - } else { - throw new JsonException("mot support type " + elementType); - } - } - - public ParserMetaInfo getElementMetaInfo() { - return elementMetaInfo; - } - - public static Class getImplClass(Class clazz) { - if(clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { - if(Collection.class.isAssignableFrom(clazz)) { - Class ret = ArrayList.class; - if(List.class.isAssignableFrom(clazz)) - ret = ArrayList.class; - else if(Queue.class.isAssignableFrom(clazz) || Deque.class.isAssignableFrom(clazz)) - ret = LinkedList.class; - else if(SortedSet.class.isAssignableFrom(clazz)) - ret = TreeSet.class; - else if(Set.class.isAssignableFrom(clazz)) - ret = HashSet.class; - return ret; - } else if(Map.class.isAssignableFrom(clazz)) { - Class ret = HashMap.class; - if(SortedMap.class.isAssignableFrom(clazz)) - ret = TreeMap.class; - return ret; - } - throw new JsonException("not support the type " + clazz); - } else - return clazz; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/DateParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/DateParser.java deleted file mode 100644 index 0a9d2bb3d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/DateParser.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class DateParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return SafeSimpleDateFormat.defaultDateFormat.parse(reader.readString()); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/DoubleParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/DoubleParser.java deleted file mode 100644 index 998a12b3c..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/DoubleParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class DoubleParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return reader.readDouble(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/EnumParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/EnumParser.java deleted file mode 100644 index ce95bcb64..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/EnumParser.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class EnumParser implements Parser { - - private EnumObj[] enumObjs; - - public EnumParser(Class clazz) { - List list = new ArrayList(); - Object[] o = clazz.getEnumConstants(); - enumObjs = new EnumObj[o.length]; - for (Object o1 : o) { - EnumObj enumObj = new EnumObj(); - enumObj.e = o1; - enumObj.key = ((Enum)o1).name().toCharArray(); - list.add(enumObj); - } - list.toArray(enumObjs); - } - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return find(reader.readChars()); - } - - private Object find(char[] key) { - for(EnumObj eo : enumObjs) { - if(Arrays.equals(eo.key, key)) - return eo.e; - } - return null; - } - - private class EnumObj { - Object e; - char[] key; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/FloatParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/FloatParser.java deleted file mode 100644 index 392c84436..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/FloatParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class FloatParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return reader.readFloat(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/IntParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/IntParser.java deleted file mode 100644 index 46a305965..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/IntParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class IntParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return reader.readInt(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/LongParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/LongParser.java deleted file mode 100644 index 95eebb44b..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/LongParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class LongParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return reader.readLong(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/MapParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/MapParser.java deleted file mode 100644 index e9860b0a1..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/MapParser.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.lang.reflect.Type; -import java.util.Map; - -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.support.JsonStringReader; - -public class MapParser extends ComplexTypeParser { - - public MapParser(Type elementType) { - super(elementType); - } - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Object convertTo(JsonStringReader reader, Class clazz) { - reader.mark(); - if(reader.isNull()) - return null; - else - reader.reset(); - - if(!reader.isObject()) - throw new JsonException("json string is not object format"); - - Map obj = null; - try { - obj = (Map)clazz.newInstance(); - } catch (Throwable e) { - e.printStackTrace(); - } - - // 判断空对象 - reader.mark(); - char c0 = reader.readAndSkipBlank(); - if(c0 == '}') - return obj; - else - reader.reset(); - - for(;;) { - String key = reader.readString(); - if(!reader.isColon()) - throw new JsonException("missing ':'"); - - obj.put(key, elementMetaInfo.getValue(reader)); - - char ch = reader.readAndSkipBlank(); - if(ch == '}') - return obj; - - if(ch != ',') - throw new JsonException("missing ','"); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/ObjectParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/ObjectParser.java deleted file mode 100644 index 8f2632a6b..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/ObjectParser.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.util.HashMap; -import java.util.Map; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.compiler.DecodeCompiler; -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.support.JsonStringReader; -import com.firefly.utils.json.support.ParserMetaInfo; - -public class ObjectParser implements Parser { - - private ParserMetaInfo[] parserMetaInfos; - private int max; - private Map map; - private boolean useMap; - - public void init(Class clazz) { - parserMetaInfos = DecodeCompiler.compile(clazz); - max = parserMetaInfos.length - 1; - if(max >= 8) { - map = new HashMap(); - for(ParserMetaInfo parserMetaInfo : parserMetaInfos) { - map.put(parserMetaInfo.getPropertyNameString(), parserMetaInfo); - } - useMap = true; - } - } - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - reader.mark(); - if(reader.isNull()) - return null; - else - reader.reset(); - - if(!reader.isObject()) - throw new JsonException("json string is not object format"); - - Object obj = null; - try { - obj = clazz.newInstance(); - } catch (Throwable e) { - e.printStackTrace(); - } - - // 判断空对象 - reader.mark(); - char c0 = reader.readAndSkipBlank(); - if(c0 == '}') - return obj; - else - reader.reset(); - - for (int i = 0;;i++) { - ParserMetaInfo parser = parserMetaInfos[i]; - char[] field = reader.readField(parser.getPropertyName()); - if(!reader.isColon()) - throw new JsonException("missing ':'"); - - if(field == null) { // 顺序相同,快速跳过 - parser.invoke(obj, reader); - } else { - ParserMetaInfo np = find(field); - if(np != null) - np.invoke(obj, reader); - else - reader.skipValue(); - } - - if(i == max) - break; - - char ch = reader.readAndSkipBlank(); - if(ch == '}') // json string 的域数量比元信息少,提前结束 - return obj; - - if(ch != ',') - throw new JsonException("missing ','"); - } - - char ch = reader.readAndSkipBlank(); - if(ch == '}') - return obj; - - if(ch != ',') - throw new JsonException("json string is not object format"); - - for(;;) { // json string 的域数量比元信息多,继续读取 - char[] field = reader.readChars(); - if(!reader.isColon()) - throw new JsonException("missing ':'"); - - ParserMetaInfo np = find(field); - if(np != null) - np.invoke(obj, reader); - else - reader.skipValue(); - - char c = reader.readAndSkipBlank(); - if(c == '}') // 读到末尾 - return obj; - - - if(c != ',') - throw new JsonException("missing ','"); - } - } - - private ParserMetaInfo find(char[] field) { - if(useMap) { - return map.get(new String(field)); - } else { - for(ParserMetaInfo parserMetaInfo : parserMetaInfos) { - if(parserMetaInfo.equals(field)) - return parserMetaInfo; - } - } - return null; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/ParserStateMachine.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/ParserStateMachine.java deleted file mode 100644 index 22e1a483b..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/ParserStateMachine.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.firefly.utils.json.parser; - -import java.util.Collection; -import java.util.Date; -import java.util.Map; - -import com.firefly.utils.collection.IdentityHashMap; -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.exception.JsonException; -import com.firefly.utils.json.support.JsonStringReader; - -public class ParserStateMachine { - - private static final IdentityHashMap, Parser> PARSER_MAP = new IdentityHashMap, Parser>(); - private static final Object LOCK = new Object(); - - static { - PARSER_MAP.put(int.class, new IntParser()); - PARSER_MAP.put(long.class, new LongParser()); - PARSER_MAP.put(short.class, new ShortParser()); - PARSER_MAP.put(float.class, new FloatParser()); - PARSER_MAP.put(double.class, new DoubleParser()); - PARSER_MAP.put(boolean.class, new BooleanParser()); - PARSER_MAP.put(char.class, new CharacterParser()); - - PARSER_MAP.put(Integer.class, PARSER_MAP.get(int.class)); - PARSER_MAP.put(Long.class, PARSER_MAP.get(long.class)); - PARSER_MAP.put(Short.class, PARSER_MAP.get(short.class)); - PARSER_MAP.put(Float.class, PARSER_MAP.get(float.class)); - PARSER_MAP.put(Double.class, PARSER_MAP.get(double.class)); - PARSER_MAP.put(Boolean.class, PARSER_MAP.get(boolean.class)); - PARSER_MAP.put(Character.class, PARSER_MAP.get(char.class)); - - PARSER_MAP.put(String.class, new StringParser()); - PARSER_MAP.put(Date.class, new DateParser()); - - PARSER_MAP.put(int[].class, new ArrayParser(int.class)); - PARSER_MAP.put(long[].class, new ArrayParser(long.class)); - PARSER_MAP.put(short[].class, new ArrayParser(short.class)); - PARSER_MAP.put(float[].class, new ArrayParser(float.class)); - PARSER_MAP.put(double[].class, new ArrayParser(double.class)); - PARSER_MAP.put(boolean[].class, new ArrayParser(boolean.class)); - PARSER_MAP.put(byte[].class, new ByteArrayParser()); - PARSER_MAP.put(char[].class, new CharArrayParser()); - - PARSER_MAP.put(Integer[].class, new ArrayParser(Integer.class)); - PARSER_MAP.put(Long[].class, new ArrayParser(Long.class)); - PARSER_MAP.put(Short[].class, new ArrayParser(Short.class)); - PARSER_MAP.put(Float[].class, new ArrayParser(Float.class)); - PARSER_MAP.put(Double[].class, new ArrayParser(Double.class)); - PARSER_MAP.put(Boolean[].class, new ArrayParser(Boolean.class)); - - PARSER_MAP.put(String[].class, new ArrayParser(String.class)); - } - - public static Parser getParser(Class clazz) { - Parser ret = PARSER_MAP.get(clazz); - if(ret == null) { - synchronized(LOCK) { - ret = PARSER_MAP.get(clazz); - if(ret == null) { - if (clazz.isEnum()) { - ret = new EnumParser(clazz); - PARSER_MAP.put(clazz, ret); - } else if (Collection.class.isAssignableFrom(clazz) - || Map.class.isAssignableFrom(clazz)) { - throw new JsonException("not support type " + clazz); - } else if (clazz.isArray()) { - Class elementClass = clazz.getComponentType(); - ret = new ArrayParser(elementClass); - PARSER_MAP.put(clazz, ret); - } else { - ret = new ObjectParser(); - PARSER_MAP.put(clazz, ret); - ((ObjectParser)ret).init(clazz); - } - } - } - } - return ret; - } - - @SuppressWarnings("unchecked") - public static T toObject(JsonStringReader reader, Class clazz) { - Parser parser = getParser(clazz); - return (T) parser.convertTo(reader, clazz); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/ShortParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/ShortParser.java deleted file mode 100644 index e898d57bd..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/ShortParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class ShortParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return (short) reader.readInt(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/parser/StringParser.java b/firefly-common/src/main/java/com/firefly/utils/json/parser/StringParser.java deleted file mode 100644 index f2597d519..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/parser/StringParser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.parser; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.support.JsonStringReader; - -public class StringParser implements Parser { - - @Override - public Object convertTo(JsonStringReader reader, Class clazz) { - return reader.readString(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ArraySerializer.java deleted file mode 100644 index 30f13283a..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ArraySerializer.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.ARRAY_PRE; -import static com.firefly.utils.json.JsonStringSymbol.ARRAY_SUF; -import static com.firefly.utils.json.JsonStringSymbol.SEPARATOR; -import static com.firefly.utils.json.JsonStringSymbol.EMPTY_ARRAY; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ArraySerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - Object[] objArray = (Object[]) obj; - int iMax = objArray.length - 1; - if (iMax == -1) { - writer.write(EMPTY_ARRAY); - return; - } - - writer.append(ARRAY_PRE); - for (int i = 0;; i++) { - SerialStateMachine.toJson(objArray[i], writer); - if (i == iMax) { - writer.append(ARRAY_SUF); - return; - } - writer.append(SEPARATOR); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/BoolSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/BoolSerializer.java deleted file mode 100644 index 50be4479c..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/BoolSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class BoolSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.writeBoolean((Boolean)obj); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/BooleanArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/BooleanArraySerializer.java deleted file mode 100644 index c62e2da9e..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/BooleanArraySerializer.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; - -import com.firefly.utils.json.support.JsonStringWriter; - -public class BooleanArraySerializer extends SimpleArraySerializer { - - public BooleanArraySerializer(boolean primitive) { - super(primitive); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - if(primitive) { - writer.writeBooleanArray((boolean[])obj); - } else { - writer.writeBooleanArray((Boolean[])obj); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ByteArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ByteArraySerializer.java deleted file mode 100644 index e6171356c..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ByteArraySerializer.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; - -import com.firefly.utils.codec.Base64; -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ByteArraySerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - writer.writeStringWithQuote(Base64.encodeToString((byte[])obj, false)); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ByteSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ByteSerializer.java deleted file mode 100644 index da54de452..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ByteSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ByteSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.writeByte((Byte)obj); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/CharArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/CharArraySerializer.java deleted file mode 100644 index 88816dd3a..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/CharArraySerializer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class CharArraySerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - writer.writeStringWithQuote(new String((char[])obj)); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/CharacterSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/CharacterSerializer.java deleted file mode 100644 index 04f3a5afa..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/CharacterSerializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.QUOTE; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class CharacterSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.append(QUOTE).append((Character)obj).append(QUOTE); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/CollectionSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/CollectionSerializer.java deleted file mode 100644 index d6ef45d64..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/CollectionSerializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.ARRAY_PRE; -import static com.firefly.utils.json.JsonStringSymbol.ARRAY_SUF; -import static com.firefly.utils.json.JsonStringSymbol.SEPARATOR; -import static com.firefly.utils.json.JsonStringSymbol.EMPTY_ARRAY; - -import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class CollectionSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - Collection collection = (Collection) obj; - if (collection.size() == 0) { - writer.write(EMPTY_ARRAY); - return; - } - - writer.append(ARRAY_PRE); - for (Iterator it = collection.iterator();;) { - SerialStateMachine.toJson(it.next(), writer); - if (!it.hasNext()) { - writer.append(ARRAY_SUF); - return; - } - writer.append(SEPARATOR); - } - - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/DateSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/DateSerializer.java deleted file mode 100644 index 5c4762a11..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/DateSerializer.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.QUOTE; - -import java.util.Date; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class DateSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.write(QUOTE + SafeSimpleDateFormat.defaultDateFormat.format((Date) obj) + QUOTE); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/DynamicObjectSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/DynamicObjectSerializer.java deleted file mode 100644 index 09303a44e..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/DynamicObjectSerializer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class DynamicObjectSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - SerialStateMachine.toJson(obj, writer); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/EnumSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/EnumSerializer.java deleted file mode 100644 index a8d9aa323..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/EnumSerializer.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.*; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class EnumSerializer implements Serializer { - - private EnumObj[] enumObjs; - - public EnumSerializer(Class clazz) { - List list = new ArrayList(); - Object[] o = clazz.getEnumConstants(); - enumObjs = new EnumObj[o.length]; - for (Object o1 : o) { - EnumObj enumObj = new EnumObj(); - enumObj.e = o1; - enumObj.value = (QUOTE + ((Enum)o1).name() + QUOTE).toCharArray(); - list.add(enumObj); - } - list.toArray(enumObjs); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - writer.write(find(obj).value); - } - - private EnumObj find(Object obj) { - for(EnumObj o : enumObjs) { - if(o.e == obj) - return o; - } - return null; - } - - private class EnumObj { - Object e; - char[] value; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/IntSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/IntSerializer.java deleted file mode 100644 index 405817933..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/IntSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class IntSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.writeInt((Integer)obj); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/IntegerArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/IntegerArraySerializer.java deleted file mode 100644 index 48953bf93..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/IntegerArraySerializer.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; -import com.firefly.utils.json.support.JsonStringWriter; - -public class IntegerArraySerializer extends SimpleArraySerializer{ - - public IntegerArraySerializer(boolean primitive) { - super(primitive); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - if(primitive) { - writer.writeIntArray((int[])obj); - } else { - writer.writeIntArray((Integer[])obj); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/LongArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/LongArraySerializer.java deleted file mode 100644 index 0dbfe2c35..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/LongArraySerializer.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; - -import com.firefly.utils.json.support.JsonStringWriter; - -public class LongArraySerializer extends SimpleArraySerializer { - - public LongArraySerializer(boolean primitive) { - super(primitive); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - if(primitive) { - writer.writeLongArray((long[])obj); - } else { - writer.writeLongArray((Long[])obj); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/LongSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/LongSerializer.java deleted file mode 100644 index 0e845c004..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/LongSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class LongSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.writeLong((Long)obj); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/MapSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/MapSerializer.java deleted file mode 100644 index f444d5bbf..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/MapSerializer.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.OBJ_PRE; -import static com.firefly.utils.json.JsonStringSymbol.OBJ_SUF; -import static com.firefly.utils.json.JsonStringSymbol.SEPARATOR; -import static com.firefly.utils.json.JsonStringSymbol.EMPTY_ARRAY; - -import java.io.IOException; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class MapSerializer implements Serializer { - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - Map map = (Map) obj; - Set> entrySet = map.entrySet(); - if (entrySet.size() == 0) { - writer.write(EMPTY_ARRAY); - return; - } - - writer.append(OBJ_PRE); - for (Iterator> it = entrySet.iterator();;) { - Entry entry = it.next(); - writer.write("\"" + entry.getKey() + "\":"); - SerialStateMachine.toJson(entry.getValue(), writer); - if (!it.hasNext()) { - writer.append(OBJ_SUF); - return; - } - writer.append(SEPARATOR); - } - - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ObjectNoCheckSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ObjectNoCheckSerializer.java deleted file mode 100644 index e30a89e69..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ObjectNoCheckSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.OBJ_PRE; -import static com.firefly.utils.json.JsonStringSymbol.OBJ_SUF; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.compiler.EncodeCompiler; -import com.firefly.utils.json.support.SerializerMetaInfo; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ObjectNoCheckSerializer implements Serializer { - - private SerializerMetaInfo[] serializerMetaInfos; - - public ObjectNoCheckSerializer(Class clazz) { - serializerMetaInfos = EncodeCompiler.compile(clazz); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - writer.append(OBJ_PRE); - for(SerializerMetaInfo metaInfo : serializerMetaInfos){ - writer.write(metaInfo.getPropertyName()); - metaInfo.toJson(obj, writer); - } - writer.append(OBJ_SUF); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ObjectSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ObjectSerializer.java deleted file mode 100644 index 8e719478d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ObjectSerializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.firefly.utils.json.serializer; - -import static com.firefly.utils.json.JsonStringSymbol.OBJ_PRE; -import static com.firefly.utils.json.JsonStringSymbol.OBJ_SUF; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.compiler.EncodeCompiler; -import com.firefly.utils.json.support.SerializerMetaInfo; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ObjectSerializer implements Serializer { - - private SerializerMetaInfo[] serializerMetaInfos; - - public ObjectSerializer(Class clazz) { - serializerMetaInfos = EncodeCompiler.compile(clazz); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) throws IOException { - if (writer.existRef(obj)) { // 防止循环引用,此处会影响一些性能 - writer.writeNull(); - return; - } - - writer.pushRef(obj); - writer.append(OBJ_PRE); - for(SerializerMetaInfo metaInfo : serializerMetaInfos){ - writer.write(metaInfo.getPropertyName()); - metaInfo.toJson(obj, writer); - } - writer.append(OBJ_SUF); - writer.popRef(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/SerialStateMachine.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/SerialStateMachine.java deleted file mode 100644 index 73f48707c..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/SerialStateMachine.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Collection; -import java.util.Date; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -import com.firefly.utils.collection.IdentityHashMap; -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.annotation.CircularReferenceCheck; -import com.firefly.utils.json.support.JsonStringWriter; - -abstract public class SerialStateMachine { - private static final IdentityHashMap, Serializer> SERIAL_MAP = new IdentityHashMap, Serializer>(); - - private static final Serializer MAP = new MapSerializer(); - private static final Serializer COLLECTION = new CollectionSerializer(); - private static final Serializer ARRAY = new ArraySerializer(); - private static final DynamicObjectSerializer DYNAMIC = new DynamicObjectSerializer(); - - static { - SERIAL_MAP.put(long.class, new LongSerializer()); - SERIAL_MAP.put(int.class, new IntSerializer()); - SERIAL_MAP.put(char.class, new CharacterSerializer()); - SERIAL_MAP.put(short.class, new ShortSerializer()); - SERIAL_MAP.put(byte.class, new ByteSerializer()); - SERIAL_MAP.put(boolean.class, new BoolSerializer()); - SERIAL_MAP.put(String.class, new StringSerializer()); - SERIAL_MAP.put(Date.class, new DateSerializer()); - SERIAL_MAP.put(double.class, new StringValueSerializer()); - SERIAL_MAP.put(long[].class, new LongArraySerializer(true)); - SERIAL_MAP.put(int[].class, new IntegerArraySerializer(true)); - SERIAL_MAP.put(short[].class, new ShortArraySerializer(true)); - SERIAL_MAP.put(boolean[].class, new BooleanArraySerializer(true)); - SERIAL_MAP.put(String[].class, new StringArraySerializer()); - SERIAL_MAP.put(byte[].class, new ByteArraySerializer()); - SERIAL_MAP.put(char[].class, new CharArraySerializer()); - - SERIAL_MAP.put(Long.class, SERIAL_MAP.get(long.class)); - SERIAL_MAP.put(Integer.class, SERIAL_MAP.get(int.class)); - SERIAL_MAP.put(Character.class, SERIAL_MAP.get(char.class)); - SERIAL_MAP.put(Short.class, SERIAL_MAP.get(short.class)); - SERIAL_MAP.put(Byte.class, SERIAL_MAP.get(byte.class)); - SERIAL_MAP.put(Boolean.class, SERIAL_MAP.get(boolean.class)); - SERIAL_MAP.put(Long[].class, new LongArraySerializer(false)); - SERIAL_MAP.put(Integer[].class, new IntegerArraySerializer(false)); - SERIAL_MAP.put(Short[].class, new ShortArraySerializer(false)); - SERIAL_MAP.put(Boolean[].class, new BooleanArraySerializer(false)); - - SERIAL_MAP.put(StringBuilder.class, SERIAL_MAP.get(String.class)); - SERIAL_MAP.put(StringBuffer.class, SERIAL_MAP.get(String.class)); - - SERIAL_MAP.put(java.sql.Date.class, SERIAL_MAP.get(Date.class)); - SERIAL_MAP.put(java.sql.Time.class, SERIAL_MAP.get(Date.class)); - SERIAL_MAP.put(java.sql.Timestamp.class, SERIAL_MAP.get(Date.class)); - - SERIAL_MAP.put(Double.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(float.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(Float.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(AtomicInteger.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(AtomicLong.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(BigDecimal.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(BigInteger.class, SERIAL_MAP.get(double.class)); - SERIAL_MAP.put(AtomicBoolean.class, SERIAL_MAP.get(double.class)); - } - - public static Serializer getSerializer(Class clazz) { - Serializer ret = SERIAL_MAP.get(clazz); - if (ret == null) { - if (clazz.isEnum()) - ret = new EnumSerializer(clazz); - else if (Map.class.isAssignableFrom(clazz)) - ret = MAP; - else if (Collection.class.isAssignableFrom(clazz)) - ret = COLLECTION; - else if (clazz.isArray()) - ret = ARRAY; - else - ret = clazz.isAnnotationPresent(CircularReferenceCheck.class) ? new ObjectSerializer( - clazz) : new ObjectNoCheckSerializer(clazz); - SERIAL_MAP.put(clazz, ret); - } - return ret; - } - - public static Serializer getSerializerInCompiling(Class clazz) { - Serializer ret = SERIAL_MAP.get(clazz); - if (ret == null || ret instanceof ObjectSerializer || ret instanceof ObjectNoCheckSerializer) { - if (clazz.isEnum()) { - ret = new EnumSerializer(clazz); - SERIAL_MAP.put(clazz, ret); - } else if (Map.class.isAssignableFrom(clazz)) { - ret = MAP; - SERIAL_MAP.put(clazz, ret); - } else if (Collection.class.isAssignableFrom(clazz)) { - ret = COLLECTION; - SERIAL_MAP.put(clazz, ret); - } else if (clazz.isArray()) { - ret = ARRAY; - SERIAL_MAP.put(clazz, ret); - } else - ret = DYNAMIC; - } - return ret; - } - - public static void toJson(Object obj, JsonStringWriter writer) - throws IOException { - if (obj == null) { - writer.writeNull(); - return; - } - - Class clazz = obj.getClass(); - Serializer serializer = getSerializer(clazz); - serializer.convertTo(writer, obj); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ShortArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ShortArraySerializer.java deleted file mode 100644 index 620135767..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ShortArraySerializer.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ShortArraySerializer extends SimpleArraySerializer { - - public ShortArraySerializer(boolean primitive) { - super(primitive); - } - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - if(primitive) { - writer.writeShortArray((short[])obj); - } else { - writer.writeShortArray((Short[])obj); - } - - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ShortSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/ShortSerializer.java deleted file mode 100644 index f9142c47d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/ShortSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class ShortSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.writeShort((Short)obj); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/SimpleArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/SimpleArraySerializer.java deleted file mode 100644 index 9cdbdd1ce..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/SimpleArraySerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; - -public abstract class SimpleArraySerializer implements Serializer { - - protected boolean primitive; - - public SimpleArraySerializer(boolean primitive) { - this.primitive = primitive; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringArraySerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringArraySerializer.java deleted file mode 100644 index 2dfd97851..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringArraySerializer.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.utils.json.serializer; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class StringArraySerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) - throws IOException { - String[] object = (String[])obj; - writer.writeStringArray(object); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringSerializer.java deleted file mode 100644 index f7562cb70..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class StringSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.writeStringWithQuote(obj.toString()); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringValueSerializer.java b/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringValueSerializer.java deleted file mode 100644 index f4db57b84..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/serializer/StringValueSerializer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.utils.json.serializer; - -import com.firefly.utils.json.Serializer; -import com.firefly.utils.json.support.JsonStringWriter; - -public class StringValueSerializer implements Serializer { - - @Override - public void convertTo(JsonStringWriter writer, Object obj) { - writer.write(String.valueOf(obj)); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/FieldInvoke.java b/firefly-common/src/main/java/com/firefly/utils/json/support/FieldInvoke.java deleted file mode 100644 index 54a30bd31..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/FieldInvoke.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.firefly.utils.json.support; - -import java.lang.reflect.Field; - -public class FieldInvoke implements PropertyInvoke { - - private Field field; - - public FieldInvoke(Field field) { - this.field = field; - } - - - - @Override - public Object get(Object obj) { - Object ret = null; - try { - ret = field.get(obj); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - return ret; - } - - - - @Override - public void set(Object obj, Object arg) { - try { - field.set(obj, arg); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/JsonStringReader.java b/firefly-common/src/main/java/com/firefly/utils/json/support/JsonStringReader.java deleted file mode 100644 index 284f2dcf6..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/JsonStringReader.java +++ /dev/null @@ -1,505 +0,0 @@ -package com.firefly.utils.json.support; - -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.io.StringWriter; -import com.firefly.utils.json.exception.JsonException; - -public class JsonStringReader { - private char[] chars; - private int pos = 0; - private final int limit; - private int mark = 0; - - public JsonStringReader(String str) { - chars = str.toCharArray(); - limit = chars.length; - } - - public int getMark() { - return mark; - } - - public void mark() { - mark = pos; - } - - public void reset() { - pos = mark; - } - - public char get(int index) { - return chars[index]; - } - - public int position() { - return pos; - } - - public int limit() { - return limit; - } - - public boolean isEndFlag(char ch) { - switch (ch) { - case ',': - case '}': - case ']': - case ' ': - case ':': - return true; - } - return false; - } - - public boolean isString() { - char c = readAndSkipBlank(); - return c == '"'; - } - - public boolean isArray() { - char c = readAndSkipBlank(); - return c == '['; - } - - public boolean isObject() { - char c = readAndSkipBlank(); - return c == '{'; - } - - public boolean isObjectEnd() { - char c = readAndSkipBlank(); - return c == '}'; - } - - public boolean isColon() { - char c = readAndSkipBlank(); - return c == ':'; - } - - public boolean isComma() { - char c = readAndSkipBlank(); - return c == ','; - } - - public boolean isNull() { - char ch = readAndSkipBlank(); - if(pos + 3 > limit) - return false; - - if(ch == 'n' && 'u' == read() && 'l' == read() && 'l' == read()) { - if(pos >= limit) - return true; - - ch = readAndSkipBlank(); - if(isEndFlag(ch)) { - pos--; - return true; - } else - return false; - } else - return false; - } - - public char read() { - return chars[pos++]; - } - - public char readAndSkipBlank() { - char c = read(); - if (c > ' ') - return c; - for (;;) { - c = read(); - if (c > ' ') - return c; - } - } - - public boolean readBoolean() { - boolean ret = false; - - mark(); - if(isNull()) - return ret; - else - reset(); - - char ch = readAndSkipBlank(); - boolean isString = (ch == '"'); - if(isString) - ch = readAndSkipBlank(); - - if(ch == 't' && 'r' == read() && 'u' == read() && 'e' == read()) - ret = true; - else if (ch == 'f' && 'a' == read() && 'l' == read() && 's' == read() && 'e' == read()) - ret = false; - - if(isString) { - ch = readAndSkipBlank(); - if(ch != '"') - throw new JsonException("read boolean error"); - } - - return ret; - } - - public int readInt() { - int value = 0; - mark(); - if(isNull()) - return value; - else - reset(); - - char ch = readAndSkipBlank(); - boolean isString = (ch == '"'); - if(isString) - ch = readAndSkipBlank(); - boolean negative = (ch == '-'); - - if(!negative) { - if(VerifyUtils.isDigit(ch)) - value = (value << 3) + (value << 1) + (ch - '0'); - else - throw new JsonException("read int error, charactor \"" + ch + "\" is not integer"); - } - - for(;;) { - ch = read(); - if(VerifyUtils.isDigit(ch)) - value = (value << 3) + (value << 1) + (ch - '0'); - else { - if(isString) { - if(ch == '"') - break; - } else { - if (isEndFlag(ch)) { - pos--; - break; - } else - throw new JsonException("read int error, charactor \"" + ch + "\" is not integer"); - } - } - - if(pos >= limit) - break; - } - return negative ? -value : value; - } - - public long readLong() { - long value = 0; - mark(); - if(isNull()) - return value; - else - reset(); - - char ch = readAndSkipBlank(); - boolean isString = (ch == '"'); - if(isString) - ch = readAndSkipBlank(); - boolean negative = (ch == '-'); - - if(!negative) { - if(VerifyUtils.isDigit(ch)) - value = (value << 3) + (value << 1) + (ch - '0'); - else - throw new JsonException("read int error, charactor \"" + ch + "\" is not integer"); - } - - for(;;) { - ch = read(); - if(VerifyUtils.isDigit(ch)) - value = (value << 3) + (value << 1) + (ch - '0'); - else { - if(isString) { - if(ch == '"') - break; - } else { - if (isEndFlag(ch)) { - pos--; - break; - } else - throw new JsonException("read int error, charactor \"" + ch + "\" is not integer"); - } - } - - if(pos >= limit) - break; - } - return negative ? -value : value; - } - - public double readDouble() { - double value = 0.0; - mark(); - if(isNull()) - return value; - else - reset(); - - char ch = readAndSkipBlank(); - boolean isString = (ch == '"'); - if(isString) - ch = readAndSkipBlank(); - pos--; - - int start = pos; - for(;;) { - ch = read(); - if(isString) { - if(ch == '"') - break; - } else { - if (isEndFlag(ch)) { - pos--; - break; - } - } - } - - int len = isString ? pos - start - 1 : pos - start; - String temp = new String(chars, start, len); - return Double.parseDouble(temp); - } - - public float readFloat() { - float value = 0.0F; - mark(); - if(isNull()) - return value; - else - reset(); - - char ch = readAndSkipBlank(); - boolean isString = (ch == '"'); - if(isString) - ch = readAndSkipBlank(); - pos--; - - int start = pos; - for(;;) { - ch = read(); - if(isString) { - if(ch == '"') - break; - } else { - if (isEndFlag(ch)) { - pos--; - break; - } - } - } - - int len = isString ? pos - start - 1 : pos - start; - String temp = new String(chars, start, len); - return Float.parseFloat(temp); - } - - public char[] readField(char[] chs) { - if(!isString()) - throw new JsonException("read field error"); - - int cur = pos; - int len = chs.length; - boolean skip = true; - - int next = pos + len; - if(next < limit && chars[next] == '"') { - for (int i = 0; i < len; i++) { - if (chs[i] != chars[cur++]) { - skip = false; - break; - } - } - } else { - skip = false; - } - - if (skip) { - pos = cur + 1; - return null; - } else { - char[] field = null; - int start = pos; - for(;;) { - char c = read(); - if(c == '"') - break; - } - int fieldLen = pos - 1 - start; - field = new char[fieldLen]; - System.arraycopy(chars, start, field, 0, fieldLen); - return field; - } - } - - public char[] readChars() { - if(!isString()) - throw new JsonException("read field error"); - - int start = pos; - for(;;) { - char c = read(); - if(c == '"') - break; - } - int fieldLen = pos - 1 - start; - char[] c = new char[fieldLen]; - System.arraycopy(chars, start, c, 0, fieldLen); - return c; - } - - public void skipValue() { - char ch = readAndSkipBlank(); - switch (ch) { - case '"': // 跳过字符串 - for(;;) { - ch = read(); - if(ch == '"') - break; - else if(ch == '\\') - pos++; - } - break; - case '[': // 跳过数组 - for(;;) { - // 判断空数组 - mark(); - ch = readAndSkipBlank(); - if(ch == ']') - break; - else - reset(); - - skipValue(); - ch = readAndSkipBlank(); - if(ch == ']') - break; - - if(ch != ',') - throw new JsonException("json string array format error"); - } - break; - case '{': // 跳过对象 - for(;;) { - // 判断空对象 - mark(); - ch = readAndSkipBlank(); - if(ch == '}') - break; - else - reset(); - - readChars(); - if(!isColon()) - throw new JsonException("json string object format error"); - - skipValue(); - ch = readAndSkipBlank(); - if(ch == '}') - break; - - if(ch != ',') - throw new JsonException("json string object format error"); - } - break; - - default: // 跳过数字或者null - for(;;) { - ch = read(); - if(isEndFlag(ch)) { - pos--; - break; - } - } - break; - } - } - - public String readString() { - mark(); - if(isNull()) - return null; - else - reset(); - - if(!isString()) - throw new JsonException("read string error"); - - StringWriter writer = new StringWriter(); - String ret = null; - - int cur = pos; - int len = 0; - for(;;) { - char ch = chars[cur++]; - if(ch == '"') { - len = cur - pos - 1; - writer.write(chars, pos, len); - pos = cur; - break; - } else if(ch == '\\') { - switch (chars[cur++]) { - case 'b': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('\b'); - pos = cur; - break; - case 'n': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('\n'); - pos = cur; - break; - case 'r': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('\r'); - pos = cur; - break; - case 'f': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('\f'); - pos = cur; - break; - case '\\': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('\\'); - pos = cur; - break; - case '/': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('/'); - pos = cur; - break; - case '"': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('"'); - pos = cur; - break; - case 't': - len = cur - 2 - pos; - writer.write(chars, pos, len); - writer.write('\t'); - pos = cur; - break; - } - } - - } - try { - ret = writer.toString(); - } finally { - writer.close(); - } - return ret; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/JsonStringWriter.java b/firefly-common/src/main/java/com/firefly/utils/json/support/JsonStringWriter.java deleted file mode 100644 index be2f7e6ba..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/JsonStringWriter.java +++ /dev/null @@ -1,448 +0,0 @@ -package com.firefly.utils.json.support; - -import java.util.Deque; -import java.util.LinkedList; - -import com.firefly.utils.io.IOUtils; -import com.firefly.utils.io.StringWriter; -import static com.firefly.utils.json.JsonStringSymbol.QUOTE; -import static com.firefly.utils.json.JsonStringSymbol.ARRAY_PRE; -import static com.firefly.utils.json.JsonStringSymbol.ARRAY_SUF; -import static com.firefly.utils.json.JsonStringSymbol.SEPARATOR; - -public class JsonStringWriter extends StringWriter { - - private Deque deque = new LinkedList(); - - public void pushRef(Object obj) { - deque.addFirst(obj); - } - - public boolean existRef(Object obj) { - return deque.contains(obj); - } - - public void popRef() { - deque.removeFirst(); - } - - private void writeJsonString0(String value) { - buf[count++] = QUOTE; - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - switch (ch) { - case '\b': - buf[count++] = '\\'; - buf[count++] = 'b'; - break; - case '\n': - buf[count++] = '\\'; - buf[count++] = 'n'; - break; - case '\r': - buf[count++] = '\\'; - buf[count++] = 'r'; - break; - case '\f': - buf[count++] = '\\'; - buf[count++] = 'f'; - break; - case '\\': - buf[count++] = '\\'; - buf[count++] = '\\'; - break; - case '/': - buf[count++] = '\\'; - buf[count++] = '/'; - break; - case '"': - buf[count++] = '\\'; - buf[count++] = '"'; - break; - case '\t': - buf[count++] = '\\'; - buf[count++] = 't'; - break; - - default: - buf[count++] = ch; - break; - } - } - buf[count++] = QUOTE; - } - - private void writeJsonString0NoFilter(String value) { - int len = value.length(); - buf[count++] = QUOTE; - value.getChars(0, len, buf, count); - count += len; - buf[count++] = QUOTE; - } - - public void writeStringWithQuoteNoFilter(String value) { - int newcount = count + value.length() + 2; - if (newcount > buf.length) { - expandCapacity(newcount); - } - writeJsonString0NoFilter(value); - } - - public void writeStringArrayNoFilter(String[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - - int iMax = arrayLen - 1; - int totalSize = 2; - for (int i = 0; i < arrayLen; i++) { - totalSize += array[i].length() + 2 + 1; - } - - int newcount = count + totalSize; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0; ; ++i) { - writeJsonString0NoFilter(array[i]); - if (i == iMax) { - buf[count++] = ARRAY_SUF; - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeStringWithQuote(String value) { - int newcount = count + value.length() * 2 + 2; - if (newcount > buf.length) { - expandCapacity(newcount); - } - writeJsonString0(value); - } - - public void writeStringArray(String[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - - int iMax = arrayLen - 1; - int totalSize = 2; - for (int i = 0; i < arrayLen; i++) { - totalSize += array[i].length() * 2 + 2 + 1; - } - - int newcount = count + totalSize; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0; ; ++i) { - writeJsonString0(array[i]); - if (i == iMax) { - buf[count++] = ARRAY_SUF; - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeIntArray(int[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - // System.out.println("current count: " + count); - int elementMaxLen = MIN_INT_VALUE.length; - int newcount = count + (elementMaxLen + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - int val = array[i]; - if (val == Integer.MIN_VALUE) { - System.arraycopy(MIN_INT_VALUE, 0, buf, count, elementMaxLen); - count += elementMaxLen; - } else { - count += (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils - .stringSize(val); - IOUtils.getChars(val, count, buf); - } - - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeIntArray(Integer[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - // System.out.println("current count: " + count); - int elementMaxLen = MIN_INT_VALUE.length; - int newcount = count + (elementMaxLen + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - int val = array[i]; - if (val == Integer.MIN_VALUE) { - System.arraycopy(MIN_INT_VALUE, 0, buf, count, elementMaxLen); - count += elementMaxLen; - } else { - count += (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils - .stringSize(val); - IOUtils.getChars(val, count, buf); - } - - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeShortArray(short[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - // System.out.println("current count: " + count); - int elementMaxLen = MIN_INT_VALUE.length; - int newcount = count + (elementMaxLen + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - int val = array[i]; - if (val == Integer.MIN_VALUE) { - System.arraycopy(MIN_INT_VALUE, 0, buf, count, elementMaxLen); - count += elementMaxLen; - } else { - count += (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils - .stringSize(val); - IOUtils.getChars(val, count, buf); - } - - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeShortArray(Short[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - // System.out.println("current count: " + count); - int elementMaxLen = MIN_INT_VALUE.length; - int newcount = count + (elementMaxLen + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - int val = array[i]; - if (val == Integer.MIN_VALUE) { - System.arraycopy(MIN_INT_VALUE, 0, buf, count, elementMaxLen); - count += elementMaxLen; - } else { - count += (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils - .stringSize(val); - IOUtils.getChars(val, count, buf); - } - - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeLongArray(long[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - // System.out.println("current count: " + count); - int elementMaxLen = MIN_LONG_VALUE.length; - int newcount = count + (elementMaxLen + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - long val = array[i]; - if (val == Long.MIN_VALUE) { - System.arraycopy(MIN_LONG_VALUE, 0, buf, count, elementMaxLen); - count += elementMaxLen; - } else { - count += (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils - .stringSize(val); - IOUtils.getChars(val, count, buf); - } - - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeLongArray(Long[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - // System.out.println("current count: " + count); - int elementMaxLen = MIN_LONG_VALUE.length; - int newcount = count + (elementMaxLen + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - long val = array[i]; - if (val == Long.MIN_VALUE) { - System.arraycopy(MIN_LONG_VALUE, 0, buf, count, elementMaxLen); - count += elementMaxLen; - } else { - count += (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils - .stringSize(val); - IOUtils.getChars(val, count, buf); - } - - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeBooleanArray(boolean[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - int newcount = count + (5 + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - // System.out.println("current count: " + count); - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - if (array[i]) { - buf[count++] = 't'; - buf[count++] = 'r'; - buf[count++] = 'u'; - buf[count++] = 'e'; - } else { - buf[count++] = 'f'; - buf[count++] = 'a'; - buf[count++] = 'l'; - buf[count++] = 's'; - buf[count++] = 'e'; - } - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } - - public void writeBooleanArray(Boolean[] array) { - int arrayLen = array.length; - if (arrayLen == 0) { - buf[count++] = ARRAY_PRE; - buf[count++] = ARRAY_SUF; - return; - } - int iMax = arrayLen - 1; - int newcount = count + (5 + 1) * arrayLen + 2 - 1; - if (newcount > buf.length) { - expandCapacity(newcount); - } - - // System.out.println("current count: " + count); - buf[count++] = ARRAY_PRE; - for (int i = 0;; i++) { - if (array[i]) { - buf[count++] = 't'; - buf[count++] = 'r'; - buf[count++] = 'u'; - buf[count++] = 'e'; - } else { - buf[count++] = 'f'; - buf[count++] = 'a'; - buf[count++] = 'l'; - buf[count++] = 's'; - buf[count++] = 'e'; - } - if (i == iMax) { - buf[count++] = ARRAY_SUF; - // System.out.println("current count: " + count); - return; - } - buf[count++] = SEPARATOR; - } - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/MetaInfo.java b/firefly-common/src/main/java/com/firefly/utils/json/support/MetaInfo.java deleted file mode 100644 index 9ec3ed90d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/MetaInfo.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.firefly.utils.json.support; - -public class MetaInfo implements Comparable { - protected PropertyInvoke propertyInvoke; - protected char[] propertyName; - protected String propertyNameString; - - public PropertyInvoke getPropertyInvoke() { - return propertyInvoke; - } - - public void setPropertyInvoke(PropertyInvoke propertyInvoke) { - this.propertyInvoke = propertyInvoke; - } - - public char[] getPropertyName() { - return propertyName; - } - - public String getPropertyNameString() { - return propertyNameString; - } - - @Override - public int compareTo(MetaInfo o) { - return propertyNameString.compareTo(o.getPropertyNameString()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime - * result - + ((propertyNameString == null) ? 0 : propertyNameString - .hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - MetaInfo other = (MetaInfo) obj; - if (propertyNameString == null) { - if (other.propertyNameString != null) - return false; - } else if (!propertyNameString.equals(other.propertyNameString)) - return false; - return true; - } - - @Override - public String toString() { - return propertyNameString; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/MethodInvoke.java b/firefly-common/src/main/java/com/firefly/utils/json/support/MethodInvoke.java deleted file mode 100644 index 500d25a53..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/MethodInvoke.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.firefly.utils.json.support; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class MethodInvoke implements PropertyInvoke { - - private Method method; - - public MethodInvoke(Method method) { - this.method = method; - } - - @Override - public void set(Object obj, Object arg) { - try { - method.invoke(obj, arg); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } - - @Override - public Object get(Object obj) { - Object ret = null; - try { - ret = method.invoke(obj); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - return ret; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/ParserMetaInfo.java b/firefly-common/src/main/java/com/firefly/utils/json/support/ParserMetaInfo.java deleted file mode 100644 index a5b8ac48d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/ParserMetaInfo.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.firefly.utils.json.support; - -import java.util.Arrays; - -import com.firefly.utils.json.Parser; - -public class ParserMetaInfo extends MetaInfo { - - private Class type; - private Parser parser; - - public void invoke(Object obj, JsonStringReader reader) { - try { - propertyInvoke.set(obj, getValue(reader)); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - public void setPropertyNameString(String propertyNameString) { - this.propertyNameString = propertyNameString; - propertyName = propertyNameString.toCharArray(); - } - - public Object getValue(JsonStringReader reader) { - return parser.convertTo(reader, type); - } - - public boolean equals(char[] field) { - return Arrays.equals(propertyName, field); - } - - public Class getType() { - return type; - } - - public void setType(Class type) { - this.type = type; - } - - public Parser getParser() { - return parser; - } - - public void setParser(Parser parser) { - this.parser = parser; - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/PropertyInvoke.java b/firefly-common/src/main/java/com/firefly/utils/json/support/PropertyInvoke.java deleted file mode 100644 index 67a058ce0..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/PropertyInvoke.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.firefly.utils.json.support; - -public interface PropertyInvoke { - - void set(Object obj, Object arg); - - Object get(Object obj); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/json/support/SerializerMetaInfo.java b/firefly-common/src/main/java/com/firefly/utils/json/support/SerializerMetaInfo.java deleted file mode 100644 index 6884a048a..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/json/support/SerializerMetaInfo.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.firefly.utils.json.support; - -import static com.firefly.utils.json.JsonStringSymbol.OBJ_SEPARATOR; -import static com.firefly.utils.json.JsonStringSymbol.QUOTE; - -import java.io.IOException; - -import com.firefly.utils.json.Serializer; - -public class SerializerMetaInfo extends MetaInfo { - - private Serializer serializer; - - public void setPropertyName(String propertyName, boolean first) { - propertyNameString = propertyName; - this.propertyName = ((first ? "" : ",") + QUOTE + propertyName + QUOTE + OBJ_SEPARATOR).toCharArray(); - } - - public void setSerializer(Serializer serializer) { - this.serializer = serializer; - } - - public Serializer getSerializer() { - return serializer; - } - - public void toJson(Object obj, JsonStringWriter writer) - throws IOException { - Object ret = propertyInvoke.get(obj); - if(ret == null) { - writer.writeNull(); - return; - } - serializer.convertTo(writer, ret); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/Log.java b/firefly-common/src/main/java/com/firefly/utils/log/Log.java deleted file mode 100644 index 2c5d4e823..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/Log.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.firefly.utils.log; - -public interface Log { - int TRACE = 0; - int DEBUG = 1; - int INFO = 2; - int WARN = 3; - int ERROR = 4; - String CL = "\r\n"; - - boolean isTraceEnable(); - - void trace(String str); - - void trace(String str, Object... objs); - - void trace(String str, Throwable throwable, Object... objs); - - boolean isDebugEnable(); - - void debug(String str); - - void debug(String str, Object... objs); - - void debug(String str, Throwable throwable, Object... objs); - - boolean isInfoEnable(); - - void info(String str); - - void info(String str, Object... objs); - - void info(String str, Throwable throwable, Object... objs); - - boolean isWarnEnable(); - - void warn(String str); - - void warn(String str, Object... objs); - - void warn(String str, Throwable throwable, Object... objs); - - boolean isErrorEnable(); - - void error(String str); - - void error(String str, Object... objs); - - void error(String str, Throwable throwable, Object... objs); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/LogException.java b/firefly-common/src/main/java/com/firefly/utils/log/LogException.java deleted file mode 100644 index ca32af424..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/LogException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.utils.log; - -public class LogException extends RuntimeException { - - private static final long serialVersionUID = -4932202708783665424L; - - public LogException(String msg) { - super(msg); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/LogFactory.java b/firefly-common/src/main/java/com/firefly/utils/log/LogFactory.java deleted file mode 100644 index 7033f05fa..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/LogFactory.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.firefly.utils.log; - -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.Map.Entry; - -import com.firefly.utils.StringUtils; -import com.firefly.utils.log.file.FileLog; -import com.firefly.utils.log.file.FileLogTask; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class LogFactory { - private Map logMap = new HashMap(); - private Map levelMap = new HashMap(); - public static final SafeSimpleDateFormat dayDateFormat = new SafeSimpleDateFormat( - "yyyy-MM-dd"); - private LogTask logTask = new FileLogTask(); - - private static class Holder { - private static LogFactory instance = new LogFactory(); - } - - public static LogFactory getInstance() { - return Holder.instance; - } - - private LogFactory() { - levelMap.put("TRACE", Log.TRACE); - levelMap.put("DEBUG", Log.DEBUG); - levelMap.put("INFO", Log.INFO); - levelMap.put("WARN", Log.WARN); - levelMap.put("ERROR", Log.ERROR); - defaultLog(); - - File configFile = null; - try { - configFile = new File(LogFactory.class.getClassLoader() - .getResource("firefly-log.properties").toURI()); - } catch (URISyntaxException e1) { - e1.printStackTrace(); - } - if (configFile != null && configFile.exists()) { - loadProperties(); - } - logTask.start(); - } - - private void defaultLog() { - String name = "firefly-system"; - FileLog fileLog = new FileLog(); - fileLog.setName(name); - fileLog.setLevel(2); - fileLog.setFileOutput(false); - fileLog.setConsoleOutput(true); - System.out.println(name + "|console"); - logMap.put(name, fileLog); - } - - private void loadProperties() { - Properties properties = new Properties(); - try { - properties.load(LogFactory.class.getClassLoader() - .getResourceAsStream("firefly-log.properties")); - } catch (IOException e) { - e.printStackTrace(); - } - for (Entry entry : properties.entrySet()) { - String name = (String) entry.getKey(); - String value = (String) entry.getValue(); - System.out.println(name + "|" + value); - - String[] strs = StringUtils.split(value, ','); - if (strs.length < 2) - throw new LogException("config format error"); - - int level = levelMap.get(strs[0]); - String path = strs[1]; - FileLog fileLog = new FileLog(); - fileLog.setName(name); - fileLog.setLevel(level); - - if ("console".equalsIgnoreCase(path)) { - fileLog.setFileOutput(false); - fileLog.setConsoleOutput(true); - } else { - File file = new File(path); - if (!file.exists()) { - boolean mkdirRet = file.mkdir(); - if (!mkdirRet) - throw new LogException("create dir " + path - + " failure"); - } - - if (!file.isDirectory()) - throw new LogException(path + " is not directory"); - - fileLog.setPath(path); - fileLog.setFileOutput(true); - if (strs.length > 2) - fileLog.setConsoleOutput("console".equalsIgnoreCase(strs[2])); - } - - logMap.put(name, fileLog); - } - } - - public void flush() { - for (Entry entry : logMap.entrySet()) { - Log log = entry.getValue(); - if (log instanceof FileLog) { - - FileLog fileLog = (FileLog) log; - fileLog.flush(); -// System.out.println(">>> flush all " + fileLog.getName()); - } - } - } - - public Log getLog(String name) { - return logMap.get(name); - } - - public LogTask getLogTask() { - return logTask; - } - - public void shutdown() { - logTask.shutdown(); - } - - public void start() { - logTask.start(); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/LogItem.java b/firefly-common/src/main/java/com/firefly/utils/log/LogItem.java deleted file mode 100644 index dcd7c5557..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/LogItem.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.firefly.utils.log; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import com.firefly.utils.StringUtils; - -public class LogItem { - private String name, content, date, level; - private Object[] objs; - private Throwable throwable; - private String logStr; - - public void setThrowable(Throwable throwable) { - this.throwable = throwable; - } - - public void setObjs(Object[] objs) { - this.objs = objs; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public void setContent(String content) { - this.content = content; - } - - public String getLevel() { - return level; - } - - public void setLevel(String level) { - this.level = level; - } - - public void setDate(String date) { - this.date = date; - } - - @Override - public String toString() { - if (logStr == null) { - content = StringUtils.replace(content, objs); - if (throwable != null) { - StringWriter str = new StringWriter(); - PrintWriter out = new PrintWriter(str); - try { - out.println(); - out.println("$err_start"); - throwable.printStackTrace(out); - out.println("$err_end"); - } finally { - out.close(); - } - content += str.toString(); - } - - logStr = level + " " + date + "\t" + content; - } - return logStr; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/LogTask.java b/firefly-common/src/main/java/com/firefly/utils/log/LogTask.java deleted file mode 100644 index 29b008d7d..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/LogTask.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.firefly.utils.log; - -public interface LogTask extends Runnable { - void start(); - - void shutdown(); - - void add(LogItem logItem); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/file/FileLog.java b/firefly-common/src/main/java/com/firefly/utils/log/file/FileLog.java deleted file mode 100644 index 6cb745675..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/file/FileLog.java +++ /dev/null @@ -1,262 +0,0 @@ -package com.firefly.utils.log.file; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Date; -import java.util.LinkedList; -import java.util.Queue; - -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.log.LogItem; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class FileLog implements Log { - private int level; - private String path; - private String name; - private boolean consoleOutput; - private boolean fileOutput; - private Queue buffer = new LinkedList(); - private static final int BATCH_SIZE = 1024; - - public void write(LogItem logItem) { - if (fileOutput) - buffer.offer(logItem); - if (fileOutput && buffer.size() >= BATCH_SIZE) - flush(); - } - - public void flush() { - if (fileOutput && buffer.size() > 0) { - BufferedWriter bufferedWriter = null; - try { - String date = LogFactory.dayDateFormat.format(new Date()); - bufferedWriter = getBufferedWriter(date); - - for (LogItem logItem = null; (logItem = buffer.poll()) != null;) { - Date d = new Date(); - String newDate = LogFactory.dayDateFormat.format(d); - if(!newDate.equals(date)) { - if (bufferedWriter != null) - try { - bufferedWriter.close(); - } catch (IOException e) { - e.printStackTrace(); - } - date = newDate; - bufferedWriter = getBufferedWriter(date); - } - - logItem.setDate(SafeSimpleDateFormat.defaultDateFormat.format(d)); - bufferedWriter.append(logItem.toString() + CL); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (bufferedWriter != null) - try { - bufferedWriter.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - private BufferedWriter getBufferedWriter(String date) throws IOException { - - File file = new File(path, name + "." + date + ".txt"); - boolean ret = false; - if (!file.exists()) { - ret = file.createNewFile(); - } else { - ret = true; - } - if (ret) - return new BufferedWriter(new FileWriter(file, true)); - else - return null; - } - - public void setConsoleOutput(boolean consoleOutput) { - this.consoleOutput = consoleOutput; - } - - public void setFileOutput(boolean fileOutput) { - this.fileOutput = fileOutput; - } - - public int getLevel() { - return level; - } - - public void setLevel(int level) { - this.level = level; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - private void add(String str, String level, Throwable throwable, - Object... objs) { - LogItem item = new LogItem(); - item.setLevel(level); - item.setName(name); - item.setContent(str); - item.setObjs(objs); - item.setThrowable(throwable); - if (consoleOutput) { - item.setDate(SafeSimpleDateFormat.defaultDateFormat.format(new Date())); - System.out.println(item.toString()); - } - if (fileOutput) - LogFactory.getInstance().getLogTask().add(item); - } - - @Override - public void trace(String str) { - if (level > Log.TRACE) - return; - add(str, "TRACE", null, new Object[0]); - } - - @Override - public void trace(String str, Object... objs) { - if (level > Log.TRACE) - return; - add(str, "TRACE", null, objs); - } - - @Override - public void trace(String str, Throwable throwable, Object... objs) { - if (level > Log.TRACE) - return; - add(str, "TRACE", null, objs); - } - - @Override - public void debug(String str) { - if (level > Log.DEBUG) - return; - add(str, "DEBUG", null, new Object[0]); - } - - @Override - public void debug(String str, Object... objs) { - if (level > Log.DEBUG) - return; - add(str, "DEBUG", null, objs); - } - - @Override - public void debug(String str, Throwable throwable, Object... objs) { - if (level > Log.DEBUG) - return; - add(str, "DEBUG", throwable, objs); - } - - @Override - public void info(String str) { - if (level > Log.INFO) - return; - add(str, "INFO", null, new Object[0]); - } - - @Override - public void info(String str, Object... objs) { - if (level > Log.INFO) - return; - add(str, "INFO", null, objs); - } - - @Override - public void info(String str, Throwable throwable, Object... objs) { - if (level > Log.INFO) - return; - add(str, "INFO", throwable, objs); - } - - @Override - public void warn(String str) { - if (level > Log.WARN) - return; - add(str, "WARN", null, new Object[0]); - } - - @Override - public void warn(String str, Object... objs) { - if (level > Log.WARN) - return; - add(str, "WARN", null, objs); - } - - @Override - public void warn(String str, Throwable throwable, Object... objs) { - if (level > Log.WARN) - return; - add(str, "WARN", throwable, objs); - } - - @Override - public void error(String str, Object... objs) { - if (level > Log.ERROR) - return; - add(str, "ERROR", null, objs); - } - - @Override - public void error(String str, Throwable throwable, Object... objs) { - if (level > Log.ERROR) - return; - add(str, "ERROR", throwable, objs); - } - - @Override - public void error(String str) { - if (level > Log.ERROR) - return; - add(str, "ERROR", null, new Object[0]); - } - - @Override - public boolean isTraceEnable() { - return level > Log.TRACE; - } - - @Override - public boolean isDebugEnable() { - return level > Log.DEBUG; - } - - @Override - public boolean isInfoEnable() { - return level > Log.INFO; - } - - @Override - public boolean isWarnEnable() { - return level > Log.WARN; - } - - @Override - public boolean isErrorEnable() { - return level > Log.ERROR; - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/log/file/FileLogTask.java b/firefly-common/src/main/java/com/firefly/utils/log/file/FileLogTask.java deleted file mode 100644 index 3c0b61ec4..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/log/file/FileLogTask.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.firefly.utils.log.file; - -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.log.LogItem; -import com.firefly.utils.log.LogTask; - -public class FileLogTask implements LogTask { - private volatile boolean start; - private Queue queue = new ConcurrentLinkedQueue(); - private Thread thread = new Thread(this); - - @Override - public void run() { - while (true) { - write(); - if (!start && queue.isEmpty()) - break; - - try { - Thread.sleep(1000L); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - } - } - - @Override - public void start() { - if (!start) { - synchronized (this) { - if (!start) { - start = true; - thread.start(); - } - } - } - } - - @Override - public void shutdown() { - start = false; - } - - @Override - public void add(LogItem logItem) { - if (!start) - return; - - if (VerifyUtils.isEmpty(logItem.getName())) - throw new IllegalArgumentException("log name is empty"); - - queue.offer(logItem); - } - - private void write() { - for (LogItem logItem = null; (logItem = queue.poll()) != null;) { - Log log = LogFactory.getInstance().getLog(logItem.getName()); - if (log instanceof FileLog) { - FileLog fileLog = (FileLog) log; - fileLog.write(logItem); - } - } -// System.out.println(">>> flush"); - LogFactory.getInstance().flush(); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/pattern/Pattern.java b/firefly-common/src/main/java/com/firefly/utils/pattern/Pattern.java deleted file mode 100644 index a8a5d23f4..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/pattern/Pattern.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.firefly.utils.pattern; - -import com.firefly.utils.StringUtils; - -public abstract class Pattern { - - private static final AllMatch ALL_MATCH = new AllMatch(); - - /** - * 根据模式匹配字符串 - * @param str 进行匹配的字符串 - * @return 返回null表示匹配失败,否则返回匹配的字符串数组 - */ - abstract public String[] match(String str); - - public static Pattern compile(String pattern, String wildcard) { - final boolean startWith = pattern.startsWith(wildcard); - final boolean endWith = pattern.endsWith(wildcard); - final String[] array = StringUtils.split(pattern, wildcard); - - switch (array.length) { - case 0: - return ALL_MATCH; // pattern全是通配符 - case 1: - if (startWith && endWith) - return new HeadAndTailMatch(array[0]); - - if (startWith) - return new HeadMatch(array[0]); - - if (endWith) - return new TailMatch(array[0]); - - return new EqualsMatch(pattern); // pattern不包含通配符 - default: - return new MultipartMatch(startWith, endWith, array); - } - } - - - - private static class MultipartMatch extends Pattern { - - private final boolean startWith, endWith; - private final String[] parts; - private int num; - - public MultipartMatch(boolean startWith, boolean endWith, String[] parts) { - super(); - this.startWith = startWith; - this.endWith = endWith; - this.parts = parts; - num = parts.length - 1; - if(startWith) - num++; - if(endWith) - num++; - } - - @Override - public String[] match(String str) { - int currentIndex = -1; - int lastIndex = -1; - String[] ret = new String[num]; - - for (int i = 0; i < parts.length; i++) { - String part = parts[i]; - int j = startWith ? i : i - 1; - currentIndex = str.indexOf(part, lastIndex + 1); - - if (currentIndex > lastIndex) { - if(i != 0 || startWith) - ret[j] = str.substring(lastIndex + 1, currentIndex); - - lastIndex = currentIndex + part.length() - 1; - continue; - } - return null; - } - - if(endWith) - ret[num - 1] = str.substring(lastIndex + 1); - - return ret; - } - - } - - private static class TailMatch extends Pattern { - private final String part; - - public TailMatch(String part) { - this.part = part; - } - - @Override - public String[] match(String str) { - int currentIndex = str.indexOf(part); - if(currentIndex == 0) { - return new String[] { str.substring(part.length()) }; - } - return null; - } - } - - private static class HeadMatch extends Pattern { - private final String part; - - public HeadMatch(String part) { - this.part = part; - } - - @Override - public String[] match(String str) { - int currentIndex = str.indexOf(part); - if(currentIndex + part.length() == str.length()) { - return new String[] { str.substring(0, currentIndex) }; - } - return null; - } - - - } - - private static class HeadAndTailMatch extends Pattern { - private final String part; - - public HeadAndTailMatch(String part) { - this.part = part; - } - - @Override - public String[] match(String str) { - int currentIndex = str.indexOf(part); - if(currentIndex >= 0) { - String[] ret = new String[]{str.substring(0, currentIndex), - str.substring(currentIndex + part.length(), str.length()) }; - return ret; - } - return null; - } - - - } - - private static class EqualsMatch extends Pattern { - private final String pattern; - - public EqualsMatch(String pattern) { - this.pattern = pattern; - } - - @Override - public String[] match(String str) { - return pattern.equals(str) ? new String[0] : null; - } - } - - private static class AllMatch extends Pattern { - - @Override - public String[] match(String str) { - return new String[]{str}; - } - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/pool/ObjectFactory.java b/firefly-common/src/main/java/com/firefly/utils/pool/ObjectFactory.java deleted file mode 100644 index f26276e78..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/pool/ObjectFactory.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.firefly.utils.pool; - -public interface ObjectFactory { - Poolable newInstance(); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/pool/ObjectPool.java b/firefly-common/src/main/java/com/firefly/utils/pool/ObjectPool.java deleted file mode 100644 index 0bddcfdee..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/pool/ObjectPool.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.firefly.utils.pool; - -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; - -public class ObjectPool { - private Queue queue; - private ObjectFactory objectFactory; - - public ObjectPool(ObjectFactory objectFactory) { - queue = new ArrayBlockingQueue(16); - this.objectFactory = objectFactory; - } - - public ObjectPool(ObjectFactory objectFactory, int size) { - queue = new ArrayBlockingQueue(size); - this.objectFactory = objectFactory; - } - - public Poolable get() { - Poolable ret = queue.poll(); - if(ret == null) { - ret = objectFactory.newInstance(); - } else { - ret.prepare(); - } - return ret; - } - - public void put(Poolable poolable) { - if(poolable != null) { - poolable.release(); - queue.offer(poolable); - } - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/pool/Poolable.java b/firefly-common/src/main/java/com/firefly/utils/pool/Poolable.java deleted file mode 100644 index bdb270cf8..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/pool/Poolable.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.firefly.utils.pool; - -public interface Poolable { - void release(); - void prepare(); -} diff --git a/firefly-common/src/main/java/com/firefly/utils/time/HashTimeWheel.java b/firefly-common/src/main/java/com/firefly/utils/time/HashTimeWheel.java deleted file mode 100644 index 09da8f644..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/time/HashTimeWheel.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.firefly.utils.time; - -import java.util.Iterator; -import java.util.concurrent.ConcurrentLinkedQueue; - -public class HashTimeWheel { - private int maxTimers = 60; // wheel的格子数量 - private long interval = 1000; // wheel旋转时间间隔 - - private ConcurrentLinkedQueue[] timerSlots; - private volatile int currentSlot = 0; - private volatile boolean start; - - public int getMaxTimers() { - return maxTimers; - } - - public void setMaxTimers(int maxTimers) { - this.maxTimers = maxTimers; - } - - public long getInterval() { - return interval; - } - - public void setInterval(long interval) { - this.interval = interval; - } - - /** - * 增加一个触发任务 - * - * @param delay - * 触发延时时间 - * @param run - * 任务处理 - */ - public void add(long delay, Runnable run) { - final int curSlot = currentSlot; - - final int ticks = delay > interval ? (int) (delay / interval) : 1; // 计算刻度长度 - final int index = (curSlot + (ticks % maxTimers)) % maxTimers; // 放到wheel的位置 - final int round = (ticks - 1) / maxTimers; // wheel旋转的圈数 - - timerSlots[index].add(new TimerTask(round, run)); - } - - @SuppressWarnings("unchecked") - public void start() { - if (!start) { - synchronized (this) { - if (!start) { - timerSlots = new ConcurrentLinkedQueue[maxTimers]; - for (int i = 0; i < timerSlots.length; i++) { - timerSlots[i] = new ConcurrentLinkedQueue(); - } - - start = true; - new Thread(new Worker(), "firefly time wheel").start(); - } - } - } - } - - public void stop() { - start = false; - timerSlots = null; - } - - private final class Worker implements Runnable { - - @Override - public void run() { - while (start) { - int currentSlotTemp = currentSlot; - ConcurrentLinkedQueue timerSlot = timerSlots[currentSlotTemp++]; - currentSlotTemp %= timerSlots.length; - - for (Iterator iterator = timerSlot.iterator(); iterator - .hasNext();) { - if (iterator.next().runTask()) - iterator.remove(); - } - - try { - Thread.sleep(interval); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - currentSlot = currentSlotTemp; - } - - } - - } - - private final class TimerTask { - private int round; - private Runnable run; - - public TimerTask(int round, Runnable run) { - this.round = round; - this.run = run; - } - - public boolean runTask() { - if (round == 0) { - run.run(); - return true; - } else { - round--; - return false; - } - } - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/time/Millisecond100Clock.java b/firefly-common/src/main/java/com/firefly/utils/time/Millisecond100Clock.java deleted file mode 100644 index 963a8023b..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/time/Millisecond100Clock.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.firefly.utils.time; - -public class Millisecond100Clock { - private static final TimeProvider TIME_PROVIDER = new TimeProvider(100); - static { - TIME_PROVIDER.start(); - } - - public static long currentTimeMillis() { - return TIME_PROVIDER.currentTimeMillis(); - } - - public static void stop() { - TIME_PROVIDER.stop(); - } -} diff --git a/firefly-common/src/main/java/com/firefly/utils/time/SafeSimpleDateFormat.java b/firefly-common/src/main/java/com/firefly/utils/time/SafeSimpleDateFormat.java deleted file mode 100644 index bfe8c9208..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/time/SafeSimpleDateFormat.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.firefly.utils.time; - -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; - -import com.firefly.utils.VerifyUtils; - -public class SafeSimpleDateFormat { - - public static final SafeSimpleDateFormat defaultDateFormat = new SafeSimpleDateFormat(); - private ThreadLocal threadLocal; - - public SafeSimpleDateFormat() { - this(""); - } - - public SafeSimpleDateFormat(final SimpleDateFormat sdf) { - if(sdf == null) - throw new IllegalArgumentException("SimpleDateFormat argument is null"); - this.threadLocal = new ThreadLocal() { - protected SimpleDateFormat initialValue() { - return (SimpleDateFormat)sdf.clone(); - } - }; - } - - public SafeSimpleDateFormat(String datePattern) { - final String p = VerifyUtils.isEmpty(datePattern) ? "yyyy-MM-dd HH:mm:ss" - : datePattern; - this.threadLocal = new ThreadLocal() { - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(p); - } - }; - } - - public Date parse(String dateStr) { - try { - return getFormat().parse(dateStr); - } catch (ParseException e) { - e.printStackTrace(); - } - return null; - } - - public String format(Date date) { - return getFormat().format(date); - } - - private DateFormat getFormat() { - return (DateFormat) threadLocal.get(); - } - - public static void main(String[] args) { - SafeSimpleDateFormat sdf = new SafeSimpleDateFormat(); - Calendar last = Calendar.getInstance(); - last.setTime(sdf.parse("2010-12-08 17:26:22")); - Calendar now = Calendar.getInstance(); - System.out.println("last:\t" + last.get(Calendar.YEAR) + "\t" - + last.get(Calendar.MONTH)); - System.out.println("now:\t" + now.get(Calendar.YEAR) + "\t" - + now.get(Calendar.MONTH)); - } - -} diff --git a/firefly-common/src/main/java/com/firefly/utils/time/TimeProvider.java b/firefly-common/src/main/java/com/firefly/utils/time/TimeProvider.java deleted file mode 100644 index 11670ad73..000000000 --- a/firefly-common/src/main/java/com/firefly/utils/time/TimeProvider.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.firefly.utils.time; - -public class TimeProvider { - private final long interval; - private volatile long current = System.currentTimeMillis(); - private volatile boolean start; - - public TimeProvider(long interval) { - this.interval = interval; - } - - public void start() { - start = true; - new Thread(new Runnable() { - @Override - public void run() { - while (start) { - try { - Thread.sleep(interval); - } catch (InterruptedException e) { - e.printStackTrace(); - } - current = System.currentTimeMillis(); - } - } - }, "filefly time provider " + interval + "ms").start(); - } - - public void stop() { - start = false; - } - - public long currentTimeMillis() { - return current; - } - -} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/AbstractActor.java b/firefly-common/src/main/java/com/fireflysource/common/actor/AbstractActor.java new file mode 100644 index 000000000..b698d042a --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/AbstractActor.java @@ -0,0 +1,272 @@ +package com.fireflysource.common.actor; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; + +import java.util.Objects; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +abstract public class AbstractActor implements Runnable, Actor, ActorInternalApi { + + private static final LazyLogger log = SystemLogger.create(AbstractActor.class); + + private final String address; + private final Dispatcher dispatcher; + private final Mailbox mailbox; + private final AtomicReference taskState = new AtomicReference<>(TaskState.IDLE); + private ActorState actorState = ActorState.RUNNING; + + public AbstractActor() { + this(UUID.randomUUID().toString(), DispatcherFactory.createDispatcher(), MailboxFactory.createMailbox()); + } + + public AbstractActor(String address, Dispatcher dispatcher, Mailbox mailbox) { + this.address = address; + this.dispatcher = dispatcher; + this.mailbox = mailbox; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public boolean offer(T message) { + if (mailbox.offerUserMessage(message)) { + dispatch(); + return true; + } else { + return false; + } + } + + @Override + public void pause() { + sendSystemMessage(SystemMessage.PAUSE); + } + + @Override + public void resume() { + sendSystemMessage(SystemMessage.RESUME); + } + + @Override + public void shutdown() { + sendSystemMessage(SystemMessage.SHUTDOWN); + } + + @Override + public void restart() { + sendSystemMessage(SystemMessage.RESTART); + } + + @Override + public ActorState getActorState() { + return actorState; + } + + @Override + public void run() { + while (true) { + boolean systemMailboxEmpty = handleSystemMessages(); + + if (actorState == ActorState.PAUSE) { + break; + } + + boolean userMailboxEmpty = handleUserMessages(); + + if (systemMailboxEmpty && userMailboxEmpty) { + break; + } + } + + dispatchNext(); + } + + private void dispatchNext() { + taskState.set(TaskState.IDLE); + switch (actorState) { + case SHUTDOWN: + case RUNNING: + if (mailbox.hasSystemMessage() || mailbox.hasUserMessage()) { + dispatch(); + } + break; + case PAUSE: + if (mailbox.hasSystemMessage()) { + dispatch(); + } + break; + } + } + + private boolean handleUserMessages() { + boolean empty; + T message = mailbox.pollUserMessage(); + if (message != null) { + switch (actorState) { + case RUNNING: + handleMessage(message); + break; + case SHUTDOWN: + handleDiscardMessage(message); + break; + } + empty = false; + } else { + empty = true; + } + return empty; + } + + protected void pauseInMessageProcessThread() { + if (actorState == ActorState.RUNNING) { + actorState = ActorState.PAUSE; + } + } + + private boolean handleSystemMessages() { + boolean empty; + SystemMessage systemMessage = mailbox.pollSystemMessage(); + if (systemMessage != null) { + switch (systemMessage) { + case PAUSE: + if (actorState == ActorState.RUNNING) { + actorState = ActorState.PAUSE; + } + break; + case RESUME: + if (actorState == ActorState.PAUSE) { + actorState = ActorState.RUNNING; + } + break; + case SHUTDOWN: + actorState = ActorState.SHUTDOWN; + break; + case RESTART: + if (actorState == ActorState.SHUTDOWN) { + actorState = ActorState.RUNNING; + } + break; + } + empty = false; + } else { + empty = true; + } + return empty; + } + + private void sendSystemMessage(SystemMessage message) { + if (mailbox.offerSystemMessage(message)) { + dispatch(); + } + } + + private void dispatch() { + if (taskState.compareAndSet(TaskState.IDLE, TaskState.BUSY)) { + dispatcher.dispatch(this); + } + } + + private void handleMessage(T message) { + try { + onReceive(message); + } catch (Exception e) { + log.error("on receive exception. address: " + getAddress(), e); + } + } + + private void handleDiscardMessage(T message) { + try { + onDiscard(message); + } catch (Exception e) { + log.error("on discard exception. address: " + getAddress(), e); + } + } + + abstract public void onReceive(T message); + + public void onDiscard(T message) { + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractActor that = (AbstractActor) o; + return address.equals(that.address); + } + + @Override + public int hashCode() { + return Objects.hash(address); + } + + enum TaskState { + IDLE, BUSY + } + + public enum SystemMessage { + PAUSE, RESUME, SHUTDOWN, RESTART + } + + public static class DispatcherImpl implements Dispatcher { + private final Executor executor; + + public DispatcherImpl(Executor executor) { + this.executor = executor; + } + + @Override + public void dispatch(Runnable runnable) { + executor.execute(runnable); + } + } + + public static class MailboxImpl implements Mailbox { + private final Queue userMessageQueue; + private final Queue systemMessageQueue; + + public MailboxImpl(Queue userMessageQueue, Queue systemMessageQueue) { + this.userMessageQueue = userMessageQueue; + this.systemMessageQueue = systemMessageQueue; + } + + @Override + public AbstractActor.SystemMessage pollSystemMessage() { + return systemMessageQueue.poll(); + } + + @Override + public boolean offerSystemMessage(AbstractActor.SystemMessage systemMessage) { + return systemMessageQueue.offer(systemMessage); + } + + @Override + public boolean hasSystemMessage() { + return systemMessageQueue.peek() != null; + } + + @Override + public T pollUserMessage() { + return userMessageQueue.poll(); + } + + @Override + public boolean offerUserMessage(T userMessage) { + return userMessageQueue.offer(userMessage); + } + + @Override + public boolean hasUserMessage() { + return userMessageQueue.peek() != null; + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/AbstractAsyncActor.java b/firefly-common/src/main/java/com/fireflysource/common/actor/AbstractAsyncActor.java new file mode 100644 index 000000000..cd170c17b --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/AbstractAsyncActor.java @@ -0,0 +1,27 @@ +package com.fireflysource.common.actor; + +import com.fireflysource.common.sys.Result; + +import java.util.concurrent.CompletableFuture; + +abstract public class AbstractAsyncActor extends AbstractActor { + + public AbstractAsyncActor() { + super(); + } + + public AbstractAsyncActor(String address, Dispatcher dispatcher, Mailbox mailbox) { + super(address, dispatcher, mailbox); + } + + @Override + public void onReceive(T message) { + pauseInMessageProcessThread(); + onReceiveAsync(message).handle((result, throwable) -> { + resume(); + return Result.DONE; + }); + } + + abstract public CompletableFuture onReceiveAsync(T message); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/Actor.java b/firefly-common/src/main/java/com/fireflysource/common/actor/Actor.java new file mode 100644 index 000000000..131fb9613 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/Actor.java @@ -0,0 +1,25 @@ +package com.fireflysource.common.actor; + +/** + * The actor interface. + * + * @param The actor message type. + */ +public interface Actor { + + /** + * Get actor id. + * + * @return The actor id. + */ + String getAddress(); + + /** + * Offer message to this actor's mailbox. + * + * @param message The message. + * @return If true, offer message success. + */ + boolean offer(T message); + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/ActorInternalApi.java b/firefly-common/src/main/java/com/fireflysource/common/actor/ActorInternalApi.java new file mode 100644 index 000000000..985cd2308 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/ActorInternalApi.java @@ -0,0 +1,34 @@ +package com.fireflysource.common.actor; + +/** + * Only call these methods in the receiving message thread. + */ +public interface ActorInternalApi { + + /** + * Pause to receive messages. + */ + void pause(); + + /** + * Resume to receive messages. + */ + void resume(); + + /** + * Shutdown the actor. + */ + void shutdown(); + + /** + * Restart the actor. + */ + void restart(); + + /** + * Get the actor internal state. + * + * @return The actor internal state. + */ + ActorState getActorState(); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/ActorState.java b/firefly-common/src/main/java/com/fireflysource/common/actor/ActorState.java new file mode 100644 index 000000000..a3ad8392d --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/ActorState.java @@ -0,0 +1,5 @@ +package com.fireflysource.common.actor; + +public enum ActorState { + PAUSE, RUNNING, SHUTDOWN +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/BlockingTask.java b/firefly-common/src/main/java/com/fireflysource/common/actor/BlockingTask.java new file mode 100644 index 000000000..da27f9976 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/BlockingTask.java @@ -0,0 +1,107 @@ +package com.fireflysource.common.actor; + +import com.fireflysource.common.func.Callback; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.Result; +import com.fireflysource.common.sys.SystemLogger; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.locks.Lock; + +import static com.fireflysource.common.sys.Result.runCaching; + +public class BlockingTask implements ForkJoinPool.ManagedBlocker { + + private static final LazyLogger log = SystemLogger.create(BlockingTask.class); + + private final Callable callable; + private final Callable> tryCallable; + private T result; + private boolean done = false; + + public BlockingTask(Callable callable, Callable> tryCallable) { + this.callable = callable; + this.tryCallable = tryCallable; + } + + @Override + public boolean block() throws InterruptedException { + try { + result = callable.call(); + } catch (Exception e) { + log.error("run blocking task exception.", e); + } + done = true; + return true; + } + + @Override + public boolean isReleasable() { + try { + Result r = tryCallable.call(); + done = r.isSuccess(); + if (done) { + result = r.getValue(); + } + } catch (Exception e) { + done = false; + } + return done; + } + + public static void runBlockingTask(Callback callback) { + runBlockingTask(() -> { + callback.call(); + return null; + }); + } + + public static Result runBlockingTask(Callable callable) { + return runBlockingTask(callable, null); + } + + public static Result runBlockingTask(Callable callable, Callable> tryCallable) { + return runCaching(() -> { + BlockingTask task = new BlockingTask<>(callable, tryCallable); + ForkJoinPool.managedBlock(task); + return task.result; + }); + } + + public static void sleep(long millisecond) { + runBlockingTask(() -> Thread.sleep(millisecond)); + } + + public static T blockingTake(final BlockingQueue queue) { + Result result = runBlockingTask(queue::take, () -> { + T t = queue.poll(); + return new Result<>(t != null, t, null); + }); + return result.getValue(); + } + + public static T blockingLock(final Lock lock, Callable callable) { + Result result = runBlockingTask(() -> { + try { + lock.lock(); + return callable.call(); + } finally { + lock.unlock(); + } + }, () -> { + boolean success = lock.tryLock(); + if (success) { + try { + return new Result<>(true, callable.call(), null); + } finally { + lock.unlock(); + } + } else { + return new Result<>(false, null, null); + } + }); + return result.getValue(); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/Dispatcher.java b/firefly-common/src/main/java/com/fireflysource/common/actor/Dispatcher.java new file mode 100644 index 000000000..ebd328aad --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/Dispatcher.java @@ -0,0 +1,14 @@ +package com.fireflysource.common.actor; + +/** + * The actor dispatcher. + */ +public interface Dispatcher { + + /** + * Dispatch the message process task. + * + * @param runnable The message process task. + */ + void dispatch(Runnable runnable); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/DispatcherFactory.java b/firefly-common/src/main/java/com/fireflysource/common/actor/DispatcherFactory.java new file mode 100644 index 000000000..253edbe76 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/DispatcherFactory.java @@ -0,0 +1,16 @@ +package com.fireflysource.common.actor; + +import com.fireflysource.common.coroutine.CoroutineDispatchers; + +import java.util.concurrent.Executor; + +public class DispatcherFactory { + + public static Dispatcher createDispatcher() { + return createDispatcher(CoroutineDispatchers.INSTANCE.getComputationThreadPool()); + } + + public static Dispatcher createDispatcher(Executor executor) { + return new AbstractActor.DispatcherImpl(executor); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/Mailbox.java b/firefly-common/src/main/java/com/fireflysource/common/actor/Mailbox.java new file mode 100644 index 000000000..6ddd4bf84 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/Mailbox.java @@ -0,0 +1,19 @@ +package com.fireflysource.common.actor; + +/** + * The actor mailbox. + */ +public interface Mailbox { + + S pollSystemMessage(); + + boolean offerSystemMessage(S systemMessage); + + boolean hasSystemMessage(); + + U pollUserMessage(); + + boolean offerUserMessage(U userMessage); + + boolean hasUserMessage(); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/actor/MailboxFactory.java b/firefly-common/src/main/java/com/fireflysource/common/actor/MailboxFactory.java new file mode 100644 index 000000000..63479c60e --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/actor/MailboxFactory.java @@ -0,0 +1,17 @@ +package com.fireflysource.common.actor; + +import org.jctools.queues.MpscLinkedQueue; +import org.jctools.queues.SpscLinkedQueue; + +import java.util.Queue; + +abstract public class MailboxFactory { + + public static Mailbox createMailbox() { + return new AbstractActor.MailboxImpl<>(new MpscLinkedQueue<>(), new SpscLinkedQueue<>()); + } + + public static Mailbox createMailbox(Queue userMessageQueue, Queue systemMessageQueue) { + return new AbstractActor.MailboxImpl<>(userMessageQueue, systemMessageQueue); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/AbstractProxyFactory.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/AbstractProxyFactory.java new file mode 100644 index 000000000..0b8e74405 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/AbstractProxyFactory.java @@ -0,0 +1,51 @@ +package com.fireflysource.common.bytecode; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class AbstractProxyFactory implements ProxyFactory { + + static final IdentityHashMap, String> primitiveWrapMap = new IdentityHashMap<>(); + public static ClassLoader classLoader; + + static { + primitiveWrapMap.put(short.class, Short.class.getCanonicalName()); + primitiveWrapMap.put(byte.class, Byte.class.getCanonicalName()); + primitiveWrapMap.put(int.class, Integer.class.getCanonicalName()); + primitiveWrapMap.put(char.class, Character.class.getCanonicalName()); + primitiveWrapMap.put(float.class, Float.class.getCanonicalName()); + primitiveWrapMap.put(double.class, Double.class.getCanonicalName()); + primitiveWrapMap.put(long.class, Long.class.getCanonicalName()); + primitiveWrapMap.put(boolean.class, Boolean.class.getCanonicalName()); + + classLoader = Thread.currentThread().getContextClassLoader(); + } + + protected final Map methodCache = new ConcurrentHashMap<>(); + protected final Map fieldCache = new ConcurrentHashMap<>(); + protected final Map, ArrayProxy> arrayCache = new ConcurrentHashMap<>(); + + @Override + public MethodProxy getMethodProxy(Method method) { + return methodCache.computeIfAbsent(method, this::createMethodProxy); + } + + abstract protected MethodProxy createMethodProxy(Method method); + + @Override + public ArrayProxy getArrayProxy(Class clazz) { + return arrayCache.computeIfAbsent(clazz, this::createArrayProxy); + } + + abstract protected ArrayProxy createArrayProxy(Class clazz); + + @Override + public FieldProxy getFieldProxy(Field field) { + return fieldCache.computeIfAbsent(field, this::createFieldProxy); + } + + abstract protected FieldProxy createFieldProxy(Field field); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/ArrayProxy.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ArrayProxy.java new file mode 100644 index 000000000..d2ef3cfc8 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ArrayProxy.java @@ -0,0 +1,12 @@ +package com.fireflysource.common.bytecode; + +/** + * @author Pengtao Qiu + */ +public interface ArrayProxy { + int size(Object array); + + Object get(Object array, int index); + + void set(Object array, int index, Object value); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/ClassProxy.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ClassProxy.java new file mode 100644 index 000000000..06a0f8022 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ClassProxy.java @@ -0,0 +1,6 @@ +package com.fireflysource.common.bytecode; + +@FunctionalInterface +public interface ClassProxy { + Object intercept(MethodProxy handler, Object originalInstance, Object[] args); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/ClassProxyFactory.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ClassProxyFactory.java new file mode 100644 index 000000000..7e4a1e76e --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ClassProxyFactory.java @@ -0,0 +1,5 @@ +package com.fireflysource.common.bytecode; + +public interface ClassProxyFactory { + T createProxy(T instance, ClassProxy proxy, MethodFilter methodFilter) throws Throwable; +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/CompilerUtils.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/CompilerUtils.java new file mode 100644 index 000000000..5a03280bc --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/CompilerUtils.java @@ -0,0 +1,138 @@ +package com.fireflysource.common.bytecode; + +import javax.tools.*; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject.Kind; +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class CompilerUtils { + + public static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + private static final Map outputJavaFile = new ConcurrentHashMap<>(); + private static final Map> classCache = new ConcurrentHashMap<>(); + private static ClassLoader classLoader = new CompilerClassLoader(CompilerUtils.class.getClassLoader()); + + public static Class compileSource(String completeClassName, String source) throws IOException { + boolean result; + try (JavaFileManager fileManager = getStringSourceJavaFileManager(compiler, + null, null, StandardCharsets.UTF_8)) { + CompilationTask task = compiler.getTask(null, fileManager, + null, null, null, + Collections.singletonList(new JavaSourceFromString(completeClassName, source))); + result = task.call(); + } + + if (!result) + return null; + + return getClassByName(completeClassName); + } + + public static Class getClassByName(String name) { + return classCache.computeIfAbsent(name, CompilerUtils::getClass); + } + + private static Class getClass(String name) { + try { + return Class.forName(name, false, classLoader); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } + } + + public static JavaFileManager getStringSourceJavaFileManager(JavaCompiler compiler, DiagnosticListener diagnosticListener, Locale locale, Charset charset) { + + return new ForwardingJavaFileManager(compiler.getStandardFileManager(diagnosticListener, locale, charset)) { + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException { + JavaFileObject jfo = new ByteJavaObject(className, kind); + outputJavaFile.put(className, jfo); + return jfo; + } + }; + } + + private static URI toURI(String name) { + try { + return new URI(name); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public static class JavaSourceFromString extends SimpleJavaFileObject { + /** + * The source code of this "file". + */ + final String code; + + /** + * Constructs a new JavaSourceFromString. + * + * @param name the name of the compilation unit represented by this file + * object + * @param code the source code for the compilation unit represented by + * this file object + */ + public JavaSourceFromString(String name, String code) { + super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } + } + + private static class ByteJavaObject extends SimpleJavaFileObject { + + private ByteArrayOutputStream byteArrayOutputStream; + + public ByteJavaObject(String name, Kind kind) { + super(toURI(name), kind); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IllegalStateException, UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream openInputStream() throws IllegalStateException, UnsupportedOperationException { + return new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + } + + @Override + public OutputStream openOutputStream() throws IllegalStateException, UnsupportedOperationException { + return byteArrayOutputStream = new ByteArrayOutputStream(); + } + } + + public static class CompilerClassLoader extends ClassLoader { + + public CompilerClassLoader(ClassLoader classLoader) { + super(classLoader); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + JavaFileObject jfo = outputJavaFile.get(name); + if (jfo != null) { + byte[] bytes = ((ByteJavaObject) jfo).byteArrayOutputStream.toByteArray(); + outputJavaFile.remove(name); + return defineClass(name, bytes, 0, bytes.length); + } + return super.findClass(name); + } + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/FieldProxy.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/FieldProxy.java new file mode 100644 index 000000000..e21e5e5f9 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/FieldProxy.java @@ -0,0 +1,14 @@ +package com.fireflysource.common.bytecode; + +import java.lang.reflect.Field; + +/** + * @author Pengtao Qiu + */ +public interface FieldProxy { + Field field(); + + Object get(Object obj); + + void set(Object obj, Object value); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavaReflectionProxyFactory.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavaReflectionProxyFactory.java new file mode 100644 index 000000000..362cc26d0 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavaReflectionProxyFactory.java @@ -0,0 +1,163 @@ +package com.fireflysource.common.bytecode; + +import com.fireflysource.common.string.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; + +/** + * @author Pengtao Qiu + */ +public class JavaReflectionProxyFactory extends AbstractProxyFactory { + + public static final JavaReflectionProxyFactory INSTANCE = new JavaReflectionProxyFactory(); + + private JavaReflectionProxyFactory() { + + } + + protected FieldProxy createFieldProxy(Field field) { + try { + String packageName = "com.firefly.utils"; + String className = "FieldReflectionProxy" + UUID.randomUUID().toString().replace("-", ""); + String completeClassName = packageName + "." + className; + + String value = ""; + Class fieldClazz = field.getType(); + if (fieldClazz.isPrimitive()) { + value += StringUtils.replace("(({})value).{}Value()", primitiveWrapMap.get(fieldClazz), fieldClazz.getCanonicalName()); + } else { + value += StringUtils.replace("({})value", fieldClazz.getCanonicalName()); + } + String source = "package " + packageName + ";\n" + + "import " + Field.class.getCanonicalName() + ";\n" + + "public class " + className + " implements " + FieldProxy.class.getCanonicalName() + " {\n" + + "private Field field;\n" + + "public " + className + "(Field field){\n" + + "\tthis.field = field;\n" + + "}\n\n" + + "public Field field(){return field;}\n" + + "public Object get(Object obj){\n" + + "\treturn " + StringUtils.replace("(({})obj).{};\n", field.getDeclaringClass().getCanonicalName(), field.getName()) + + "}\n\n" + + + "public void set(Object obj, Object value){\n" + + StringUtils.replace("\t(({})obj).{} = ", field.getDeclaringClass().getCanonicalName(), field.getName()) + + value + ";\n" + + "}\n" + + "}"; + + Class fieldProxyClass = CompilerUtils.compileSource(completeClassName, source); + if (fieldProxyClass == null) { + return null; + } else { + return (FieldProxy) fieldProxyClass.getConstructor(Field.class).newInstance(field); + } + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + protected MethodProxy createMethodProxy(Method method) { + try { + String packageName = "com.firefly.utils"; + String className = "MethodReflectionProxy" + UUID.randomUUID().toString().replace("-", ""); + String completeClassName = packageName + "." + className; + + Class[] paramClazz = method.getParameterTypes(); + String returnCode = ""; + if (!method.getReturnType().equals(Void.TYPE)) { + returnCode += "return "; + } + + returnCode += "((" + method.getDeclaringClass().getCanonicalName() + ")obj)." + method.getName() + "("; + if (paramClazz.length > 0) { + int max = paramClazz.length - 1; + for (int i = 0; ; i++) { + Class param = paramClazz[i]; + if (param.isPrimitive()) { + returnCode += StringUtils.replace("(({})args[{}]).{}Value()", primitiveWrapMap.get(param), i, param.getCanonicalName()); + } else { + returnCode += "(" + param.getCanonicalName() + ")args[" + i + "]"; + } + + if (i == max) + break; + + returnCode += ","; + } + } + returnCode += ");"; + + if (method.getReturnType().equals(Void.TYPE)) { + returnCode += "\n\treturn null;"; + } + + String source = "package " + packageName + ";\n" + + "import " + Method.class.getCanonicalName() + ";\n" + + "public class " + className + " implements " + MethodProxy.class.getCanonicalName() + " {\n" + + "private Method method;\n" + + "public " + className + "(Method method){\n" + + "\tthis.method = method;\n" + + "}\n\n" + + "public Method method(){return method;}\n\n" + + "public Object invoke(Object obj, Object[] args){\n" + + "\tif(args == null || args.length != " + paramClazz.length + ")\n" + + "\t\tthrow new IllegalArgumentException(\"arguments error\");\n\n" + + "\t" + returnCode + "\n" + + "}\n" + + "}"; + + Class methodProxyClass = CompilerUtils.compileSource(completeClassName, source); + if (methodProxyClass == null) + return null; + + return (MethodProxy) methodProxyClass.getConstructor(Method.class).newInstance(method); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + protected ArrayProxy createArrayProxy(Class clazz) { + try { + String packageName = "com.firefly.utils"; + String className = "ArrayReflectionProxy" + UUID.randomUUID().toString().replace("-", ""); + String completeClassName = packageName + "." + className; + + Class componentType = clazz.getComponentType(); + String v; + if (componentType.isPrimitive()) { + v = StringUtils.replace("(({})value).{}Value()", primitiveWrapMap.get(componentType), componentType.getCanonicalName()); + } else { + v = "(" + clazz.getComponentType().getCanonicalName() + ")value;\n"; + } + + String source = "package " + packageName + ";\n" + + "public class " + className + " implements " + ArrayProxy.class.getCanonicalName() + " {\n" + + "@Override\n" + + "public int size(Object array){\n" + + "\treturn ((" + clazz.getCanonicalName() + ")array).length;\n" + + "}\n\n" + + + "@Override\n" + + "public Object get(Object array, int index){\n" + + "\treturn ((" + clazz.getCanonicalName() + ")array)[index];\n" + + "}\n\n" + + + "@Override\n" + + "public void set(Object array, int index, Object value){\n" + + "\t((" + clazz.getCanonicalName() + ")array)[index] = " + v + ";" + + "}\n\n" + + "}"; + + Class arrayProxyClazz = CompilerUtils.compileSource(completeClassName, source); + if (arrayProxyClazz == null) + return null; + + return (ArrayProxy) arrayProxyClazz.getConstructor().newInstance(); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistClassProxyFactory.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistClassProxyFactory.java new file mode 100644 index 000000000..a92e74a3a --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistClassProxyFactory.java @@ -0,0 +1,183 @@ +package com.fireflysource.common.bytecode; + +import javassist.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static com.fireflysource.common.string.StringUtils.replace; + +public class JavassistClassProxyFactory implements ClassProxyFactory { + + public static final JavassistClassProxyFactory INSTANCE = new JavassistClassProxyFactory(); + + private JavassistClassProxyFactory() { + } + + @SuppressWarnings("unchecked") + @Override + public T createProxy(T instance, ClassProxy proxy, MethodFilter methodFilter) throws Throwable { + Class clazz = instance.getClass(); + Method[] methods = Arrays.stream(clazz.getMethods()).filter(m -> filterMethods(methodFilter, m)).toArray(Method[]::new); + if (methods.length == 0) { + return instance; + } + + ClassPool classPool = ClassPool.getDefault(); + classPool.insertClassPath(new ClassClassPath(ClassProxyFactory.class)); + CtClass cc = buildClass(classPool, clazz); + buildPrivateFields(clazz, cc); + buildConstructor(classPool, clazz, cc); + List methodCodes = buildMethodCodes(methods); + for (String str : methodCodes) { + cc.addMethod(CtMethod.make(str, cc)); + } + + MethodProxy[] methodProxies = getMethodProxies(methods); + return (T) JavassistUtils + .getClass(cc) + .getConstructor(ClassProxy.class, clazz, MethodProxy[].class) + .newInstance(proxy, instance, methodProxies); + } + + + private List buildMethodCodes(Method[] methods) { + return IntStream + .range(0, methods.length) + .boxed() + .map(index -> buildMethodCode(methods[index], index)) + .collect(Collectors.toList()); + } + + private String buildMethodCode(Method m, Integer index) { + Class[] parameters = m.getParameterTypes(); + return buildMethodSignatureLine(m, parameters) + "{\n" + + convertParametersToObjectArray(parameters) + + buildInvokeInterceptMethodAndReturnLine(m, index) + + "}"; + } + + private MethodProxy[] getMethodProxies(Method[] methods) { + return Arrays.stream(methods) + .map(JavassistReflectionProxyFactory.INSTANCE::getMethodProxy) + .toArray(MethodProxy[]::new); + } + + private String buildMethodSignatureLine(Method m, Class[] parameters) { + String t = "public {} {} ({})"; + return replace(t, m.getReturnType().getCanonicalName(), m.getName(), buildParameters(parameters)); + } + + private String convertParametersToObjectArray(Class[] parameters) { + if (parameters == null || parameters.length == 0) { + return "Object[] args = new Object[0];\n"; + } else { + return "Object[] args = new Object[]{" + + buildParameterObjectArray(parameters) + + "};\n"; + } + } + + private String buildInvokeInterceptMethodAndReturnLine(Method m, Integer index) { + if (!m.getReturnType().equals(void.class)) { + if (m.getReturnType().isPrimitive()) { + String t = "\t{} ret = (({})classProxy.intercept(methodProxies[{}], originalInstance, args)).{}Value();\n" + + "\treturn ret;\n"; + return replace(t, + m.getReturnType().getCanonicalName(), + AbstractProxyFactory.primitiveWrapMap.get(m.getReturnType()), + index, + m.getReturnType().getCanonicalName()); + } else { + String t = "\t{} ret = ({})classProxy.intercept(methodProxies[{}], originalInstance, args);\n" + + "\treturn ret;\n"; + return replace(t, + m.getReturnType().getCanonicalName(), + m.getReturnType().getCanonicalName(), + index); + } + } else { + String t = "\tclassProxy.intercept(methodProxies[{}], originalInstance, args);\n"; + return replace(t, index); + } + } + + + private String buildParameterObjectArray(Class[] parameters) { + return IntStream.range(0, parameters.length) + .boxed() + .map(index -> convertTypeToObject(parameters, index)) + .collect(Collectors.joining(",")); + } + + private String convertTypeToObject(Class[] parameters, Integer index) { + final Class parameter = parameters[index]; + String objectParam; + if (parameter.isPrimitive()) { + String t = "(Object){}.valueOf(arg{})"; + objectParam = replace(t, AbstractProxyFactory.primitiveWrapMap.get(parameters[index]), index); + } else { + objectParam = "(Object)arg" + index; + } + return objectParam; + } + + private String buildParameters(Class[] parameters) { + if (parameters == null || parameters.length == 0) { + return ""; + } + return IntStream + .range(0, parameters.length) + .boxed() + .map(index -> parameters[index].getCanonicalName() + " arg" + index) + .collect(Collectors.joining(",")); + } + + private boolean filterMethods(MethodFilter filter, Method m) { + return !m.getDeclaringClass().equals(Object.class) + && !Modifier.isFinal(m.getModifiers()) + && !Modifier.isStatic(m.getModifiers()) + && !Modifier.isNative(m.getModifiers()) + && Optional.ofNullable(filter).map(f -> f.accept(m)).orElse(true); + } + + private CtClass buildClass(ClassPool classPool, Class clazz) throws NotFoundException, CannotCompileException { + String className = "com.fireflysource.common.bytecode.ClassProxy" + UUID.randomUUID().toString().replace("-", ""); + CtClass cc = classPool.makeClass(className); + cc.setSuperclass(classPool.get(clazz.getName())); + return cc; + } + + private void buildPrivateFields(Class clazz, CtClass cc) throws CannotCompileException { + cc.addField(CtField.make("private " + ClassProxy.class.getCanonicalName() + " classProxy;", cc)); + cc.addField(CtField.make("private " + clazz.getCanonicalName() + " originalInstance;", cc)); + cc.addField(CtField.make("private " + MethodProxy[].class.getCanonicalName() + " methodProxies;", cc)); + } + + private void buildConstructor(ClassPool classPool, Class clazz, CtClass cc) throws CannotCompileException, NotFoundException { + CtConstructor empty = new CtConstructor(null, cc); + empty.setBody("{}"); + cc.addConstructor(empty); + + CtConstructor constructor = new CtConstructor(new CtClass[]{ + classPool.get(ClassProxy.class.getName()), + classPool.get(clazz.getName()), + classPool.get(MethodProxy[].class.getName()) + }, cc); + String bodyTemplate = "{" + + "this.classProxy = ({})$1;" + + "this.originalInstance = ({})$2;" + + "this.methodProxies = ({})$3;" + + "}"; + String body = replace(bodyTemplate, + ClassProxy.class.getCanonicalName(), clazz.getCanonicalName(), MethodProxy[].class.getCanonicalName()); + constructor.setBody(body); + cc.addConstructor(constructor); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistReflectionProxyFactory.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistReflectionProxyFactory.java new file mode 100644 index 000000000..3e8c9c4b2 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistReflectionProxyFactory.java @@ -0,0 +1,226 @@ +package com.fireflysource.common.bytecode; + +import com.fireflysource.common.string.StringUtils; +import javassist.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; + +/** + * @author Pengtao Qiu + */ +public class JavassistReflectionProxyFactory extends AbstractProxyFactory { + + public static final JavassistReflectionProxyFactory INSTANCE = new JavassistReflectionProxyFactory(); + + private JavassistReflectionProxyFactory() { + + } + + protected ArrayProxy createArrayProxy(Class clazz) { + try { + ClassPool classPool = ClassPool.getDefault(); + classPool.insertClassPath(new ClassClassPath(ArrayProxy.class)); + + CtClass cc = classPool.makeClass("com.fireflysource.common.bytecode.ArrayField" + UUID.randomUUID().toString().replace("-", "")); + cc.addInterface(classPool.get(ArrayProxy.class.getName())); + + cc.addMethod(CtMethod.make(createArraySizeCode(clazz), cc)); + cc.addMethod(CtMethod.make(createArrayGetCode(clazz), cc)); + cc.addMethod(CtMethod.make(createArraySetCode(clazz), cc)); + + return (ArrayProxy) JavassistUtils.getClass(cc).getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private String createArraySetCode(Class clazz) { + StringBuilder code = new StringBuilder("public void set(Object array, int index, Object value){\n"); + + Class componentType = clazz.getComponentType(); + if (componentType.isPrimitive()) { + String t = "\t(({})array)[index] = (({})value).{}Value();\n"; + code.append(StringUtils.replace(t, + clazz.getCanonicalName(), + primitiveWrapMap.get(componentType), + componentType.getCanonicalName())); + } else { + String t = "\t(({})array)[index] = ({})value;\n"; + code.append(StringUtils.replace(t, clazz.getCanonicalName(), componentType.getCanonicalName())); + } + + code.append("}"); + return code.toString(); + } + + private String createArrayGetCode(Class clazz) { + StringBuilder code = new StringBuilder("public Object get(Object array, int index){\n"); + + Class componentType = clazz.getComponentType(); + if (componentType.isPrimitive()) { + String t = "\treturn (Object){}.valueOf((({})array)[index]);\n"; + code.append(StringUtils.replace(t, primitiveWrapMap.get(componentType), clazz.getCanonicalName())); + } else { + String t = "\treturn (({})array)[index];\n"; + code.append(StringUtils.replace(t, clazz.getCanonicalName())); + } + + code.append("}"); + return code.toString(); + } + + private String createArraySizeCode(Class clazz) { + String t = "public int size(Object array){\n" + + "\treturn (({})array).length;\n" + + "}"; + return StringUtils.replace(t, clazz.getCanonicalName()); + } + + protected FieldProxy createFieldProxy(Field field) { + try { + ClassPool classPool = ClassPool.getDefault(); + classPool.insertClassPath(new ClassClassPath(FieldProxy.class)); + + CtClass cc = classPool.makeClass("com.fireflysource.common.bytecode.ProxyField" + UUID.randomUUID().toString().replace("-", "")); + cc.addInterface(classPool.get(FieldProxy.class.getName())); + cc.addField(CtField.make("private java.lang.reflect.Field field;", cc)); + + CtConstructor constructor = new CtConstructor(new CtClass[]{classPool.get(Field.class.getName())}, cc); + constructor.setBody("{this.field = (java.lang.reflect.Field)$1;}"); + cc.addConstructor(constructor); + + cc.addMethod(CtMethod.make("public java.lang.reflect.Field field(){return field;}", cc)); + cc.addMethod(CtMethod.make(createFieldGetterMethodCode(field), cc)); + cc.addMethod(CtMethod.make(createFieldSetterMethodCode(field), cc)); + + return (FieldProxy) JavassistUtils.getClass(cc).getConstructor(Field.class).newInstance(field); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private String createFieldGetterMethodCode(Field field) { + Class fieldClazz = field.getType(); + StringBuilder code = new StringBuilder("public Object get(Object obj){\n"); + + if (fieldClazz.isPrimitive()) { + String t = "\treturn (Object){}.valueOf( (({})obj).{} );\n"; + code.append(StringUtils.replace(t, + primitiveWrapMap.get(fieldClazz), + field.getDeclaringClass().getCanonicalName(), + field.getName())); + } else { + String t = "\treturn (({})obj).{};\n"; + code.append(StringUtils.replace(t, + field.getDeclaringClass().getCanonicalName(), + field.getName())); + } + + code.append("}"); + return code.toString(); + } + + private String createFieldSetterMethodCode(Field field) { + Class fieldClazz = field.getType(); + StringBuilder code = new StringBuilder("public void set(Object obj, Object value){\n"); + + if (fieldClazz.isPrimitive()) { + String t = "\t(({})obj).{} = (({})value).{}Value();\n"; + code.append(StringUtils.replace(t, + field.getDeclaringClass().getCanonicalName(), field.getName(), + primitiveWrapMap.get(fieldClazz), fieldClazz.getCanonicalName())); + } else { + String t = "\t(({})obj).{} = ({})value;\n"; + code.append(StringUtils.replace(t, + field.getDeclaringClass().getCanonicalName(), field.getName(), + fieldClazz.getCanonicalName())); + } + + code.append("}"); + return code.toString(); + } + + protected MethodProxy createMethodProxy(Method method) { + try { + ClassPool classPool = ClassPool.getDefault(); + classPool.insertClassPath(new ClassClassPath(MethodProxy.class)); + + CtClass cc = classPool.makeClass("com.fireflysource.common.bytecode.ProxyMethod" + UUID.randomUUID().toString().replace("-", "")); + + cc.addInterface(classPool.get(MethodProxy.class.getName())); + cc.addField(CtField.make("private java.lang.reflect.Method method;", cc)); + + CtConstructor constructor = new CtConstructor(new CtClass[]{classPool.get(Method.class.getName())}, cc); + constructor.setBody("{this.method = (java.lang.reflect.Method)$1;}"); + cc.addConstructor(constructor); + + cc.addMethod(CtMethod.make("public java.lang.reflect.Method method(){return method;}", cc)); + cc.addMethod(CtMethod.make(createInvokeMethodCode(method), cc)); + + return (MethodProxy) JavassistUtils.getClass(cc).getConstructor(Method.class).newInstance(method); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private String createInvokeMethodCode(Method method) { + Class[] paramClazz = method.getParameterTypes(); + StringBuilder code = new StringBuilder(); + + code.append("public Object invoke(Object obj, Object[] args){\n "); + if (paramClazz.length > 0) { + String t = "\tif(args == null || args.length != {})\n" + + "\t\tthrow new IllegalArgumentException(\"arguments error\");\n\n"; + code.append(StringUtils.replace(t, paramClazz.length)); + } + if (method.getReturnType().equals(Void.TYPE)) { + code.append('\t').append(createMethodCall(method)).append(";\n") + .append("\treturn null;\n"); + } else { + code.append("\treturn "); + if (method.getReturnType().isPrimitive()) { + code.append(StringUtils.replace("(Object){}.valueOf(", primitiveWrapMap.get(method.getReturnType()))) + .append(createMethodCall(method)) + .append(");\n"); + } else { + code.append(createMethodCall(method)).append(";\n"); + } + } + + code.append('}'); + return code.toString(); + } + + private String createMethodCall(Method method) { + Class[] paramClazz = method.getParameterTypes(); + StringBuilder code = new StringBuilder(); + + if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) { + code.append(method.getDeclaringClass().getCanonicalName()); + } else { + code.append(StringUtils.replace("(({})obj)", method.getDeclaringClass().getCanonicalName())); + } + + code.append('.').append(method.getName()).append('('); + if (paramClazz.length > 0) { + int max = paramClazz.length - 1; + for (int i = 0; ; i++) { + Class param = paramClazz[i]; + if (param.isPrimitive()) { + code.append(StringUtils.replace("(({})args[{}]).{}Value()", primitiveWrapMap.get(param), i, param.getCanonicalName())); + } else { + code.append(StringUtils.replace("({})args[{}]", param.getCanonicalName(), i)); + } + + if (i == max) { + break; + } + code.append(", "); + } + } + code.append(')'); + return code.toString(); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistUtils.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistUtils.java new file mode 100644 index 000000000..94ea86cc7 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/JavassistUtils.java @@ -0,0 +1,17 @@ +package com.fireflysource.common.bytecode; + +import com.fireflysource.common.sys.JavaVersion; +import javassist.CtClass; + +public class JavassistUtils { + + public static Class getClass(CtClass cc) throws Exception { + Class clazz; + if (JavaVersion.VERSION.getPlatform() < 9) { + clazz = cc.toClass(Thread.currentThread().getContextClassLoader(), null); + } else { + clazz = cc.toClass(JavassistUtils.class); + } + return clazz; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/MethodFilter.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/MethodFilter.java new file mode 100644 index 000000000..df368a622 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/MethodFilter.java @@ -0,0 +1,8 @@ +package com.fireflysource.common.bytecode; + +import java.lang.reflect.Method; + +@FunctionalInterface +public interface MethodFilter { + boolean accept(Method method); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/MethodProxy.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/MethodProxy.java new file mode 100644 index 000000000..9139f0abd --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/MethodProxy.java @@ -0,0 +1,19 @@ +package com.fireflysource.common.bytecode; + +import java.lang.reflect.Method; + +/** + * @author Pengtao Qiu + */ +public interface MethodProxy { + Method method(); + + /** + * Executes this method + * + * @param obj The instance of object that contains this method + * @param args The parameters of this method + * @return Return value of this method + */ + Object invoke(Object obj, Object... args); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/bytecode/ProxyFactory.java b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ProxyFactory.java new file mode 100644 index 000000000..e4d2e335a --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/bytecode/ProxyFactory.java @@ -0,0 +1,15 @@ +package com.fireflysource.common.bytecode; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * @author Pengtao Qiu + */ +public interface ProxyFactory { + ArrayProxy getArrayProxy(Class clazz); + + FieldProxy getFieldProxy(Field field); + + MethodProxy getMethodProxy(Method method); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Base64.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Base64.java new file mode 100644 index 000000000..b26edf02b --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Base64.java @@ -0,0 +1,745 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; + +/** + * Provides Base64 encoding and decoding as defined by RFC 2045. + * + *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ *

+ * The class can be parameterized in the following manner with various constructors: + *

+ *
    + *
  • URL-safe mode: Default off.
  • + *
  • Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of + * 4 in the encoded data. + *
  • Line separator: Default is CRLF ("\r\n")
  • + *
+ *

+ * The URL-safe parameter is only applied to encode operations. Decoding seamlessly handles both modes. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only + * encode/decode character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, + * UTF-8, etc). + *

+ *

+ * This class is thread-safe. + *

+ * + * @version $Id: Base64.java 1635986 2014-11-01 16:27:52Z tn $ + * @see RFC 2045 + * @since 1.0 + */ +public class Base64 extends BaseNCodec { + + /** + * BASE32 characters are 6 bits in length. + * They are formed by taking a block of 3 octets to form a 24-bit string, + * which is converted into 4 BASE64 characters. + */ + private static final int BITS_PER_ENCODED_BYTE = 6; + private static final int BYTES_PER_UNENCODED_BLOCK = 3; + private static final int BYTES_PER_ENCODED_BLOCK = 4; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + *

+ * N.B. The next major release may break compatibility and make this field private. + *

+ * + * @see RFC 2045 section 2.1 + */ + static final byte[] CHUNK_SEPARATOR = {'\r', '\n'}; + + /** + * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet" + * equivalents as specified in Table 1 of RFC 2045. + *

+ * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] STANDARD_ENCODE_TABLE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + /** + * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / + * changed to - and _ to make the encoded Base64 results more URL-SAFE. + * This table is only used when the Base64's mode is set to URL-SAFE. + */ + private static final byte[] URL_SAFE_ENCODE_TABLE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + }; + + /** + * This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified + * in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64 + * alphabet but fall within the bounds of the array are translated to -1. + *

+ * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both + * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit). + *

+ * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] DECODE_TABLE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }; + + /** + * Base64 uses 6-bit fields. + */ + /** + * Mask used to extract 6 bits, used when encoding + */ + private static final int MASK_6BITS = 0x3f; + + // The static final fields above are used for the original static byte[] methods on Base64. + // The private member fields below are used with the new streaming approach, which requires + // some state be preserved between calls of encode() and decode(). + + /** + * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able + * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch + * between the two modes. + */ + private final byte[] encodeTable; + + // Only one decode table currently; keep for consistency with Base32 code + private final byte[] decodeTable = DECODE_TABLE; + + /** + * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * decodeSize = 3 + lineSeparator.length; + */ + private final int decodeSize; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * encodeSize = 4 + lineSeparator.length; + */ + private final int encodeSize; + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ */ + public Base64() { + this(0); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode. + *

+ * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ * + * @param urlSafe if true, URL-safe encoding is used. In most cases this should be set to + * false. + * @since 1.4 + */ + public Base64(final boolean urlSafe) { + this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @since 1.4 + */ + public Base64(final int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator Each line of encoded data will end with this sequence of bytes. + * @throws IllegalArgumentException Thrown when the provided lineSeparator included some base64 characters. + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator) { + this(lineLength, lineSeparator, false); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator Each line of encoded data will end with this sequence of bytes. + * @param urlSafe Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode + * operations. Decoding seamlessly handles both modes. + * Note: no padding is added when using the URL-safe alphabet. + * @throws IllegalArgumentException The provided lineSeparator included some base64 characters. That's not going to work! + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) { + super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, + lineLength, + lineSeparator == null ? 0 : lineSeparator.length); + // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0 + // @see test case Base64Test.testConstructors() + if (lineSeparator != null) { + if (containsAlphabetOrPad(lineSeparator)) { + final String sep = new String(lineSeparator, StandardCharsets.UTF_8); + throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]"); + } + if (lineLength > 0) { // null line-sep forces no chunking rather than throwing IAE + this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length; + this.lineSeparator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length); + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + this.decodeSize = this.encodeSize - 1; + this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; + } + + /** + * Returns our current encode mode. True if we're URL-SAFE, false otherwise. + * + * @return true if we're in URL-SAFE mode, false otherwise. + * @since 1.4 + */ + public boolean isUrlSafe() { + return this.encodeTable == URL_SAFE_ENCODE_TABLE; + } + + /** + *

+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with + * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last + * remaining bytes (if not multiple of 3). + *

+ *

Note: no padding is added when encoding using the URL-safe alphabet.

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in byte[] array of binary data to base64 encode. + * @param inPos Position to start reading data from. + * @param inAvail Amount of bytes available from input for encoding. + * @param context the context to be used + */ + @Override + void encode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + context.eof = true; + if (0 == context.modulus && lineLength == 0) { + return; // no leftovers to process and not using chunking + } + final byte[] buffer = ensureBufferSize(encodeSize, context); + final int savedPos = context.pos; + switch (context.modulus) { // 0-2 + case 0: // nothing to do here + break; + case 1: // 8 bits = 6 + 2 + // top 6 bits: + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS]; + // remaining 2: + buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + } + break; + + case 2: // 16 bits = 6 + 6 + 4 + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + } + break; + default: + throw new IllegalStateException("Impossible modulus " + context.modulus); + } + context.currentLinePos += context.pos - savedPos; // keep track of current line position + // if currentPos == 0 we are at the start of a line, so don't add CRLF + if (lineLength > 0 && context.currentLinePos > 0) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(encodeSize, context); + context.modulus = (context.modulus + 1) % BYTES_PER_UNENCODED_BLOCK; + int b = in[inPos++]; + if (b < 0) { + b += 256; + } + context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE + if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS]; + context.currentLinePos += BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0 && lineLength <= context.currentLinePos) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + context.currentLinePos = 0; + } + } + } + } + } + + /** + *

+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once + * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" + * call is not necessary when decoding, but it doesn't hurt, either. + *

+ *

+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are + * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in, + * garbage-out philosophy: it will not check the provided data for validity. + *

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in byte[] array of ascii data to base64 decode. + * @param inPos Position to start reading data from. + * @param inAvail Amount of bytes available from input for encoding. + * @param context the context to be used + */ + @Override + void decode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + if (inAvail < 0) { + context.eof = true; + } + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + final byte b = in[inPos++]; + if (b == pad) { + // We're done. + context.eof = true; + break; + } else { + if (b >= 0 && b < DECODE_TABLE.length) { + final int result = DECODE_TABLE[b]; + if (result >= 0) { + context.modulus = (context.modulus + 1) % BYTES_PER_ENCODED_BLOCK; + context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result; + if (context.modulus == 0) { + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); + } + } + } + } + } + + // Two forms of EOF as far as base64 decoder is concerned: actual + // EOF (-1) and first time '=' character is encountered in stream. + // This approach makes the '=' padding characters completely optional. + if (context.eof && context.modulus != 0) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + + // We have some spare bits remaining + // Output all whole multiples of 8 bits and ignore the rest + switch (context.modulus) { +// case 0 : // impossible, as excluded above + case 1: // 6 bits - ignore entirely + // TODO not currently tested; perhaps it is impossible? + break; + case 2: // 12 bits = 8 + 4 + context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits + buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); + break; + case 3: // 18 bits = 8 + 8 + 2 + context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); + break; + default: + throw new IllegalStateException("Impossible modulus " + context.modulus); + } + } + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param arrayOctet byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is empty; + * false, otherwise + * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0. + */ + @Deprecated + public static boolean isArrayByteBase64(final byte[] arrayOctet) { + return isBase64(arrayOctet); + } + + /** + * Returns whether or not the octet is in the base 64 alphabet. + * + * @param octet The value to test + * @return true if the value is defined in the the base 64 alphabet, false otherwise. + * @since 1.4 + */ + public static boolean isBase64(final byte octet) { + return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1); + } + + /** + * Tests a given String to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param base64 String to test + * @return true if all characters in the String are valid characters in the Base64 alphabet or if + * the String is empty; false, otherwise + * @since 1.5 + */ + public static boolean isBase64(final String base64) { + return isBase64(base64.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param arrayOctet byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is empty; + * false, otherwise + * @since 1.5 + */ + public static boolean isBase64(final byte[] arrayOctet) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) { + return false; + } + } + return true; + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * @param binaryData binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 representation. + */ + public static byte[] encodeBase64(final byte[] binaryData) { + return encodeBase64(binaryData, false); + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + *

+ * NOTE: We changed the behaviour of this method from multi-line chunking (commons-codec-1.4) to + * single-line non-chunking (commons-codec-1.5). + * + * @param binaryData binary data to encode + * @return String containing Base64 characters. + * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not). + */ + public static String encodeBase64String(final byte[] binaryData) { + return new String(encodeBase64(binaryData, false), StandardCharsets.UTF_8); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * + * @param binaryData binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 representation. + * @since 1.4 + */ + public static byte[] encodeBase64URLSafe(final byte[] binaryData) { + return encodeBase64(binaryData, false, true); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * + * @param binaryData binary data to encode + * @return String containing Base64 characters + * @since 1.4 + */ + public static String encodeBase64URLSafeString(final byte[] binaryData) { + return new String(encodeBase64(binaryData, false, true), StandardCharsets.UTF_8); + } + + /** + * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks + * + * @param binaryData binary data to encode + * @return Base64 characters chunked in 76 character blocks + */ + public static byte[] encodeBase64Chunked(final byte[] binaryData) { + return encodeBase64(binaryData, true); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData Array containing binary data to encode. + * @param isChunked if true this encoder will chunk the base64 output into 76 character blocks + * @return Base64-encoded data. + * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) { + return encodeBase64(binaryData, isChunked, false); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData Array containing binary data to encode. + * @param isChunked if true this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe if true this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @return Base64-encoded data. + * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) { + return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData Array containing binary data to encode. + * @param isChunked if true this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe if true this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @param maxResultSize The maximum result size to accept. + * @return Base64-encoded data. + * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than maxResultSize + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, + final boolean urlSafe, final int maxResultSize) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + + // Create this so can use the super-class method + // Also ensures that the same roundings are performed by the ctor and the code + final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe); + final long len = b64.getEncodedLength(binaryData); + if (len > maxResultSize) { + throw new IllegalArgumentException("Input array too big, the output array would be bigger (" + + len + + ") than the specified maximum size of " + + maxResultSize); + } + + return b64.encode(binaryData); + } + + /** + * Decodes a Base64 String into octets. + *

+ * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

+ * + * @param base64String String containing Base64 data + * @return Array containing decoded data. + * @since 1.4 + */ + public static byte[] decodeBase64(final String base64String) { + return new Base64().decode(base64String); + } + + /** + * Decodes Base64 data into octets. + *

+ * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

+ * + * @param base64Data Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase64(final byte[] base64Data) { + return new Base64().decode(base64Data); + } + + // Implementation of the Encoder Interface + + // Implementation of integer encoding used for crypto + + /** + * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature. + * + * @param pArray a byte array containing base64 character data + * @return A BigInteger + * @since 1.4 + */ + public static BigInteger decodeInteger(final byte[] pArray) { + return new BigInteger(1, decodeBase64(pArray)); + } + + /** + * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature. + * + * @param bigInt a BigInteger + * @return A byte array containing base64 character data + * @throws NullPointerException if null is passed in + * @since 1.4 + */ + public static byte[] encodeInteger(final BigInteger bigInt) { + if (bigInt == null) { + throw new NullPointerException("encodeInteger called with null parameter"); + } + return encodeBase64(toIntegerBytes(bigInt), false); + } + + /** + * Returns a byte-array representation of a BigInteger without sign bit. + * + * @param bigInt BigInteger to be converted + * @return a byte array representation of the BigInteger parameter + */ + static byte[] toIntegerBytes(final BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[bitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + /** + * Returns whether or not the octet is in the Base64 alphabet. + * + * @param octet The value to test + * @return true if the value is defined in the the Base64 alphabet false otherwise. + */ + @Override + protected boolean isInAlphabet(final byte octet) { + return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Base64Utils.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Base64Utils.java new file mode 100644 index 000000000..72f2edefd --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Base64Utils.java @@ -0,0 +1,155 @@ +package com.fireflysource.common.codec.base64; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public abstract class Base64Utils { + + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private static final Base64Delegate delegate = new CommonsCodecBase64Delegate(); + + /** + * Base64-encode the given byte array. + * + * @param src the original byte array (may be {@code null}) + * @return the encoded byte array (or {@code null} if the input was + * {@code null}) + */ + public static byte[] encode(byte[] src) { + return delegate.encode(src); + } + + /** + * Base64-decode the given byte array. + * + * @param src the encoded byte array (may be {@code null}) + * @return the original byte array (or {@code null} if the input was + * {@code null}) + */ + public static byte[] decode(byte[] src) { + return delegate.decode(src); + } + + /** + * Base64-encode the given byte array using the RFC 4868 + * "URL and Filename Safe Alphabet". + * + * @param src the original byte array (may be {@code null}) + * @return the encoded byte array (or {@code null} if the input was + * {@code null}) + */ + public static byte[] encodeUrlSafe(byte[] src) { + return delegate.encodeUrlSafe(src); + } + + /** + * Base64-decode the given byte array using the RFC 4868 + * "URL and Filename Safe Alphabet". + * + * @param src the encoded byte array (may be {@code null}) + * @return the original byte array (or {@code null} if the input was + * {@code null}) + */ + public static byte[] decodeUrlSafe(byte[] src) { + return delegate.decodeUrlSafe(src); + } + + /** + * Base64-encode the given byte array to a String. + * + * @param src the original byte array (may be {@code null}) + * @return the encoded byte array as a UTF-8 String (or {@code null} if the + * input was {@code null}) + */ + public static String encodeToString(byte[] src) { + if (src == null) { + return null; + } + if (src.length == 0) { + return ""; + } + + return new String(delegate.encode(src), DEFAULT_CHARSET); + } + + /** + * Base64-decode the given byte array from an UTF-8 String. + * + * @param src the encoded UTF-8 String (may be {@code null}) + * @return the original byte array (or {@code null} if the input was + * {@code null}) + */ + public static byte[] decodeFromString(String src) { + if (src == null) { + return null; + } + if (src.length() == 0) { + return new byte[0]; + } + + return delegate.decode(src.getBytes(DEFAULT_CHARSET)); + } + + /** + * Base64-encode the given byte array to a String using the RFC 4868 + * "URL and Filename Safe Alphabet". + * + * @param src the original byte array (may be {@code null}) + * @return the encoded byte array as a UTF-8 String (or {@code null} if the + * input was {@code null}) + */ + public static String encodeToUrlSafeString(byte[] src) { + return new String(delegate.encodeUrlSafe(src), DEFAULT_CHARSET); + } + + /** + * Base64-decode the given byte array from an UTF-8 String using the RFC + * 4868 "URL and Filename Safe Alphabet". + * + * @param src the encoded UTF-8 String (may be {@code null}) + * @return the original byte array (or {@code null} if the input was + * {@code null}) + */ + public static byte[] decodeFromUrlSafeString(String src) { + return delegate.decodeUrlSafe(src.getBytes(DEFAULT_CHARSET)); + } + + interface Base64Delegate { + + byte[] encode(byte[] src); + + byte[] decode(byte[] src); + + byte[] encodeUrlSafe(byte[] src); + + byte[] decodeUrlSafe(byte[] src); + } + + static class CommonsCodecBase64Delegate implements Base64Delegate { + + private final Base64 base64 = new Base64(); + + private final Base64 base64UrlSafe = new Base64(0, null, true); + + @Override + public byte[] encode(byte[] src) { + return this.base64.encode(src); + } + + @Override + public byte[] decode(byte[] src) { + return this.base64.decode(src); + } + + @Override + public byte[] encodeUrlSafe(byte[] src) { + return this.base64UrlSafe.encode(src); + } + + @Override + public byte[] decodeUrlSafe(byte[] src) { + return this.base64UrlSafe.decode(src); + } + + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BaseNCodec.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BaseNCodec.java new file mode 100644 index 000000000..d08828ed5 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BaseNCodec.java @@ -0,0 +1,514 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Abstract superclass for Base-N encoders and decoders. + * + *

+ * This class is thread-safe. + *

+ * + * @version $Id: BaseNCodec.java 1634404 2014-10-26 23:06:10Z ggregory $ + */ +public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder { + + /** + * Holds thread context so classes can be thread-safe. + *

+ * This class is not itself thread-safe; each thread must allocate its own copy. + * + * @since 1.7 + */ + static class Context { + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + int ibitWorkArea; + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + long lbitWorkArea; + + /** + * Buffer for streaming. + */ + byte[] buffer; + + /** + * Position where next character should be written in the buffer. + */ + int pos; + + /** + * Position where next character should be read from the buffer. + */ + int readPos; + + /** + * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless, + * and must be thrown away. + */ + boolean eof; + + /** + * Variable tracks how many characters have been written to the current line. Only used when encoding. We use + * it to make sure each encoded line never goes beyond lineLength (if lineLength > 0). + */ + int currentLinePos; + + /** + * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This + * variable helps track that. + */ + int modulus; + + Context() { + } + + /** + * Returns a String useful for debugging (especially within a debugger.) + * + * @return a String useful for debugging. + */ + @SuppressWarnings("boxing") // OK to ignore boxing here + @Override + public String toString() { + return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, " + + "modulus=%s, pos=%s, readPos=%s]", this.getClass().getSimpleName(), Arrays.toString(buffer), + currentLinePos, eof, ibitWorkArea, lbitWorkArea, modulus, pos, readPos); + } + } + + /** + * EOF + * + * @since 1.7 + */ + static final int EOF = -1; + + /** + * MIME chunk size per RFC 2045 section 6.8. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 2045 section 6.8 + */ + public static final int MIME_CHUNK_SIZE = 76; + + /** + * PEM chunk size per RFC 1421 section 4.3.2.4. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 1421 section 4.3.2.4 + */ + public static final int PEM_CHUNK_SIZE = 64; + + private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; + + /** + * Defines the default buffer size - currently {@value} + * - must be large enough for at least one encoded block+separator + */ + private static final int DEFAULT_BUFFER_SIZE = 8192; + + /** + * Mask used to extract 8 bits, used in decoding bytes + */ + protected static final int MASK_8BITS = 0xff; + + /** + * Byte used to pad output. + */ + protected static final byte PAD_DEFAULT = '='; // Allow static access to default + + /** + * @deprecated Use {@link #pad}. Will be removed in 2.0. + */ + @Deprecated + protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to vary later + + protected final byte pad; // instance variable just in case it needs to vary later + + /** + * Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32 + */ + private final int unencodedBlockSize; + + /** + * Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32 + */ + private final int encodedBlockSize; + + /** + * Chunksize for encoding. Not used when decoding. + * A value of zero or less implies no chunking of the encoded data. + * Rounded down to nearest multiple of encodedBlockSize. + */ + protected final int lineLength; + + /** + * Size of chunk separator. Not used unless {@link #lineLength} > 0. + */ + private final int chunkSeparatorLength; + + /** + * Note lineLength is rounded down to the nearest multiple of {@link #encodedBlockSize} + * If chunkSeparatorLength is zero, then chunking is disabled. + * + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length lineLength + * @param chunkSeparatorLength the chunk separator length, if relevant + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength) { + this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT); + } + + /** + * Note lineLength is rounded down to the nearest multiple of {@link #encodedBlockSize} + * If chunkSeparatorLength is zero, then chunking is disabled. + * + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length lineLength + * @param chunkSeparatorLength the chunk separator length, if relevant + * @param pad byte used as padding byte. + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength, final byte pad) { + this.unencodedBlockSize = unencodedBlockSize; + this.encodedBlockSize = encodedBlockSize; + final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0; + this.lineLength = useChunking ? (lineLength / encodedBlockSize) * encodedBlockSize : 0; + this.chunkSeparatorLength = chunkSeparatorLength; + + this.pad = pad; + } + + /** + * Returns true if this object has buffered data for reading. + * + * @param context the context to be used + * @return true if there is data still available for reading. + */ + boolean hasData(final Context context) { // package protected for access from I/O streams + return context.buffer != null; + } + + /** + * Returns the amount of buffered data available for reading. + * + * @param context the context to be used + * @return The amount of buffered data available for reading. + */ + int available(final Context context) { // package protected for access from I/O streams + return context.buffer != null ? context.pos - context.readPos : 0; + } + + /** + * Get the default buffer size. Can be overridden. + * + * @return {@link #DEFAULT_BUFFER_SIZE} + */ + protected int getDefaultBufferSize() { + return DEFAULT_BUFFER_SIZE; + } + + /** + * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}. + * + * @param context the context to be used + */ + private byte[] resizeBuffer(final Context context) { + if (context.buffer == null) { + context.buffer = new byte[getDefaultBufferSize()]; + context.pos = 0; + context.readPos = 0; + } else { + final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR]; + System.arraycopy(context.buffer, 0, b, 0, context.buffer.length); + context.buffer = b; + } + return context.buffer; + } + + /** + * Ensure that the buffer has room for size bytes + * + * @param size minimum spare space required + * @param context the context to be used + * @return the buffer + */ + protected byte[] ensureBufferSize(final int size, final Context context) { + if ((context.buffer == null) || (context.buffer.length < context.pos + size)) { + return resizeBuffer(context); + } + return context.buffer; + } + + /** + * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail + * bytes. Returns how many bytes were actually extracted. + *

+ * Package protected for access from I/O streams. + * + * @param b byte[] array to extract the buffered data into. + * @param bPos position in byte[] array to start extraction at. + * @param bAvail amount of bytes we're allowed to extract. We may extract fewer (if fewer are available). + * @param context the context to be used + * @return The number of bytes successfully extracted into the provided byte[] array. + */ + int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) { + if (context.buffer != null) { + final int len = Math.min(available(context), bAvail); + System.arraycopy(context.buffer, context.readPos, b, bPos, len); + context.readPos += len; + if (context.readPos >= context.pos) { + context.buffer = null; // so hasData() will return false, and this method can return -1 + } + return len; + } + return context.eof ? EOF : 0; + } + + /** + * Checks if a byte value is whitespace or not. + * Whitespace is taken to mean: space, tab, CR, LF + * + * @param byteToCheck the byte to check + * @return true if byte is whitespace, false otherwise + */ + protected static boolean isWhiteSpace(final byte byteToCheck) { + switch (byteToCheck) { + case ' ': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } + } + + /** + * Encodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of + * the Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[]. + * + * @param obj Object to encode + * @return An object (of type byte[]) containing the Base-N encoded data which corresponds to the byte[] supplied. + * @throws EncoderException if the parameter supplied is not of type byte[] + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof byte[])) { + throw new EncoderException("Parameter supplied to Base-N encode is not a byte[]"); + } + return encode((byte[]) obj); + } + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the Base-N alphabet. + * Uses UTF8 encoding. + * + * @param pArray a byte array containing binary data + * @return A String containing only Base-N character data + */ + public String encodeToString(final byte[] pArray) { + return new String(encode(pArray), StandardCharsets.UTF_8); + } + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet. + * Uses UTF8 encoding. + * + * @param pArray a byte array containing binary data + * @return String containing only character data in the appropriate alphabet. + */ + public String encodeAsString(final byte[] pArray) { + return new String(encode(pArray), StandardCharsets.UTF_8); + } + + /** + * Decodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of + * the Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[] or String. + * + * @param obj Object to decode + * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] or String + * supplied. + * @throws DecoderException if the parameter supplied is not of type byte[] + */ + @Override + public Object decode(final Object obj) throws DecoderException { + if (obj instanceof byte[]) { + return decode((byte[]) obj); + } else if (obj instanceof String) { + return decode((String) obj); + } else { + throw new DecoderException("Parameter supplied to Base-N decode is not a byte[] or a String"); + } + } + + /** + * Decodes a String containing characters in the Base-N alphabet. + * + * @param pArray A String containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] decode(final String pArray) { + return decode(pArray.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Decodes a byte[] containing characters in the Base-N alphabet. + * + * @param pArray A byte array containing Base-N character data + * @return a byte array containing binary data + */ + @Override + public byte[] decode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + decode(pArray, 0, pArray.length, context); + decode(pArray, 0, EOF, context); // Notify decoder of EOF. + final byte[] result = new byte[context.pos]; + readResults(result, 0, result.length, context); + return result; + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet. + * + * @param pArray a byte array containing binary data + * @return A byte array containing only the basen alphabetic character data + */ + @Override + public byte[] encode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + encode(pArray, 0, pArray.length, context); + encode(pArray, 0, EOF, context); // Notify encoder of EOF. + final byte[] buf = new byte[context.pos - context.readPos]; + readResults(buf, 0, buf.length, context); + return buf; + } + + // package protected for access from I/O streams + abstract void encode(byte[] pArray, int i, int length, Context context); + + // package protected for access from I/O streams + abstract void decode(byte[] pArray, int i, int length, Context context); + + /** + * Returns whether or not the octet is in the current alphabet. + * Does not allow whitespace or pad. + * + * @param value The value to test + * @return true if the value is defined in the current alphabet, false otherwise. + */ + protected abstract boolean isInAlphabet(byte value); + + /** + * Tests a given byte array to see if it contains only valid characters within the alphabet. + * The method optionally treats whitespace and pad as valid. + * + * @param arrayOctet byte array to test + * @param allowWSPad if true, then whitespace and PAD are also allowed + * @return true if all bytes are valid characters in the alphabet or if the byte array is empty; + * false, otherwise + */ + public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isInAlphabet(arrayOctet[i]) && + (!allowWSPad || (arrayOctet[i] != pad) && !isWhiteSpace(arrayOctet[i]))) { + return false; + } + } + return true; + } + + /** + * Tests a given String to see if it contains only valid characters within the alphabet. + * The method treats whitespace and PAD as valid. + * + * @param basen String to test + * @return true if all characters in the String are valid characters in the alphabet or if + * the String is empty; false, otherwise + * @see #isInAlphabet(byte[], boolean) + */ + public boolean isInAlphabet(final String basen) { + return isInAlphabet(basen.getBytes(StandardCharsets.UTF_8), true); + } + + /** + * Tests a given byte array to see if it contains any characters within the alphabet or PAD. + *

+ * Intended for use in checking line-ending arrays + * + * @param arrayOctet byte array to test + * @return true if any byte is a valid character in the alphabet or PAD; false otherwise + */ + protected boolean containsAlphabetOrPad(final byte[] arrayOctet) { + if (arrayOctet == null) { + return false; + } + for (final byte element : arrayOctet) { + if (pad == element || isInAlphabet(element)) { + return true; + } + } + return false; + } + + /** + * Calculates the amount of space needed to encode the supplied array. + * + * @param pArray byte[] array which will later be encoded + * @return amount of space needed to encoded the supplied array. + * Returns a long since a max-len array will require > Integer.MAX_VALUE + */ + public long getEncodedLength(final byte[] pArray) { + // Calculate non-chunked size - rounded up to allow for padding + // cast to long is needed to avoid possibility of overflow + long len = ((pArray.length + unencodedBlockSize - 1) / unencodedBlockSize) * (long) encodedBlockSize; + if (lineLength > 0) { // We're using chunking + // Round up to nearest multiple + len += ((len + lineLength - 1) / lineLength) * chunkSeparatorLength; + } + return len; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BinaryDecoder.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BinaryDecoder.java new file mode 100644 index 000000000..b6ab96125 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BinaryDecoder.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +/** + * Defines common decoding methods for byte array decoders. + * + * @version $Id: BinaryDecoder.java 1379145 2012-08-30 21:02:52Z tn $ + */ +public interface BinaryDecoder extends Decoder { + + /** + * Decodes a byte array and returns the results as a byte array. + * + * @param source A byte array which has been encoded with the appropriate encoder + * @return a byte array that contains decoded content + * @throws DecoderException A decoder exception is thrown if a Decoder encounters a failure condition during the decode process. + */ + byte[] decode(byte[] source) throws DecoderException; +} + diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BinaryEncoder.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BinaryEncoder.java new file mode 100644 index 000000000..54c9f1ed9 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/BinaryEncoder.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +/** + * Defines common encoding methods for byte array encoders. + * + * @version $Id: BinaryEncoder.java 1379145 2012-08-30 21:02:52Z tn $ + */ +public interface BinaryEncoder extends Encoder { + + /** + * Encodes a byte array and return the encoded data as a byte array. + * + * @param source Data to be encoded + * @return A byte array containing the encoded data + * @throws EncoderException thrown if the Encoder encounters a failure condition during the encoding process. + */ + byte[] encode(byte[] source) throws EncoderException; +} + diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Decoder.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Decoder.java new file mode 100644 index 000000000..96125e2f4 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Decoder.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +/** + * Provides the highest level of abstraction for Decoders. + *

+ * This is the sister interface of {@link Encoder}. All Decoders implement this common generic interface. + * Allows a user to pass a generic Object to any Decoder implementation in the codec package. + *

+ * One of the two interfaces at the center of the codec package. + * + * @version $Id: Decoder.java 1379145 2012-08-30 21:02:52Z tn $ + */ +public interface Decoder { + + /** + * Decodes an "encoded" Object and returns a "decoded" Object. Note that the implementation of this interface will + * try to cast the Object parameter to the specific type expected by a particular Decoder implementation. If a + * {@link ClassCastException} occurs this decode method will throw a DecoderException. + * + * @param source the object to decode + * @return a 'decoded" object + * @throws DecoderException a decoder exception can be thrown for any number of reasons. Some good candidates are that the + * parameter passed to this method is null, a param cannot be cast to the appropriate type for a + * specific encoder. + */ + Object decode(Object source) throws DecoderException; +} + diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/DecoderException.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/DecoderException.java new file mode 100644 index 000000000..48d3408f9 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/DecoderException.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +/** + * Thrown when there is a failure condition during the decoding process. This exception is thrown when a {@link Decoder} + * encounters a decoding specific exception such as invalid data, or characters outside of the expected range. + * + * @version $Id: DecoderException.java 1619948 2014-08-22 22:53:55Z ggregory $ + */ +public class DecoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public DecoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message The detail message which is saved for later retrieval by the {@link #getMessage()} method. + */ + public DecoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + * + * @param message The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final Throwable cause) { + super(cause); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Encoder.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Encoder.java new file mode 100644 index 000000000..53630da04 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/Encoder.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +/** + * Provides the highest level of abstraction for Encoders. + *

+ * This is the sister interface of {@link Decoder}. Every implementation of Encoder provides this + * common generic interface which allows a user to pass a generic Object to any Encoder implementation + * in the codec package. + * + * @version $Id: Encoder.java 1379145 2012-08-30 21:02:52Z tn $ + */ +public interface Encoder { + + /** + * Encodes an "Object" and returns the encoded content as an Object. The Objects here may just be + * byte[] or Strings depending on the implementation used. + * + * @param source An object to encode + * @return An "encoded" Object + * @throws EncoderException An encoder exception is thrown if the encoder experiences a failure condition during the encoding + * process. + */ + Object encode(Object source) throws EncoderException; +} + diff --git a/firefly-common/src/main/java/com/fireflysource/common/codec/base64/EncoderException.java b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/EncoderException.java new file mode 100644 index 000000000..c534fd6ba --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/codec/base64/EncoderException.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 com.fireflysource.common.codec.base64; + +/** + * Thrown when there is a failure condition during the encoding process. This exception is thrown when an + * {@link Encoder} encounters a encoding specific exception such as invalid data, inability to calculate a checksum, + * characters outside of the expected range. + * + * @version $Id: EncoderException.java 1619948 2014-08-22 22:53:55Z ggregory $ + */ +public class EncoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public EncoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message a useful message relating to the encoder specific error. + */ + public EncoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + *

+ * + * @param message The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final Throwable cause) { + super(cause); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/CollectionUtils.java b/firefly-common/src/main/java/com/fireflysource/common/collection/CollectionUtils.java new file mode 100644 index 000000000..ca585f1d2 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/CollectionUtils.java @@ -0,0 +1,61 @@ +package com.fireflysource.common.collection; + +import java.util.*; +import java.util.stream.Collectors; + +public class CollectionUtils { + + public static boolean isEmpty(Object[] array) { + return array == null || array.length == 0; + } + + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + public static Set intersect(Set a, Set b) { + if (isEmpty(a) || isEmpty(b)) { + return new HashSet<>(); + } else { + Set set = new HashSet<>(a); + set.retainAll(b); + return set; + } + } + + public static Set union(Set a, Set b) { + Set set = new HashSet<>(a); + set.addAll(b); + return set; + } + + public static boolean hasIntersection(Set a, Set b) { + if (isEmpty(a) || isEmpty(b)) { + return false; + } + + if (a.size() < b.size()) { + if (a.size() < 8) { + return a.stream().anyMatch(b::contains); + } else { + return a.parallelStream().anyMatch(b::contains); + } + } else { + if (b.size() < 8) { + return b.stream().anyMatch(a::contains); + } else { + return b.parallelStream().anyMatch(a::contains); + } + } + } + + @SafeVarargs + public static Set newHashSet(T... values) { + return Arrays.stream(values).collect(Collectors.toSet()); + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/array/ArrayUtils.java b/firefly-common/src/main/java/com/fireflysource/common/collection/array/ArrayUtils.java new file mode 100644 index 000000000..9dfc0b79e --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/array/ArrayUtils.java @@ -0,0 +1,1066 @@ +package com.fireflysource.common.collection.array; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Utility methods for Array manipulation + */ +@SuppressWarnings("unused") +public class ArrayUtils { + + public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + @SuppressWarnings("rawtypes") + public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + public static final long[] EMPTY_LONG_ARRAY = new long[0]; + + public static final Long[] EMPTY_LONG_OBJECT_ARRAY = new Long[0]; + + public static final int[] EMPTY_INT_ARRAY = new int[0]; + + public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0]; + + public static final short[] EMPTY_SHORT_ARRAY = new short[0]; + + public static final Short[] EMPTY_SHORT_OBJECT_ARRAY = new Short[0]; + + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + public static final Byte[] EMPTY_BYTE_OBJECT_ARRAY = new Byte[0]; + + public static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; + + public static final Double[] EMPTY_DOUBLE_OBJECT_ARRAY = new Double[0]; + + public static final float[] EMPTY_FLOAT_ARRAY = new float[0]; + + public static final Float[] EMPTY_FLOAT_OBJECT_ARRAY = new Float[0]; + + public static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; + + public static final Boolean[] EMPTY_BOOLEAN_OBJECT_ARRAY = new Boolean[0]; + + public static final char[] EMPTY_CHAR_ARRAY = new char[0]; + + public static final Character[] EMPTY_CHARACTER_OBJECT_ARRAY = new Character[0]; + + public static T[] removeFromArray(T[] array, Object item) { + if (item == null || array == null) + return array; + for (int i = array.length; i-- > 0; ) { + if (item.equals(array[i])) { + Class c = array.getClass().getComponentType(); + @SuppressWarnings("unchecked") + T[] na = (T[]) Array.newInstance(c, Array.getLength(array) - 1); + if (i > 0) + System.arraycopy(array, 0, na, 0, i); + if (i + 1 < array.length) + System.arraycopy(array, i + 1, na, i, array.length - (i + 1)); + return na; + } + } + return array; + } + + /** + * Add element to an array + * + * @param array The array to add to (or null) + * @param item The item to add + * @param type The type of the array (in case of null array) + * @param the array entry type + * @return new array with contents of array plus item + */ + public static T[] addToArray(T[] array, T item, Class type) { + if (array == null) { + if (type == null && item != null) { + type = item.getClass(); + } + @SuppressWarnings("unchecked") + T[] na = (T[]) Array.newInstance(type, 1); + na[0] = item; + return na; + } else { + T[] na = Arrays.copyOf(array, array.length + 1); + na[array.length] = item; + return na; + } + } + + /** + * Add element to the start of an array + * + * @param array The array to add to (or null) + * @param item The item to add + * @param type The type of the array (in case of null array) + * @param the array entry type + * @return new array with contents of array plus item + */ + public static T[] prependToArray(T item, T[] array, Class type) { + if (array == null) { + if (type == null && item != null) { + type = item.getClass(); + } + @SuppressWarnings("unchecked") + T[] na = (T[]) Array.newInstance(type, 1); + na[0] = item; + return na; + } else { + Class c = array.getClass().getComponentType(); + @SuppressWarnings("unchecked") + T[] na = (T[]) Array.newInstance(c, Array.getLength(array) + 1); + System.arraycopy(array, 0, na, 1, array.length); + na[0] = item; + return na; + } + } + + /** + * @param array Any array of object + * @param the array entry type + * @return A new modifiable list initialised with the elements from + * array. + */ + public static List asMutableList(E[] array) { + if (array == null || array.length == 0) + return new ArrayList<>(); + return new ArrayList<>(Arrays.asList(array)); + } + + public static T[] removeNulls(T[] array) { + for (T t : array) { + if (t == null) { + List list = new ArrayList<>(); + for (T t2 : array) { + if (t2 != null) { + list.add(t2); + } + } + return list.toArray(Arrays.copyOf(array, list.size())); + } + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Object[] nullToEmpty(Object[] array) { + if (array == null || array.length == 0) { + return EMPTY_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static String[] nullToEmpty(String[] array) { + if (array == null || array.length == 0) { + return EMPTY_STRING_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static long[] nullToEmpty(long[] array) { + if (array == null || array.length == 0) { + return EMPTY_LONG_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static int[] nullToEmpty(int[] array) { + if (array == null || array.length == 0) { + return EMPTY_INT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static short[] nullToEmpty(short[] array) { + if (array == null || array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static char[] nullToEmpty(char[] array) { + if (array == null || array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static byte[] nullToEmpty(byte[] array) { + if (array == null || array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static double[] nullToEmpty(double[] array) { + if (array == null || array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static float[] nullToEmpty(float[] array) { + if (array == null || array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static boolean[] nullToEmpty(boolean[] array) { + if (array == null || array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Long[] nullToEmpty(Long[] array) { + if (array == null || array.length == 0) { + return EMPTY_LONG_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Integer[] nullToEmpty(Integer[] array) { + if (array == null || array.length == 0) { + return EMPTY_INTEGER_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Short[] nullToEmpty(Short[] array) { + if (array == null || array.length == 0) { + return EMPTY_SHORT_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Character[] nullToEmpty(Character[] array) { + if (array == null || array.length == 0) { + return EMPTY_CHARACTER_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Byte[] nullToEmpty(Byte[] array) { + if (array == null || array.length == 0) { + return EMPTY_BYTE_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Double[] nullToEmpty(Double[] array) { + if (array == null || array.length == 0) { + return EMPTY_DOUBLE_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Float[] nullToEmpty(Float[] array) { + if (array == null || array.length == 0) { + return EMPTY_FLOAT_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a null + * reference to an empty one.

+ *

+ *

This method returns an empty array for a null input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty public static references in this class.

+ * + * @param array the array to check for null or empty + * @return the same array, public static empty array if null or empty input + * @since 2.5 + */ + public static Boolean[] nullToEmpty(Boolean[] array) { + if (array == null || array.length == 0) { + return EMPTY_BOOLEAN_OBJECT_ARRAY; + } + return array; + } + + // Primitive/Object array converters + // ---------------------------------------------------------------------- + + // Character array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Characters to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Character array, may be null + * @return a char array, null if null array input + * @throws NullPointerException if array content is null + */ + public static char[] toPrimitive(Character[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + final char[] result = new char[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Character to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Character array, may be null + * @param valueForNull the value to insert if null found + * @return a char array, null if null array input + */ + public static char[] toPrimitive(Character[] array, char valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + final char[] result = new char[array.length]; + for (int i = 0; i < array.length; i++) { + Character b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive chars to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a char array + * @return a Character array, null if null array input + */ + public static Character[] toObject(char[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHARACTER_OBJECT_ARRAY; + } + final Character[] result = new Character[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Long array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Longs to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Long array, may be null + * @return a long array, null if null array input + * @throws NullPointerException if array content is null + */ + public static long[] toPrimitive(Long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Long to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Long array, may be null + * @param valueForNull the value to insert if null found + * @return a long array, null if null array input + */ + public static long[] toPrimitive(Long[] array, long valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + Long b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive longs to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a long array + * @return a Long array, null if null array input + */ + public static Long[] toObject(long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_OBJECT_ARRAY; + } + final Long[] result = new Long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Int array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Integers to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Integer array, may be null + * @return an int array, null if null array input + * @throws NullPointerException if array content is null + */ + public static int[] toPrimitive(Integer[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INT_ARRAY; + } + final int[] result = new int[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Integer to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Integer array, may be null + * @param valueForNull the value to insert if null found + * @return an int array, null if null array input + */ + public static int[] toPrimitive(Integer[] array, int valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INT_ARRAY; + } + final int[] result = new int[array.length]; + for (int i = 0; i < array.length; i++) { + Integer b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive ints to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array an int array + * @return an Integer array, null if null array input + */ + public static Integer[] toObject(int[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INTEGER_OBJECT_ARRAY; + } + final Integer[] result = new Integer[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Short array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Shorts to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Short array, may be null + * @return a byte array, null if null array input + * @throws NullPointerException if array content is null + */ + public static short[] toPrimitive(Short[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + final short[] result = new short[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Short to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Short array, may be null + * @param valueForNull the value to insert if null found + * @return a byte array, null if null array input + */ + public static short[] toPrimitive(Short[] array, short valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + final short[] result = new short[array.length]; + for (int i = 0; i < array.length; i++) { + Short b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive shorts to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a short array + * @return a Short array, null if null array input + */ + public static Short[] toObject(short[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_OBJECT_ARRAY; + } + final Short[] result = new Short[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Byte array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Bytes to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Byte array, may be null + * @return a byte array, null if null array input + * @throws NullPointerException if array content is null + */ + public static byte[] toPrimitive(Byte[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Bytes to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Byte array, may be null + * @param valueForNull the value to insert if null found + * @return a byte array, null if null array input + */ + public static byte[] toPrimitive(Byte[] array, byte valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + Byte b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive bytes to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a byte array + * @return a Byte array, null if null array input + */ + public static Byte[] toObject(byte[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_OBJECT_ARRAY; + } + final Byte[] result = new Byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Double array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Doubles to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Double array, may be null + * @return a double array, null if null array input + * @throws NullPointerException if array content is null + */ + public static double[] toPrimitive(Double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Doubles to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Double array, may be null + * @param valueForNull the value to insert if null found + * @return a double array, null if null array input + */ + public static double[] toPrimitive(Double[] array, double valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + Double b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive doubles to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a double array + * @return a Double array, null if null array input + */ + public static Double[] toObject(double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_OBJECT_ARRAY; + } + final Double[] result = new Double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Float array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Floats to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Float array, may be null + * @return a float array, null if null array input + * @throws NullPointerException if array content is null + */ + public static float[] toPrimitive(Float[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + final float[] result = new float[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Floats to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Float array, may be null + * @param valueForNull the value to insert if null found + * @return a float array, null if null array input + */ + public static float[] toPrimitive(Float[] array, float valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + final float[] result = new float[array.length]; + for (int i = 0; i < array.length; i++) { + Float b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive floats to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a float array + * @return a Float array, null if null array input + */ + public static Float[] toObject(float[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_OBJECT_ARRAY; + } + final Float[] result = new Float[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + // Boolean array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Booleans to primitives.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Boolean array, may be null + * @return a boolean array, null if null array input + * @throws NullPointerException if array content is null + */ + public static boolean[] toPrimitive(Boolean[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + final boolean[] result = new boolean[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + *

Converts an array of object Booleans to primitives handling null.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a Boolean array, may be null + * @param valueForNull the value to insert if null found + * @return a boolean array, null if null array input + */ + public static boolean[] toPrimitive(Boolean[] array, boolean valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + final boolean[] result = new boolean[array.length]; + for (int i = 0; i < array.length; i++) { + Boolean b = array[i]; + result[i] = (b == null ? valueForNull : b); + } + return result; + } + + /** + *

Converts an array of primitive booleans to objects.

+ *

+ *

This method returns null for a null input array.

+ * + * @param array a boolean array + * @return a Boolean array, null if null array input + */ + public static Boolean[] toObject(boolean[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_OBJECT_ARRAY; + } + final Boolean[] result = new Boolean[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = (array[i] ? Boolean.TRUE : Boolean.FALSE); + } + return result; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/list/LazyList.java b/firefly-common/src/main/java/com/fireflysource/common/collection/list/LazyList.java new file mode 100644 index 000000000..7cf4576ab --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/list/LazyList.java @@ -0,0 +1,385 @@ +package com.fireflysource.common.collection.list; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.*; + +/** + * Lazy List creation. + *

+ * A List helper class that attempts to avoid unnecessary List creation. If a + * method needs to create a List to return, but it is expected that this will + * either be empty or frequently contain a single item, then using LazyList will + * avoid additional object creations by using {@link Collections#EMPTY_LIST} or + * {@link Collections#singletonList(Object)} where possible. + *

+ *

+ * LazyList works by passing an opaque representation of the list in and out of + * all the LazyList methods. This opaque object is either null for an empty + * list, an Object for a list with a single entry or an {@link ArrayList} for a + * list of items. + *

+ * Usage + * + *
+ * Object lazylist = null;
+ * while (loopCondition) {
+ * 	Object item = getItem();
+ * 	if (item.isToBeAdded())
+ * 		lazylist = LazyList.add(lazylist, item);
+ * }
+ * return LazyList.getList(lazylist);
+ * 
+ *

+ * An ArrayList of default size is used as the initial LazyList. + * + * @see List + */ +@SuppressWarnings("serial") +public class LazyList implements Cloneable, Serializable { + private static final String[] __EMPTY_STRING_ARRAY = new String[0]; + + private LazyList() { + } + + /** + * Add an item to a LazyList + * + * @param list The list to add to or null if none yet created. + * @param item The item to add. + * @return The lazylist created or added to. + */ + @SuppressWarnings("unchecked") + public static Object add(Object list, Object item) { + if (list == null) { + if (item instanceof List || item == null) { + List l = new ArrayList<>(); + l.add(item); + return l; + } + + return item; + } + + if (list instanceof List) { + ((List) list).add(item); + return list; + } + + List l = new ArrayList<>(); + l.add(list); + l.add(item); + return l; + } + + /** + * Add an item to a LazyList + * + * @param list The list to add to or null if none yet created. + * @param index The index to add the item at. + * @param item The item to add. + * @return The lazylist created or added to. + */ + @SuppressWarnings("unchecked") + public static Object add(Object list, int index, Object item) { + if (list == null) { + if (index > 0 || item instanceof List || item == null) { + List l = new ArrayList<>(); + l.add(index, item); + return l; + } + return item; + } + + if (list instanceof List) { + ((List) list).add(index, item); + return list; + } + + List l = new ArrayList<>(); + l.add(list); + l.add(index, item); + return l; + } + + /** + * Add the contents of a Collection to a LazyList + * + * @param list The list to add to or null if none yet created. + * @param collection The Collection whose contents should be added. + * @return The lazylist created or added to. + */ + public static Object addCollection(Object list, Collection collection) { + Iterator i = collection.iterator(); + while (i.hasNext()) + list = LazyList.add(list, i.next()); + return list; + } + + /** + * Add the contents of an array to a LazyList + * + * @param list The list to add to or null if none yet created. + * @param array The array whose contents should be added. + * @return The lazylist created or added to. + */ + public static Object addArray(Object list, Object[] array) { + for (int i = 0; array != null && i < array.length; i++) + list = LazyList.add(list, array[i]); + return list; + } + + /** + * Ensure the capacity of the underlying list. + * + * @param list the list to grow + * @param initialSize the size to grow to + * @return the new List with new size + */ + public static Object ensureSize(Object list, int initialSize) { + if (list == null) + return new ArrayList(initialSize); + if (list instanceof ArrayList) { + ArrayList ol = (ArrayList) list; + if (ol.size() > initialSize) + return ol; + ArrayList nl = new ArrayList<>(initialSize); + nl.addAll(ol); + return nl; + } + List l = new ArrayList<>(initialSize); + l.add(list); + return l; + } + + public static Object remove(Object list, Object o) { + if (list == null) + return null; + + if (list instanceof List) { + List l = (List) list; + l.remove(o); + if (l.size() == 0) + return null; + return list; + } + + if (list.equals(o)) + return null; + return list; + } + + public static Object remove(Object list, int i) { + if (list == null) + return null; + + if (list instanceof List) { + List l = (List) list; + l.remove(i); + if (l.size() == 0) + return null; + return list; + } + + if (i == 0) + return null; + return list; + } + + /** + * Get the real List from a LazyList. + * + * @param list A LazyList returned from LazyList.add(Object) + * @param the list entry type + * @return The List of added items, which may be an EMPTY_LIST or a + * SingletonList. + */ + public static List getList(Object list) { + return getList(list, false); + } + + /** + * Get the real List from a LazyList. + * + * @param list A LazyList returned from LazyList.add(Object) or null + * @param nullForEmpty If true, null is returned instead of an empty list. + * @param the list entry type + * @return The List of added items, which may be null, an EMPTY_LIST or a + * SingletonList. + */ + @SuppressWarnings("unchecked") + public static List getList(Object list, boolean nullForEmpty) { + if (list == null) { + if (nullForEmpty) + return null; + return Collections.emptyList(); + } + if (list instanceof List) + return (List) list; + + return (List) Collections.singletonList(list); + } + + /** + * Simple utility method to test if List has at least 1 entry. + * + * @param list a LazyList, {@link List} or {@link Object} + * @return true if not-null and is not empty + */ + public static boolean hasEntry(Object list) { + if (list == null) + return false; + if (list instanceof List) + return !((List) list).isEmpty(); + return true; + } + + /** + * Simple utility method to test if List is empty + * + * @param list a LazyList, {@link List} or {@link Object} + * @return true if null or is empty + */ + public static boolean isEmpty(Object list) { + if (list == null) + return true; + if (list instanceof List) + return ((List) list).isEmpty(); + return false; + } + + public static String[] toStringArray(Object list) { + if (list == null) + return __EMPTY_STRING_ARRAY; + + if (list instanceof List) { + List l = (List) list; + String[] a = new String[l.size()]; + for (int i = l.size(); i-- > 0; ) { + Object o = l.get(i); + if (o != null) + a[i] = o.toString(); + } + return a; + } + + return new String[]{list.toString()}; + } + + /** + * Convert a lazylist to an array + * + * @param list The list to convert + * @param clazz The class of the array, which may be a primitive type + * @return array of the lazylist entries passed in + */ + public static Object toArray(Object list, Class clazz) { + if (list == null) + return Array.newInstance(clazz, 0); + + if (list instanceof List) { + List l = (List) list; + if (clazz.isPrimitive()) { + Object a = Array.newInstance(clazz, l.size()); + for (int i = 0; i < l.size(); i++) + Array.set(a, i, l.get(i)); + return a; + } + return l.toArray((Object[]) Array.newInstance(clazz, l.size())); + + } + + Object a = Array.newInstance(clazz, 1); + Array.set(a, 0, list); + return a; + } + + /** + * The size of a lazy List + * + * @param list A LazyList returned from LazyList.add(Object) or null + * @return the size of the list. + */ + public static int size(Object list) { + if (list == null) + return 0; + if (list instanceof List) + return ((List) list).size(); + return 1; + } + + /** + * Get item from the list + * + * @param list A LazyList returned from LazyList.add(Object) or null + * @param i int index + * @param the list entry type + * @return the item from the list. + */ + @SuppressWarnings("unchecked") + public static E get(Object list, int i) { + if (list == null) + throw new IndexOutOfBoundsException(); + + if (list instanceof List) + return (E) ((List) list).get(i); + + if (i == 0) + return (E) list; + + throw new IndexOutOfBoundsException(); + } + + public static boolean contains(Object list, Object item) { + if (list == null) + return false; + + if (list instanceof List) + return ((List) list).contains(item); + + return list.equals(item); + } + + public static Object clone(Object list) { + if (list == null) + return null; + if (list instanceof List) + return new ArrayList((List) list); + return list; + } + + public static String toString(Object list) { + if (list == null) + return "[]"; + if (list instanceof List) + return list.toString(); + return "[" + list + "]"; + } + + @SuppressWarnings("unchecked") + public static Iterator iterator(Object list) { + if (list == null) { + List empty = Collections.emptyList(); + return empty.iterator(); + } + if (list instanceof List) { + return ((List) list).iterator(); + } + List l = getList(list); + return l.iterator(); + } + + @SuppressWarnings("unchecked") + public static ListIterator listIterator(Object list) { + if (list == null) { + List empty = Collections.emptyList(); + return empty.listIterator(); + } + if (list instanceof List) + return ((List) list).listIterator(); + + List l = getList(list); + return l.listIterator(); + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/map/MultiMap.java b/firefly-common/src/main/java/com/fireflysource/common/collection/map/MultiMap.java new file mode 100644 index 000000000..827ce1490 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/map/MultiMap.java @@ -0,0 +1,320 @@ +package com.fireflysource.common.collection.map; + +import java.util.*; + +/** + * A multi valued Map. + * + * @param the entry type for multimap values + */ +public class MultiMap extends HashMap> { + private static final long serialVersionUID = -1127515104096783129L; + + public MultiMap() { + super(); + } + + public MultiMap(Map> map) { + super(map); + } + + public MultiMap(MultiMap map) { + super(map); + } + + /** + * Get multiple values. Single valued entries are converted to singleton + * lists. + * + * @param name The entry key. + * @return Unmodifieable List of values. + */ + public List getValues(String name) { + List vals = super.get(name); + if ((vals == null) || vals.isEmpty()) { + return null; + } + return vals; + } + + /** + * Get a value from a multiple value. If the value is not a multivalue, then + * index 0 retrieves the value or null. + * + * @param name The entry key. + * @param i Index of element to get. + * @return Unmodifieable List of values. + */ + public V getValue(String name, int i) { + List vals = getValues(name); + if (vals == null) { + return null; + } + if (i == 0 && vals.isEmpty()) { + return null; + } + return vals.get(i); + } + + /** + * Get value as String. Single valued items are converted to a String with + * the toString() Object method. Multi valued entries are converted to a + * comma separated List. No quoting of commas within values is performed. + * + * @param name The entry key. + * @return String value. + */ + public String getString(String name) { + List vals = get(name); + if ((vals == null) || (vals.isEmpty())) { + return null; + } + + if (vals.size() == 1) { + // simple form. + return vals.get(0).toString(); + } + + // delimited form + StringBuilder values = new StringBuilder(128); + for (V e : vals) { + if (e != null) { + if (values.length() > 0) + values.append(','); + values.append(e.toString()); + } + } + return values.toString(); + } + + /** + * Put multi valued entry. + * + * @param name The entry key. + * @param value The simple value + * @return The previous value or null. + */ + public List put(String name, V value) { + if (value == null) { + return super.put(name, null); + } + List vals = new ArrayList<>(); + vals.add(value); + return put(name, vals); + } + + /** + * Shorthand version of putAll + * + * @param input the input map + */ + public void putAllValues(Map input) { + for (Entry entry : input.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** + * Put multi valued entry. + * + * @param name The entry key. + * @param values The List of multiple values. + * @return The previous value or null. + */ + public List putValues(String name, List values) { + return super.put(name, values); + } + + /** + * Put multi valued entry. + * + * @param name The entry key. + * @param values The array of multiple values. + * @return The previous value or null. + */ + @SafeVarargs + public final List putValues(String name, V... values) { + List list = new ArrayList<>(); + list.addAll(Arrays.asList(values)); + return super.put(name, list); + } + + /** + * Add value to multi valued entry. If the entry is single valued, it is + * converted to the first value of a multi valued entry. + * + * @param name The entry key. + * @param value The entry value. + */ + public void add(String name, V value) { + List lo = get(name); + if (lo == null) { + lo = new ArrayList<>(); + } + lo.add(value); + super.put(name, lo); + } + + /** + * Add values to multi valued entry. If the entry is single valued, it is + * converted to the first value of a multi valued entry. + * + * @param name The entry key. + * @param values The List of multiple values. + */ + public void addValues(String name, List values) { + List lo = get(name); + if (lo == null) { + lo = new ArrayList<>(); + } + lo.addAll(values); + put(name, lo); + } + + /** + * Add values to multi valued entry. If the entry is single valued, it is + * converted to the first value of a multi valued entry. + * + * @param name The entry key. + * @param values The String array of multiple values. + */ + public void addValues(String name, V[] values) { + List lo = get(name); + if (lo == null) { + lo = new ArrayList<>(); + } + lo.addAll(Arrays.asList(values)); + put(name, lo); + } + + /** + * Merge values. + * + * @param map the map to overlay on top of this one, merging together values + * if needed. + * @return true if an existing key was merged with potentially new values, + * false if either no change was made, or there were only new keys. + */ + public boolean addAllValues(MultiMap map) { + boolean merged = false; + + if ((map == null) || (map.isEmpty())) { + // done + return merged; + } + + for (Entry> entry : map.entrySet()) { + String name = entry.getKey(); + List values = entry.getValue(); + + if (this.containsKey(name)) { + merged = true; + } + + this.addValues(name, values); + } + + return merged; + } + + /** + * Remove value. + * + * @param name The entry key. + * @param value The entry value. + * @return true if it was removed. + */ + public boolean removeValue(String name, V value) { + List lo = get(name); + if ((lo == null) || (lo.isEmpty())) { + return false; + } + boolean ret = lo.remove(value); + if (lo.isEmpty()) { + remove(name); + } else { + put(name, lo); + } + return ret; + } + + /** + * Test for a specific single value in the map. + *

+ * NOTE: This is a SLOW operation, and is actively discouraged. + * + * @param value the value to search for + * @return true if contains simple value + */ + public boolean containsSimpleValue(V value) { + for (List vals : values()) { + if ((vals.size() == 1) && vals.contains(value)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + Iterator>> iter = entrySet().iterator(); + StringBuilder sb = new StringBuilder(); + sb.append('{'); + boolean delim = false; + while (iter.hasNext()) { + Entry> e = iter.next(); + if (delim) { + sb.append(", "); + } + String key = e.getKey(); + List vals = e.getValue(); + sb.append(key); + sb.append('='); + if (vals.size() == 1) { + sb.append(vals.get(0)); + } else { + sb.append(vals); + } + delim = true; + } + sb.append('}'); + return sb.toString(); + } + + /** + * @return Map of String arrays + */ + public Map toStringArrayMap() { + HashMap map = new HashMap(size() * 3 / 2) { + + private static final long serialVersionUID = -6129887569971781626L; + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append('{'); + for (String k : super.keySet()) { + if (b.length() > 1) + b.append(','); + b.append(k); + b.append('='); + b.append(Arrays.asList(super.get(k))); + } + + b.append('}'); + return b.toString(); + } + }; + + for (Entry> entry : entrySet()) { + String[] a = null; + if (entry.getValue() != null) { + a = new String[entry.getValue().size()]; + a = entry.getValue().toArray(a); + } + map.put(entry.getKey(), a); + } + return map; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/trie/AbstractTrie.java b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/AbstractTrie.java new file mode 100644 index 000000000..b9ea94f01 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/AbstractTrie.java @@ -0,0 +1,50 @@ +package com.fireflysource.common.collection.trie; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public abstract class AbstractTrie implements Trie { + final boolean _caseInsensitive; + + protected AbstractTrie(boolean insensitive) { + _caseInsensitive = insensitive; + } + + @Override + public boolean put(V v) { + return put(v.toString(), v); + } + + @Override + public V remove(String s) { + V o = get(s); + put(s, null); + return o; + } + + @Override + public V get(String s) { + return get(s, 0, s.length()); + } + + @Override + public V get(ByteBuffer b) { + return get(b, 0, b.remaining()); + } + + @Override + public V getBest(String s) { + return getBest(s, 0, s.length()); + } + + @Override + public V getBest(byte[] b, int offset, int len) { + return getBest(new String(b, offset, len, StandardCharsets.UTF_8)); + } + + @Override + public boolean isCaseInsensitive() { + return _caseInsensitive; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/trie/ArrayTernaryTrie.java b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/ArrayTernaryTrie.java new file mode 100644 index 000000000..c27f65ee5 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/ArrayTernaryTrie.java @@ -0,0 +1,582 @@ +package com.fireflysource.common.collection.trie; + +import java.nio.ByteBuffer; +import java.util.*; + +/** + *

A Ternary Trie String lookup data structure.

+ *

+ * This Trie is of a fixed size and cannot grow (which can be a good thing with the regards to DOS when used as a cache). + *

+ *

+ * The Trie is stored in 3 arrays: + *

+ *
+ *
char[] tree
This is semantically 2 dimensional array flattened into a 1 dimensional char array. The second dimension + * is that every 4 sequential elements represents a row of: character; hi index; eq index; low index, used to build a + * ternary trie of key strings.
+ *
String[] key
An array of key values where each element matches a row in the tree array. A non-zero key element + * indicates that the tree row is a complete key rather than an intermediate character of a longer key.
+ *
V[] value
An array of values corresponding to the key array
+ *
+ *

The lookup of a value will iterate through the tree array matching characters. If the equal tree branch follows, + * then the key array looks up to see if this is a complete match. If a match finds then the value array looks up + * to return the matching value. + *

+ *

+ * This Trie may instantiate either as case-sensitive or insensitive. + *

+ *

This Trie is not Threadsafe and contains no mutual exclusion + * or deliberate memory barriers. It is intended for an ArrayTrie to be + * built by a single thread and then used concurrently by multiple threads + * and not mutated during that access. If concurrent mutations of the + * Trie is required external locks need to be applied. + *

+ * + * @param the Entry type + */ +@SuppressWarnings("unchecked") +public class ArrayTernaryTrie extends AbstractTrie { + + public static final char[] LOWER_CASES = {'\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007', '\010', + '\011', '\012', '\013', '\014', '\015', '\016', '\017', '\020', '\021', '\022', '\023', '\024', '\025', + '\026', '\027', '\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037', '\040', '\041', '\042', + '\043', '\044', '\045', '\046', '\047', '\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057', + '\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067', '\070', '\071', '\072', '\073', '\074', + '\075', '\076', '\077', '\100', '\141', '\142', '\143', '\144', '\145', '\146', '\147', '\150', '\151', + '\152', '\153', '\154', '\155', '\156', '\157', '\160', '\161', '\162', '\163', '\164', '\165', '\166', + '\167', '\170', '\171', '\172', '\133', '\134', '\135', '\136', '\137', '\140', '\141', '\142', '\143', + '\144', '\145', '\146', '\147', '\150', '\151', '\152', '\153', '\154', '\155', '\156', '\157', '\160', + '\161', '\162', '\163', '\164', '\165', '\166', '\167', '\170', '\171', '\172', '\173', '\174', '\175', + '\176', '\177'}; + /** + * The Size of a Trie row is the char, and the low, equal and high + * child pointers + */ + private static final int ROW_SIZE = 4; + private static int LO = 1; + private static int EQ = 2; + private static int HI = 3; + /** + * The Trie rows in a single array which allows a lookup of row,character + * to the next row in the Trie. This is actually a 2 dimensional + * array that has been flattened to achieve locality of reference. + */ + private final char[] tree; + + /** + * The key (if any) for a Trie row. + * A row may be a leaf, a node or both in the Trie tree. + */ + private final String[] key; + + /** + * The value (if any) for a Trie row. + * A row may be a leaf, a node or both in the Trie tree. + */ + private final V[] value; + + /** + * The number of rows allocated + */ + private char rows; + + /** + * Create a case-insensitive Trie of default capacity. + */ + public ArrayTernaryTrie() { + this(128); + } + + /** + * Create a Trie of default capacity + * + * @param insensitive true if the Trie is insensitive to the case of the key. + */ + public ArrayTernaryTrie(boolean insensitive) { + this(insensitive, 128); + } + + /** + * Create a case-insensitive Trie + * + * @param capacity The capacity of the Trie, which is in the worst case + * is the total number of characters of all keys stored in the Trie. + * The capacity needed is dependent of the shared prefixes of the keys. + * For example, a capacity of 6 nodes require to store the keys "foo" + * and "bar", but a capacity of only 4 is required to + * store "bar" and "bat". + */ + public ArrayTernaryTrie(int capacity) { + this(true, capacity); + } + + /** + * Create a Trie + * + * @param insensitive true if the Trie is insensitive to the case of the key. + * @param capacity The capacity of the Trie, which is in the worst case + * is the total number of characters of all keys stored in the Trie. + * The capacity needed is dependent of the shared prefixes of the keys. + * For example, a capacity of 6 nodes require to store the keys "foo" + * and "bar", but a capacity of only 4 is required to + * store "bar" and "bat". + */ + public ArrayTernaryTrie(boolean insensitive, int capacity) { + super(insensitive); + value = (V[]) new Object[capacity]; + tree = new char[capacity * ROW_SIZE]; + key = new String[capacity]; + } + + /** + * Copy Trie and change capacity by a factor + * + * @param trie the trie to copy from + * @param factor the factor to grow the capacity by + */ + public ArrayTernaryTrie(ArrayTernaryTrie trie, double factor) { + super(trie.isCaseInsensitive()); + int capacity = (int) (trie.value.length * factor); + rows = trie.rows; + value = Arrays.copyOf(trie.value, capacity); + tree = Arrays.copyOf(trie.tree, capacity * ROW_SIZE); + key = Arrays.copyOf(trie.key, capacity); + } + + public static int hilo(int diff) { + // branchless equivalent to return ((diff<0)?LO:HI); + // return 3+2*((diff&Integer.MIN_VALUE)>>Integer.SIZE-1); + return 1 + (diff | Integer.MAX_VALUE) / (Integer.MAX_VALUE / 2); + } + + @Override + public void clear() { + rows = 0; + Arrays.fill(value, null); + Arrays.fill(tree, (char) 0); + Arrays.fill(key, null); + } + + @Override + public boolean put(String s, V v) { + int t = 0; + int limit = s.length(); + int last; + for (int k = 0; k < limit; k++) { + char c = s.charAt(k); + if (isCaseInsensitive() && c < 128) + c = LOWER_CASES[c]; + + while (true) { + int row = ROW_SIZE * t; + + // Do we need to create the new row? + if (t == rows) { + rows++; + if (rows >= key.length) { + rows--; + return false; + } + tree[row] = c; + } + + char n = tree[row]; + int diff = n - c; + if (diff == 0) + t = tree[last = (row + EQ)]; + else if (diff < 0) + t = tree[last = (row + LO)]; + else + t = tree[last = (row + HI)]; + + // do we need a new row? + if (t == 0) { + t = rows; + tree[last] = (char) t; + } + + if (diff == 0) break; + } + } + + // Do we need to create the new row? + if (t == rows) { + rows++; + if (rows >= key.length) { + rows--; + return false; + } + } + + // Put the key and value + key[t] = v == null ? null : s; + value[t] = v; + + return true; + } + + @Override + public V get(String s, int offset, int len) { + int t = 0; + for (int i = 0; i < len; ) { + char c = s.charAt(offset + i++); + if (isCaseInsensitive() && c < 128) + c = LOWER_CASES[c]; + + while (true) { + int row = ROW_SIZE * t; + char n = tree[row]; + int diff = n - c; + + if (diff == 0) { + t = tree[row + EQ]; + if (t == 0) return null; + break; + } + + t = tree[row + hilo(diff)]; + if (t == 0) return null; + } + } + + return value[t]; + } + + @Override + public V get(ByteBuffer b, int offset, int len) { + int t = 0; + offset += b.position(); + + for (int i = 0; i < len; ) { + byte c = (byte) (b.get(offset + i++) & 0x7f); + if (isCaseInsensitive()) + c = (byte) LOWER_CASES[c]; + + while (true) { + int row = ROW_SIZE * t; + char n = tree[row]; + int diff = n - c; + + if (diff == 0) { + t = tree[row + EQ]; + if (t == 0) return null; + break; + } + + t = tree[row + hilo(diff)]; + if (t == 0) return null; + } + } + return value[t]; + } + + @Override + public V getBest(String s) { + return getBest(0, s, 0, s.length()); + } + + @Override + public V getBest(String s, int offset, int length) { + return getBest(0, s, offset, length); + } + + private V getBest(int t, String s, int offset, int len) { + int node = t; + int end = offset + len; + loop: + while (offset < end) { + char c = s.charAt(offset++); + len--; + if (isCaseInsensitive() && c < 128) + c = LOWER_CASES[c]; + + while (true) { + int row = ROW_SIZE * t; + char n = tree[row]; + int diff = n - c; + + if (diff == 0) { + t = tree[row + EQ]; + if (t == 0) break loop; + + // if this node is a match, recurse to remember + if (key[t] != null) { + node = t; + V better = getBest(t, s, offset, len); + if (better != null) return better; + } + break; + } + + t = tree[row + hilo(diff)]; + if (t == 0) break loop; + } + } + return value[node]; + } + + @Override + public V getBest(ByteBuffer b, int offset, int len) { + if (b.hasArray()) return getBest(0, b.array(), b.arrayOffset() + b.position() + offset, len); + else return getBest(0, b, offset, len); + } + + private V getBest(int t, byte[] b, int offset, int len) { + int node = t; + int end = offset + len; + loop: + while (offset < end) { + byte c = (byte) (b[offset++] & 0x7f); + len--; + if (isCaseInsensitive()) + c = (byte) LOWER_CASES[c]; + + while (true) { + int row = ROW_SIZE * t; + char n = tree[row]; + int diff = n - c; + + if (diff == 0) { + t = tree[row + EQ]; + if (t == 0) break loop; + + // if this node is a match, recurse to remember + if (key[t] != null) { + node = t; + V better = getBest(t, b, offset, len); + if (better != null) return better; + } + break; + } + + t = tree[row + hilo(diff)]; + if (t == 0) break loop; + } + } + return (V) value[node]; + } + + private V getBest(int t, ByteBuffer b, int offset, int len) { + int node = t; + int o = offset + b.position(); + + loop: + for (int i = 0; i < len; i++) { + byte c = (byte) (b.get(o + i) & 0x7f); + if (isCaseInsensitive()) + c = (byte) LOWER_CASES[c]; + + while (true) { + int row = ROW_SIZE * t; + char n = tree[row]; + int diff = n - c; + + if (diff == 0) { + t = tree[row + EQ]; + if (t == 0) break loop; + + // if this node is a match, recurse to remember + if (key[t] != null) { + node = t; + V best = getBest(t, b, offset + i + 1, len - i - 1); + if (best != null) return best; + } + break; + } + + t = tree[row + hilo(diff)]; + if (t == 0) break loop; + } + } + return value[node]; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + for (int r = 0; r <= rows; r++) { + if (key[r] != null && value[r] != null) { + buf.append(','); + buf.append(key[r]); + buf.append('='); + buf.append(value[r].toString()); + } + } + if (buf.length() == 0) return "{}"; + + buf.setCharAt(0, '{'); + buf.append('}'); + return buf.toString(); + } + + @Override + public Set keySet() { + Set keys = new HashSet<>(); + + for (int r = 0; r <= rows; r++) { + if (key[r] != null && value[r] != null) + keys.add(key[r]); + } + return keys; + } + + public int size() { + int s = 0; + for (int r = 0; r <= rows; r++) { + if (key[r] != null && value[r] != null) + s++; + } + return s; + } + + public boolean isEmpty() { + for (int r = 0; r <= rows; r++) { + if (key[r] != null && value[r] != null) + return false; + } + return true; + } + + public Set> entrySet() { + Set> entries = new HashSet<>(); + for (int r = 0; r <= rows; r++) { + if (key[r] != null && value[r] != null) + entries.add(new AbstractMap.SimpleEntry<>(key[r], value[r])); + } + return entries; + } + + @Override + public boolean isFull() { + return rows + 1 == key.length; + } + + public void dump() { + for (int r = 0; r < rows; r++) { + char c = tree[r * ROW_SIZE]; + System.err.printf("%4d [%s,%d,%d,%d] '%s':%s%n", + r, + (c < ' ' || c > 127) ? ("" + (int) c) : "'" + c + "'", + (int) tree[r * ROW_SIZE + LO], + (int) tree[r * ROW_SIZE + EQ], + (int) tree[r * ROW_SIZE + HI], + key[r], + value[r]); + } + + } + + public static class Growing implements Trie { + private final int growBy; + private ArrayTernaryTrie trie; + + public Growing() { + this(1024, 1024); + } + + public Growing(int capacity, int growBy) { + this.growBy = growBy; + trie = new ArrayTernaryTrie<>(capacity); + } + + public Growing(boolean insensitive, int capacity, int growby) { + growBy = growby; + trie = new ArrayTernaryTrie<>(insensitive, capacity); + } + + public boolean put(V v) { + return put(v.toString(), v); + } + + public int hashCode() { + return trie.hashCode(); + } + + public V remove(String s) { + return trie.remove(s); + } + + public V get(String s) { + return trie.get(s); + } + + public V get(ByteBuffer b) { + return trie.get(b); + } + + public V getBest(byte[] b, int offset, int len) { + return trie.getBest(b, offset, len); + } + + public boolean isCaseInsensitive() { + return trie.isCaseInsensitive(); + } + + public boolean equals(Object obj) { + return trie.equals(obj); + } + + public void clear() { + trie.clear(); + } + + public boolean put(String s, V v) { + boolean added = trie.put(s, v); + while (!added && growBy > 0) { + ArrayTernaryTrie bigger = new ArrayTernaryTrie<>(trie.key.length + growBy); + for (Map.Entry entry : trie.entrySet()) + bigger.put(entry.getKey(), entry.getValue()); + trie = bigger; + added = trie.put(s, v); + } + + return added; + } + + public V get(String s, int offset, int len) { + return trie.get(s, offset, len); + } + + public V get(ByteBuffer b, int offset, int len) { + return trie.get(b, offset, len); + } + + public V getBest(String s) { + return trie.getBest(s); + } + + public V getBest(String s, int offset, int length) { + return trie.getBest(s, offset, length); + } + + public V getBest(ByteBuffer b, int offset, int len) { + return trie.getBest(b, offset, len); + } + + public String toString() { + return trie.toString(); + } + + public Set keySet() { + return trie.keySet(); + } + + public boolean isFull() { + return false; + } + + public void dump() { + trie.dump(); + } + + public boolean isEmpty() { + return trie.isEmpty(); + } + + public int size() { + return trie.size(); + } + + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/trie/ArrayTrie.java b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/ArrayTrie.java new file mode 100644 index 000000000..a36b9fc8b --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/ArrayTrie.java @@ -0,0 +1,401 @@ +package com.fireflysource.common.collection.trie; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + *

A Trie String lookup data structure using a fixed size array.

+ *

This implementation is always case-insensitive and is optimal for + * a few fixed strings with few special characters. The + * Trie is stored in an array of lookup tables, each indexed by the + * next character of the key. Frequently used characters directly + * index in each lookup table, whilst infrequently used characters + * must use a big character table. + *

+ *

This Trie is very space efficient if the key characters are + * from ' ', '+', '-', ':', ';', '.', 'A' to 'Z' or 'a' to 'z'. + * Other ISO-8859-1 characters can be used by the key, but less space + * efficiently. + *

+ *

This Trie is not Threadsafe and contains no mutual exclusion + * or deliberate memory barriers. It is intended for an ArrayTrie to be + * built by a single thread and then used concurrently by multiple threads + * and not mutated during that access. If concurrent mutations of the + * Trie is required external locks need to be applied. + *

+ * + * @param the element of entry + */ +public class ArrayTrie extends AbstractTrie { + /** + * The Size of a Trie row is how many characters can be looked + * up directly without going to a big index. This is set at + * 32 to cover case-insensitive alphabet and a few other common + * characters. + */ + private static final int ROW_SIZE = 32; + + /** + * The index lookup table, this maps a character as a byte + * (ISO-8859-1 or UTF8) to an index within a Trie row + */ + private static final int[] LOOKUP = + { // 0 1 2 3 4 5 6 7 8 9 A B C D E F + /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1, + /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1, + /*4*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*5*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + }; + + /** + * The Trie rows in a single array which allows a lookup of row,character + * to the next row in the Trie. This is actually a 2 dimensional + * array that has been flattened to achieve locality of reference. + * The first ROW_SIZE entries are for row 0, then next ROW_SIZE + * entries are for row 1 etc. So in general instead of using + * rows[row][index], we use rows[row*ROW_SIZE+index] to look up + * the next row for a given character. + *

+ * The array is of characters rather than integers to save space. + */ + private final char[] rowIndex; + + /** + * The key (if any) for a Trie row. + * A row may be a leaf, a node or both in the Trie tree. + */ + private final String[] key; + + /** + * The value (if any) for a Trie row. + * A row may be a leaf, a node or both in the Trie tree. + */ + private final V[] value; + + /** + * A big index for each row. + * If a character outside of the lookup map needs, + * then a big index will be created for the row, with + * 256 entries, one for each possible byte. + */ + private char[][] bigIndex; + + /** + * The number of rows allocated + */ + private char rows; + + public ArrayTrie() { + this(128); + } + + /** + * @param capacity The capacity of the trie, which at the worst case + * is the total number of characters of all keys stored in the Trie. + * The capacity needed is dependent of the shared prefixes of the keys. + * For example, a capacity of 6 nodes require to store the keys "foo" + * and "bar", but a capacity of only 4 is required to + * store "bar" and "bat". + */ + @SuppressWarnings("unchecked") + public ArrayTrie(int capacity) { + super(true); + value = (V[]) new Object[capacity]; + rowIndex = new char[capacity * 32]; + key = new String[capacity]; + } + + @Override + public void clear() { + rows = 0; + Arrays.fill(value, null); + Arrays.fill(rowIndex, (char) 0); + Arrays.fill(key, null); + } + + @Override + public boolean put(String s, V v) { + int t = 0; + int k; + int limit = s.length(); + for (k = 0; k < limit; k++) { + char c = s.charAt(k); + + int index = LOOKUP[c & 0x7f]; + if (index >= 0) { + int idx = t * ROW_SIZE + index; + t = rowIndex[idx]; + if (t == 0) { + if (++rows >= value.length) + return false; + t = rowIndex[idx] = rows; + } + } else if (c > 127) + throw new IllegalArgumentException("non ascii character"); + else { + if (bigIndex == null) + bigIndex = new char[value.length][]; + if (t >= bigIndex.length) + return false; + char[] big = bigIndex[t]; + if (big == null) + big = bigIndex[t] = new char[128]; + t = big[c]; + if (t == 0) { + if (rows == value.length) + return false; + t = big[c] = ++rows; + } + } + } + + if (t >= key.length) { + rows = (char) key.length; + return false; + } + + key[t] = v == null ? null : s; + value[t] = v; + return true; + } + + @Override + public V get(String s, int offset, int len) { + int t = 0; + for (int i = 0; i < len; i++) { + char c = s.charAt(offset + i); + int index = LOOKUP[c & 0x7f]; + if (index >= 0) { + int idx = t * ROW_SIZE + index; + t = rowIndex[idx]; + if (t == 0) + return null; + } else { + char[] big = bigIndex == null ? null : bigIndex[t]; + if (big == null) + return null; + t = big[c]; + if (t == 0) + return null; + } + } + return value[t]; + } + + @Override + public V get(ByteBuffer b, int offset, int len) { + int t = 0; + for (int i = 0; i < len; i++) { + byte c = b.get(offset + i); + int index = LOOKUP[c & 0x7f]; + if (index >= 0) { + int idx = t * ROW_SIZE + index; + t = rowIndex[idx]; + if (t == 0) + return null; + } else { + char[] big = bigIndex == null ? null : bigIndex[t]; + if (big == null) + return null; + t = big[c]; + if (t == 0) + return null; + } + } + return value[t]; + } + + @Override + public V getBest(byte[] b, int offset, int len) { + return getBest(0, b, offset, len); + } + + @Override + public V getBest(ByteBuffer b, int offset, int len) { + if (b.hasArray()) + return getBest(0, b.array(), b.arrayOffset() + b.position() + offset, len); + return getBest(0, b, offset, len); + } + + @Override + public V getBest(String s, int offset, int len) { + return getBest(0, s, offset, len); + } + + private V getBest(int t, String s, int offset, int len) { + int pos = offset; + for (int i = 0; i < len; i++) { + char c = s.charAt(pos++); + int index = LOOKUP[c & 0x7f]; + if (index >= 0) { + int idx = t * ROW_SIZE + index; + int nt = rowIndex[idx]; + if (nt == 0) + break; + t = nt; + } else { + char[] big = bigIndex == null ? null : bigIndex[t]; + if (big == null) + return null; + int nt = big[c]; + if (nt == 0) + break; + t = nt; + } + + // Is the next Trie is a match + if (key[t] != null) { + // Recurse so we can remember this possibility + V best = getBest(t, s, offset + i + 1, len - i - 1); + if (best != null) + return best; + return value[t]; + } + } + return value[t]; + } + + private V getBest(int t, byte[] b, int offset, int len) { + for (int i = 0; i < len; i++) { + byte c = b[offset + i]; + int index = LOOKUP[c & 0x7f]; + if (index >= 0) { + int idx = t * ROW_SIZE + index; + int nt = rowIndex[idx]; + if (nt == 0) + break; + t = nt; + } else { + char[] big = bigIndex == null ? null : bigIndex[t]; + if (big == null) + return null; + int nt = big[c]; + if (nt == 0) + break; + t = nt; + } + + // Is the next Trie is a match + if (key[t] != null) { + // Recurse so we can remember this possibility + V best = getBest(t, b, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return value[t]; + } + + private V getBest(int t, ByteBuffer b, int offset, int len) { + int pos = b.position() + offset; + for (int i = 0; i < len; i++) { + byte c = b.get(pos++); + int index = LOOKUP[c & 0x7f]; + if (index >= 0) { + int idx = t * ROW_SIZE + index; + int nt = rowIndex[idx]; + if (nt == 0) + break; + t = nt; + } else { + char[] big = bigIndex == null ? null : bigIndex[t]; + if (big == null) + return null; + int nt = big[c]; + if (nt == 0) + break; + t = nt; + } + + // Is the next Trie is a match + if (key[t] != null) { + // Recurse so we can remember this possibility + V best = getBest(t, b, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return value[t]; + } + + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + toString(buf, 0); + + if (buf.length() == 0) + return "{}"; + + buf.setCharAt(0, '{'); + buf.append('}'); + return buf.toString(); + } + + + private void toString(Appendable out, int t) { + if (value[t] != null) { + try { + out.append(','); + out.append(key[t]); + out.append('='); + out.append(value[t].toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + for (int i = 0; i < ROW_SIZE; i++) { + int idx = t * ROW_SIZE + i; + if (rowIndex[idx] != 0) + toString(out, rowIndex[idx]); + } + + char[] big = bigIndex == null ? null : bigIndex[t]; + if (big != null) { + for (int i : big) + if (i != 0) + toString(out, i); + } + + } + + @Override + public Set keySet() { + Set keys = new HashSet<>(); + keySet(keys, 0); + return keys; + } + + private void keySet(Set set, int t) { + if (t < value.length && value[t] != null) + set.add(key[t]); + + for (int i = 0; i < ROW_SIZE; i++) { + int idx = t * ROW_SIZE + i; + if (idx < rowIndex.length && rowIndex[idx] != 0) + keySet(set, rowIndex[idx]); + } + + char[] big = bigIndex == null || t >= bigIndex.length ? null : bigIndex[t]; + if (big != null) { + for (int i : big) + if (i != 0) + keySet(set, i); + } + } + + @Override + public boolean isFull() { + return rows + 1 >= key.length; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/trie/TreeTrie.java b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/TreeTrie.java new file mode 100644 index 000000000..23c224480 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/TreeTrie.java @@ -0,0 +1,326 @@ +package com.fireflysource.common.collection.trie; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; + + +/** + * A Trie String lookup data structure using a tree + *

+ * This implementation is always case-insensitive and is optimal for a variable + * number of fixed strings with few special characters. + *

+ *

+ * This Trie is stored in a Tree and is unlimited in capacity + *

+ *

+ *

+ * This Trie is not Threadsafe and contains no mutual exclusion or deliberate + * memory barriers. It is intended for an ArrayTrie to be built by a single + * thread and then used concurrently by multiple threads and not mutated during + * that access. If concurrent mutations of the Trie is required external locks + * need to be applied. + *

+ * + * @param the entry type + */ +public class TreeTrie extends AbstractTrie { + private static final int[] LOOKUP = + { // 0 1 2 3 4 5 6 7 8 9 A B C D E F + /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1, + /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1, + /*4*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*5*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + }; + private static final int INDEX = 32; + private final TreeTrie[] nextIndex; + private final List> nextOther = new ArrayList<>(); + private final char ch; + private String key; + private V value; + + @SuppressWarnings("unchecked") + public TreeTrie() { + super(true); + nextIndex = new TreeTrie[INDEX]; + ch = 0; + } + + @SuppressWarnings("unchecked") + private TreeTrie(char c) { + super(true); + nextIndex = new TreeTrie[INDEX]; + this.ch = c; + } + + private static void toString(Appendable out, TreeTrie t) { + if (t != null) { + if (t.value != null) { + try { + out.append(','); + out.append(t.key); + out.append('='); + out.append(t.value.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + for (int i = 0; i < INDEX; i++) { + if (t.nextIndex[i] != null) + toString(out, t.nextIndex[i]); + } + for (int i = t.nextOther.size(); i-- > 0; ) + toString(out, t.nextOther.get(i)); + } + } + + private static void keySet(Set set, TreeTrie t) { + if (t != null) { + if (t.key != null) + set.add(t.key); + + for (int i = 0; i < INDEX; i++) { + if (t.nextIndex[i] != null) + keySet(set, t.nextIndex[i]); + } + for (int i = t.nextOther.size(); i-- > 0; ) + keySet(set, t.nextOther.get(i)); + } + } + + @Override + public void clear() { + Arrays.fill(nextIndex, null); + nextOther.clear(); + key = null; + value = null; + } + + @Override + public boolean put(String s, V v) { + TreeTrie t = this; + int limit = s.length(); + for (int k = 0; k < limit; k++) { + char c = s.charAt(k); + + int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + if (index >= 0) { + if (t.nextIndex[index] == null) + t.nextIndex[index] = new TreeTrie(c); + t = t.nextIndex[index]; + } else { + TreeTrie n = null; + for (int i = t.nextOther.size(); i-- > 0; ) { + n = t.nextOther.get(i); + if (n.ch == c) + break; + n = null; + } + if (n == null) { + n = new TreeTrie(c); + t.nextOther.add(n); + } + t = n; + } + } + t.key = v == null ? null : s; + t.value = v; + return true; + } + + @Override + public V get(String s, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + char c = s.charAt(offset + i); + int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + if (index >= 0) { + if (t.nextIndex[index] == null) + return null; + t = t.nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t.nextOther.size(); j-- > 0; ) { + n = t.nextOther.get(j); + if (n.ch == c) + break; + n = null; + } + if (n == null) + return null; + t = n; + } + } + return t.value; + } + + @Override + public V get(ByteBuffer b, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + byte c = b.get(offset + i); + int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + if (index >= 0) { + if (t.nextIndex[index] == null) + return null; + t = t.nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t.nextOther.size(); j-- > 0; ) { + n = t.nextOther.get(j); + if (n.ch == c) + break; + n = null; + } + if (n == null) + return null; + t = n; + } + } + return t.value; + } + + @Override + public V getBest(byte[] b, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + byte c = b[offset + i]; + int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + if (index >= 0) { + if (t.nextIndex[index] == null) + break; + t = t.nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t.nextOther.size(); j-- > 0; ) { + n = t.nextOther.get(j); + if (n.ch == c) + break; + n = null; + } + if (n == null) + break; + t = n; + } + + // Is the next Trie is a match + if (t.key != null) { + // Recurse so we can remember this possibility + V best = t.getBest(b, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return t.value; + } + + @Override + public V getBest(String s, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + byte c = (byte) (0xff & s.charAt(offset + i)); + int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + if (index >= 0) { + if (t.nextIndex[index] == null) + break; + t = t.nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t.nextOther.size(); j-- > 0; ) { + n = t.nextOther.get(j); + if (n.ch == c) + break; + n = null; + } + if (n == null) + break; + t = n; + } + + // Is the next Trie is a match + if (t.key != null) { + // Recurse so we can remember this possibility + V best = t.getBest(s, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return t.value; + } + + @Override + public V getBest(ByteBuffer b, int offset, int len) { + if (b.hasArray()) + return getBest(b.array(), b.arrayOffset() + b.position() + offset, len); + return getBestByteBuffer(b, offset, len); + } + + private V getBestByteBuffer(ByteBuffer b, int offset, int len) { + TreeTrie t = this; + int pos = b.position() + offset; + for (int i = 0; i < len; i++) { + byte c = b.get(pos++); + int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + if (index >= 0) { + if (t.nextIndex[index] == null) + break; + t = t.nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t.nextOther.size(); j-- > 0; ) { + n = t.nextOther.get(j); + if (n.ch == c) + break; + n = null; + } + if (n == null) + break; + t = n; + } + + // Is the next Trie is a match + if (t.key != null) { + // Recurse so we can remember this possibility + V best = t.getBest(b, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return t.value; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + toString(buf, this); + + if (buf.length() == 0) + return "{}"; + + buf.setCharAt(0, '{'); + buf.append('}'); + return buf.toString(); + } + + @Override + public Set keySet() { + Set keys = new HashSet<>(); + keySet(keys, this); + return keys; + } + + @Override + public boolean isFull() { + return false; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/collection/trie/Trie.java b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/Trie.java new file mode 100644 index 000000000..af8581c6c --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/collection/trie/Trie.java @@ -0,0 +1,117 @@ +package com.fireflysource.common.collection.trie; + +import java.nio.ByteBuffer; +import java.util.Set; + + +/** + * A Trie String lookup data structure. + * + * @param the Trie entry type + */ +public interface Trie { + + /** + * Put an entry into the Trie + * + * @param s The key for the entry + * @param v The value of the entry + * @return True if the Trie had capacity to add the field. + */ + boolean put(String s, V v); + + /** + * Put a value as both a key and a value. + * + * @param v The value and key + * @return True if the Trie had capacity to add the field. + */ + boolean put(V v); + + V remove(String s); + + /** + * Get an exact match from a String key + * + * @param s The key + * @return the value for the string key + */ + V get(String s); + + /** + * Get an exact match from a String key + * + * @param s The key + * @param offset The offset within the string of the key + * @param len the length of the key + * @return the value for the string / offset / length + */ + V get(String s, int offset, int len); + + /** + * Get an exact match from a segment of a ByteBuufer as key + * + * @param b The buffer + * @return The value or null if not found + */ + V get(ByteBuffer b); + + /** + * Get an exact match from a segment of a ByteBuufer as key + * + * @param b The buffer + * @param offset The offset within the buffer of the key + * @param len the length of the key + * @return The value or null if not found + */ + V get(ByteBuffer b, int offset, int len); + + /** + * Get the best match from key in a String. + * + * @param s The string + * @return The value or null if not found + */ + V getBest(String s); + + /** + * Get the best match from key in a String. + * + * @param s The string + * @param offset The offset within the string of the key + * @param len the length of the key + * @return The value or null if not found + */ + V getBest(String s, int offset, int len); + + /** + * Get the best match from key in a byte array. + * The key is assumed to by ISO_8859_1 characters. + * + * @param b The buffer + * @param offset The offset within the array of the key + * @param len the length of the key + * @return The value or null if not found + */ + V getBest(byte[] b, int offset, int len); + + /** + * Get the best match from key in a byte buffer. + * The key is assumed to by ISO_8859_1 characters. + * + * @param b The buffer + * @param offset The offset within the buffer of the key + * @param len the length of the key + * @return The value or null if not found + */ + V getBest(ByteBuffer b, int offset, int len); + + Set keySet(); + + boolean isFull(); + + boolean isCaseInsensitive(); + + void clear(); + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/AtomicBiInteger.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/AtomicBiInteger.java new file mode 100644 index 000000000..22bb06211 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/AtomicBiInteger.java @@ -0,0 +1,254 @@ +package com.fireflysource.common.concurrent; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * An AtomicLong with additional methods to treat it as two hi/lo integers. + */ +public class AtomicBiInteger extends AtomicLong { + + public AtomicBiInteger() { + } + + public AtomicBiInteger(long encoded) { + super(encoded); + } + + public AtomicBiInteger(int hi, int lo) { + super(encode(hi, lo)); + } + + /** + * @return the hi value + */ + public int getHi() { + return getHi(get()); + } + + /** + * @return the lo value + */ + public int getLo() { + return getLo(get()); + } + + /** + * Atomically sets the hi value without changing the lo value. + * + * @param hi the new hi value + * @return the previous hi value + */ + public int getAndSetHi(int hi) { + while (true) { + long encoded = get(); + long update = encodeHi(encoded, hi); + if (compareAndSet(encoded, update)) + return getHi(encoded); + } + } + + /** + * Atomically sets the lo value without changing the hi value. + * + * @param lo the new lo value + * @return the previous lo value + */ + public int getAndSetLo(int lo) { + while (true) { + long encoded = get(); + long update = encodeLo(encoded, lo); + if (compareAndSet(encoded, update)) + return getLo(encoded); + } + } + + /** + * Sets the hi and lo values. + * + * @param hi the new hi value + * @param lo the new lo value + */ + public void set(int hi, int lo) { + set(encode(hi, lo)); + } + + /** + *

Atomically sets the hi value to the given updated value + * only if the current value {@code ==} the expected value.

+ *

Concurrent changes to the lo value result in a retry.

+ * + * @param expectHi the expected hi value + * @param hi the new hi value + * @return {@code true} if successful. False return indicates that + * the actual hi value was not equal to the expected hi value. + */ + public boolean compareAndSetHi(int expectHi, int hi) { + while (true) { + long encoded = get(); + if (getHi(encoded) != expectHi) + return false; + long update = encodeHi(encoded, hi); + if (compareAndSet(encoded, update)) + return true; + } + } + + /** + *

Atomically sets the lo value to the given updated value + * only if the current value {@code ==} the expected value.

+ *

Concurrent changes to the hi value result in a retry.

+ * + * @param expectLo the expected lo value + * @param lo the new lo value + * @return {@code true} if successful. False return indicates that + * the actual lo value was not equal to the expected lo value. + */ + public boolean compareAndSetLo(int expectLo, int lo) { + while (true) { + long encoded = get(); + if (getLo(encoded) != expectLo) + return false; + long update = encodeLo(encoded, lo); + if (compareAndSet(encoded, update)) + return true; + } + } + + /** + * Atomically sets the values to the given updated values only if + * the current encoded value {@code ==} the expected encoded value. + * + * @param encoded the expected encoded value + * @param hi the new hi value + * @param lo the new lo value + * @return {@code true} if successful. False return indicates that + * the actual encoded value was not equal to the expected encoded value. + */ + public boolean compareAndSet(long encoded, int hi, int lo) { + long update = encode(hi, lo); + return compareAndSet(encoded, update); + } + + /** + * Atomically sets the hi and lo values to the given updated values only if + * the current hi and lo values {@code ==} the expected hi and lo values. + * + * @param expectHi the expected hi value + * @param hi the new hi value + * @param expectLo the expected lo value + * @param lo the new lo value + * @return {@code true} if successful. False return indicates that + * the actual hi and lo values were not equal to the expected hi and lo value. + */ + public boolean compareAndSet(int expectHi, int hi, int expectLo, int lo) { + long encoded = encode(expectHi, expectLo); + long update = encode(hi, lo); + return compareAndSet(encoded, update); + } + + /** + * Atomically adds the given delta to the current hi value, returning the updated hi value. + * + * @param delta the delta to apply + * @return the updated hi value + */ + public int addAndGetHi(int delta) { + while (true) { + long encoded = get(); + int hi = getHi(encoded) + delta; + long update = encodeHi(encoded, hi); + if (compareAndSet(encoded, update)) + return hi; + } + } + + /** + * Atomically adds the given delta to the current lo value, returning the updated lo value. + * + * @param delta the delta to apply + * @return the updated lo value + */ + public int addAndGetLo(int delta) { + while (true) { + long encoded = get(); + int lo = getLo(encoded) + delta; + long update = encodeLo(encoded, lo); + if (compareAndSet(encoded, update)) + return lo; + } + } + + /** + * Atomically adds the given deltas to the current hi and lo values. + * + * @param deltaHi the delta to apply to the hi value + * @param deltaLo the delta to apply to the lo value + */ + public void add(int deltaHi, int deltaLo) { + while (true) { + long encoded = get(); + long update = encode(getHi(encoded) + deltaHi, getLo(encoded) + deltaLo); + if (compareAndSet(encoded, update)) + return; + } + } + + /** + * Gets a hi value from the given encoded value. + * + * @param encoded the encoded value + * @return the hi value + */ + public static int getHi(long encoded) { + return (int) ((encoded >> 32) & 0xFFFF_FFFFL); + } + + /** + * Gets a lo value from the given encoded value. + * + * @param encoded the encoded value + * @return the lo value + */ + public static int getLo(long encoded) { + return (int) (encoded & 0xFFFF_FFFFL); + } + + /** + * Encodes hi and lo values into a long. + * + * @param hi the hi value + * @param lo the lo value + * @return the encoded value + */ + public static long encode(int hi, int lo) { + long h = ((long) hi) & 0xFFFF_FFFFL; + long l = ((long) lo) & 0xFFFF_FFFFL; + return (h << 32) + l; + } + + /** + * Sets the hi value into the given encoded value. + * + * @param encoded the encoded value + * @param hi the hi value + * @return the new encoded value + */ + public static long encodeHi(long encoded, int hi) { + long h = ((long) hi) & 0xFFFF_FFFFL; + long l = encoded & 0xFFFF_FFFFL; + return (h << 32) + l; + } + + /** + * Sets the lo value into the given encoded value. + * + * @param encoded the encoded value + * @param lo the lo value + * @return the new encoded value + */ + public static long encodeLo(long encoded, int lo) { + long h = (encoded >> 32) & 0xFFFF_FFFFL; + long l = ((long) lo) & 0xFFFF_FFFFL; + return (h << 32) + l; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/Atomics.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/Atomics.java new file mode 100644 index 000000000..0cfcdf584 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/Atomics.java @@ -0,0 +1,48 @@ +package com.fireflysource.common.concurrent; + +import java.util.concurrent.atomic.AtomicInteger; + +abstract public class Atomics { + + public static int getAndDecrement(AtomicInteger i, int minValue) { + return i.getAndUpdate(prev -> { + if (prev > minValue) { + return prev - 1; + } else { + return minValue; + } + }); + } + + public static int getAndIncrement(AtomicInteger i, int maxValue) { + return i.getAndUpdate(prev -> { + if (prev < maxValue) { + return prev + 1; + } else { + return maxValue; + } + }); + } + + public static boolean updateMin(AtomicInteger currentMin, int newValue) { + int oldValue = currentMin.get(); + while (newValue < oldValue) { + if (currentMin.compareAndSet(oldValue, newValue)) { + return true; + } + oldValue = currentMin.get(); + } + return false; + } + + public static boolean updateMax(AtomicInteger currentMax, int newValue) { + int oldValue = currentMax.get(); + while (newValue > oldValue) { + if (currentMax.compareAndSet(oldValue, newValue)) { + return true; + } + oldValue = currentMax.get(); + } + return false; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/AutoLock.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/AutoLock.java new file mode 100644 index 000000000..9291d0029 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/AutoLock.java @@ -0,0 +1,129 @@ +package com.fireflysource.common.concurrent; + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +/** + *

Reentrant lock that can be used in a try-with-resources statement.

+ *

Typical usage:

+ *
+ * try (AutoLock lock = this.lock.lock())
+ * {
+ *     // Something
+ * }
+ * 
+ */ +public class AutoLock implements AutoCloseable, Serializable { + private static final long serialVersionUID = 3300696774541816341L; + + private final ReentrantLock _lock = new ReentrantLock(); + + /** + *

Acquires the lock.

+ * + * @return this AutoLock for unlocking + */ + public AutoLock lock() { + _lock.lock(); + return this; + } + + public void lock(Runnable runnable) { + try (AutoLock ignored = this.lock()) { + runnable.run(); + } + } + + public T lock(Supplier supplier) { + try (AutoLock ignored = this.lock()) { + return supplier.get(); + } + } + + /** + * @return whether this lock is held by the current thread + * @see ReentrantLock#isHeldByCurrentThread() + */ + public boolean isHeldByCurrentThread() { + return _lock.isHeldByCurrentThread(); + } + + /** + * @return a {@link Condition} associated with this lock + */ + public Condition newCondition() { + return _lock.newCondition(); + } + + // Package-private for testing only. + boolean isLocked() { + return _lock.isLocked(); + } + + @Override + public void close() { + _lock.unlock(); + } + + /** + *

A reentrant lock with a condition that can be used in a try-with-resources statement.

+ *

Typical usage:

+ *
+     * // Waiting
+     * try (AutoLock lock = _lock.lock())
+     * {
+     *     lock.await();
+     * }
+     *
+     * // Signaling
+     * try (AutoLock lock = _lock.lock())
+     * {
+     *     lock.signalAll();
+     * }
+     * 
+ */ + public static class WithCondition extends AutoLock { + private final Condition _condition = newCondition(); + + @Override + public AutoLock.WithCondition lock() { + return (WithCondition) super.lock(); + } + + /** + * @see Condition#signal() + */ + public void signal() { + _condition.signal(); + } + + /** + * @see Condition#signalAll() + */ + public void signalAll() { + _condition.signalAll(); + } + + /** + * @throws InterruptedException if the current thread is interrupted + * @see Condition#await() + */ + public void await() throws InterruptedException { + _condition.await(); + } + + /** + * @param time the time to wait + * @param unit the time unit + * @return false if the waiting time elapsed + * @throws InterruptedException if the current thread is interrupted + * @see Condition#await(long, TimeUnit) + */ + public boolean await(long time, TimeUnit unit) throws InterruptedException { + return _condition.await(time, unit); + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/CompletableFutures.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/CompletableFutures.java new file mode 100644 index 000000000..252755f48 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/CompletableFutures.java @@ -0,0 +1,91 @@ +package com.fireflysource.common.concurrent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +abstract public class CompletableFutures { + /** + * Returns a new stage that, when this stage completes + * exceptionally, is executed with this stage's exception as the + * argument to the supplied function. Otherwise, if this stage + * completes normally, then the returned stage also completes + * normally with the same value. + * + *

This differs from + * {@link java.util.concurrent.CompletionStage#exceptionally(java.util.function.Function)} + * in that the function should return a {@link java.util.concurrent.CompletionStage} rather than + * the value directly. + * + * @param stage the {@link CompletionStage} to compose + * @param fn the function computes the value of the + * returned {@link CompletionStage} if this stage completed + * exceptionally + * @param the type of the input stage's value. + * @return the new {@link CompletionStage} + */ + public static CompletionStage exceptionallyCompose(CompletionStage stage, Function> fn) { + return dereference(wrap(stage).exceptionally(fn)); + } + + public static CompletionStage dereference(CompletionStage> stage) { + return stage.thenCompose(Function.identity()); + } + + private static CompletionStage> wrap(CompletionStage future) { + return future.thenApply(CompletableFuture::completedFuture); + } + + /** + * Create a failed future. + * + * @param t The exception. + * @param The future result type. + * @return The failed future. + */ + public static CompletableFuture failedFuture(Throwable t) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(t); + return future; + } + + /** + * Retry the async operation. + * + * @param retryCount The max retry times. + * @param supplier The async operation function. + * @param beforeRetry The callback before retries async operation. + * @param The future result type. + * @return The operation result future. + */ + public static CompletableFuture retry(int retryCount, Supplier> supplier, BiConsumer beforeRetry) { + return exceptionallyCompose(supplier.get(), e -> { + if (retryCount > 0) { + beforeRetry.accept(e, retryCount); + return retry(retryCount - 1, supplier, beforeRetry); + } else { + return failedFuture(e); + } + }).toCompletableFuture(); + } + + public static CompletableFuture doFinally(CompletionStage stage, BiFunction> function) { + CompletableFuture future = new CompletableFuture<>(); + stage.handle((value, throwable) -> { + function.apply(value, throwable).handle((v, t) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(value); + } + return v; + }); + return value; + }); + return future; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/ExecutorServiceUtils.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/ExecutorServiceUtils.java new file mode 100644 index 000000000..03909eb7e --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/ExecutorServiceUtils.java @@ -0,0 +1,55 @@ +package com.fireflysource.common.concurrent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * The executor service common utilities. + * + * @author Pengtao Qiu + */ +abstract public class ExecutorServiceUtils { + + /** + * Blocks until all tasks have completed execution after a shutdown + * request, or the timeout occurs, or the current thread is + * interrupted, whichever happens first. + * + * @param pool The thread pool that will shutdown. + * @param timeout The maximum time to wait. The time unit is second. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool, long timeout) { + shutdownAndAwaitTermination(pool, timeout, TimeUnit.SECONDS); + } + + /** + * Blocks until all tasks have completed execution after a shutdown + * request, or the timeout occurs, or the current thread is + * interrupted, whichever happens first. + * + * @param pool The thread pool that will shutdown. + * @param timeout The maximum time to wait. + * @param unit The time unit of the timeout argument. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool, long timeout, TimeUnit unit) { + try { + // Disable new tasks from being submitted + pool.shutdown(); + // Wait a while for existing tasks to terminate + if (!pool.awaitTermination(timeout, unit)) { + pool.shutdownNow(); // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!pool.awaitTermination(timeout, unit)) + System.err.println("Pool did not terminate"); + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted + pool.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/IteratingCallback.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/IteratingCallback.java new file mode 100644 index 000000000..5444fa5d2 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/IteratingCallback.java @@ -0,0 +1,437 @@ +package com.fireflysource.common.concurrent; + +import com.fireflysource.common.sys.Result; + +import java.io.IOException; +import java.util.function.Consumer; + +/** + * This specialized callback implements a pattern that allows + * a large job to be broken into smaller tasks using iteration + * rather than recursion. + *

+ * A typical example is the write of a large content to a socket, + * divided in chunks. Chunk C1 is written by thread T1, which + * also invokes the callback, which writes chunk C2, which invokes + * the callback again, which writes chunk C3, and so forth. + *

+ *

+ * The problem with the example is that if the callback thread + * is the same that performs the I/O operation, then the process + * is recursive and may result in a stack overflow. + * To avoid the stack overflow, a thread dispatch must be performed, + * causing context switching and cache misses, affecting performance. + *

+ *

+ * To avoid this issue, this callback uses an AtomicReference to + * record whether success callback has been called during the processing + * of a sub task, and if so then the processing iterates rather than + * recurring. + *

+ *

+ * Subclasses must implement method {@link #process()} where the sub + * task is executed and a suitable {@link Action} is + * returned to this callback to indicate the overall progress of the job. + * This callback is passed to the asynchronous execution of each sub + * task and a call the success result on this callback represents + * the completion of the sub task. + *

+ */ +public abstract class IteratingCallback implements Consumer> { + /** + * The internal states of this callback + */ + private enum State { + /** + * This callback is IDLE, ready to iterate. + */ + IDLE, + + /** + * This callback is iterating calls to {@link #process()} and is dealing with + * the returns. To get into processing state, it much of held the lock state + * and set iterating to true. + */ + PROCESSING, + + /** + * Waiting for a schedule callback + */ + PENDING, + + /** + * Called by a schedule callback + */ + CALLED, + + /** + * The overall job has succeeded as indicated by a {@link Action#SUCCEEDED} return + * from {@link IteratingCallback#process()} + */ + SUCCEEDED, + + /** + * The overall job has failed as indicated by a call to {@link IteratingCallback#failed(Throwable)} + */ + FAILED, + + /** + * This callback has been closed and cannot be reset. + */ + CLOSED + } + + /** + * The indication of the overall progress of the overall job that + * implementations of {@link #process()} must return. + */ + protected enum Action { + /** + * Indicates that {@link #process()} has no more work to do, + * but the overall job is not completed yet, probably waiting + * for additional events to trigger more work. + */ + IDLE, + /** + * Indicates that {@link #process()} is executing asynchronously + * a sub task, where the execution has started but the callback + * may have not yet been invoked. + */ + SCHEDULED, + + /** + * Indicates that {@link #process()} has completed the overall job. + */ + SUCCEEDED + } + + private final AutoLock _lock = new AutoLock(); + private State _state; + private boolean _iterate; + + protected IteratingCallback() { + _state = State.IDLE; + } + + protected IteratingCallback(boolean needReset) { + _state = needReset ? State.SUCCEEDED : State.IDLE; + } + + /** + * Method called by {@link #iterate()} to process the sub task. + *

+ * Implementations must start the asynchronous execution of the sub task + * (if any) and return an appropriate action: + *

+ *
    + *
  • {@link Action#IDLE} when no sub tasks are available for execution + * but the overall job is not completed yet
  • + *
  • {@link Action#SCHEDULED} when the sub task asynchronous execution + * has been started
  • + *
  • {@link Action#SUCCEEDED} when the overall job is completed
  • + *
+ * + * @return the appropriate Action + * @throws Throwable if the sub task processing throws + */ + protected abstract Action process() throws Throwable; + + /** + * Invoked when the overall task has completed successfully. + * + * @see #onCompleteFailure(Throwable) + */ + protected void onCompleteSuccess() { + } + + /** + * Invoked when the overall task has completed with a failure. + * + * @param cause the throwable to indicate cause of failure + * @see #onCompleteSuccess() + */ + protected void onCompleteFailure(Throwable cause) { + } + + /** + * This method must be invoked by applications to start the processing + * of sub tasks. It can be called at any time by any thread, and it's + * contract is that when called, then the {@link #process()} method will + * be called during or soon after, either by the calling thread or by + * another thread. + */ + public void iterate() { + boolean process = false; + + try (AutoLock lock = _lock.lock()) { + switch (_state) { + case PENDING: + case CALLED: + // process will be called when callback is handled + break; + + case IDLE: + _state = State.PROCESSING; + process = true; + break; + + case PROCESSING: + _iterate = true; + break; + + case FAILED: + case SUCCEEDED: + break; + + case CLOSED: + default: + throw new IllegalStateException(toString()); + } + } + if (process) + processing(); + } + + private void processing() { + // This should only ever be called when in processing state, however a failed or close call + // may happen concurrently, so state is not assumed. + + boolean onCompleteSuccess = false; + + // While we are processing + processing: + while (true) { + // Call process to get the action that we have to take. + Action action; + try { + action = process(); + } catch (Throwable x) { + failed(x); + break; + } + + // acted on the action we have just received + try (AutoLock lock = _lock.lock()) { + switch (_state) { + case PROCESSING: { + switch (action) { + case IDLE: { + // Has iterate been called while we were processing? + if (_iterate) { + // yes, so skip idle and keep processing + _iterate = false; + _state = State.PROCESSING; + continue processing; + } + + // No, so we can go idle + _state = State.IDLE; + break processing; + } + + case SCHEDULED: { + // we won the race against the callback, so the callback has to process and we can break processing + _state = State.PENDING; + break processing; + } + + case SUCCEEDED: { + // we lost the race against the callback, + _iterate = false; + _state = State.SUCCEEDED; + onCompleteSuccess = true; + break processing; + } + + default: + break; + } + throw new IllegalStateException(String.format("%s[action=%s]", this, action)); + } + + case CALLED: { + if (action != Action.SCHEDULED) + throw new IllegalStateException(String.format("%s[action=%s]", this, action)); + // we lost the race, so we have to keep processing + _state = State.PROCESSING; + continue processing; + } + + case SUCCEEDED: + case FAILED: + case CLOSED: + break processing; + + case IDLE: + case PENDING: + default: + throw new IllegalStateException(String.format("%s[action=%s]", this, action)); + } + } + } + + if (onCompleteSuccess) + onCompleteSuccess(); + } + + @Override + public void accept(Result result) { + if (result.isSuccess()) { + this.succeeded(); + } else { + this.failed(result.getThrowable()); + } + } + + /** + * Invoked when the sub task succeeds. + * Subclasses that override this method must always remember to call + * {@code super.succeeded()}. + */ + public void succeeded() { + boolean process = false; + try (AutoLock lock = _lock.lock()) { + switch (_state) { + case PROCESSING: { + _state = State.CALLED; + break; + } + case PENDING: { + _state = State.PROCESSING; + process = true; + break; + } + case CLOSED: + case FAILED: { + // Too late! + break; + } + default: { + throw new IllegalStateException(toString()); + } + } + } + if (process) + processing(); + } + + /** + * Invoked when the sub task fails. + * Subclasses that override this method must always remember to call + * {@code super.failed(Throwable)}. + */ + public void failed(Throwable x) { + boolean failure = false; + try (AutoLock lock = _lock.lock()) { + switch (_state) { + case SUCCEEDED: + case FAILED: + case IDLE: + case CLOSED: + case CALLED: + // too late!. + break; + + case PENDING: + case PROCESSING: { + _state = State.FAILED; + failure = true; + break; + } + default: + throw new IllegalStateException(toString()); + } + } + if (failure) + onCompleteFailure(x); + } + + public void close() { + String failure = null; + try (AutoLock lock = _lock.lock()) { + switch (_state) { + case IDLE: + case SUCCEEDED: + case FAILED: + _state = State.CLOSED; + break; + + case CLOSED: + break; + + default: + failure = String.format("Close %s in state %s", this, _state); + _state = State.CLOSED; + } + } + + if (failure != null) + onCompleteFailure(new IOException(failure)); + } + + /* + * only for testing + * @return whether this callback is idle and {@link #iterate()} needs to be called + */ + boolean isIdle() { + try (AutoLock lock = _lock.lock()) { + return _state == State.IDLE; + } + } + + public boolean isClosed() { + try (AutoLock lock = _lock.lock()) { + return _state == State.CLOSED; + } + } + + /** + * @return whether this callback has failed + */ + public boolean isFailed() { + try (AutoLock lock = _lock.lock()) { + return _state == State.FAILED; + } + } + + /** + * @return whether this callback has succeeded + */ + public boolean isSucceeded() { + try (AutoLock lock = _lock.lock()) { + return _state == State.SUCCEEDED; + } + } + + /** + * Resets this callback. + *

+ * A callback can only be reset to IDLE from the + * SUCCEEDED or FAILED states or if it is already IDLE. + *

+ * + * @return true if the reset was successful + */ + public boolean reset() { + try (AutoLock lock = _lock.lock()) { + switch (_state) { + case IDLE: + return true; + + case SUCCEEDED: + case FAILED: + _iterate = false; + _state = State.IDLE; + return true; + + default: + return false; + } + } + } + + @Override + public String toString() { + return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), _state); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/concurrent/SingleThreadExecutorService.java b/firefly-common/src/main/java/com/fireflysource/common/concurrent/SingleThreadExecutorService.java new file mode 100644 index 000000000..227c30da1 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/concurrent/SingleThreadExecutorService.java @@ -0,0 +1,100 @@ +package com.fireflysource.common.concurrent; + +import org.jctools.queues.MpscBlockingConsumerArrayQueue; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SingleThreadExecutorService extends AbstractExecutorService { + + private static final ThreadFactory defaultThreadFactory = r -> new Thread(r, "Firefly-MPSC-thread"); + private final Termination termination = new Termination(); + private final List notExecutedTasks = new LinkedList<>(); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final MpscBlockingConsumerArrayQueue queue; + private final Thread thread; + + public SingleThreadExecutorService(int capacity) { + this(capacity, defaultThreadFactory); + } + + public SingleThreadExecutorService(int capacity, ThreadFactory threadFactory) { + this.queue = new MpscBlockingConsumerArrayQueue<>(capacity); + thread = threadFactory.newThread(() -> { + while (true) { + if (executeTasks()) { + break; + } + } + }); + thread.start(); + } + + private boolean executeTasks() { + boolean exit; + try { + Runnable task = queue.take(); + if (task == termination) { + exit = true; + } else { + task.run(); + exit = false; + } + } catch (InterruptedException e) { + queue.drain(notExecutedTasks::add); + exit = true; + } + return exit; + } + + @Override + public void shutdown() { + if (queue.offer(termination)) { + isShutdown.set(true); + } + } + + @Override + public List shutdownNow() { + shutdown(); + thread.interrupt(); + return notExecutedTasks; + } + + @Override + public boolean isShutdown() { + return isShutdown.get(); + } + + @Override + public boolean isTerminated() { + return !thread.isAlive(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + if (isShutdown.get()) { + thread.join(unit.toMillis(timeout)); + } + return isTerminated(); + } + + @Override + public void execute(Runnable command) { + if (!queue.offer(command)) { + throw new RejectedExecutionException(); + } + } + + private static class Termination implements Runnable { + @Override + public void run() { + + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/exception/UnknownTypeException.java b/firefly-common/src/main/java/com/fireflysource/common/exception/UnknownTypeException.java new file mode 100644 index 000000000..bbb3998e3 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/exception/UnknownTypeException.java @@ -0,0 +1,11 @@ +package com.fireflysource.common.exception; + +public class UnknownTypeException extends RuntimeException { + + public UnknownTypeException() { + } + + public UnknownTypeException(String message) { + super(message); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/func/Callback.java b/firefly-common/src/main/java/com/fireflysource/common/func/Callback.java new file mode 100644 index 000000000..c06943e61 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/func/Callback.java @@ -0,0 +1,9 @@ +package com.fireflysource.common.func; + +/** + * @author Pengtao Qiu + */ +@FunctionalInterface +public interface Callback { + void call() throws Exception; +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/func/FunctionInterfaceUtils.java b/firefly-common/src/main/java/com/fireflysource/common/func/FunctionInterfaceUtils.java new file mode 100644 index 000000000..acae59bf3 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/func/FunctionInterfaceUtils.java @@ -0,0 +1,18 @@ +package com.fireflysource.common.func; + +import java.util.function.Consumer; + +/** + * @author Pengtao Qiu + */ +abstract public class FunctionInterfaceUtils { + + public static final Callback NOOP_CALLBACK = () -> { + }; + + + public static Consumer createEmptyConsumer() { + return t -> { + }; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/io/AsyncCloseable.java b/firefly-common/src/main/java/com/fireflysource/common/io/AsyncCloseable.java new file mode 100644 index 000000000..e9243ba72 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/io/AsyncCloseable.java @@ -0,0 +1,10 @@ +package com.fireflysource.common.io; + +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; + +public interface AsyncCloseable extends Closeable { + + CompletableFuture closeAsync(); + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/io/BufferUtils.java b/firefly-common/src/main/java/com/fireflysource/common/io/BufferUtils.java new file mode 100644 index 000000000..c25eb51a6 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/io/BufferUtils.java @@ -0,0 +1,1047 @@ +package com.fireflysource.common.io; + +import com.fireflysource.common.collection.CollectionUtils; +import com.fireflysource.common.object.TypeUtils; + +import java.io.*; +import java.nio.Buffer; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Buffer utility methods. + *

The standard JVM {@link ByteBuffer} can exist in two modes: In fill mode the valid + * data is between 0 and pos; In flush mode the valid data is between the pos and the limit. + * The various ByteBuffer methods assume a mode and some of them will switch or enforce a mode: + * Allocate and clear set fill mode; flip and compact switch modes; read and write assume fill + * and flush modes. This duality can result in confusing code such as: + *

+ *
+ *     buffer.clear();
+ *     channel.write(buffer);
+ * 
+ *

+ * Which looks as if it should write no data, but in fact writes the buffer worth of garbage. + *

+ *

+ * The BufferUtils class provides a set of utilities that operate on the convention that ByteBuffers + * will always be left, passed in an API or returned from a method in the flush mode - ie with + * valid data between the pos and limit. This convention is adopted so as to avoid confusion as to + * what state a buffer is in and to avoid excessive copying of data that can result with the usage + * of compress.

+ *

+ * Thus this class provides alternate implementations of {@link #allocate(int)}, + * {@link #allocateDirect(int)} and {@link #clear(ByteBuffer)} that leave the buffer + * in flush mode. Thus the following tests will pass: + *

+ *
+ *     ByteBuffer buffer = BufferUtils.allocate(1024);
+ *     assert(buffer.remaining()==0);
+ *     BufferUtils.clear(buffer);
+ *     assert(buffer.remaining()==0);
+ * 
+ *

If the BufferUtils methods {@link #fill(ByteBuffer, byte[], int, int)}, + * {@link #append(ByteBuffer, byte[], int, int)} or {@link #put(ByteBuffer, ByteBuffer)} are used, + * then the caller does not need to explicitly switch the buffer to fill mode. + * If the caller wishes to use other ByteBuffer bases libraries to fill a buffer, + * then they can use explicit calls of #flipToFill(ByteBuffer) and #flipToFlush(ByteBuffer, int) + * to change modes. Note because this convention attempts to avoid the copies of compact, the position + * is not set to zero on each fill cycle and so its value must be remembered: + *

+ *
+ *      int pos = BufferUtils.flipToFill(buffer);
+ *      try
+ *      {
+ *          buffer.put(data);
+ *      }
+ *      finally
+ *      {
+ *          flipToFlush(buffer, pos);
+ *      }
+ * 
+ *

+ * The flipToFill method will effectively clear the buffer if it is empty and will compact the buffer if there is no space. + *

+ */ +public class BufferUtils { + public static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); + static final int TEMP_BUFFER_SIZE = 4096; + static final byte SPACE = 0x20; + static final byte MINUS = '-'; + static final byte[] DIGIT = {(byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F'}; + private final static int[] decDivisors = {1000000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1}; + private final static int[] hexDivisors = {0x10000000, 0x1000000, 0x100000, 0x10000, 0x1000, 0x100, 0x10, 0x1}; + private final static long[] decDivisorsL = {1000000000000000000L, 100000000000000000L, 10000000000000000L, 1000000000000000L, 100000000000000L, 10000000000000L, 1000000000000L, 100000000000L, 10000000000L, 1000000000L, 100000000L, 10000000L, 1000000L, 100000L, 10000L, 1000L, 100L, 10L, 1L}; + + /** + * Allocate ByteBuffer in flush mode. + * The position and limit will both be zero, indicating that the buffer is + * empty and must be flipped before any data is put to it. + * + * @param capacity capacity of the allocated ByteBuffer + * @return Buffer + */ + public static ByteBuffer allocate(int capacity) { + ByteBuffer buf = ByteBuffer.allocate(capacity); + buf.limit(0); + return buf; + } + + /** + * Allocate ByteBuffer in flush mode. + * The position and limit will both be zero, indicating that the buffer is + * empty and in flush mode. + * + * @param capacity capacity of the allocated ByteBuffer + * @return Buffer + */ + public static ByteBuffer allocateDirect(int capacity) { + ByteBuffer buf = ByteBuffer.allocateDirect(capacity); + buf.limit(0); + return buf; + } + + /** + * Clear the buffer to be empty in flush mode. + * The position and limit are set to 0; + * + * @param buffer The buffer to clear. + */ + public static void clear(ByteBuffer buffer) { + if (buffer != null) { + buffer.position(0); + buffer.limit(0); + } + } + + /** + * Clear the buffer to be empty in fill mode. + * The position is set to 0 and the limit is set to the capacity. + * + * @param buffer The buffer to clear. + */ + public static void clearToFill(ByteBuffer buffer) { + if (buffer != null) { + buffer.position(0); + buffer.limit(buffer.capacity()); + } + } + + /** + * Flip the buffer to fill mode. + * The position is set to the first unused position in the buffer + * (the old limit) and the limit is set to the capacity. + * If the buffer is empty, then this call is effectively {@link #clearToFill(ByteBuffer)}. + * If there is no unused space to fill, a {@link ByteBuffer#compact()} is done to attempt + * to create space. + *

+ * This method is used as a replacement to {@link ByteBuffer#compact()}. + * + * @param buffer The buffer to flip + * @return The position of the valid data before the flipped position. This value should be + * passed to a subsequent call to {@link #flipToFlush(ByteBuffer, int)} + */ + public static int flipToFill(ByteBuffer buffer) { + int position = buffer.position(); + int limit = buffer.limit(); + if (position == limit) { + buffer.position(0); + buffer.limit(buffer.capacity()); + return 0; + } + + int capacity = buffer.capacity(); + if (limit == capacity) { + buffer.compact(); + return 0; + } + + buffer.position(limit); + buffer.limit(capacity); + return position; + } + + /** + * Flip the buffer to Flush mode. + * The limit is set to the first unused byte(the old position) and + * the position is set to the passed position. + *

+ * This method is used as a replacement of {@link Buffer#flip()}. + * + * @param buffer the buffer to be flipped + * @param position The position of valid data to flip to. This should + * be the return value of the previous call to {@link #flipToFill(ByteBuffer)} + */ + public static void flipToFlush(ByteBuffer buffer, int position) { + buffer.limit(buffer.position()); + buffer.position(position); + } + + /** + * Convert a ByteBuffer to a byte array. + * + * @param buffer The buffer to convert in flush mode. The buffer is not altered. + * @return An array of bytes duplicated from the buffer. + */ + public static byte[] toArray(ByteBuffer buffer) { + if (buffer.hasArray()) { + byte[] array = buffer.array(); + int from = buffer.arrayOffset() + buffer.position(); + return Arrays.copyOfRange(array, from, from + buffer.remaining()); + } else { + byte[] to = new byte[buffer.remaining()]; + buffer.slice().get(to); + return to; + } + } + + /** + * Convert a buffer collection to a byte array. + * + * @param buffers The buffer to convert in flush mode. The buffer is not altered. + * @return An array of bytes duplicated from the buffer. + */ + public static byte[] toArray(Collection buffers) { + List list = buffers.stream().map(BufferUtils::toArray).collect(Collectors.toList()); + int count = list.stream().mapToInt(arr -> arr.length).sum(); + if (count < 0) { + throw new IllegalArgumentException("The buffers are too big"); + } + byte[] result = new byte[count]; + int index = 0; + for (byte[] bytes : list) { + System.arraycopy(bytes, 0, result, index, bytes.length); + index += bytes.length; + } + return result; + } + + /** + * @param buf the buffer to check + * @return true if buf is equal to EMPTY_BUFFER + */ + public static boolean isTheEmptyBuffer(ByteBuffer buf) { + @SuppressWarnings("ReferenceEquality") + boolean isTheEmptyBuffer_ = (buf == EMPTY_BUFFER); + return isTheEmptyBuffer_; + } + + /** + * Check for an empty or null buffer. + * + * @param buf the buffer to check + * @return true if the buffer is null or empty. + */ + public static boolean isEmpty(ByteBuffer buf) { + return buf == null || buf.remaining() == 0; + } + + /** + * Check for a non null and non empty buffer. + * + * @param buf the buffer to check + * @return true if the buffer is not null and not empty. + */ + public static boolean hasContent(ByteBuffer buf) { + return buf != null && buf.remaining() > 0; + } + + /** + * Check for a non null and full buffer. + * + * @param buf the buffer to check + * @return true if the buffer is not null and the limit equals the capacity. + */ + public static boolean isFull(ByteBuffer buf) { + return buf != null && buf.limit() == buf.capacity(); + } + + /** + * Get remaining from null checked buffer + * + * @param buffer The buffer to get the remaining from, in flush mode. + * @return 0 if the buffer is null, else the bytes remaining in the buffer. + */ + public static int length(ByteBuffer buffer) { + return buffer == null ? 0 : buffer.remaining(); + } + + /** + * Get the space from the limit to the capacity + * + * @param buffer the buffer to get the space from + * @return space + */ + public static int space(ByteBuffer buffer) { + if (buffer == null) + return 0; + return buffer.capacity() - buffer.limit(); + } + + /** + * Compact the buffer + * + * @param buffer the buffer to compact + * @return true if the compact made a full buffer have space + */ + public static boolean compact(ByteBuffer buffer) { + if (buffer.position() == 0) + return false; + boolean full = buffer.limit() == buffer.capacity(); + buffer.compact().flip(); + return full && buffer.limit() < buffer.capacity(); + } + + /** + * Put data from one buffer into another, avoiding over/under flows + * + * @param from Buffer to take bytes from in flush mode + * @param to Buffer to put bytes to in fill mode. + * @return number of bytes moved + */ + public static int put(ByteBuffer from, ByteBuffer to) { + int put; + int remaining = from.remaining(); + if (remaining > 0) { + if (remaining <= to.remaining()) { + to.put(from); + put = remaining; + from.position(from.limit()); + } else if (from.hasArray()) { + put = to.remaining(); + to.put(from.array(), from.arrayOffset() + from.position(), put); + from.position(from.position() + put); + } else { + put = to.remaining(); + ByteBuffer slice = from.slice(); + slice.limit(put); + to.put(slice); + from.position(from.position() + put); + } + } else { + put = 0; + } + return put; + } + + /** + * Put data from one buffer into another, avoiding over/under flows + * + * @param from Buffer to take bytes from in flush mode + * @param to Buffer to put bytes to in flush mode. The buffer is flipToFill before the put and flipToFlush after. + * @return number of bytes moved + * @deprecated use {@link #append(ByteBuffer, ByteBuffer)} + */ + public static int flipPutFlip(ByteBuffer from, ByteBuffer to) { + return append(to, from); + } + + /** + * Append bytes to a buffer. + * + * @param to Buffer is flush mode + * @param b bytes to append + * @param off offset into byte + * @param len length to append + * @throws BufferOverflowException if unable to append buffer due to space limits + */ + public static void append(ByteBuffer to, byte[] b, int off, int len) throws BufferOverflowException { + int pos = flipToFill(to); + try { + to.put(b, off, len); + } finally { + flipToFlush(to, pos); + } + } + + /** + * Appends a byte to a buffer + * + * @param to Buffer is flush mode + * @param b byte to append + */ + public static void append(ByteBuffer to, byte b) { + int pos = flipToFill(to); + try { + to.put(b); + } finally { + flipToFlush(to, pos); + } + } + + /** + * Appends a buffer to a buffer + * + * @param to Buffer is flush mode + * @param b buffer to append + * @return The position of the valid data before the flipped position. + */ + public static int append(ByteBuffer to, ByteBuffer b) { + int pos = flipToFill(to); + try { + return put(b, to); + } finally { + flipToFlush(to, pos); + } + } + + /** + * Like append, but does not throw {@link BufferOverflowException} + * + * @param to Buffer The buffer to fill to. The buffer will be flipped to fill mode and then flipped back to flush mode. + * @param b bytes The bytes to fill + * @param off offset into bytes + * @param len length to fill + * @return the number of bytes taken from the buffer. + */ + public static int fill(ByteBuffer to, byte[] b, int off, int len) { + int pos = flipToFill(to); + try { + int remaining = to.remaining(); + int take = remaining < len ? remaining : len; + to.put(b, off, take); + return take; + } finally { + flipToFlush(to, pos); + } + } + + public static void readFrom(File file, ByteBuffer buffer) throws IOException { + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + FileChannel channel = raf.getChannel(); + long needed = raf.length(); + + while (needed > 0 && buffer.hasRemaining()) + needed = needed - channel.read(buffer); + } + } + + public static void readFrom(InputStream is, int needed, ByteBuffer buffer) throws IOException { + ByteBuffer tmp = allocate(8192); + + while (needed > 0 && buffer.hasRemaining()) { + int l = is.read(tmp.array(), 0, 8192); + if (l < 0) + break; + tmp.position(0); + tmp.limit(l); + buffer.put(tmp); + } + } + + public static void writeTo(ByteBuffer buffer, OutputStream out) throws IOException { + if (buffer.hasArray()) { + out.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + // update buffer position, in way similar to non-array version of writeTo + buffer.position(buffer.position() + buffer.remaining()); + } else { + byte[] bytes = new byte[TEMP_BUFFER_SIZE]; + while (buffer.hasRemaining()) { + int byteCountToWrite = Math.min(buffer.remaining(), TEMP_BUFFER_SIZE); + buffer.get(bytes, 0, byteCountToWrite); + out.write(bytes, 0, byteCountToWrite); + } + } + } + + /** + * Convert the buffer to an ISO-8859-1 String + * + * @param buffer The buffer to convert in flush mode. The buffer is unchanged + * @return The buffer as a string. + */ + public static String toString(ByteBuffer buffer) { + return toString(buffer, StandardCharsets.ISO_8859_1); + } + + /** + * Convert the buffer to an UTF-8 String + * + * @param buffer The buffer to convert in flush mode. The buffer is unchanged + * @return The buffer as a string. + */ + public static String toUTF8String(ByteBuffer buffer) { + return toString(buffer, StandardCharsets.UTF_8); + } + + /** + * Convert the buffer to an ISO-8859-1 String + * + * @param buffer The buffer to convert in flush mode. The buffer is unchanged + * @param charset The {@link Charset} to use to convert the bytes + * @return The buffer as a string. + */ + public static String toString(ByteBuffer buffer, Charset charset) { + if (buffer == null) + return null; + byte[] array = buffer.hasArray() ? buffer.array() : null; + if (array == null) { + byte[] to = new byte[buffer.remaining()]; + buffer.slice().get(to); + return new String(to, 0, to.length, charset); + } + return new String(array, buffer.arrayOffset() + buffer.position(), buffer.remaining(), charset); + } + + public static String toString(List buffers, Charset charset) { + ByteBuffer buffer = merge(buffers); + if (buffer.hasRemaining()) { + return toString(buffer, charset); + } else { + return ""; + } + } + + /** + * Convert a partial buffer to a String. + * + * @param buffer the buffer to convert + * @param position The position in the buffer to start the string from + * @param length The length of the buffer + * @param charset The {@link Charset} to use to convert the bytes + * @return The buffer as a string. + */ + public static String toString(ByteBuffer buffer, int position, int length, Charset charset) { + if (buffer == null) + return null; + byte[] array = buffer.hasArray() ? buffer.array() : null; + if (array == null) { + ByteBuffer ro = buffer.asReadOnlyBuffer(); + ro.position(position); + ro.limit(position + length); + byte[] to = new byte[length]; + ro.get(to); + return new String(to, 0, to.length, charset); + } + return new String(array, buffer.arrayOffset() + position, length, charset); + } + + /** + * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown + * + * @param buffer A buffer containing an integer in flush mode. The position is not changed. + * @return an int + */ + public static int toInt(ByteBuffer buffer) { + return toInt(buffer, buffer.position(), buffer.remaining()); + } + + /** + * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an + * IllegalArgumentException is thrown + * + * @param buffer A buffer containing an integer in flush mode. The position is not changed. + * @param position the position in the buffer to start reading from + * @param length the length of the buffer to use for conversion + * @return an int of the buffer bytes + */ + public static int toInt(ByteBuffer buffer, int position, int length) { + int val = 0; + boolean started = false; + boolean minus = false; + + int limit = position + length; + + if (length <= 0) + throw new NumberFormatException(toString(buffer, position, length, StandardCharsets.UTF_8)); + + for (int i = position; i < limit; i++) { + byte b = buffer.get(i); + if (b <= SPACE) { + if (started) + break; + } else if (b >= '0' && b <= '9') { + val = val * 10 + (b - '0'); + started = true; + } else if (b == MINUS && !started) { + minus = true; + } else + break; + } + + if (started) + return minus ? (-val) : val; + throw new NumberFormatException(toString(buffer)); + } + + /** + * Convert buffer to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown + * + * @param buffer A buffer containing an integer in flush mode. The position is updated. + * @return an int + */ + public static int takeInt(ByteBuffer buffer) { + int val = 0; + boolean started = false; + boolean minus = false; + int i; + for (i = buffer.position(); i < buffer.limit(); i++) { + byte b = buffer.get(i); + if (b <= SPACE) { + if (started) + break; + } else if (b >= '0' && b <= '9') { + val = val * 10 + (b - '0'); + started = true; + } else if (b == MINUS && !started) { + minus = true; + } else + break; + } + + if (started) { + buffer.position(i); + return minus ? (-val) : val; + } + throw new NumberFormatException(toString(buffer)); + } + + /** + * Convert buffer to an long. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown + * + * @param buffer A buffer containing an integer in flush mode. The position is not changed. + * @return an int + */ + public static long toLong(ByteBuffer buffer) { + long val = 0; + boolean started = false; + boolean minus = false; + + for (int i = buffer.position(); i < buffer.limit(); i++) { + byte b = buffer.get(i); + if (b <= SPACE) { + if (started) + break; + } else if (b >= '0' && b <= '9') { + val = val * 10L + (b - '0'); + started = true; + } else if (b == MINUS && !started) { + minus = true; + } else + break; + } + + if (started) + return minus ? (-val) : val; + throw new NumberFormatException(toString(buffer)); + } + + public static void putHexInt(ByteBuffer buffer, int n) { + if (n < 0) { + buffer.put((byte) '-'); + + if (n == Integer.MIN_VALUE) { + buffer.put((byte) (0x7f & '8')); + buffer.put((byte) (0x7f & '0')); + buffer.put((byte) (0x7f & '0')); + buffer.put((byte) (0x7f & '0')); + buffer.put((byte) (0x7f & '0')); + buffer.put((byte) (0x7f & '0')); + buffer.put((byte) (0x7f & '0')); + buffer.put((byte) (0x7f & '0')); + + return; + } + n = -n; + } + + if (n < 0x10) { + buffer.put(DIGIT[n]); + } else { + boolean started = false; + // This assumes constant time int arithmatic + for (int hexDivisor : hexDivisors) { + if (n < hexDivisor) { + if (started) + buffer.put((byte) '0'); + continue; + } + + started = true; + int d = n / hexDivisor; + buffer.put(DIGIT[d]); + n = n - d * hexDivisor; + } + } + } + + public static void putDecInt(ByteBuffer buffer, int n) { + if (n < 0) { + buffer.put((byte) '-'); + + if (n == Integer.MIN_VALUE) { + buffer.put((byte) '2'); + n = 147483648; + } else + n = -n; + } + + if (n < 10) { + buffer.put(DIGIT[n]); + } else { + boolean started = false; + // This assumes constant time int arithmatic + for (int decDivisor : decDivisors) { + if (n < decDivisor) { + if (started) + buffer.put((byte) '0'); + continue; + } + + started = true; + int d = n / decDivisor; + buffer.put(DIGIT[d]); + n = n - d * decDivisor; + } + } + } + + public static void putDecLong(ByteBuffer buffer, long n) { + if (n < 0) { + buffer.put((byte) '-'); + + if (n == Long.MIN_VALUE) { + buffer.put((byte) '9'); + n = 223372036854775808L; + } else + n = -n; + } + + if (n < 10) { + buffer.put(DIGIT[(int) n]); + } else { + boolean started = false; + // This assumes constant time int arithmatic + for (long aDecDivisorsL : decDivisorsL) { + if (n < aDecDivisorsL) { + if (started) + buffer.put((byte) '0'); + continue; + } + + started = true; + long d = n / aDecDivisorsL; + buffer.put(DIGIT[(int) d]); + n = n - d * aDecDivisorsL; + } + } + } + + public static ByteBuffer toBuffer(int value) { + ByteBuffer buf = ByteBuffer.allocate(32); + putDecInt(buf, value); + return buf; + } + + public static ByteBuffer toBuffer(long value) { + ByteBuffer buf = ByteBuffer.allocate(32); + putDecLong(buf, value); + return buf; + } + + public static ByteBuffer toBuffer(String s) { + return toBuffer(s, StandardCharsets.ISO_8859_1); + } + + public static ByteBuffer toBuffer(String s, Charset charset) { + if (s == null) + return EMPTY_BUFFER; + return toBuffer(s.getBytes(charset)); + } + + /** + * Create a new ByteBuffer using provided byte array. + * + * @param array the byte array to back buffer with. + * @return ByteBuffer with provided byte array, in flush mode + */ + public static ByteBuffer toBuffer(byte[] array) { + if (array == null) + return EMPTY_BUFFER; + return toBuffer(array, 0, array.length); + } + + /** + * Create a new ByteBuffer using the provided byte array. + * + * @param array the byte array to use. + * @param offset the offset within the byte array to use from + * @param length the length in bytes of the array to use + * @return ByteBuffer with provided byte array, in flush mode + */ + public static ByteBuffer toBuffer(byte[] array, int offset, int length) { + if (array == null) + return EMPTY_BUFFER; + return ByteBuffer.wrap(array, offset, length); + } + + public static ByteBuffer toDirectBuffer(String s) { + return toDirectBuffer(s, StandardCharsets.ISO_8859_1); + } + + public static ByteBuffer toDirectBuffer(String s, Charset charset) { + if (s == null) + return EMPTY_BUFFER; + byte[] bytes = s.getBytes(charset); + ByteBuffer buf = ByteBuffer.allocateDirect(bytes.length); + buf.put(bytes); + buf.flip(); + return buf; + } + + public static ByteBuffer toMappedBuffer(File file) throws IOException { + try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ)) { + return channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + } + } + + /** + * @param buffer the buffer to test + * @return {@code false} + * @deprecated don't use - there is no way to reliably tell if a ByteBuffer is mapped. + */ + @Deprecated + public static boolean isMappedBuffer(ByteBuffer buffer) { + return false; + } + + public static String toSummaryString(ByteBuffer buffer) { + if (buffer == null) + return "null"; + StringBuilder buf = new StringBuilder(); + buf.append("[p="); + buf.append(buffer.position()); + buf.append(",l="); + buf.append(buffer.limit()); + buf.append(",c="); + buf.append(buffer.capacity()); + buf.append(",r="); + buf.append(buffer.remaining()); + buf.append("]"); + return buf.toString(); + } + + public static String toDetailString(ByteBuffer[] buffer) { + StringBuilder builder = new StringBuilder(); + builder.append('['); + for (int i = 0; i < buffer.length; i++) { + if (i > 0) builder.append(','); + builder.append(toDetailString(buffer[i])); + } + builder.append(']'); + return builder.toString(); + } + + /** + * Convert Buffer to string ID independent of content + */ + private static void idString(ByteBuffer buffer, StringBuilder out) { + out.append(buffer.getClass().getSimpleName()); + out.append("@"); + if (buffer.hasArray() && buffer.arrayOffset() == 4) { + out.append('T'); + byte[] array = buffer.array(); + TypeUtils.toHex(array[0], out); + TypeUtils.toHex(array[1], out); + TypeUtils.toHex(array[2], out); + TypeUtils.toHex(array[3], out); + } else + out.append(Integer.toHexString(System.identityHashCode(buffer))); + } + + /** + * Convert Buffer to string ID independent of content + * + * @param buffer the buffet to generate a string ID from + * @return A string showing the buffer ID + */ + public static String toIDString(ByteBuffer buffer) { + StringBuilder buf = new StringBuilder(); + idString(buffer, buf); + return buf.toString(); + } + + /** + * Convert Buffer to a detail debug string of pointers and content + * + * @param buffer the buffer to generate a detail string from + * @return A string showing the pointers and content of the buffer + */ + public static String toDetailString(ByteBuffer buffer) { + if (buffer == null) + return "null"; + + StringBuilder buf = new StringBuilder(); + idString(buffer, buf); + buf.append("[p="); + buf.append(buffer.position()); + buf.append(",l="); + buf.append(buffer.limit()); + buf.append(",c="); + buf.append(buffer.capacity()); + buf.append(",r="); + buf.append(buffer.remaining()); + buf.append("]={"); + + appendDebugString(buf, buffer); + + buf.append("}"); + + return buf.toString(); + } + + private static void appendDebugString(StringBuilder buf, ByteBuffer buffer) { + try { + for (int i = 0; i < buffer.position(); i++) { + appendContentChar(buf, buffer.get(i)); + if (i == 16 && buffer.position() > 32) { + buf.append("..."); + i = buffer.position() - 16; + } + } + buf.append("<<<"); + for (int i = buffer.position(); i < buffer.limit(); i++) { + appendContentChar(buf, buffer.get(i)); + if (i == buffer.position() + 16 && buffer.limit() > buffer.position() + 32) { + buf.append("..."); + i = buffer.limit() - 16; + } + } + buf.append(">>>"); + int limit = buffer.limit(); + buffer.limit(buffer.capacity()); + for (int i = limit; i < buffer.capacity(); i++) { + appendContentChar(buf, buffer.get(i)); + if (i == limit + 16 && buffer.capacity() > limit + 32) { + buf.append("..."); + i = buffer.capacity() - 16; + } + } + buffer.limit(limit); + } catch (Throwable x) { + buf.append("!!concurrent mod!!"); + } + } + + private static void appendContentChar(StringBuilder buf, byte b) { + if (b == '\\') + buf.append("\\\\"); + else if ((b >= 0x20) && (b <= 0x7E)) // limit to 7-bit printable US-ASCII character space + buf.append((char) b); + else if (b == '\r') + buf.append("\\r"); + else if (b == '\n') + buf.append("\\n"); + else if (b == '\t') + buf.append("\\t"); + else + buf.append("\\x").append(TypeUtils.toHexString(b)); + } + + /** + * Convert buffer to a Hex Summary String. + * + * @param buffer the buffer to generate a hex byte summary from + * @return A string showing a summary of the content in hex + */ + public static String toHexSummary(ByteBuffer buffer) { + if (buffer == null) + return "null"; + StringBuilder buf = new StringBuilder(); + + buf.append("b[").append(buffer.remaining()).append("]="); + for (int i = buffer.position(); i < buffer.limit(); i++) { + TypeUtils.toHex(buffer.get(i), buf); + if (i == buffer.position() + 24 && buffer.limit() > buffer.position() + 32) { + buf.append("..."); + i = buffer.limit() - 8; + } + } + return buf.toString(); + } + + /** + * Convert buffer to a Hex String. + * + * @param buffer the buffer to generate a hex byte summary from + * @return A hex string + */ + public static String toHexString(ByteBuffer buffer) { + if (buffer == null) + return "null"; + return TypeUtils.toHexString(toArray(buffer)); + } + + public static void putCRLF(ByteBuffer buffer) { + buffer.put((byte) 13); + buffer.put((byte) 10); + } + + public static boolean isPrefix(ByteBuffer prefix, ByteBuffer buffer) { + if (prefix.remaining() > buffer.remaining()) + return false; + int bi = buffer.position(); + for (int i = prefix.position(); i < prefix.limit(); i++) { + if (prefix.get(i) != buffer.get(bi++)) { + return false; + } + } + return true; + } + + public static ByteBuffer ensureCapacity(ByteBuffer buffer, int capacity) { + if (buffer == null) { + return allocate(capacity); + } + if (buffer.capacity() >= capacity) { + return buffer; + } + if (buffer.hasArray()) { + return ByteBuffer.wrap( + Arrays.copyOfRange(buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + capacity), + buffer.position(), buffer.remaining()); + } else { + ByteBuffer newBuffer = allocateDirect(capacity); + append(newBuffer, buffer); + flipToFill(buffer); + return newBuffer; + } + } + + public static ByteBuffer addCapacity(ByteBuffer buffer, int capacity) { + int srcPos = buffer.position(); + int newCapacity = srcPos + capacity; + ByteBuffer newBuffer; + if (buffer.hasArray()) { + newBuffer = ByteBuffer.wrap(Arrays.copyOfRange(buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + newCapacity)); + newBuffer.position(srcPos); + } else { + newBuffer = BufferUtils.allocateDirect(newCapacity); + BufferUtils.flipToFill(newBuffer); + BufferUtils.flipToFlush(buffer, 0); + newBuffer.put(buffer); + } + return newBuffer; + } + + public static ByteBuffer merge(List buffers) { + if (CollectionUtils.isEmpty(buffers)) return EMPTY_BUFFER; + + int size = buffers.stream().collect(Collectors.summingInt(ByteBuffer::remaining)); + if (size == 0) return EMPTY_BUFFER; + + ByteBuffer newBuffer = allocate(size); + int pos = flipToFill(newBuffer); + buffers.forEach(srcBuffer -> put(srcBuffer, newBuffer)); + flipToFlush(newBuffer, pos); + return newBuffer; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/io/ByteArrayOutputStream2.java b/firefly-common/src/main/java/com/fireflysource/common/io/ByteArrayOutputStream2.java new file mode 100644 index 000000000..e80bdf3c9 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/io/ByteArrayOutputStream2.java @@ -0,0 +1,44 @@ +package com.fireflysource.common.io; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; + +/** + * ByteArrayOutputStream with public internals + */ +public class ByteArrayOutputStream2 extends ByteArrayOutputStream { + public ByteArrayOutputStream2() { + super(); + } + + public ByteArrayOutputStream2(int size) { + super(size); + } + + public byte[] getBuf() { + return buf; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public void reset(int minSize) { + reset(); + if (buf.length < minSize) { + buf = new byte[minSize]; + } + } + + public void writeUnchecked(int b) { + buf[count++] = (byte) b; + } + + public String toString(Charset charset) { + return new String(buf, 0, count, charset); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/io/IO.java b/firefly-common/src/main/java/com/fireflysource/common/io/IO.java new file mode 100644 index 000000000..048fbea5d --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/io/IO.java @@ -0,0 +1,425 @@ +package com.fireflysource.common.io; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.GatheringByteChannel; +import java.nio.charset.Charset; + +public class IO { + + public final static String CRLF = "\015\012"; + + public final static byte[] CRLF_BYTES = {(byte) '\015', (byte) '\012'}; + + public static final int bufferSize = 64 * 1024; + private static final NullOS NULL_STREAM = new NullOS(); + private static final ClosedIS CLOSED_STREAM = new ClosedIS(); + private static final NullWrite NULL_WRITER = new NullWrite(); + private static final PrintWriter NULL_PRINT_WRITER = new PrintWriter(NULL_WRITER); + + /** + * Copy Stream in to Stream out until EOF or exception. + * + * @param in the input stream to read from (until EOF) + * @param out the output stream to write to + * @throws IOException if unable to copy streams + */ + public static void copy(InputStream in, OutputStream out) throws IOException { + copy(in, out, -1); + } + + /** + * Copy Reader to Writer out until EOF or exception. + * + * @param in the read to read from (until EOF) + * @param out the writer to write to + * @throws IOException if unable to copy the streams + */ + public static void copy(Reader in, Writer out) throws IOException { + copy(in, out, -1); + } + + /** + * Copy Stream in to Stream for byteCount bytes or until EOF or exception. + * + * @param in the stream to read from + * @param out the stream to write to + * @param byteCount the number of bytes to copy + * @throws IOException if unable to copy the streams + */ + public static void copy(InputStream in, OutputStream out, long byteCount) throws IOException { + byte[] buffer = new byte[bufferSize]; + int len; + + if (byteCount >= 0) { + while (byteCount > 0) { + int max = byteCount < bufferSize ? (int) byteCount : bufferSize; + len = in.read(buffer, 0, max); + + if (len == -1) { + break; + } + byteCount -= len; + out.write(buffer, 0, len); + } + } else { + while (true) { + len = in.read(buffer, 0, bufferSize); + if (len < 0) { + break; + } + out.write(buffer, 0, len); + } + } + } + + /** + * Copy Reader to Writer for byteCount bytes or until EOF or exception. + * + * @param in the Reader to read from + * @param out the Writer to write to + * @param byteCount the number of bytes to copy + * @throws IOException if unable to copy streams + */ + public static void copy(Reader in, Writer out, long byteCount) throws IOException { + char[] buffer = new char[bufferSize]; + int len; + + if (byteCount >= 0) { + while (byteCount > 0) { + if (byteCount < bufferSize) { + len = in.read(buffer, 0, (int) byteCount); + } else { + len = in.read(buffer, 0, bufferSize); + } + if (len == -1) { + break; + } + byteCount -= len; + out.write(buffer, 0, len); + } + } else if (out instanceof PrintWriter) { + PrintWriter pout = (PrintWriter) out; + while (!pout.checkError()) { + len = in.read(buffer, 0, bufferSize); + if (len == -1) + break; + out.write(buffer, 0, len); + } + } else { + while (true) { + len = in.read(buffer, 0, bufferSize); + if (len == -1) { + break; + } + out.write(buffer, 0, len); + } + } + } + + /** + * Copy files or directories + * + * @param from the file to copy + * @param to the destination to copy to + * @throws IOException if unable to copy + */ + public static void copy(File from, File to) throws IOException { + if (from.isDirectory()) { + copyDir(from, to); + } else { + copyFile(from, to); + } + } + + public static void copyDir(File from, File to) throws IOException { + if (to.exists()) { + if (!to.isDirectory()) { + throw new IllegalArgumentException(to.toString()); + } + } else { + boolean success = to.mkdirs(); + if (!success) { + return; + } + } + + File[] files = from.listFiles(); + if (files != null) { + for (File file : files) { + String name = file.getName(); + if (".".equals(name) || "..".equals(name)) { + continue; + } + copy(file, new File(to, name)); + } + } + } + + public static void copyFile(File from, File to) throws IOException { + try (InputStream in = new FileInputStream(from); OutputStream out = new FileOutputStream(to)) { + copy(in, out); + } + } + + /** + * Read input stream to string. + * + * @param in the stream to read from (until EOF) + * @return the String parsed from stream (default Charset) + * @throws IOException if unable to read the stream (or handle the charset) + */ + public static String toString(InputStream in) throws IOException { + return toString(in, (Charset) null); + } + + /** + * Read input stream to string. + * + * @param in the stream to read from (until EOF) + * @param encoding the encoding to use (can be null to use default Charset) + * @return the String parsed from the stream + * @throws IOException if unable to read the stream (or handle the charset) + */ + public static String toString(InputStream in, String encoding) throws IOException { + return toString(in, encoding == null ? null : Charset.forName(encoding)); + } + + /** + * Read input stream to string. + * + * @param in the stream to read from (until EOF) + * @param encoding the Charset to use (can be null to use default Charset) + * @return the String parsed from the stream + * @throws IOException if unable to read the stream (or handle the charset) + */ + public static String toString(InputStream in, Charset encoding) throws IOException { + StringWriter writer = new StringWriter(); + InputStreamReader reader = encoding == null ? new InputStreamReader(in) : new InputStreamReader(in, encoding); + + copy(reader, writer); + return writer.toString(); + } + + /** + * Read input stream to string. + * + * @param in the reader to read from (until EOF) + * @return the String parsed from the reader + * @throws IOException if unable to read the stream (or handle the charset) + */ + public static String toString(Reader in) throws IOException { + StringWriter writer = new StringWriter(); + copy(in, writer); + return writer.toString(); + } + + /** + * Delete File. This delete will recursively delete directories - BE + * CAREFULL + * + * @param file The file (or directory) to be deleted. + * @return true if anything was deleted. (note: this does not mean that all + * content in a directory was deleted) + */ + public static boolean delete(File file) { + if (!file.exists()) { + return false; + } + if (file.isDirectory()) { + File[] files = file.listFiles(); + for (int i = 0; files != null && i < files.length; i++) { + delete(files[i]); + } + } + return file.delete(); + } + + /** + * Closes an arbitrary closable, and logs exceptions at ignore level + * + * @param closeable the closeable to close + */ + public static void close(Closeable closeable) { + try { + if (closeable != null) + closeable.close(); + } catch (IOException ignore) { + } + } + + /** + * closes an input stream, and logs exceptions + * + * @param is the input stream to close + */ + public static void close(InputStream is) { + close((Closeable) is); + } + + /** + * closes an output stream, and logs exceptions + * + * @param os the output stream to close + */ + public static void close(OutputStream os) { + close((Closeable) os); + } + + /** + * closes a reader, and logs exceptions + * + * @param reader the reader to close + */ + public static void close(Reader reader) { + close((Closeable) reader); + } + + /** + * closes a writer, and logs exceptions + * + * @param writer the writer to close + */ + public static void close(Writer writer) { + close((Closeable) writer); + } + + public static byte[] readBytes(InputStream in) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + copy(in, bout); + return bout.toByteArray(); + } + + /** + * A gathering write utility wrapper. + *

+ * This method wraps a gather write with a loop that handles the limitations + * of some operating systems that have a limit on the number of buffers + * written. The method loops on the write until either all the content is + * written or no progress is made. + * + * @param out The GatheringByteChannel to write to + * @param buffers The buffers to write + * @param offset The offset into the buffers array + * @param length The length in buffers to write + * @return The total bytes written + * @throws IOException if unable write to the GatheringByteChannel + */ + public static long write(GatheringByteChannel out, ByteBuffer[] buffers, int offset, int length) + throws IOException { + long total = 0; + write: + while (length > 0) { + // Write as much as we can + long wrote = out.write(buffers, offset, length); + + // If we can't write any more, give up + if (wrote == 0) + break; + + // count the total + total += wrote; + + // Look for unwritten content + for (int i = offset; i < buffers.length; i++) { + if (buffers[i].hasRemaining()) { + // loop with new offset and length; + length = length - (i - offset); + offset = i; + continue write; + } + } + length = 0; + } + + return total; + } + + /** + * @return An outputstream to nowhere + */ + public static OutputStream getNullStream() { + return NULL_STREAM; + } + + /** + * @return An outputstream to nowhere + */ + public static InputStream getClosedStream() { + return CLOSED_STREAM; + } + + /** + * @return An writer to nowhere + */ + public static Writer getNullWriter() { + return NULL_WRITER; + } + + /** + * @return An writer to nowhere + */ + public static PrintWriter getNullPrintWriter() { + return NULL_PRINT_WRITER; + } + + private static class NullOS extends OutputStream { + @Override + public void close() { + } + + @Override + public void flush() { + } + + @Override + public void write(byte[] b) { + } + + @Override + public void write(byte[] b, int i, int l) { + } + + @Override + public void write(int b) { + } + } + + private static class ClosedIS extends InputStream { + @Override + public int read() throws IOException { + return -1; + } + } + + private static class NullWrite extends Writer { + @Override + public void close() { + } + + @Override + public void flush() { + } + + @Override + public void write(char[] b) { + } + + @Override + public void write(char[] b, int o, int l) { + } + + @Override + public void write(int b) { + } + + @Override + public void write(String s) { + } + + @Override + public void write(String s, int o, int l) { + } + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/io/InputChannel.java b/firefly-common/src/main/java/com/fireflysource/common/io/InputChannel.java new file mode 100644 index 000000000..14787c9b1 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/io/InputChannel.java @@ -0,0 +1,30 @@ +package com.fireflysource.common.io; + +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannel; +import java.util.concurrent.CompletableFuture; + +public interface InputChannel extends AsynchronousChannel, AsyncCloseable { + + int END_OF_STREAM_FLAG = -1; + + /** + * Read the content asynchronously. + * + * @param byteBuffer The buffer into which bytes are to be transferred. + * @return The number of bytes read. If return -1, it presents the end of the content. + */ + CompletableFuture read(ByteBuffer byteBuffer); + + /** + * Return the end of stream flag future wrap. + * + * @return The end of stream flag. + */ + default CompletableFuture endStream() { + CompletableFuture future = new CompletableFuture<>(); + future.complete(END_OF_STREAM_FLAG); + return future; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/io/OutputChannel.java b/firefly-common/src/main/java/com/fireflysource/common/io/OutputChannel.java new file mode 100644 index 000000000..703a1cdab --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/io/OutputChannel.java @@ -0,0 +1,17 @@ +package com.fireflysource.common.io; + +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannel; +import java.util.concurrent.CompletableFuture; + +public interface OutputChannel extends AsynchronousChannel, AsyncCloseable { + + /** + * Write the content asynchronously. + * + * @param byteBuffer The buffer into which bytes are to be transferred. + * @return The number of bytes write. If return -1, it presents the end of the content. + */ + CompletableFuture write(ByteBuffer byteBuffer); + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibLoader.java b/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibLoader.java new file mode 100644 index 000000000..4d842c9fd --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibLoader.java @@ -0,0 +1,123 @@ +package com.fireflysource.common.jni; + +import com.fireflysource.common.concurrent.AutoLock; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.HashSet; +import java.util.Set; + +public class JniLibLoader { + + private static final LazyLogger logger = SystemLogger.create(JniLibLoader.class); + private static final Set loadedLibs = new HashSet<>(); + private static final AutoLock lock = new AutoLock(); + + /** + * Load JNI lib by lib name. + * + * @param libName The lib name. + */ + public static void load(String libName) { + String libPath = getLibPath(libName); + loadByLibPath(libPath); + } + + /** + * Load JNI lib by lib file path. + * + * @param libPath The lib file path of the lib file. + */ + public static void loadByLibPath(String libPath) { + lock.lock(() -> { + if (libPath.startsWith("/")) { + throw new IllegalArgumentException("The lib path must be not start with /"); + } + + if (loadedLibs.contains(libPath)) { + logger.info("The lib is loaded. path: {}", libPath); + return; + } + + File file = createLibTempFile(libPath); + copyLibToTempFile(libPath, file); + + logger.info("Start to load lib. path: {}", libPath); + System.load(getLibCanonicalPath(libPath, file)); + loadedLibs.add(libPath); + logger.info("Load lib success. path: {}", libPath); + }); + } + + public static String getLibPath(String libName) { + String osName = System.getProperty("os.name").toLowerCase(); + String libSuffix; + String libDir; + if (osName.contains("mac")) { + libSuffix = ".dylib"; + libDir = "macos"; + } else if (osName.contains("win")) { + libSuffix = ".dll"; + libDir = "windows"; + } else { + libSuffix = ".so"; + libDir = "linux"; + } + return "lib/" + libDir + "/lib" + libName + libSuffix; + } + + public static String getLibFileName(String libPath) { + int pos = libPath.lastIndexOf("/"); + String libFileName; + if (pos >= 0) { + libFileName = libPath.substring(pos + 1); + } else { + libFileName = libPath; + } + return libFileName; + } + + private static String getLibCanonicalPath(String libPath, File file) { + String tempFilePath; + try { + tempFilePath = file.getCanonicalPath(); + } catch (IOException e) { + logger.error("Get lib temp file path exception.", e); + throw new JniLibTempFileException("get lib temp file path exception. path: " + libPath); + } + return tempFilePath; + } + + private static void copyLibToTempFile(String libPath, File file) { + try (InputStream input = JniLibLoader.class.getResourceAsStream("/" + libPath)) { + if (input == null) { + throw new JniLibNotFoundException("The lib not found. path: " + libPath); + } + + logger.info("Copy lib to temp file. lib path: {}, temp file: {}", libPath, file.toPath()); + Files.copy(input, file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + logger.error("Copy lib exception.", e); + throw new JniLibTempFileException("Copy lib exception. lib file path: " + file.toPath()); + } + } + + private static File createLibTempFile(String libPath) { + String libFileName = getLibFileName(libPath); + File file; + try { + file = Files.createTempFile("jni", libFileName).toFile(); + } catch (IOException e) { + logger.error("Create lib temp file exception.", e); + throw new JniLibTempFileException("create lib temp file exception. file name: " + libFileName); + } + file.deleteOnExit(); + return file; + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibNotFoundException.java b/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibNotFoundException.java new file mode 100644 index 000000000..14d0fb342 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibNotFoundException.java @@ -0,0 +1,7 @@ +package com.fireflysource.common.jni; + +public class JniLibNotFoundException extends RuntimeException { + public JniLibNotFoundException(String message) { + super(message); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibTempFileException.java b/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibTempFileException.java new file mode 100644 index 000000000..e8e50c3bc --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/jni/JniLibTempFileException.java @@ -0,0 +1,7 @@ +package com.fireflysource.common.jni; + +public class JniLibTempFileException extends RuntimeException { + public JniLibTempFileException(String message) { + super(message); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/lifecycle/AbstractLifeCycle.java b/firefly-common/src/main/java/com/fireflysource/common/lifecycle/AbstractLifeCycle.java new file mode 100644 index 000000000..f247112c5 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/lifecycle/AbstractLifeCycle.java @@ -0,0 +1,41 @@ +package com.fireflysource.common.lifecycle; + +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class AbstractLifeCycle implements LifeCycle { + + protected AtomicBoolean start = new AtomicBoolean(false); + + @Override + public boolean isStarted() { + return start.get(); + } + + @Override + public boolean isStopped() { + return !start.get(); + } + + @Override + public void start() { + if (start.compareAndSet(false, true)) { + init(); + } + } + + @Override + public void stop() { + if (start.compareAndSet(true, false)) { + try { + destroy(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + abstract protected void init(); + + abstract protected void destroy(); + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/lifecycle/LifeCycle.java b/firefly-common/src/main/java/com/fireflysource/common/lifecycle/LifeCycle.java new file mode 100644 index 000000000..97f1a63d8 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/lifecycle/LifeCycle.java @@ -0,0 +1,11 @@ +package com.fireflysource.common.lifecycle; + +public interface LifeCycle { + void start(); + + void stop(); + + boolean isStarted(); + + boolean isStopped(); +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/lifecycle/ShutdownTasks.java b/firefly-common/src/main/java/com/fireflysource/common/lifecycle/ShutdownTasks.java new file mode 100644 index 000000000..2cd097e02 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/lifecycle/ShutdownTasks.java @@ -0,0 +1,36 @@ +package com.fireflysource.common.lifecycle; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ShutdownTasks { + + private static final Queue tasks = new ConcurrentLinkedQueue<>(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread(ShutdownTasks::stop, "Firefly-Shutdown-Tasks-Thread")); + } + + public static void register(Runnable runnable) { + tasks.add(runnable); + } + + public static boolean remove(Runnable runnable) { + return tasks.remove(runnable); + } + + public static void stop() { + while (true) { + Runnable task = tasks.poll(); + if (task == null) { + break; + } + + try { + task.run(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/math/MathUtils.java b/firefly-common/src/main/java/com/fireflysource/common/math/MathUtils.java new file mode 100644 index 000000000..a37ddb698 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/math/MathUtils.java @@ -0,0 +1,37 @@ +package com.fireflysource.common.math; + +public class MathUtils { + private MathUtils() { + } + + /** + * Returns whether the sum of the arguments overflows an {@code int}. + * + * @param a the first value + * @param b the second value + * @return whether the sum of the arguments overflows an {@code int} + */ + public static boolean sumOverflows(int a, int b) { + try { + Math.addExact(a, b); + return false; + } catch (ArithmeticException x) { + return true; + } + } + + /** + * Returns the sum of its arguments, capping to {@link Long#MAX_VALUE} if they overflow. + * + * @param a the first value + * @param b the second value + * @return the sum of the values, capped to {@link Long#MAX_VALUE} + */ + public static long cappedAdd(long a, long b) { + try { + return Math.addExact(a, b); + } catch (ArithmeticException x) { + return Long.MAX_VALUE; + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/object/Assert.java b/firefly-common/src/main/java/com/fireflysource/common/object/Assert.java new file mode 100644 index 000000000..edd9b3702 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/object/Assert.java @@ -0,0 +1,590 @@ +package com.fireflysource.common.object; + +import com.fireflysource.common.collection.CollectionUtils; +import com.fireflysource.common.string.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Assertion utility class that assists in validating arguments. + * + *

Useful for identifying programmer errors early and clearly at runtime. + * + *

For example, if the contract of a public method states it does not + * allow {@code null} arguments, {@code Assert} can be used to validate that + * contract. Doing this clearly indicates a contract violation when it + * occurs and protects the class's invariants. + * + *

Typically used to validate method arguments rather than configuration + * properties, to check for cases that are usually programmer errors rather + * than configuration errors. In contrast to configuration initialization + * code, there is usually no point in falling back to defaults in such methods. + * + *

This class is similar to JUnit's assertion library. If an argument value is + * deemed invalid, an {@link IllegalArgumentException} is thrown (typically). + * For example: + * + *

+ * Assert.notNull(clazz, "The class must not be null");
+ * Assert.isTrue(i > 0, "The value must be greater than zero");
+ * + *

Mainly for internal use within the framework; consider + * Apache's Commons Lang + * for a more comprehensive suite of {@code String} utilities. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Sam Brannen + * @author Colin Sampaleanu + * @author Rob Harrop + * @since 1.1.2 + */ +public abstract class Assert { + + /** + * Assert a boolean expression, throwing an {@code IllegalStateException} + * if the expression evaluates to {@code false}. + *

Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} + * on an assertion failure. + *

Assert.state(id == null, "The id property must not already be initialized");
+ * + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalStateException if {@code expression} is {@code false} + */ + public static void state(boolean expression, String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalStateException} + * if the expression evaluates to {@code false}. + *

Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} + * on an assertion failure. + *

+     * Assert.state(id == null,
+     *     () -> "ID for " + entity.getName() + " must not already be initialized");
+     * 
+ * + * @param expression a boolean expression + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalStateException if {@code expression} is {@code false} + * @since 5.0 + */ + public static void state(boolean expression, Supplier messageSupplier) { + if (!expression) { + throw new IllegalStateException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} + * if the expression evaluates to {@code false}. + *
Assert.isTrue(i > 0, "The value must be greater than zero");
+ * + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if {@code expression} is {@code false} + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} + * if the expression evaluates to {@code false}. + *
+     * Assert.isTrue(i > 0, () -> "The value '" + i + "' must be greater than zero");
+     * 
+ * + * @param expression a boolean expression + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if {@code expression} is {@code false} + * @since 5.0 + */ + public static void isTrue(boolean expression, Supplier messageSupplier) { + if (!expression) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an object is {@code null}. + *
Assert.isNull(value, "The value must be null");
+ * + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is not {@code null} + */ + public static void isNull(Object object, String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is {@code null}. + *
+     * Assert.isNull(value, () -> "The value '" + value + "' must be null");
+     * 
+ * + * @param object the object to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object is not {@code null} + * @since 5.0 + */ + public static void isNull(Object object, Supplier messageSupplier) { + if (object != null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an object is not {@code null}. + *
Assert.notNull(clazz, "The class must not be null");
+ * + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is {@code null} + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not {@code null}. + *
+     * Assert.notNull(clazz, () -> "The class '" + clazz.getName() + "' must not be null");
+     * 
+ * + * @param object the object to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object is {@code null} + * @since 5.0 + */ + public static void notNull(Object object, Supplier messageSupplier) { + if (object == null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + *
Assert.hasLength(name, "Name must not be empty");
+ * + * @param text the String to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the text is empty + * @see StringUtils#hasLength + */ + public static void hasLength(String text, String message) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + *
+     * Assert.hasLength(name, () -> "Name for account '" + account.getId() + "' must not be empty");
+     * 
+ * + * @param text the String to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the text is empty + * @see StringUtils#hasLength + * @since 5.0 + */ + public static void hasLength(String text, Supplier messageSupplier) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the given String contains valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * + * @param text the String to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the text does not contain valid text content + * @see StringUtils#hasText + */ + public static void hasText(String text, String message) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String contains valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + *
+     * Assert.hasText(name, () -> "Name for account '" + account.getId() + "' must not be empty");
+     * 
+ * + * @param text the String to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the text does not contain valid text content + * @see StringUtils#hasText + * @since 5.0 + */ + public static void hasText(String text, Supplier messageSupplier) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * + * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the text contains the substring + */ + public static void doesNotContain(String textToSearch, String substring, String message) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.contains(substring)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + *
+     * Assert.doesNotContain(name, forbidden, () -> "Name must not contain '" + forbidden + "'");
+     * 
+ * + * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the text contains the substring + * @since 5.0 + */ + public static void doesNotContain(String textToSearch, String substring, Supplier messageSupplier) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.contains(substring)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an array contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
Assert.notEmpty(array, "The array must contain elements");
+ * + * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array is {@code null} or contains no elements + */ + public static void notEmpty(Object[] array, String message) { + if (CollectionUtils.isEmpty(array)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
+     * Assert.notEmpty(array, () -> "The " + arrayType + " array must contain elements");
+     * 
+ * + * @param array the array to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object array is {@code null} or contains no elements + * @since 5.0 + */ + public static void notEmpty(Object[] array, Supplier messageSupplier) { + if (CollectionUtils.isEmpty(array)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an array contains no {@code null} elements. + *

Note: Does not complain if the array is empty! + *

Assert.noNullElements(array, "The array must contain non-null elements");
+ * + * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array contains a {@code null} element + */ + public static void noNullElements(Object[] array, String message) { + if (array != null) { + for (Object element : array) { + if (element == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that an array contains no {@code null} elements. + *

Note: Does not complain if the array is empty! + *

+     * Assert.noNullElements(array, () -> "The " + arrayType + " array must contain non-null elements");
+     * 
+ * + * @param array the array to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object array contains a {@code null} element + * @since 5.0 + */ + public static void noNullElements(Object[] array, Supplier messageSupplier) { + if (array != null) { + for (Object element : array) { + if (element == null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + } + } + + /** + * Assert that a collection contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
Assert.notEmpty(collection, "Collection must contain elements");
+ * + * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection is {@code null} or + * contains no elements + */ + public static void notEmpty(Collection collection, String message) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a collection contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
+     * Assert.notEmpty(collection, () -> "The " + collectionType + " collection must contain elements");
+     * 
+ * + * @param collection the collection to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the collection is {@code null} or + * contains no elements + * @since 5.0 + */ + public static void notEmpty(Collection collection, Supplier messageSupplier) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that a Map contains entries; that is, it must not be {@code null} + * and must contain at least one entry. + *
Assert.notEmpty(map, "Map must contain entries");
+ * + * @param map the map to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the map is {@code null} or contains no entries + */ + public static void notEmpty(Map map, String message) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map contains entries; that is, it must not be {@code null} + * and must contain at least one entry. + *
+     * Assert.notEmpty(map, () -> "The " + mapType + " map must contain entries");
+     * 
+ * + * @param map the map to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the map is {@code null} or contains no entries + * @since 5.0 + */ + public static void notEmpty(Map map, Supplier messageSupplier) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo, "Foo expected");
+ * + * @param type the type to check against + * @param obj the object to check + * @param message a message which will be prepended to provide further context. + * If it is empty or ends in ":" or ";" or "," or ".", a full exception message + * will be appended. If it ends in a space, the name of the offending object's + * type will be appended. In any other case, a ":" with a space and the name + * of the offending object's type will be appended. + * @throws IllegalArgumentException if the object is not an instance of type + */ + public static void isInstanceOf(Class type, Object obj, String message) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + instanceCheckFailed(type, obj, message); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + *
+     * Assert.instanceOf(Foo.class, foo, () -> "Processing " + Foo.class.getSimpleName() + ":");
+     * 
+ * + * @param type the type to check against + * @param obj the object to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails. See {@link #isInstanceOf(Class, Object, String)} for details. + * @throws IllegalArgumentException if the object is not an instance of type + * @since 5.0 + */ + public static void isInstanceOf(Class type, Object obj, Supplier messageSupplier) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + instanceCheckFailed(type, obj, nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * + * @param type the type to check against + * @param obj the object to check + * @throws IllegalArgumentException if the object is not an instance of type + */ + public static void isInstanceOf(Class type, Object obj) { + isInstanceOf(type, obj, ""); + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + *
Assert.isAssignable(Number.class, myClass, "Number expected");
+ * + * @param superType the super type to check against + * @param subType the sub type to check + * @param message a message which will be prepended to provide further context. + * If it is empty or ends in ":" or ";" or "," or ".", a full exception message + * will be appended. If it ends in a space, the name of the offending sub type + * will be appended. In any other case, a ":" with a space and the name of the + * offending sub type will be appended. + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType, String message) { + notNull(superType, "Super type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + assignableCheckFailed(superType, subType, message); + } + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + *
+     * Assert.isAssignable(Number.class, myClass, () -> "Processing " + myAttributeName + ":");
+     * 
+ * + * @param superType the super type to check against + * @param subType the sub type to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails. See {@link #isAssignable(Class, Class, String)} for details. + * @throws IllegalArgumentException if the classes are not assignable + * @since 5.0 + */ + public static void isAssignable(Class superType, Class subType, Supplier messageSupplier) { + notNull(superType, "Super type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + assignableCheckFailed(superType, subType, nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + *
Assert.isAssignable(Number.class, myClass);
+ * + * @param superType the super type to check + * @param subType the sub type to check + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType) { + isAssignable(superType, subType, ""); + } + + + private static void instanceCheckFailed(Class type, Object obj, String msg) { + String className = (obj != null ? obj.getClass().getName() : "null"); + String result = ""; + boolean defaultMessage = true; + if (StringUtils.hasLength(msg)) { + if (endsWithSeparator(msg)) { + result = msg + " "; + } else { + result = messageWithTypeName(msg, className); + defaultMessage = false; + } + } + if (defaultMessage) { + result = result + ("Object of class [" + className + "] must be an instance of " + type); + } + throw new IllegalArgumentException(result); + } + + private static void assignableCheckFailed(Class superType, Class subType, String msg) { + String result = ""; + boolean defaultMessage = true; + if (StringUtils.hasLength(msg)) { + if (endsWithSeparator(msg)) { + result = msg + " "; + } else { + result = messageWithTypeName(msg, subType); + defaultMessage = false; + } + } + if (defaultMessage) { + result = result + (subType + " is not assignable to " + superType); + } + throw new IllegalArgumentException(result); + } + + private static boolean endsWithSeparator(String msg) { + return (msg.endsWith(":") || msg.endsWith(";") || msg.endsWith(",") || msg.endsWith(".")); + } + + private static String messageWithTypeName(String msg, Object typeName) { + return msg + (msg.endsWith(" ") ? "" : ": ") + typeName; + } + + + private static String nullSafeGet(Supplier messageSupplier) { + return (messageSupplier != null ? messageSupplier.get() : null); + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/object/TypeUtils.java b/firefly-common/src/main/java/com/fireflysource/common/object/TypeUtils.java new file mode 100644 index 000000000..5ac058bc4 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/object/TypeUtils.java @@ -0,0 +1,441 @@ +package com.fireflysource.common.object; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * TYPE Utilities. Provides various static utility methods for manipulating + * types and their string representations. + */ +public class TypeUtils { + + public static final Class[] NO_ARGS = new Class[]{}; + public static final int CR = '\015'; + public static final int LF = '\012'; + + private static final HashMap> name2Class = new HashMap<>(); + /* ------------------------------------------------------------ */ + private static final HashMap, String> class2Name = new HashMap<>(); + private static final HashMap, Method> class2Value = new HashMap<>(); + + static { + name2Class.put("boolean", Boolean.TYPE); + name2Class.put("byte", Byte.TYPE); + name2Class.put("char", Character.TYPE); + name2Class.put("double", Double.TYPE); + name2Class.put("float", Float.TYPE); + name2Class.put("int", Integer.TYPE); + name2Class.put("long", Long.TYPE); + name2Class.put("short", Short.TYPE); + name2Class.put("void", Void.TYPE); + + name2Class.put("java.lang.Boolean.TYPE", Boolean.TYPE); + name2Class.put("java.lang.Byte.TYPE", Byte.TYPE); + name2Class.put("java.lang.Character.TYPE", Character.TYPE); + name2Class.put("java.lang.Double.TYPE", Double.TYPE); + name2Class.put("java.lang.Float.TYPE", Float.TYPE); + name2Class.put("java.lang.Integer.TYPE", Integer.TYPE); + name2Class.put("java.lang.Long.TYPE", Long.TYPE); + name2Class.put("java.lang.Short.TYPE", Short.TYPE); + name2Class.put("java.lang.Void.TYPE", Void.TYPE); + + name2Class.put("java.lang.Boolean", Boolean.class); + name2Class.put("java.lang.Byte", Byte.class); + name2Class.put("java.lang.Character", Character.class); + name2Class.put("java.lang.Double", Double.class); + name2Class.put("java.lang.Float", Float.class); + name2Class.put("java.lang.Integer", Integer.class); + name2Class.put("java.lang.Long", Long.class); + name2Class.put("java.lang.Short", Short.class); + + name2Class.put("Boolean", Boolean.class); + name2Class.put("Byte", Byte.class); + name2Class.put("Character", Character.class); + name2Class.put("Double", Double.class); + name2Class.put("Float", Float.class); + name2Class.put("Integer", Integer.class); + name2Class.put("Long", Long.class); + name2Class.put("Short", Short.class); + + name2Class.put(null, Void.TYPE); + name2Class.put("string", String.class); + name2Class.put("String", String.class); + name2Class.put("java.lang.String", String.class); + } + + static { + class2Name.put(Boolean.TYPE, "boolean"); + class2Name.put(Byte.TYPE, "byte"); + class2Name.put(Character.TYPE, "char"); + class2Name.put(Double.TYPE, "double"); + class2Name.put(Float.TYPE, "float"); + class2Name.put(Integer.TYPE, "int"); + class2Name.put(Long.TYPE, "long"); + class2Name.put(Short.TYPE, "short"); + class2Name.put(Void.TYPE, "void"); + + class2Name.put(Boolean.class, "java.lang.Boolean"); + class2Name.put(Byte.class, "java.lang.Byte"); + class2Name.put(Character.class, "java.lang.Character"); + class2Name.put(Double.class, "java.lang.Double"); + class2Name.put(Float.class, "java.lang.Float"); + class2Name.put(Integer.class, "java.lang.Integer"); + class2Name.put(Long.class, "java.lang.Long"); + class2Name.put(Short.class, "java.lang.Short"); + + class2Name.put(null, "void"); + class2Name.put(String.class, "java.lang.String"); + } + + static { + try { + Class[] s = {String.class}; + + class2Value.put(Boolean.TYPE, + Boolean.class.getMethod("valueOf", s)); + class2Value.put(Byte.TYPE, + Byte.class.getMethod("valueOf", s)); + class2Value.put(Double.TYPE, + Double.class.getMethod("valueOf", s)); + class2Value.put(Float.TYPE, + Float.class.getMethod("valueOf", s)); + class2Value.put(Integer.TYPE, + Integer.class.getMethod("valueOf", s)); + class2Value.put(Long.TYPE, + Long.class.getMethod("valueOf", s)); + class2Value.put(Short.TYPE, + Short.class.getMethod("valueOf", s)); + + class2Value.put(Boolean.class, + Boolean.class.getMethod("valueOf", s)); + class2Value.put(Byte.class, + Byte.class.getMethod("valueOf", s)); + class2Value.put(Double.class, + Double.class.getMethod("valueOf", s)); + class2Value.put(Float.class, + Float.class.getMethod("valueOf", s)); + class2Value.put(Integer.class, + Integer.class.getMethod("valueOf", s)); + class2Value.put(Long.class, + Long.class.getMethod("valueOf", s)); + class2Value.put(Short.class, + Short.class.getMethod("valueOf", s)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Array to List. + *

+ * Works like {@link Arrays#asList(Object...)}, but handles null arrays. + * + * @param a the array to convert to a list + * @param the array and list entry type + * @return a list backed by the array. + */ + public static List asList(T[] a) { + if (a == null) + return Collections.emptyList(); + return Arrays.asList(a); + } + + + /** + * Class from a canonical name for a type. + * + * @param name A class or type name. + * @return A class , which may be a primitive TYPE field.. + */ + public static Class fromName(String name) { + return name2Class.get(name); + } + + /** + * The canonical name for a type. + * + * @param type A class , which may be a primitive TYPE field. + * @return Canonical name. + */ + public static String toName(Class type) { + return class2Name.get(type); + } + + /** + * Convert String value to instance. + * + * @param type The class of the instance, which may be a primitive TYPE field. + * @param value The value as string. + * @return The value as an Object. + */ + public static Object valueOf(Class type, String value) { + try { + if (type.equals(String.class)) + return value; + + Method m = class2Value.get(type); + if (m != null) + return m.invoke(null, value); + + if (type.equals(Character.TYPE) || + type.equals(Character.class)) + return value.charAt(0); + + Constructor c = type.getConstructor(String.class); + return c.newInstance(value); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException x) { + throw new IllegalArgumentException(x); + } catch (InvocationTargetException x) { + if (x.getTargetException() instanceof Error) + throw (Error) x.getTargetException(); + + } + return null; + } + + /** + * Convert String value to instance. + * + * @param type classname or type (eg int) + * @param value The value as a string. + * @return The value as an Object. + */ + public static Object valueOf(String type, String value) { + return valueOf(fromName(type), value); + } + + /** + * Parse an int from a substring. + * Negative numbers are not handled. + * + * @param s String + * @param offset Offset within string + * @param length Length of integer or -1 for remainder of string + * @param base base of the integer + * @return the parsed integer + * @throws NumberFormatException if the string cannot be parsed + */ + public static int parseInt(String s, int offset, int length, int base) + throws NumberFormatException { + int value = 0; + + if (length < 0) + length = s.length() - offset; + + for (int i = 0; i < length; i++) { + char c = s.charAt(offset + i); + + int digit = convertHexDigit((int) c); + if (digit < 0 || digit >= base) + throw new NumberFormatException(s.substring(offset, offset + length)); + value = value * base + digit; + } + return value; + } + + /** + * Parse an int from a byte array of ascii characters. + * Negative numbers are not handled. + * + * @param b byte array + * @param offset Offset within string + * @param length Length of integer or -1 for remainder of string + * @param base base of the integer + * @return the parsed integer + * @throws NumberFormatException if the array cannot be parsed into an integer + */ + public static int parseInt(byte[] b, int offset, int length, int base) + throws NumberFormatException { + int value = 0; + + if (length < 0) + length = b.length - offset; + + for (int i = 0; i < length; i++) { + char c = (char) (0xff & b[offset + i]); + + int digit = c - '0'; + if (digit < 0 || digit >= base || digit >= 10) { + digit = 10 + c - 'A'; + if (digit < 10 || digit >= base) + digit = 10 + c - 'a'; + } + if (digit < 0 || digit >= base) + throw new NumberFormatException(new String(b, offset, length)); + value = value * base + digit; + } + return value; + } + + public static byte[] parseBytes(String s, int base) { + byte[] bytes = new byte[s.length() / 2]; + for (int i = 0; i < s.length(); i += 2) + bytes[i / 2] = (byte) TypeUtils.parseInt(s, i, 2, base); + return bytes; + } + + public static String toString(byte[] bytes, int base) { + StringBuilder buf = new StringBuilder(); + for (byte b : bytes) { + int bi = 0xff & b; + int c = '0' + (bi / base) % base; + if (c > '9') + c = 'a' + (c - '0' - 10); + buf.append((char) c); + c = '0' + bi % base; + if (c > '9') + c = 'a' + (c - '0' - 10); + buf.append((char) c); + } + return buf.toString(); + } + + /** + * @param c An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + public static byte convertHexDigit(byte c) { + byte b = (byte) ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (b < 0 || b > 15) + throw new NumberFormatException("!hex " + c); + return b; + } + + /** + * @param c An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + public static int convertHexDigit(char c) { + int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (d < 0 || d > 15) + throw new NumberFormatException("!hex " + c); + return d; + } + + /** + * @param c An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + public static int convertHexDigit(int c) { + int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (d < 0 || d > 15) + throw new NumberFormatException("!hex " + c); + return d; + } + + public static void toHex(byte b, Appendable buf) { + try { + int d = 0xf & ((0xF0 & b) >> 4); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & b; + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void toHex(int value, Appendable buf) throws IOException { + int d = 0xf & ((0xF0000000 & value) >> 28); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & ((0x0F000000 & value) >> 24); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & ((0x00F00000 & value) >> 20); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & ((0x000F0000 & value) >> 16); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & ((0x0000F000 & value) >> 12); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & ((0x00000F00 & value) >> 8); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & ((0x000000F0 & value) >> 4); + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & value; + buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + + Integer.toString(0, 36); + } + + public static void toHex(long value, Appendable buf) throws IOException { + toHex((int) (value >> 32), buf); + toHex((int) value, buf); + } + + public static String toHexString(byte b) { + return toHexString(new byte[]{b}, 0, 1); + } + + public static String toHexString(byte[] b) { + return toHexString(b, 0, b.length); + } + + public static String toHexString(byte[] b, int offset, int length) { + StringBuilder buf = new StringBuilder(); + for (int i = offset; i < offset + length; i++) { + int bi = 0xff & b[i]; + int c = '0' + (bi / 16) % 16; + if (c > '9') + c = 'A' + (c - '0' - 10); + buf.append((char) c); + c = '0' + bi % 16; + if (c > '9') + c = 'a' + (c - '0' - 10); + buf.append((char) c); + } + return buf.toString(); + } + + public static byte[] fromHexString(String s) { + if (s.length() % 2 != 0) + throw new IllegalArgumentException(s); + byte[] array = new byte[s.length() / 2]; + for (int i = 0; i < array.length; i++) { + int b = Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16); + array[i] = (byte) (0xff & b); + } + return array; + } + + public static void dump(Class c) { + System.err.println("Dump: " + c); + dump(c.getClassLoader()); + } + + public static void dump(ClassLoader cl) { + System.err.println("Dump Loaders:"); + while (cl != null) { + System.err.println(" loader " + cl); + cl = cl.getParent(); + } + } + + /** + * @param o Object to test for true + * @return True if passed object is not null and is either a Boolean with value true or evaluates to a string that evaluates to true. + */ + public static boolean isTrue(Object o) { + if (o == null) + return false; + if (o instanceof Boolean) + return (Boolean) o; + return Boolean.parseBoolean(o.toString()); + } + + /** + * @param o Object to test for false + * @return True if passed object is not null and is either a Boolean with value false or evaluates to a string that evaluates to false. + */ + public static boolean isFalse(Object o) { + if (o == null) + return false; + if (o instanceof Boolean) + return !(Boolean) o; + return "false".equalsIgnoreCase(o.toString()); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/ref/Cleaner.java b/firefly-common/src/main/java/com/fireflysource/common/ref/Cleaner.java new file mode 100644 index 000000000..8dcb546c3 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/ref/Cleaner.java @@ -0,0 +1,204 @@ +package com.fireflysource.common.ref; + +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.Objects; +import java.util.concurrent.ThreadFactory; + + +/** + * {@code Cleaner} manages a set of object references and corresponding cleaning actions. + *

+ * Cleaning actions are {@link #register(Object object, Runnable action) registered} + * to run after the cleaner is notified that the object has become + * phantom reachable. + * The cleaner uses {@link PhantomReference} and {@link ReferenceQueue} to be + * notified when the reachability + * changes. + *

+ * Each cleaner operates independently, managing the pending cleaning actions + * and handling threading and termination when the cleaner is no longer in use. + * Registering an object reference and corresponding cleaning action returns + * a {@link Cleanable Cleanable}. The most efficient use is to explicitly invoke + * the {@link Cleanable#clean clean} method when the object is closed or + * no longer needed. + * The cleaning action is a {@link Runnable} to be invoked at most once when + * the object has become phantom reachable unless it has already been explicitly cleaned. + * Note that the cleaning action must not refer to the object being registered. + * If so, the object will not become phantom reachable and the cleaning action + * will not be invoked automatically. + *

+ * The execution of the cleaning action is performed + * by a thread associated with the cleaner. + * All exceptions thrown by the cleaning action are ignored. + * The cleaner and other cleaning actions are not affected by + * exceptions in a cleaning action. + * The thread runs until all registered cleaning actions have + * completed and the cleaner itself is reclaimed by the garbage collector. + *

+ * The behavior of cleaners during {@link System#exit(int) System.exit} + * is implementation specific. No guarantees are made relating + * to whether cleaning actions are invoked or not. + *

+ * Unless otherwise noted, passing a {@code null} argument to a constructor or + * method in this class will cause a + * {@link java.lang.NullPointerException NullPointerException} to be thrown. + * + * @apiNote The cleaning action is invoked only after the associated object becomes + * phantom reachable, so it is important that the object implementing the + * cleaning action does not hold references to the object. + * In this example, a static class encapsulates the cleaning state and action. + * An "inner" class, anonymous or not, must not be used because it implicitly + * contains a reference to the outer instance, preventing it from becoming + * phantom reachable. + * The choice of a new cleaner or sharing an existing cleaner is determined + * by the use case. + *

+ * If the CleaningExample is used in a try-finally block then the + * {@code close} method calls the cleaning action. + * If the {@code close} method is not called, the cleaning action is called + * by the Cleaner when the CleaningExample instance has become phantom reachable. + *

{@code
+ * public class CleaningExample implements AutoCloseable {
+ *        // A cleaner, preferably one shared within a library
+ *        private static final Cleaner cleaner = ;
+ *
+ *        static class State implements Runnable {
+ *
+ *            State(...) {
+ *                // initialize State needed for cleaning action
+ *            }
+ *
+ *            public void run() {
+ *                // cleanup action accessing State, executed at most once
+ *            }
+ *        }
+ *
+ *        private final State state;
+ *        private final Cleaner.Cleanable cleanable;
+ *
+ *        public CleaningExample() {
+ *            this.state = new State(...);
+ *            this.cleanable = cleaner.register(this, state);
+ *        }
+ *
+ *        public void close() {
+ *            cleanable.clean();
+ *        }
+ *    }
+ * }
+ * The cleaning action could be a lambda but all too easily will capture + * the object reference, by referring to fields of the object being cleaned, + * preventing the object from becoming phantom reachable. + * Using a static nested class, as above, will avoid accidentally retaining the + * object reference. + *

+ * + * Cleaning actions should be prepared to be invoked concurrently with + * other cleaning actions. + * Typically the cleaning actions should be very quick to execute + * and not block. If the cleaning action blocks, it may delay processing + * other cleaning actions registered to the same cleaner. + * All cleaning actions registered to a cleaner should be mutually compatible. + * @since 9 + */ +public class Cleaner { + + /** + * The Cleaner implementation. + */ + final CleanerImpl impl; + + static { + CleanerImpl.setCleanerImplAccess(cleaner -> cleaner.impl); + } + + /** + * Construct a Cleaner implementation and start it. + */ + private Cleaner() { + impl = new CleanerImpl(); + } + + /** + * Returns a new {@code Cleaner}. + *

+ * The cleaner creates a {@link Thread#setDaemon(boolean) daemon thread} + * to process the phantom reachable objects and to invoke cleaning actions. + * The {@linkplain java.lang.Thread#getContextClassLoader context class loader} + * of the thread is set to the + * {@link ClassLoader#getSystemClassLoader() system class loader}. + * The thread has no permissions, enforced only if a + * {@link java.lang.System#setSecurityManager(SecurityManager) SecurityManager is set}. + *

+ * The cleaner terminates when it is phantom reachable and all of the + * registered cleaning actions are complete. + * + * @return a new {@code Cleaner} + * @throws SecurityException if the current thread is not allowed to + * create or start the thread. + */ + public static Cleaner create() { + Cleaner cleaner = new Cleaner(); + cleaner.impl.start(cleaner, r -> new Thread(r, "Firefly-Cleaner-Thread")); + return cleaner; + } + + /** + * Returns a new {@code Cleaner} using a {@code Thread} from the {@code ThreadFactory}. + *

+ * A thread from the thread factory's {@link ThreadFactory#newThread(Runnable) newThread} + * method is set to be a {@link Thread#setDaemon(boolean) daemon thread} + * and started to process phantom reachable objects and invoke cleaning actions. + * On each call the {@link ThreadFactory#newThread(Runnable) thread factory} + * must provide a Thread that is suitable for performing the cleaning actions. + *

+ * The cleaner terminates when it is phantom reachable and all of the + * registered cleaning actions are complete. + * + * @param threadFactory a {@code ThreadFactory} to return a new {@code Thread} + * to process cleaning actions + * @return a new {@code Cleaner} + * @throws IllegalThreadStateException if the thread from the thread + * factory was {@link Thread.State#NEW not a new thread}. + * @throws SecurityException if the current thread is not allowed to + * create or start the thread. + */ + public static Cleaner create(ThreadFactory threadFactory) { + Objects.requireNonNull(threadFactory, "threadFactory"); + Cleaner cleaner = new Cleaner(); + cleaner.impl.start(cleaner, threadFactory); + return cleaner; + } + + /** + * Registers an object and a cleaning action to run when the object + * becomes phantom reachable. + * Refer to the API Note above for + * cautions about the behavior of cleaning actions. + * + * @param obj the object to monitor + * @param action a {@code Runnable} to invoke when the object becomes phantom reachable + * @return a {@code Cleanable} instance + */ + public Cleanable register(Object obj, Runnable action) { + Objects.requireNonNull(obj, "obj"); + Objects.requireNonNull(action, "action"); + return new CleanerImpl.PhantomCleanableRef(obj, this, action); + } + + /** + * {@code Cleanable} represents an object and a + * cleaning action registered in a {@code Cleaner}. + * + * @since 9 + */ + public interface Cleanable { + /** + * Unregisters the cleanable and invokes the cleaning action. + * The cleanable cleaning action is invoked at most once + * regardless of the number of calls to {@code clean}. + */ + void clean(); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/ref/CleanerImpl.java b/firefly-common/src/main/java/com/fireflysource/common/ref/CleanerImpl.java new file mode 100644 index 000000000..a7d4a83f2 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/ref/CleanerImpl.java @@ -0,0 +1,177 @@ +package com.fireflysource.common.ref; + +import com.fireflysource.common.ref.Cleaner.Cleanable; + +import java.lang.ref.ReferenceQueue; +import java.util.concurrent.ThreadFactory; +import java.util.function.Function; + +/** + * CleanerImpl manages a set of object references and corresponding cleaning actions. + * CleanerImpl provides the functionality of {@link com.fireflysource.common.ref.Cleaner}. + */ +public final class CleanerImpl implements Runnable { + + /** + * An object to access the CleanerImpl from a Cleaner; set by Cleaner init. + */ + private static Function cleanerImplAccess = null; + + /** + * Heads of a CleanableList for each reference type. + */ + final PhantomCleanable phantomCleanableList; + + // The ReferenceQueue of pending cleaning actions + final ReferenceQueue queue; + + /** + * Called by Cleaner static initialization to provide the function + * to map from Cleaner to CleanerImpl. + * + * @param access a function to map from Cleaner to CleanerImpl + */ + public static void setCleanerImplAccess(Function access) { + if (cleanerImplAccess == null) { + cleanerImplAccess = access; + } else { + throw new InternalError("cleanerImplAccess"); + } + } + + /** + * Called to get the CleanerImpl for a Cleaner. + * + * @param cleaner the cleaner + * @return the corresponding CleanerImpl + */ + static CleanerImpl getCleanerImpl(Cleaner cleaner) { + return cleanerImplAccess.apply(cleaner); + } + + /** + * Constructor for CleanerImpl. + */ + public CleanerImpl() { + queue = new ReferenceQueue<>(); + phantomCleanableList = new PhantomCleanableRef(); + } + + /** + * Starts the Cleaner implementation. + * Ensure this is the CleanerImpl for the Cleaner. + * When started waits for Cleanables to be queued. + * + * @param cleaner the cleaner + * @param threadFactory the thread factory + */ + public void start(Cleaner cleaner, ThreadFactory threadFactory) { + if (getCleanerImpl(cleaner) != this) { + throw new AssertionError("wrong cleaner"); + } + // schedule a nop cleaning action for the cleaner, so the associated thread + // will continue to run at least until the cleaner is reclaimable. + new CleanerCleanable(cleaner); + + // now that there's at least one cleaning action, for the cleaner, + // we can start the associated thread, which runs until + // all cleaning actions have been run. + Thread thread = threadFactory.newThread(this); + thread.setDaemon(true); + thread.start(); + } + + /** + * Process queued Cleanables as long as the cleanable lists are not empty. + * A Cleanable is in one of the lists for each Object and for the Cleaner + * itself. + * Terminates when the Cleaner is no longer reachable and + * has been cleaned and there are no more Cleanable instances + * for which the object is reachable. + *

+ * If the thread is a ManagedLocalsThread, the threadlocals + * are erased before each cleanup + */ + @Override + public void run() { + while (!phantomCleanableList.isListEmpty()) { + try { + // Wait for a Ref, with a timeout to avoid getting hung + // due to a race with clear/clean + Cleanable ref = (Cleanable) queue.remove(60 * 1000L); + if (ref != null) { + ref.clean(); + } + } catch (Throwable e) { + // ignore exceptions from the cleanup action + // (including interruption of cleanup thread) + } + } + } + + /** + * Perform cleaning on an unreachable PhantomReference. + */ + public static final class PhantomCleanableRef extends PhantomCleanable { + private final Runnable action; + + /** + * Constructor for a phantom cleanable reference. + * + * @param obj the object to monitor + * @param cleaner the cleaner + * @param action the action Runnable + */ + public PhantomCleanableRef(Object obj, Cleaner cleaner, Runnable action) { + super(obj, cleaner); + this.action = action; + } + + /** + * Constructor used only for root of phantom cleanable list. + */ + PhantomCleanableRef() { + super(); + this.action = null; + } + + @Override + protected void performCleanup() { + action.run(); + } + + /** + * Prevent access to referent even when it is still alive. + * + * @throws UnsupportedOperationException always + */ + @Override + public Object get() { + throw new UnsupportedOperationException("get"); + } + + /** + * Direct clearing of the referent is not supported. + * + * @throws UnsupportedOperationException always + */ + @Override + public void clear() { + throw new UnsupportedOperationException("clear"); + } + } + + /** + * A PhantomCleanable implementation for tracking the Cleaner itself. + */ + static final class CleanerCleanable extends PhantomCleanable { + CleanerCleanable(Cleaner cleaner) { + super(cleaner, cleaner); + } + + @Override + protected void performCleanup() { + // no action + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/ref/PhantomCleanable.java b/firefly-common/src/main/java/com/fireflysource/common/ref/PhantomCleanable.java new file mode 100644 index 000000000..8e6a7bf82 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/ref/PhantomCleanable.java @@ -0,0 +1,150 @@ +package com.fireflysource.common.ref; + +import com.fireflysource.common.concurrent.AutoLock; + +import java.lang.ref.PhantomReference; +import java.util.Objects; + +/** + * PhantomCleanable subclasses efficiently encapsulate cleanup state and + * the cleaning action. + * Subclasses implement the abstract {@link #performCleanup()} method + * to provide the cleaning action. + * When constructed, the object reference and the {@link Cleaner.Cleanable Cleanable} + * are registered with the {@link Cleaner}. + * The Cleaner invokes {@link Cleaner.Cleanable#clean() clean} after the + * referent becomes phantom reachable. + */ +public abstract class PhantomCleanable extends PhantomReference + implements Cleaner.Cleanable { + + /** + * Links to previous and next in a doubly-linked list. + */ + PhantomCleanable prev = this, next = this; + + /** + * The list of PhantomCleanable; synchronizes insert and remove. + */ + private final PhantomCleanable list; + private final AutoLock lock = new AutoLock(); + + /** + * Constructs new {@code PhantomCleanable} with + * {@code non-null referent} and {@code non-null cleaner}. + * The {@code cleaner} is not retained; it is only used to + * register the newly constructed {@link Cleaner.Cleanable Cleanable}. + * + * @param referent the referent to track + * @param cleaner the {@code Cleaner} to register with + */ + public PhantomCleanable(T referent, Cleaner cleaner) { + super(Objects.requireNonNull(referent), CleanerImpl.getCleanerImpl(cleaner).queue); + this.list = CleanerImpl.getCleanerImpl(cleaner).phantomCleanableList; + insert(); + } + + /** + * Construct a new root of the list; not inserted. + */ + PhantomCleanable() { + super(null, null); + this.list = this; + } + + /** + * Insert this PhantomCleanable after the list head. + */ + private void insert() { + lock.lock(() -> { + prev = list; + next = list.next; + next.prev = this; + list.next = this; + }); + } + + /** + * Remove this PhantomCleanable from the list. + * + * @return true if Cleanable was removed or false if not because + * it had already been removed before + */ + private boolean remove() { + return lock.lock(() -> { + if (next != this) { + next.prev = prev; + prev.next = next; + prev = this; + next = this; + return true; + } + return false; + }); + } + + /** + * Returns true if the list's next reference refers to itself. + * + * @return true if the list is empty + */ + boolean isListEmpty() { + return lock.lock(() -> list == list.next); + } + + /** + * Unregister this PhantomCleanable and invoke {@link #performCleanup()}, + * ensuring at-most-once semantics. + */ + @Override + public final void clean() { + if (remove()) { + super.clear(); + performCleanup(); + } + } + + /** + * Unregister this PhantomCleanable and clear the reference. + * Due to inherent concurrency, {@link #performCleanup()} may still be invoked. + */ + @Override + public void clear() { + if (remove()) { + super.clear(); + } + } + + /** + * The {@code performCleanup} abstract method is overridden + * to implement the cleaning logic. + * The {@code performCleanup} method should not be called except + * by the {@link #clean} method which ensures at most once semantics. + */ + protected abstract void performCleanup(); + + /** + * This method always throws {@link UnsupportedOperationException}. + * Enqueuing details of {@link Cleaner.Cleanable} + * are a private implementation detail. + * + * @throws UnsupportedOperationException always + */ + @SuppressWarnings("deprecation") + @Override + public final boolean isEnqueued() { + throw new UnsupportedOperationException("isEnqueued"); + } + + /** + * This method always throws {@link UnsupportedOperationException}. + * Enqueuing details of {@link Cleaner.Cleanable} + * are a private implementation detail. + * + * @throws UnsupportedOperationException always + */ + @Override + public final boolean enqueue() { + throw new UnsupportedOperationException("enqueue"); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/reflection/ReflectionUtils.java b/firefly-common/src/main/java/com/fireflysource/common/reflection/ReflectionUtils.java new file mode 100644 index 000000000..d129e4bf4 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/reflection/ReflectionUtils.java @@ -0,0 +1,215 @@ +package com.fireflysource.common.reflection; + +import com.fireflysource.common.bytecode.*; +import com.fireflysource.common.service.ServiceUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Pengtao Qiu + */ +public class ReflectionUtils { + public static final ProxyFactory DEFAULT_PROXY_FACTORY = ServiceUtils.loadService(ProxyFactory.class, JavassistReflectionProxyFactory.INSTANCE); + private static final Map, Map> getterCache = new ConcurrentHashMap<>(); + private static final Map, Map> setterCache = new ConcurrentHashMap<>(); + private static final Map, Map> propertyCache = new ConcurrentHashMap<>(); + + public static void setProperty(Object obj, String property, Object value) throws Throwable { + getFields(obj.getClass()).get(property).set(obj, value); + } + + public static Object getProperty(Object obj, String property) throws Throwable { + return getFields(obj.getClass()).get(property).get(obj); + } + + /** + * Invokes an object's "setter" method by property name + * + * @param obj The instance of an object + * @param property The property name of this object + * @param value The parameter of "setter" method that you want to set + * @throws Throwable A runtime exception + */ + public static void set(Object obj, String property, Object value) throws Throwable { + getSetterMethod(obj.getClass(), property).invoke(obj, value); + } + + /** + * Invokes an object's "getter" method by property name + * + * @param obj The instance of an object + * @param property The property name of this object + * @return The value of this property + * @throws Throwable A runtime exception + */ + public static Object get(Object obj, String property) throws Throwable { + return getGetterMethod(obj.getClass(), property).invoke(obj); + } + + public static Object arrayGet(Object array, int index) { + return getArrayProxy(array.getClass()).get(array, index); + } + + public static void arraySet(Object array, int index, Object value) { + getArrayProxy(array.getClass()).set(array, index, value); + } + + public static int arraySize(Object array) { + return getArrayProxy(array.getClass()).size(array); + } + + public static ArrayProxy getArrayProxy(Class clazz) { + return DEFAULT_PROXY_FACTORY.getArrayProxy(clazz); + } + + public static FieldProxy getFieldProxy(Field field) { + return DEFAULT_PROXY_FACTORY.getFieldProxy(field); + } + + public static MethodProxy getMethodProxy(Method method) { + return DEFAULT_PROXY_FACTORY.getMethodProxy(method); + } + + /** + * Gets the all interface names of this class + * + * @param c The class of one object + * @return Returns the all interface names + */ + public static String[] getInterfaceNames(Class c) { + Class[] interfaces = c.getInterfaces(); + List names = new ArrayList<>(); + for (Class i : interfaces) { + names.add(i.getName()); + } + return names.toArray(new String[0]); + } + + public static String getPropertyName(Method method) { + String methodName = method.getName(); + int index = (methodName.charAt(0) == 'i' ? 2 : 3); + char c = methodName.charAt(index); + if (Character.isLowerCase(c)) { + return methodName.substring(index); + } else { + return Character.toLowerCase(methodName.charAt(index)) + methodName.substring(index + 1); + } + } + + public static Method getSetterMethod(Class clazz, String propertyName) { + return getSetterMethods(clazz).get(propertyName); + } + + public static Map getSetterMethods(Class clazz) { + return setterCache.computeIfAbsent(clazz, key -> getSetterMethods(key, null)); + } + + public static Map getSetterMethods(Class clazz, BeanMethodFilter filter) { + Map setMethodMap = new HashMap<>(); + Method[] methods = clazz.getMethods(); + + for (Method method : methods) { + method.setAccessible(true); + + if (Modifier.isStatic(method.getModifiers())) continue; + if (Modifier.isAbstract(method.getModifiers())) continue; + if (method.getName().length() < 4) continue; + if (!method.getName().startsWith("set")) continue; + if (!method.getReturnType().equals(Void.TYPE)) continue; + if (method.getParameterTypes().length != 1) continue; + + String propertyName = getPropertyName(method); + if (filter == null || filter.accept(propertyName, method)) { + setMethodMap.put(propertyName, method); + } + } + return setMethodMap; + } + + public static Method getGetterMethod(Class clazz, String propertyName) { + return getGetterMethods(clazz).get(propertyName); + } + + public static Map getGetterMethods(Class clazz) { + return getterCache.computeIfAbsent(clazz, key -> getGetterMethods(key, null)); + } + + public static Map getGetterMethods(Class clazz, BeanMethodFilter filter) { + Map getMethodMap = new HashMap<>(); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + method.setAccessible(true); + + if (Modifier.isStatic(method.getModifiers())) continue; + if (Modifier.isAbstract(method.getModifiers())) continue; + if (method.getName().equals("getClass")) continue; + if (!(method.getName().startsWith("is") || method.getName().startsWith("get"))) continue; + if (method.getParameterTypes().length != 0) continue; + if (method.getReturnType() == void.class) continue; + + String methodName = method.getName(); + int index = (methodName.charAt(0) == 'i' ? 2 : 3); + if (methodName.length() < index + 1) continue; + + String propertyName = getPropertyName(method); + if (filter == null || filter.accept(propertyName, method)) { + getMethodMap.put(propertyName, method); + } + } + return getMethodMap; + } + + public static Map getFields(Class clazz) { + return propertyCache.computeIfAbsent(clazz, key -> getFields(key, null)); + } + + public static Map getFields(Class clazz, BeanFieldFilter filter) { + Map fieldMap = new HashMap<>(); + Field[] fields = clazz.getFields(); + for (Field field : fields) { + field.setAccessible(true); + if (Modifier.isStatic(field.getModifiers())) continue; + + String propertyName = field.getName(); + if (filter == null || filter.accept(propertyName, field)) + fieldMap.put(propertyName, field); + } + return fieldMap; + } + + public static void copy(Object src, Object dest) { + Map getterMethodMap = getGetterMethods(src.getClass()); + Map setterMethodMap = getSetterMethods(dest.getClass()); + + for (Map.Entry entry : setterMethodMap.entrySet()) { + Method getter = getterMethodMap.get(entry.getKey()); + if (getter == null) continue; + + try { + Object obj = getter.invoke(src); + if (obj != null) { + entry.getValue().invoke(dest, obj); + } + } catch (Throwable t) { + System.err.println("copy object exception, " + t.getMessage()); + } + } + } + + @FunctionalInterface + public interface BeanMethodFilter { + boolean accept(String propertyName, Method method); + } + + @FunctionalInterface + public interface BeanFieldFilter { + boolean accept(String propertyName, Field field); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/service/ServiceUtils.java b/firefly-common/src/main/java/com/fireflysource/common/service/ServiceUtils.java new file mode 100644 index 000000000..c0e7ab1b5 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/service/ServiceUtils.java @@ -0,0 +1,21 @@ +package com.fireflysource.common.service; + +import java.util.ServiceLoader; + +/** + * @author Pengtao Qiu + */ +abstract public class ServiceUtils { + + public static T loadService(Class clazz, T defaultService) { + T service = null; + ServiceLoader serviceLoader = ServiceLoader.load(clazz); + for (T t : serviceLoader) { + service = t; + } + if (service == null) { + service = defaultService; + } + return service; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/slf4j/LazyLogger.java b/firefly-common/src/main/java/com/fireflysource/common/slf4j/LazyLogger.java new file mode 100644 index 000000000..146102830 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/slf4j/LazyLogger.java @@ -0,0 +1,252 @@ +package com.fireflysource.common.slf4j; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.MarkerIgnoringBase; + +import java.util.function.Supplier; + +/** + * @author Pengtao Qiu + */ +public class LazyLogger extends MarkerIgnoringBase { + + private Logger logger; + + public LazyLogger() { + } + + public LazyLogger(Logger logger) { + this.logger = logger; + } + + public static LazyLogger create() { + StackTraceElement[] arr = Thread.currentThread().getStackTrace(); + return new LazyLogger(LoggerFactory.getLogger(arr[2].getClassName())); + } + + public static LazyLogger create(String name) { + return new LazyLogger(LoggerFactory.getLogger(name)); + } + + public static LazyLogger create(Class clazz) { + return new LazyLogger(LoggerFactory.getLogger(clazz)); + } + + public Logger getLogger() { + return logger; + } + + public void setLogger(Logger logger) { + this.logger = logger; + } + + public void trace(Supplier supplier) { + if (logger.isTraceEnabled()) { + logger.trace(supplier.get()); + } + } + + public void trace(Throwable t, Supplier supplier) { + if (logger.isTraceEnabled()) { + logger.trace(supplier.get(), t); + } + } + + public void debug(Supplier supplier) { + if (logger.isDebugEnabled()) { + logger.debug(supplier.get()); + } + } + + public void debug(Throwable t, Supplier supplier) { + if (logger.isDebugEnabled()) { + logger.debug(supplier.get(), t); + } + } + + public void info(Supplier supplier) { + if (logger.isInfoEnabled()) { + logger.info(supplier.get()); + } + } + + public void info(Throwable t, Supplier supplier) { + if (logger.isInfoEnabled()) { + logger.info(supplier.get(), t); + } + } + + public void warn(Supplier supplier) { + if (logger.isWarnEnabled()) { + logger.warn(supplier.get()); + } + } + + public void warn(Throwable t, Supplier supplier) { + if (logger.isWarnEnabled()) { + logger.warn(supplier.get(), t); + } + } + + public void error(Supplier supplier) { + if (logger.isErrorEnabled()) { + logger.error(supplier.get()); + } + } + + public void error(Throwable t, Supplier supplier) { + if (logger.isErrorEnabled()) { + logger.error(supplier.get(), t); + } + } + + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + logger.trace(msg); + } + + @Override + public void trace(String format, Object arg) { + logger.trace(format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + logger.trace(format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) { + logger.trace(format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + logger.trace(msg, t); + } + + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + logger.debug(msg); + } + + @Override + public void debug(String format, Object arg) { + logger.debug(format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + logger.debug(format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) { + logger.debug(format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + logger.debug(msg, t); + } + + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String msg) { + logger.info(msg); + } + + @Override + public void info(String format, Object arg) { + logger.info(format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + logger.info(format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + logger.info(format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + logger.info(msg, t); + } + + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + logger.warn(msg); + } + + @Override + public void warn(String format, Object arg) { + logger.warn(format, arg); + } + + @Override + public void warn(String format, Object... arguments) { + logger.warn(format, arguments); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + logger.warn(format, arg1, arg2); + } + + @Override + public void warn(String msg, Throwable t) { + logger.warn(msg, t); + } + + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String msg) { + logger.error(msg); + } + + @Override + public void error(String format, Object arg) { + logger.error(format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + logger.error(format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) { + logger.error(format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + logger.error(msg, t); + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/string/Pattern.java b/firefly-common/src/main/java/com/fireflysource/common/string/Pattern.java new file mode 100644 index 000000000..e24d7e5cd --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/string/Pattern.java @@ -0,0 +1,168 @@ +package com.fireflysource.common.string; + +public abstract class Pattern { + + private static class Holder { + private static final AllMatch ALL_MATCH = new AllMatch(); + } + + /** + * Matches a string according to the specified pattern + * + * @param str Target string + * @return If it returns null, that represents matching failure, + * else it returns an array contains all strings matched. + */ + abstract public String[] match(String str); + + public static Pattern compile(String pattern, String wildcard) { + final boolean startWith = pattern.startsWith(wildcard); + final boolean endWith = pattern.endsWith(wildcard); + final String[] array = StringUtils.split(pattern, wildcard); + + switch (array.length) { + case 0: + return Holder.ALL_MATCH; + case 1: + if (startWith && endWith) + return new HeadAndTailMatch(array[0]); + + if (startWith) + return new HeadMatch(array[0]); + + if (endWith) + return new TailMatch(array[0]); + + return new EqualsMatch(pattern); + default: + return new MultipartMatch(startWith, endWith, array); + } + } + + + private static class MultipartMatch extends Pattern { + + private final boolean startWith, endWith; + private final String[] parts; + private int num; + + MultipartMatch(boolean startWith, boolean endWith, String[] parts) { + super(); + this.startWith = startWith; + this.endWith = endWith; + this.parts = parts; + num = parts.length - 1; + if (startWith) + num++; + if (endWith) + num++; + } + + @Override + public String[] match(String str) { + int currentIndex = -1; + int lastIndex = -1; + String[] ret = new String[num]; + + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + int j = startWith ? i : i - 1; + currentIndex = str.indexOf(part, lastIndex + 1); + + if (currentIndex > lastIndex) { + if (i != 0 || startWith) + ret[j] = str.substring(lastIndex + 1, currentIndex); + + lastIndex = currentIndex + part.length() - 1; + continue; + } + return null; + } + + if (endWith) + ret[num - 1] = str.substring(lastIndex + 1); + + return ret; + } + + } + + private static class TailMatch extends Pattern { + private final String part; + + TailMatch(String part) { + this.part = part; + } + + @Override + public String[] match(String str) { + int currentIndex = str.indexOf(part); + if (currentIndex == 0) { + return new String[]{str.substring(part.length())}; + } + return null; + } + } + + private static class HeadMatch extends Pattern { + private final String part; + + HeadMatch(String part) { + this.part = part; + } + + @Override + public String[] match(String str) { + int currentIndex = str.indexOf(part); + if (currentIndex != -1 && currentIndex + part.length() == str.length()) { + try { + return new String[]{str.substring(0, currentIndex)}; + } catch (Exception e) { + e.printStackTrace(); + System.out.println(str + ", " + currentIndex + ", " + part); + } + } + return null; + } + + + } + + private static class HeadAndTailMatch extends Pattern { + private final String part; + + HeadAndTailMatch(String part) { + this.part = part; + } + + @Override + public String[] match(String str) { + int currentIndex = str.indexOf(part); + if (currentIndex >= 0) { + return new String[]{str.substring(0, currentIndex), str.substring(currentIndex + part.length())}; + } + return null; + } + } + + private static class EqualsMatch extends Pattern { + private final String pattern; + + EqualsMatch(String pattern) { + this.pattern = pattern; + } + + @Override + public String[] match(String str) { + return pattern.equals(str) ? new String[0] : null; + } + } + + private static class AllMatch extends Pattern { + + @Override + public String[] match(String str) { + return new String[]{str}; + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/string/QuotedStringTokenizer.java b/firefly-common/src/main/java/com/fireflysource/common/string/QuotedStringTokenizer.java new file mode 100644 index 000000000..6e5531ae0 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/string/QuotedStringTokenizer.java @@ -0,0 +1,472 @@ +package com.fireflysource.common.string; + +import com.fireflysource.common.object.TypeUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +/** + * StringTokenizer with Quoting support. + *

+ * This class is a copy of the java.util.StringTokenizer API and the behaviour + * is the same, except that single and double quoted string values are + * recognised. Delimiters within quotes are not considered delimiters. Quotes + * can be escaped with '\'. + * + * @see StringTokenizer + */ +public class QuotedStringTokenizer extends StringTokenizer { + private final static String DEFAULT_DELIMITER = "\t\n\r"; + private static final char[] escapes = new char[32]; + + static { + Arrays.fill(escapes, (char) 0xFFFF); + escapes['\b'] = 'b'; + escapes['\t'] = 't'; + escapes['\n'] = 'n'; + escapes['\f'] = 'f'; + escapes['\r'] = 'r'; + } + + private String string; + private String delimiter = DEFAULT_DELIMITER; + private boolean returnQuotes; + private boolean returnDelimiters; + private StringBuilder token; + private boolean hasToken = false; + private int index = 0; + private int lastStart = 0; + private boolean isDouble = true; + private boolean isSingle = true; + + public QuotedStringTokenizer(String str, String delimiter, boolean returnDelimiters, boolean returnQuotes) { + super(""); + string = str; + if (delimiter != null) + this.delimiter = delimiter; + this.returnDelimiters = returnDelimiters; + this.returnQuotes = returnQuotes; + + if (this.delimiter.indexOf('\'') >= 0 || this.delimiter.indexOf('"') >= 0) + throw new Error("Can't use quotes as delimiters: " + this.delimiter); + + token = new StringBuilder(string.length() > 1024 ? 512 : string.length() / 2); + } + + public QuotedStringTokenizer(String str, String delimiter, boolean returnDelimiters) { + this(str, delimiter, returnDelimiters, false); + } + + public QuotedStringTokenizer(String str, String delimiter) { + this(str, delimiter, false, false); + } + + public QuotedStringTokenizer(String str) { + this(str, null, false, false); + } + + /** + * Quote a string. The string is quoted only if quoting is required due to + * embedded delimiters, quote characters or the empty string. + * + * @param s The string to quote. + * @param delim the delimiter to use to quote the string + * @return quoted string + */ + public static String quoteIfNeeded(String s, String delim) { + if (s == null) + return null; + if (s.length() == 0) + return "\"\""; + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' || c == '"' || c == '\'' || Character.isWhitespace(c) || delim.indexOf(c) >= 0) { + StringBuilder b = new StringBuilder(s.length() + 8); + quote(b, s); + return b.toString(); + } + } + + return s; + } + + /** + * Quote a string. The string is quoted only if quoting is required due to + * embeded delimiters, quote characters or the empty string. + * + * @param s The string to quote. + * @return quoted string + */ + public static String quote(String s) { + if (s == null) + return null; + if (s.length() == 0) + return "\"\""; + + StringBuilder b = new StringBuilder(s.length() + 8); + quote(b, s); + return b.toString(); + + } + + /** + * Quote a string into an Appendable. Only quotes and backslash are escaped. + * + * @param buffer The Appendable + * @param input The String to quote. + */ + public static void quoteOnly(Appendable buffer, String input) { + if (input == null) + return; + + try { + buffer.append('"'); + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + if (c == '"' || c == '\\') + buffer.append('\\'); + buffer.append(c); + } + buffer.append('"'); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + /** + * Quote a string into an Appendable. The characters ", \, \n, \r, \t, \f + * and \b are escaped + * + * @param buffer The Appendable + * @param input The String to quote. + */ + public static void quote(Appendable buffer, String input) { + if (input == null) + return; + + try { + buffer.append('"'); + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + if (c >= 32) { + if (c == '"' || c == '\\') + buffer.append('\\'); + buffer.append(c); + } else { + char escape = escapes[c]; + if (escape == 0xFFFF) { + // Unicode escape + buffer.append('\\').append('u').append('0').append('0'); + if (c < 0x10) + buffer.append('0'); + buffer.append(Integer.toString(c, 16)); + } else { + buffer.append('\\').append(escape); + } + } + } + buffer.append('"'); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + public static String unquoteOnly(String s) { + return unquoteOnly(s, false); + } + + /** + * Unquote a string, NOT converting unicode sequences + * + * @param s The string to unquote. + * @param lenient if true, will leave in backslashes that aren't valid escapes + * @return quoted string. + */ + public static String unquoteOnly(String s, boolean lenient) { + if (s == null) + return null; + if (s.length() < 2) + return s; + + char first = s.charAt(0); + char last = s.charAt(s.length() - 1); + if (first != last || (first != '"' && first != '\'')) + return s; + + StringBuilder b = new StringBuilder(s.length() - 2); + boolean escape = false; + for (int i = 1; i < s.length() - 1; i++) { + char c = s.charAt(i); + + if (escape) { + escape = false; + if (lenient && !isValidEscaping(c)) { + b.append('\\'); + } + b.append(c); + } else if (c == '\\') { + escape = true; + } else { + b.append(c); + } + } + + return b.toString(); + } + + public static String unquote(String s) { + return unquote(s, false); + } + + /** + * Unquote a string. + * + * @param s The string to unquote. + * @param lenient true if unquoting should be lenient to escaped content, + * leaving some alone, false if string unescaping + * @return quoted string + */ + public static String unquote(String s, boolean lenient) { + if (s == null) + return null; + if (s.length() < 2) + return s; + + char first = s.charAt(0); + char last = s.charAt(s.length() - 1); + if (first != last || (first != '"' && first != '\'')) + return s; + + StringBuilder b = new StringBuilder(s.length() - 2); + boolean escape = false; + for (int i = 1; i < s.length() - 1; i++) { + char c = s.charAt(i); + + if (escape) { + escape = false; + switch (c) { + case 'n': + b.append('\n'); + break; + case 'r': + b.append('\r'); + break; + case 't': + b.append('\t'); + break; + case 'f': + b.append('\f'); + break; + case 'b': + b.append('\b'); + break; + case '\\': + b.append('\\'); + break; + case '/': + b.append('/'); + break; + case '"': + b.append('"'); + break; + case 'u': + b.append((char) ((TypeUtils.convertHexDigit((byte) s.charAt(i++)) << 24) + + (TypeUtils.convertHexDigit((byte) s.charAt(i++)) << 16) + + (TypeUtils.convertHexDigit((byte) s.charAt(i++)) << 8) + + (TypeUtils.convertHexDigit((byte) s.charAt(i++))))); + break; + default: + if (lenient && !isValidEscaping(c)) { + b.append('\\'); + } + b.append(c); + } + } else if (c == '\\') { + escape = true; + } else { + b.append(c); + } + } + + return b.toString(); + } + + /** + * Check that char c (which is preceded by a backslash) is a valid escape + * sequence. + * + * @param c + * @return + */ + private static boolean isValidEscaping(char c) { + return ((c == 'n') || (c == 'r') || (c == 't') || (c == 'f') || (c == 'b') || (c == '\\') || (c == '/') + || (c == '"') || (c == 'u')); + } + + public static boolean isQuoted(String s) { + return s != null && s.length() > 0 && s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"'; + } + + @Override + public boolean hasMoreTokens() { + // Already found a token + if (hasToken) + return true; + + lastStart = index; + + int state = 0; + boolean escape = false; + while (index < string.length()) { + char c = string.charAt(index++); + + switch (state) { + case 0: // Start + if (delimiter.indexOf(c) >= 0) { + if (returnDelimiters) { + token.append(c); + return hasToken = true; + } + } else if (c == '\'' && isSingle) { + if (returnQuotes) + token.append(c); + state = 2; + } else if (c == '\"' && isDouble) { + if (returnQuotes) + token.append(c); + state = 3; + } else { + token.append(c); + hasToken = true; + state = 1; + } + break; + + case 1: // Token + hasToken = true; + if (delimiter.indexOf(c) >= 0) { + if (returnDelimiters) + index--; + return hasToken; + } else if (c == '\'' && isSingle) { + if (returnQuotes) + token.append(c); + state = 2; + } else if (c == '\"' && isDouble) { + if (returnQuotes) + token.append(c); + state = 3; + } else { + token.append(c); + } + break; + + case 2: // Single Quote + hasToken = true; + if (escape) { + escape = false; + token.append(c); + } else if (c == '\'') { + if (returnQuotes) + token.append(c); + state = 1; + } else if (c == '\\') { + if (returnQuotes) + token.append(c); + escape = true; + } else { + token.append(c); + } + break; + + case 3: // Double Quote + hasToken = true; + if (escape) { + escape = false; + token.append(c); + } else if (c == '\"') { + if (returnQuotes) + token.append(c); + state = 1; + } else if (c == '\\') { + if (returnQuotes) + token.append(c); + escape = true; + } else { + token.append(c); + } + break; + } + } + + return hasToken; + } + + @Override + public String nextToken() throws NoSuchElementException { + if (!hasMoreTokens() || token == null) + throw new NoSuchElementException(); + String t = token.toString(); + token.setLength(0); + hasToken = false; + return t; + } + + @Override + public String nextToken(String delim) throws NoSuchElementException { + delimiter = delim; + index = lastStart; + token.setLength(0); + hasToken = false; + return nextToken(); + } + + @Override + public boolean hasMoreElements() { + return hasMoreTokens(); + } + + @Override + public Object nextElement() throws NoSuchElementException { + return nextToken(); + } + + /** + * Not implemented. + */ + @Override + public int countTokens() { + return -1; + } + + /** + * @return handle double quotes if true + */ + public boolean getDouble() { + return isDouble; + } + + /** + * @param d handle double quotes if true + */ + public void setDouble(boolean d) { + isDouble = d; + } + + /** + * @return handle single quotes if true + */ + public boolean getSingle() { + return isSingle; + } + + /** + * @param single handle single quotes if true + */ + public void setSingle(boolean single) { + isSingle = single; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/string/SearchPattern.java b/firefly-common/src/main/java/com/fireflysource/common/string/SearchPattern.java new file mode 100644 index 000000000..a636abb4f --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/string/SearchPattern.java @@ -0,0 +1,166 @@ +package com.fireflysource.common.string; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * SearchPattern + *

+ * Fast searching for patterns within strings and arrays of bytes. + * Uses an implementation of the Boyer–Moore–Horspool algorithm + * with a 256 character alphabet. + *

+ * The algorithm has an average-case complexity of O(n) + * on random text and O(nm) in the worst case. + * where: + * m = pattern length + * n = length of data to search + */ +public class SearchPattern { + static final int alphabetSize = 256; + private int[] table; + private byte[] pattern; + + /** + * Produces a SearchPattern instance which can be used + * to find matches of the pattern in data + * + * @param pattern byte array containing the pattern + * @return a new SearchPattern instance using the given pattern + */ + public static SearchPattern compile(byte[] pattern) { + return new SearchPattern(Arrays.copyOf(pattern, pattern.length)); + } + + /** + * Produces a SearchPattern instance which can be used + * to find matches of the pattern in data + * + * @param pattern string containing the pattern + * @return a new SearchPattern instance using the given pattern + */ + public static SearchPattern compile(String pattern) { + return new SearchPattern(pattern.getBytes(StandardCharsets.UTF_8)); + } + + /** + * @param pattern byte array containing the pattern used for matching + */ + private SearchPattern(byte[] pattern) { + this.pattern = pattern; + + if (pattern.length == 0) + throw new IllegalArgumentException("Empty Pattern"); + + //Build up the pre-processed table for this pattern. + table = new int[alphabetSize]; + for (int i = 0; i < table.length; ++i) { + table[i] = pattern.length; + } + for (int i = 0; i < pattern.length - 1; ++i) { + table[0xff & pattern[i]] = pattern.length - 1 - i; + } + } + + /** + * Search for a complete match of the pattern within the data + * + * @param data The data in which to search for. The data may be arbitrary binary data, + * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded. + * @param offset The offset within the data to start the search + * @param length The length of the data to search + * @return The index within the data array at which the first instance of the pattern or -1 if not found + */ + public int match(byte[] data, int offset, int length) { + validate(data, offset, length); + + int skip = offset; + while (skip <= offset + length - pattern.length) { + for (int i = pattern.length - 1; data[skip + i] == pattern[i]; i--) { + if (i == 0) + return skip; + } + + skip += table[0xff & data[skip + pattern.length - 1]]; + } + + return -1; + } + + /** + * Search for a partial match of the pattern at the end of the data. + * + * @param data The data in which to search for. The data may be arbitrary binary data, + * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded. + * @param offset The offset within the data to start the search + * @param length The length of the data to search + * @return the length of the partial pattern matched and 0 for no match. + */ + public int endsWith(byte[] data, int offset, int length) { + validate(data, offset, length); + + int skip = (pattern.length <= length) ? (offset + length - pattern.length) : offset; + while (skip < offset + length) { + for (int i = (offset + length - 1) - skip; data[skip + i] == pattern[i]; --i) { + if (i == 0) + return (offset + length - skip); + } + + if (skip + pattern.length - 1 < data.length) + skip += table[0xff & data[skip + pattern.length - 1]]; + else + skip++; + } + + return 0; + } + + /** + * Search for a possibly partial match of the pattern at the start of the data. + * + * @param data The data in which to search for. The data may be arbitrary binary data, + * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded. + * @param offset The offset within the data to start the search + * @param length The length of the data to search + * @param matched The length of the partial pattern already matched + * @return the length of the partial pattern matched and 0 for no match. + */ + public int startsWith(byte[] data, int offset, int length, int matched) { + validate(data, offset, length); + + int matchedCount = 0; + + for (int i = 0; i < pattern.length - matched && i < length; i++) { + if (data[offset + i] == pattern[i + matched]) + matchedCount++; + else + return 0; + } + + return matched + matchedCount; + } + + /** + * Performs legality checks for standard arguments input into SearchPattern methods. + * + * @param data The data in which to search for. The data may be arbitrary binary data, + * but the pattern will always be {@link StandardCharsets#US_ASCII} encoded. + * @param offset The offset within the data to start the search + * @param length The length of the data to search + */ + private void validate(byte[] data, int offset, int length) { + if (offset < 0) + throw new IllegalArgumentException("offset was negative"); + else if (length < 0) + throw new IllegalArgumentException("length was negative"); + else if (offset + length > data.length) + throw new IllegalArgumentException("(offset+length) out of bounds of data[]"); + } + + /** + * @return The length of the pattern in bytes. + */ + public int getLength() { + return pattern.length; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/string/StringUtils.java b/firefly-common/src/main/java/com/fireflysource/common/string/StringUtils.java new file mode 100644 index 000000000..a5fa9b588 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/string/StringUtils.java @@ -0,0 +1,859 @@ +package com.fireflysource.common.string; + +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class StringUtils { + + public static final String EMPTY = ""; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final String FOLDER_SEPARATOR = "/"; + private static final char EXTENSION_SEPARATOR = '.'; + private static final char[] LOWER_CASE = {'\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007', '\010', + '\011', '\012', '\013', '\014', '\015', '\016', '\017', '\020', '\021', '\022', '\023', '\024', '\025', + '\026', '\027', '\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037', '\040', '\041', '\042', + '\043', '\044', '\045', '\046', '\047', '\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057', + '\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067', '\070', '\071', '\072', '\073', '\074', + '\075', '\076', '\077', '\100', '\141', '\142', '\143', '\144', '\145', '\146', '\147', '\150', '\151', + '\152', '\153', '\154', '\155', '\156', '\157', '\160', '\161', '\162', '\163', '\164', '\165', '\166', + '\167', '\170', '\171', '\172', '\133', '\134', '\135', '\136', '\137', '\140', '\141', '\142', '\143', + '\144', '\145', '\146', '\147', '\150', '\151', '\152', '\153', '\154', '\155', '\156', '\157', '\160', + '\161', '\162', '\163', '\164', '\165', '\166', '\167', '\170', '\171', '\172', '\173', '\174', '\175', + '\176', '\177'}; + private static final Trie CHARSETS = new ArrayTrie<>(256); + + private static final String ISO_8859_1 = "iso-8859-1"; + private static final String UTF8 = "utf-8"; + private static final String __UTF16 = "utf-16"; + + static { + CHARSETS.put("utf-8", UTF8); + CHARSETS.put("utf8", UTF8); + CHARSETS.put("utf-16", __UTF16); + CHARSETS.put("utf16", __UTF16); + CHARSETS.put("iso-8859-1", ISO_8859_1); + CHARSETS.put("iso_8859_1", ISO_8859_1); + } + + /** + *

+ * Splits the provided text into an array, using whitespace as the + * separator. Whitespace is defined by {@link Character#isWhitespace(char)}. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. For more control over the split + * use the StrTokenizer class. + *

+ *

+ *

+ * A null input String returns null. + *

+ *

+ *

+     * StringUtils.split(null)       = null
+     * StringUtils.split("")         = []
+     * StringUtils.split("abc def")  = ["abc", "def"]
+     * StringUtils.split("abc  def") = ["abc", "def"]
+     * StringUtils.split(" abc ")    = ["abc"]
+     * 
+ * + * @param str the String to parse, may be null + * @return an array of parsed Strings, null if null String + * input + */ + public static String[] split(String str) { + return split(str, null, -1); + } + + /** + *

+ * Splits the provided text into an array, separators specified. This is an + * alternative to using StringTokenizer. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. For more control over the split + * use the StrTokenizer class. + *

+ *

+ *

+ * A null input String returns null. A + * null separatorChars splits on whitespace. + *

+ *

+ *

+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("abc def", null) = ["abc", "def"]
+     * StringUtils.split("abc def", " ")  = ["abc", "def"]
+     * StringUtils.split("abc  def", " ") = ["abc", "def"]
+     * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, null + * splits on whitespace + * @return an array of parsed Strings, null if null String + * input + */ + public static String[] split(String str, String separatorChars) { + return splitWorker(str, separatorChars, -1, false); + } + + /** + *

+ * Splits the provided text into an array, separator specified. This is an + * alternative to using StringTokenizer. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. For more control over the split + * use the StrTokenizer class. + *

+ *

+ *

+ * A null input String returns null. + *

+ *

+ *

+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("a.b.c", '.')    = ["a", "b", "c"]
+     * StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
+     * StringUtils.split("a:b:c", '.')    = ["a:b:c"]
+     * StringUtils.split("a b c", ' ')    = ["a", "b", "c"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChar the character used as the delimiter + * @return an array of parsed Strings, null if null String + * input + * @since 2.0 + */ + public static String[] split(String str, char separatorChar) { + return splitWorker(str, separatorChar, false); + } + + /** + *

+ * Splits the provided text into an array with a maximum length, separators + * specified. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. + *

+ *

+ *

+ * A null input String returns null. A + * null separatorChars splits on whitespace. + *

+ *

+ *

+ * If more than max delimited substrings are found, the last + * returned string includes all characters after the first + * max - 1 returned strings (including separator characters). + *

+ *

+ *

+     * StringUtils.split(null, *, *)            = null
+     * StringUtils.split("", *, *)              = []
+     * StringUtils.split("ab de fg", null, 0)   = ["ab", "cd", "ef"]
+     * StringUtils.split("ab   de fg", null, 0) = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, null + * splits on whitespace + * @param max the maximum number of elements to include in the array. A zero + * or negative value implies no limit + * @return an array of parsed Strings, null if null String + * input + */ + public static String[] split(String str, String separatorChars, int max) { + return splitWorker(str, separatorChars, max, false); + } + + /** + * Performs the logic for the split and + * splitPreserveAllTokens methods that return a maximum array + * length. + * + * @param str the String to parse, may be null + * @param separatorChars the separate character + * @param max the maximum number of elements to include in the array. A zero + * or negative value implies no limit. + * @param preserveAllTokens if true, adjacent separators are treated as empty + * token separators; if false, adjacent separators + * are treated as one separator. + * @return an array of parsed Strings, null if null String + * input + */ + private static String[] splitWorker(String str, String separatorChars, int max, boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + // Direct code is quicker than StringTokenizer. + // Also, StringTokenizer uses isSpace() not isWhitespace() + + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return EMPTY_STRING_ARRAY; + } + List list = new ArrayList<>(); + int sizePlus1 = 1; + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + if (separatorChars == null) { + // Null separator means use whitespace + while (i < len) { + if (Character.isWhitespace(str.charAt(i))) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else if (separatorChars.length() == 1) { + // Optimise 1 character case + char sep = separatorChars.charAt(0); + while (i < len) { + if (str.charAt(i) == sep) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else { + // standard case + while (i < len) { + if (separatorChars.indexOf(str.charAt(i)) >= 0) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } + if (match || (preserveAllTokens && lastMatch)) { + list.add(str.substring(start, i)); + } + return list.toArray(EMPTY_STRING_ARRAY); + } + + /** + * Performs the logic for the split and + * splitPreserveAllTokens methods that do not return a maximum + * array length. + * + * @param str the String to parse, may be null + * @param separatorChar the separate character + * @param preserveAllTokens if true, adjacent separators are treated as empty + * token separators; if false, adjacent separators + * are treated as one separator. + * @return an array of parsed Strings, null if null String + * input + */ + private static String[] splitWorker(String str, char separatorChar, boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return EMPTY_STRING_ARRAY; + } + List list = new ArrayList<>(); + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + while (i < len) { + if (str.charAt(i) == separatorChar) { + if (match || preserveAllTokens) { + list.add(str.substring(start, i)); + match = false; + lastMatch = true; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + if (match || (preserveAllTokens && lastMatch)) { + list.add(str.substring(start, i)); + } + return list.toArray(EMPTY_STRING_ARRAY); + } + + /** + *

+ * Splits the provided text into an array, separator string specified. + *

+ *

+ *

+ * The separator(s) will not be included in the returned String array. + * Adjacent separators are treated as one separator. + *

+ *

+ *

+ * A null input String returns null. A + * null separator splits on whitespace. + *

+ *

+ *

+     * StringUtils.splitByWholeSeparator(null, *)               = null
+     * StringUtils.splitByWholeSeparator("", *)                 = []
+     * StringUtils.splitByWholeSeparator("ab de fg", null)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab   de fg", null)    = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":")       = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * null splits on whitespace + * @return an array of parsed Strings, null if null String was + * input + */ + public static String[] splitByWholeSeparator(String str, String separator) { + return splitByWholeSeparatorWorker(str, separator, -1, false); + } + + /** + *

+ * Splits the provided text into an array, separator string specified. + * Returns a maximum of max substrings. + *

+ *

+ *

+ * The separator(s) will not be included in the returned String array. + * Adjacent separators are treated as one separator. + *

+ *

+ *

+ * A null input String returns null. A + * null separator splits on whitespace. + *

+ *

+ *

+     * StringUtils.splitByWholeSeparator(null, *, *)               = null
+     * StringUtils.splitByWholeSeparator("", *, *)                 = []
+     * StringUtils.splitByWholeSeparator("ab de fg", null, 0)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab   de fg", null, 0)    = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":", 2)       = ["ab", "cd:ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 5) = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 2) = ["ab", "cd-!-ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * null splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @return an array of parsed Strings, null if null String was + * input + */ + public static String[] splitByWholeSeparator(String str, String separator, int max) { + return splitByWholeSeparatorWorker(str, separator, max, false); + } + + /** + * Performs the logic for the + * splitByWholeSeparatorPreserveAllTokens methods. + * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * null splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @param preserveAllTokens if true, adjacent separators are treated as empty + * token separators; if false, adjacent separators + * are treated as one separator. + * @return an array of parsed Strings, null if null String + * input + * @since 2.4 + */ + private static String[] splitByWholeSeparatorWorker(String str, String separator, int max, + boolean preserveAllTokens) { + if (str == null) { + return null; + } + + int len = str.length(); + + if (len == 0) { + return EMPTY_STRING_ARRAY; + } + + if ((separator == null) || (EMPTY.equals(separator))) { + // Split on whitespace. + return splitWorker(str, null, max, preserveAllTokens); + } + + int separatorLength = separator.length(); + + ArrayList substrings = new ArrayList<>(); + int numberOfSubstrings = 0; + int beg = 0; + int end = 0; + while (end < len) { + end = str.indexOf(separator, beg); + + if (end > -1) { + if (end > beg) { + numberOfSubstrings += 1; + + if (numberOfSubstrings == max) { + end = len; + substrings.add(str.substring(beg)); + } else { + // The following is OK, because String.substring( beg, + // end ) excludes + // the character at the position 'end'. + // System.out.println("sub " + beg + "|" + end +"|" + + // str.substring(beg, end)); + substrings.add(str.substring(beg, end)); + + // Set the starting point for the next search. + // The following is equivalent to beg = end + + // (separatorLength - 1) + 1, + // which is the right calculation: + beg = end + separatorLength; + } + } else { + // We found a consecutive occurrence of the separator, so + // skip it. + if (preserveAllTokens) { + numberOfSubstrings += 1; + if (numberOfSubstrings == max) { + end = len; + substrings.add(str.substring(beg)); + } else { + substrings.add(EMPTY); + } + } + beg = end + separatorLength; + } + } else { + // String.substring( beg ) goes from 'beg' to the end of the + // String. + // System.out.println("sub~~ " + beg + "|" + end +"|" + + // str.substring(beg)); + String t = str.substring(beg); + if (!t.equals(EMPTY)) + substrings.add(str.substring(beg)); + end = len; + } + } + + return substrings.toArray(EMPTY_STRING_ARRAY); + } + + public static boolean hasText(String str) { + return hasText((CharSequence) str); + } + + public static boolean hasText(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + public static boolean hasLength(String str) { + return hasLength((CharSequence) str); + } + + /** + * Replace the pattern using a map, such as a pattern, such as A pattern is + * "hello ${foo}" and the map is {"foo" : "world"}, when you execute this + * function, the result is "hello world" + * + * @param s The pattern string. + * @param map The key-value + * @return The string replaced. + */ + public static String replace(String s, Map map) { + StringBuilder ret = new StringBuilder((int) (s.length() * 1.5)); + int cursor = 0; + for (int start, end; (start = s.indexOf("${", cursor)) != -1 && (end = s.indexOf("}", start)) != -1; ) { + ret.append(s, cursor, start).append(map.get(s.substring(start + 2, end))); + cursor = end + 1; + } + ret.append(s, cursor, s.length()); + return ret.toString(); + } + + public static String replace(String s, Object... objs) { + if (objs == null || objs.length == 0) + return s; + if (!s.contains("{}")) + return s; + + StringBuilder ret = new StringBuilder((int) (s.length() * 1.5)); + int cursor = 0; + int index = 0; + for (int start; (start = s.indexOf("{}", cursor)) != -1; ) { + ret.append(s, cursor, start); + if (index < objs.length) { + Object obj = objs[index]; + try { + if (obj != null) { + if (obj instanceof AbstractCollection) { + ret.append(Arrays.toString(((AbstractCollection) obj).toArray())); + } else { + ret.append(obj); + } + } else { + ret.append("null"); + } + } catch (Throwable ignored) { + } + } else { + ret.append("{}"); + } + cursor = start + 2; + index++; + } + ret.append(s, cursor, s.length()); + return ret.toString(); + } + + public static String replaceStr(String s, String sub, String with) { + if (s == null) { + return null; + } + + int c = 0; + int i = s.indexOf(sub, c); + if (i == -1) { + return s; + } + StringBuilder buf = new StringBuilder(s.length() + with.length()); + do { + buf.append(s, c, i); + buf.append(with); + c = i + sub.length(); + } + while ((i = s.indexOf(sub, c)) != -1); + if (c < s.length()) { + buf.append(s.substring(c)); + } + return buf.toString(); + } + + public static String escapeXML(String str) { + if (str == null) + return ""; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + switch (c) { + case '\u00FF': + case '\u0024': + break; + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '\"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + default: + if (c >= '\u0000' && c <= '\u001F') + break; + if (c >= '\uE000' && c <= '\uF8FF') + break; + if (c >= '\uFFF0' && c <= '\uFFFF') + break; + sb.append(c); + break; + } + } + return sb.toString(); + } + + /** + * Convert a string that is unicode form to a normal string. + * + * @param s The unicode form of a string, e.g. "\\u8001\\u9A6C" + * @return Normal string + */ + public static String unicodeToString(String s) { + StringBuilder sb = new StringBuilder(); + StringTokenizer st = new StringTokenizer(s, "\\u"); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (token.length() > 4) { + sb.append((char) Integer.parseInt(token.substring(0, 4), 16)); + sb.append(token.substring(4)); + } else { + sb.append((char) Integer.parseInt(token, 16)); + } + } + return sb.toString(); + } + + + /** + * Extract the filename extension from the given Java resource path, + * e.g. "mypath/myfile.txt" -> "txt". + * + * @param path the file path (may be {@code null}) + * @return the extracted filename extension, or {@code null} if none + */ + public static String getFilenameExtension(String path) { + if (path == null) { + return null; + } + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return null; + } + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return null; + } + return path.substring(extIndex + 1); + } + + /** + * Extract the filename from the given Java resource path, + * e.g. {@code "mypath/myfile.txt" -> "myfile.txt"}. + * + * @param path the file path (may be {@code null}) + * @return the extracted filename, or {@code null} if none + */ + public static String getFilename(String path) { + if (path == null) { + return null; + } + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + public static byte[] getUtf8Bytes(String string) { + return string.getBytes(StandardCharsets.UTF_8); + } + + public static byte[] getBytes(String string) { + return string.getBytes(StandardCharsets.ISO_8859_1); + } + + /** + * Convert String to an integer. Parses up to the first non-numeric + * character. If no number is found an IllegalArgumentException is thrown + * + * @param string A String containing an integer. + * @param from The index to start parsing from + * @return an int + */ + public static int toInt(String string, int from) { + int val = 0; + boolean started = false; + boolean minus = false; + + for (int i = from; i < string.length(); i++) { + char b = string.charAt(i); + if (b <= ' ') { + if (started) + break; + } else if (b >= '0' && b <= '9') { + val = val * 10 + (b - '0'); + started = true; + } else if (b == '-' && !started) { + minus = true; + } else + break; + } + + if (started) + return minus ? (-val) : val; + throw new NumberFormatException(string); + } + + /** + * append hex digit + * + * @param buf the buffer to append to + * @param b the byte to append + * @param base the base of the hex output (almost always 16). + */ + public static void append(StringBuilder buf, byte b, int base) { + int bi = 0xff & b; + int c = '0' + (bi / base) % base; + if (c > '9') + c = 'a' + (c - '0' - 10); + buf.append((char) c); + c = '0' + bi % base; + if (c > '9') + c = 'a' + (c - '0' - 10); + buf.append((char) c); + } + + /** + * Append 2 digits (zero padded) to the StringBuilder + * + * @param buf the buffer to append to + * @param i the value to append + */ + public static void append2digits(StringBuilder buf, int i) { + if (i < 100) { + buf.append((char) (i / 10 + '0')); + buf.append((char) (i % 10 + '0')); + } + } + + /** + * fast lower case conversion. Only works on ascii (not unicode) + * + * @param s the string to convert + * @return a lower case version of s + */ + public static String asciiToLowerCase(String s) { + char[] c = null; + int i = s.length(); + + // look for first conversion + while (i-- > 0) { + char c1 = s.charAt(i); + if (c1 <= 127) { + char c2 = LOWER_CASE[c1]; + if (c1 != c2) { + c = s.toCharArray(); + c[i] = c2; + break; + } + } + } + + while (i-- > 0) { + if (c != null && c[i] <= 127) + c[i] = LOWER_CASE[c[i]]; + } + return c == null ? s : new String(c); + } + + /** + * Convert alternate charset names (eg utf8) to normalized name (eg UTF-8). + * + * @param s the charset to normalize + * @return the normalized charset (or null if normalized version not found) + */ + public static String normalizeCharset(String s) { + String n = CHARSETS.get(s); + return (n == null) ? s : n; + } + + /** + * Convert alternate charset names (eg utf8) to normalized name (eg UTF-8). + * + * @param s the charset to normalize + * @param offset the offset in the charset + * @param length the length of the charset in the input param + * @return the normalized charset (or null if not found) + */ + public static String normalizeCharset(String s, int offset, int length) { + String n = CHARSETS.get(s, offset, length); + return (n == null) ? s.substring(offset, offset + length) : n; + } + + public static boolean isHex(String str, int offset, int length) { + if (offset + length > str.length()) { + return false; + } + + for (int i = offset; i < (offset + length); i++) { + char c = str.charAt(i); + if (!(((c >= 'a') && (c <= 'f')) || + ((c >= 'A') && (c <= 'F')) || + ((c >= '0') && (c <= '9')))) { + return false; + } + } + return true; + } + + /** + * Truncate a string to a max size. + * + * @param str the string to possibly truncate + * @param maxSize the maximum size of the string + * @return the truncated string. if str param is null, then the returned string will also be null. + */ + public static String truncate(String str, int maxSize) { + if (str == null) { + return null; + } + if (str.length() <= maxSize) { + return str; + } + return str.substring(0, maxSize); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/string/Utf8Appendable.java b/firefly-common/src/main/java/com/fireflysource/common/string/Utf8Appendable.java new file mode 100644 index 000000000..f1b7bb8f0 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/string/Utf8Appendable.java @@ -0,0 +1,234 @@ +package com.fireflysource.common.string; + +import com.fireflysource.common.object.TypeUtils; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Utf8 Appendable abstract base class + *

+ * This abstract class wraps a standard {@link Appendable} and + * provides methods to append UTF-8 encoded bytes, that are converted into + * characters. + *

+ * This class is stateful and up to 4 calls to {@link #append(byte)} may be + * needed before state a character is appended to the string buffer. + *

+ * The UTF-8 decoding is done by this class and no additional buffers or Readers + * are used. The UTF-8 code was inspired by + * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + *

+ * License information for Bjoern Hoehrmann's code: + *

+ * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de> + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ +public abstract class Utf8Appendable { + public static final char REPLACEMENT = '\ufffd'; + public static final byte[] REPLACEMENT_UTF8 = new byte[]{(byte) 0xEF, (byte) 0xBF, (byte) 0xBD}; + private static final int UTF8_ACCEPT = 0; + private static final int UTF8_REJECT = 12; + private static final byte[] BYTE_TABLE = + { + // The first part of the table maps bytes to character classes that + // to reduce the size of the transition table and create bitmasks. + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 + }; + private static final byte[] TRANS_TABLE = + { + // The second part is a transition table that maps a combination + // of a state of the automaton and a character class to a state. + 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, + 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 + }; + protected final Appendable appendable; + protected int _state = UTF8_ACCEPT; + private int codep; + + public Utf8Appendable(Appendable appendable) { + this.appendable = appendable; + } + + public abstract int length(); + + protected void reset() { + _state = UTF8_ACCEPT; + } + + private void checkCharAppend() throws IOException { + if (_state != UTF8_ACCEPT) { + appendable.append(REPLACEMENT); + int state = _state; + _state = UTF8_ACCEPT; + throw new NotUtf8Exception("char appended in state " + state); + } + } + + public void append(char c) { + try { + checkCharAppend(); + appendable.append(c); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void append(String s) { + try { + checkCharAppend(); + appendable.append(s); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void append(String s, int offset, int length) { + try { + checkCharAppend(); + appendable.append(s, offset, offset + length); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void append(byte b) { + try { + appendByte(b); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void append(ByteBuffer buf) { + try { + while (buf.remaining() > 0) { + appendByte(buf.get()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void append(byte[] b, int offset, int length) { + try { + int end = offset + length; + for (int i = offset; i < end; i++) + appendByte(b[i]); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public boolean append(byte[] b, int offset, int length, int maxChars) { + try { + int end = offset + length; + for (int i = offset; i < end; i++) { + if (length() > maxChars) + return false; + appendByte(b[i]); + } + return true; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected void appendByte(byte b) throws IOException { + + if (b > 0 && _state == UTF8_ACCEPT) { + appendable.append((char) (b & 0xFF)); + } else { + int i = b & 0xFF; + int type = BYTE_TABLE[i]; + codep = _state == UTF8_ACCEPT ? (0xFF >> type) & i : (i & 0x3F) | (codep << 6); + int next = TRANS_TABLE[_state + type]; + + switch (next) { + case UTF8_ACCEPT: + _state = next; + if (codep < Character.MIN_HIGH_SURROGATE) { + appendable.append((char) codep); + } else { + for (char c : Character.toChars(codep)) + appendable.append(c); + } + break; + + case UTF8_REJECT: + String reason = "byte " + TypeUtils.toHexString(b) + " in state " + (_state / 12); + codep = 0; + _state = UTF8_ACCEPT; + appendable.append(REPLACEMENT); + throw new NotUtf8Exception(reason); + + default: + _state = next; + + } + } + } + + public boolean isUtf8SequenceComplete() { + return _state == UTF8_ACCEPT; + } + + protected void checkState() { + if (!isUtf8SequenceComplete()) { + codep = 0; + _state = UTF8_ACCEPT; + try { + appendable.append(REPLACEMENT); + } catch (IOException e) { + throw new RuntimeException(e); + } + throw new NotUtf8Exception("incomplete UTF8 sequence"); + } + } + + public String toReplacedString() { + if (!isUtf8SequenceComplete()) { + codep = 0; + _state = UTF8_ACCEPT; + try { + appendable.append(REPLACEMENT); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return appendable.toString(); + } + + @SuppressWarnings("serial") + public static class NotUtf8Exception extends IllegalArgumentException { + public NotUtf8Exception(String reason) { + super("Not valid UTF8! " + reason); + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/string/Utf8StringBuilder.java b/firefly-common/src/main/java/com/fireflysource/common/string/Utf8StringBuilder.java new file mode 100644 index 000000000..be2d1d962 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/string/Utf8StringBuilder.java @@ -0,0 +1,51 @@ +package com.fireflysource.common.string; + +/** + * UTF-8 StringBuilder. + *

+ * This class wraps a standard {@link StringBuilder} and provides + * methods to append UTF-8 encoded bytes, that are converted into characters. + *

+ * This class is stateful and up to 4 calls to {@link #append(byte)} may be + * needed before state a character is appended to the string buffer. + *

+ * The UTF-8 decoding is done by this class and no additional buffers or Readers + * are used. The UTF-8 code was inspired by + * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + */ +public class Utf8StringBuilder extends Utf8Appendable { + final StringBuilder buffer; + + public Utf8StringBuilder() { + super(new StringBuilder()); + buffer = (StringBuilder) appendable; + } + + public Utf8StringBuilder(int capacity) { + super(new StringBuilder(capacity)); + buffer = (StringBuilder) appendable; + } + + @Override + public int length() { + return buffer.length(); + } + + @Override + public void reset() { + super.reset(); + buffer.setLength(0); + } + + public StringBuilder getStringBuilder() { + checkState(); + return buffer; + } + + @Override + public String toString() { + checkState(); + return buffer.toString(); + } + +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/sys/JavaVersion.java b/firefly-common/src/main/java/com/fireflysource/common/sys/JavaVersion.java new file mode 100644 index 000000000..604ee401f --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/sys/JavaVersion.java @@ -0,0 +1,115 @@ +package com.fireflysource.common.sys; + +/** + * Java Version Utility class. + *

Parses java versions to extract a consistent set of version parts

+ */ +public class JavaVersion { + + public static final JavaVersion VERSION = parse(System.getProperty("java.version")); + + public static JavaVersion parse(String v) { + // $VNUM is a dot-separated list of integers of arbitrary length + String[] split = v.split("[^0-9]"); + int len = Math.min(split.length, 3); + int[] version = new int[len]; + for (int i = 0; i < len; i++) { + try { + version[i] = Integer.parseInt(split[i]); + } catch (Throwable e) { + len = i - 1; + break; + } + } + + return new JavaVersion( + v, + (version[0] >= 9 || len == 1) ? version[0] : version[1], + version[0], + len > 1 ? version[1] : 0, + len > 2 ? version[2] : 0); + } + + private final String version; + private final int platform; + private final int major; + private final int minor; + private final int micro; + + private JavaVersion(String version, int platform, int major, int minor, int micro) { + this.version = version; + this.platform = platform; + this.major = major; + this.minor = minor; + this.micro = micro; + } + + /** + * @return the string from which this JavaVersion was created + */ + public String getVersion() { + return version; + } + + /** + *

Returns the Java Platform version, such as {@code 8} for JDK 1.8.0_92 and {@code 9} for JDK 9.2.4.

+ * + * @return the Java Platform version + */ + public int getPlatform() { + return platform; + } + + /** + *

Returns the major number version, such as {@code 1} for JDK 1.8.0_92 and {@code 9} for JDK 9.2.4.

+ * + * @return the major number version + */ + public int getMajor() { + return major; + } + + /** + *

Returns the minor number version, such as {@code 8} for JDK 1.8.0_92 and {@code 2} for JDK 9.2.4.

+ * + * @return the minor number version + */ + public int getMinor() { + return minor; + } + + /** + *

Returns the micro number version (aka security number), such as {@code 0} for JDK 1.8.0_92 and {@code 4} for JDK 9.2.4.

+ * + * @return the micro number version + */ + public int getMicro() { + return micro; + } + + /** + *

Returns the update number version, such as {@code 92} for JDK 1.8.0_92 and {@code 0} for JDK 9.2.4.

+ * + * @return the update number version + */ + @Deprecated + public int getUpdate() { + return 0; + } + + /** + *

Returns the remaining string after the version numbers, such as {@code -internal} for + * JDK 1.8.0_92-internal and {@code -ea} for JDK 9-ea, or {@code +13} for JDK 9.2.4+13.

+ * + * @return the remaining string after the version numbers + */ + @Deprecated + public String getSuffix() { + return null; + } + + @Override + public String toString() { + return version; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/sys/ProjectVersion.java b/firefly-common/src/main/java/com/fireflysource/common/sys/ProjectVersion.java new file mode 100644 index 000000000..f1f081c0b --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/sys/ProjectVersion.java @@ -0,0 +1,55 @@ +package com.fireflysource.common.sys; + +import java.io.InputStream; +import java.util.Properties; + +import static java.lang.System.lineSeparator; + +/** + * @author Pengtao Qiu + */ +public class ProjectVersion { + + private String value; + private String githubUrl; + + private ProjectVersion() { + try (InputStream is = ProjectVersion.class.getResourceAsStream("/firefly_version.properties")) { + Properties properties = new Properties(); + properties.load(is); + value = properties.getProperty("firefly.version"); + githubUrl = properties.getProperty("github.url"); + } catch (Exception ignored) { + } + } + + public static String getValue() { + return Holder.instance.value; + } + + public static String getGithubUrl() { + return Holder.instance.githubUrl; + } + + public static String getAsciiArt() { + return "\033[31;0m\n" + + "______ _ __ _ \n" + + "| ___(_) / _| | \n" + + "| |_ _ _ __ ___| |_| |_ _ \n" + + "| _| | | '__/ _ \\ _| | | | |\n" + + "| | | | | | __/ | | | |_| |\n" + + "\\_| |_|_| \\___|_| |_|\\__, |\n" + + " __/ |\n" + + " |___/ \n\033[0m"; + } + + public static String logo() { + return lineSeparator() + "Github: " + getGithubUrl() + lineSeparator() + + "Version: " + getValue() + lineSeparator() + + getAsciiArt() + lineSeparator(); + } + + private static class Holder { + private static final ProjectVersion instance = new ProjectVersion(); + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/sys/Result.java b/firefly-common/src/main/java/com/fireflysource/common/sys/Result.java new file mode 100644 index 000000000..de49b45e7 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/sys/Result.java @@ -0,0 +1,100 @@ +package com.fireflysource.common.sys; + +import com.fireflysource.common.string.StringUtils; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import static com.fireflysource.common.func.FunctionInterfaceUtils.createEmptyConsumer; + +/** + * @author Pengtao Qiu + */ +public class Result { + + public static final CompletableFuture DONE = doneFuture(); + public static final Result SUCCESS = new Result<>(true, null, null); + @SuppressWarnings("rawtypes") + private static final Consumer EMPTY = createEmptyConsumer(); + + private final boolean success; + private final T value; + private final Throwable throwable; + + public Result(boolean success, T value, Throwable throwable) { + this.success = success; + this.value = value; + this.throwable = throwable; + } + + public static void done(CompletableFuture future) { + future.complete(null); + } + + public static CompletableFuture doneFuture() { + return CompletableFuture.completedFuture(null); + } + + @SuppressWarnings("unchecked") + public static Consumer emptyConsumer() { + return EMPTY; + } + + public static Result createFailedResult(T value, Throwable throwable) { + return new Result<>(false, value, throwable); + } + + public static Result createFailedResult(Throwable throwable) { + return new Result<>(false, null, throwable); + } + + public static Result createSuccessResult() { + return new Result<>(true, null, null); + } + + @SuppressWarnings("unchecked") + public static Consumer> discard() { + return EMPTY; + } + + public static Consumer> futureToConsumer(CompletableFuture future) { + return result -> { + if (result.isSuccess()) { + future.complete(result.getValue()); + } else { + future.completeExceptionally(result.getThrowable()); + } + }; + } + + public static Result runCaching(Callable callable) { + try { + T t = callable.call(); + return new Result<>(true, t, null); + } catch (Throwable t) { + return new Result<>(false, null, t); + } + } + + public boolean isSuccess() { + return success; + } + + public T getValue() { + return value; + } + + public Throwable getThrowable() { + return throwable; + } + + @Override + public String toString() { + return "Result{" + + "success=" + success + + ", value=" + value + + ", throwable=" + (throwable != null && StringUtils.hasText(throwable.getMessage()) ? throwable.getMessage() : "null") + + '}'; + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/common/sys/SystemLogger.java b/firefly-common/src/main/java/com/fireflysource/common/sys/SystemLogger.java new file mode 100644 index 000000000..cd8b94d35 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/common/sys/SystemLogger.java @@ -0,0 +1,48 @@ +package com.fireflysource.common.sys; + +import com.fireflysource.common.bytecode.JavassistClassProxyFactory; +import com.fireflysource.common.lifecycle.ShutdownTasks; +import com.fireflysource.common.slf4j.LazyLogger; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import java.io.Closeable; +import java.io.IOException; + +/** + * @author Pengtao Qiu + */ +public class SystemLogger { + + private static final LazyLogger system = LazyLogger.create("firefly-system"); + + static { + ShutdownTasks.register(SystemLogger::stop); + } + + public static LazyLogger create(Class clazz) { + try { + String className = clazz.getSimpleName(); + return JavassistClassProxyFactory.INSTANCE.createProxy(system, ((handler, originalInstance, args) -> { + try (MDC.MDCCloseable ignored = MDC.putCloseable("class", className)) { + return handler.invoke(originalInstance, args); + } + }), null); + } catch (Throwable e) { + system.error("create system logger exception", e); + throw new IllegalStateException(e); + } + } + + public static void stop() { + ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); + if (iLoggerFactory instanceof Closeable) { + try { + ((Closeable) iLoggerFactory).close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/firefly-common/src/main/java/com/fireflysource/doc/FeignedCommonDoc.java b/firefly-common/src/main/java/com/fireflysource/doc/FeignedCommonDoc.java new file mode 100644 index 000000000..5b232ab04 --- /dev/null +++ b/firefly-common/src/main/java/com/fireflysource/doc/FeignedCommonDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedCommonDoc { +} diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/annotation/CompilerPluginAnnotation.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/annotation/CompilerPluginAnnotation.kt new file mode 100644 index 000000000..9255c4a37 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/annotation/CompilerPluginAnnotation.kt @@ -0,0 +1,8 @@ +package com.fireflysource.common.annotation + +/** + * @author Pengtao Qiu + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class NoArg \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/concurrent/CompletableFutureExtension.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/concurrent/CompletableFutureExtension.kt new file mode 100644 index 000000000..160717ee4 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/concurrent/CompletableFutureExtension.kt @@ -0,0 +1,20 @@ +package com.fireflysource.common.concurrent + +import com.fireflysource.common.sys.Result +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage + +inline fun CompletionStage.exceptionallyCompose(crossinline block: (Throwable) -> CompletionStage): CompletableFuture { + return CompletableFutures.exceptionallyCompose(this) { block(it) }.toCompletableFuture() +} + +inline fun CompletionStage.exceptionallyAccept(crossinline block: (Throwable) -> Unit): CompletableFuture { + return this.exceptionally { + block(it) + null + }.thenCompose { Result.DONE }.toCompletableFuture() +} + +inline fun CompletionStage.doFinally(crossinline block: (T?, Throwable?) -> CompletableFuture): CompletableFuture { + return CompletableFutures.doFinally(this) { value, throwable -> block(value, throwable) } +} \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/ChannelExtension.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/ChannelExtension.kt new file mode 100644 index 000000000..15430c867 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/ChannelExtension.kt @@ -0,0 +1,21 @@ +package com.fireflysource.common.coroutine + +import kotlinx.coroutines.channels.Channel + +inline fun Channel.consumeAll(crossinline block: (T) -> Unit) { + try { + while (true) { + val result = this.tryReceive() + if (result.isFailure) { + break + } + val message = result.getOrNull() ?: break + block(message) + } + } catch (ignore: Exception) { + } +} + +fun Channel.clear() { + this.consumeAll { } +} \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CommonCoroutinePool.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CommonCoroutinePool.kt new file mode 100644 index 000000000..55512cfc8 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CommonCoroutinePool.kt @@ -0,0 +1,146 @@ +package com.fireflysource.common.coroutine + +import com.fireflysource.common.concurrent.ExecutorServiceUtils.shutdownAndAwaitTermination +import com.fireflysource.common.coroutine.CoroutineDispatchers.awaitTerminationTimeout +import com.fireflysource.common.ref.Cleaner +import kotlinx.coroutines.* +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicInteger + +/** + * @author Pengtao Qiu + */ +object CoroutineDispatchers { + + val availableProcessors = Runtime.getRuntime().availableProcessors() + val awaitTerminationTimeout = + Integer.getInteger("com.fireflysource.common.coroutine.awaitTerminationTimeout", 5).toLong() + + val defaultPoolSize: Int = + Integer.getInteger("com.fireflysource.common.coroutine.defaultPoolSize", availableProcessors) + + val ioBlockingPoolSize: Int = + Integer.getInteger("com.fireflysource.common.coroutine.ioBlockingPoolSize", availableProcessors * 8) + val ioBlockingPoolKeepAliveTime: Long = + Integer.getInteger("com.fireflysource.common.coroutine.ioBlockingPoolKeepAliveTime", 30).toLong() + val ioBlockingPoolQueueSize: Int = + Integer.getInteger("com.fireflysource.common.coroutine.ioBlockingPoolQueueSize", 10000) + + + val ioBlockingThreadPool: ExecutorService by lazy { + val threadId = AtomicInteger() + ThreadPoolExecutor( + availableProcessors * 2, ioBlockingPoolSize, + ioBlockingPoolKeepAliveTime, TimeUnit.SECONDS, + LinkedBlockingQueue(ioBlockingPoolQueueSize) + ) { runnable -> Thread(runnable, "firefly-io-blocking-pool-" + threadId.getAndIncrement()) } + } + + val singleThreadPool: ExecutorService by lazy { + ThreadPoolExecutor( + 1, 1, 0, TimeUnit.MILLISECONDS, + LinkedTransferQueue() + ) { runnable -> Thread(runnable, "firefly-single-thread-pool") } + } + + val computationThreadPool: ExecutorService by lazy { + ForkJoinPool(defaultPoolSize, { pool -> + val worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool) + worker.name = "firefly-computation-pool-" + worker.poolIndex + worker + }, null, true) + } + + val computation: CoroutineDispatcher by lazy { computationThreadPool.asCoroutineDispatcher() } + val ioBlocking: CoroutineDispatcher by lazy { ioBlockingThreadPool.asCoroutineDispatcher() } + val singleThread: CoroutineDispatcher by lazy { singleThreadPool.asCoroutineDispatcher() } + + val scheduler: ScheduledExecutorService by lazy { + Executors.newScheduledThreadPool(defaultPoolSize) { + Thread(it, "firefly-scheduler-thread") + } + } + + fun newSingleThreadExecutor(name: String): ExecutorService { + val executor = ThreadPoolExecutor( + 1, 1, 0, TimeUnit.MILLISECONDS, + LinkedTransferQueue() + ) { runnable -> Thread(runnable, name) } + return FinalizableExecutorService(executor) + } + + fun newComputationThreadExecutor(name: String, asyncMode: Boolean = true): ExecutorService { + val executor = ForkJoinPool(defaultPoolSize, { pool -> + val worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool) + worker.name = name + "-" + worker.poolIndex + worker + }, null, asyncMode) + return FinalizableExecutorService(executor) + } + + fun newSingleThreadDispatcher(name: String): CoroutineDispatcher { + return newSingleThreadExecutor(name).asCoroutineDispatcher() + } + + fun newComputationThreadDispatcher(name: String, asyncMode: Boolean = true): CoroutineDispatcher { + return newComputationThreadExecutor(name, asyncMode).asCoroutineDispatcher() + } + + fun stopAll() { + shutdownAndAwaitTermination(computationThreadPool, awaitTerminationTimeout) + shutdownAndAwaitTermination(singleThreadPool, awaitTerminationTimeout) + shutdownAndAwaitTermination(ioBlockingThreadPool, awaitTerminationTimeout) + shutdownAndAwaitTermination(scheduler, awaitTerminationTimeout) + } +} + +val applicationCleaner: Cleaner = Cleaner.create() + +class ExecutorCleanTask(private val executor: ExecutorService) : Runnable { + override fun run() { + if (!executor.isShutdown) { + shutdownAndAwaitTermination(executor, awaitTerminationTimeout) + } + } +} + +class FinalizableExecutorService(private val executor: ExecutorService) : ExecutorService by executor { + + init { + applicationCleaner.register(this, ExecutorCleanTask(executor)) + } +} + +class FinalizableScheduledExecutorService(private val executor: ScheduledExecutorService) : + ScheduledExecutorService by executor { + + init { + applicationCleaner.register(this, ExecutorCleanTask(executor)) + } +} + +val applicationScope = CoroutineScope(CoroutineName("Firefly-Application")) + +inline fun compute(crossinline block: suspend CoroutineScope.() -> Unit): Job = + applicationScope.launch(CoroutineDispatchers.computation) { block(this) } + +inline fun computeAsync(crossinline block: suspend CoroutineScope.() -> T): Deferred = + applicationScope.async(CoroutineDispatchers.computation) { block(this) } + +inline fun blocking(crossinline block: suspend CoroutineScope.() -> Unit): Job = + applicationScope.launch(CoroutineDispatchers.ioBlocking) { block(this) } + +inline fun blockingAsync(crossinline block: suspend CoroutineScope.() -> T): Deferred = + applicationScope.async(CoroutineDispatchers.ioBlocking) { block(this) } + +inline fun event(crossinline block: suspend CoroutineScope.() -> Unit): Job = + applicationScope.launch(CoroutineDispatchers.singleThread) { block(this) } + +inline fun eventAsync(crossinline block: suspend CoroutineScope.() -> T): Deferred = + applicationScope.async(CoroutineDispatchers.singleThread) { block(this) } + +inline fun CoroutineScope.blocking(crossinline block: suspend CoroutineScope.() -> Unit): Job = + this.launch(CoroutineDispatchers.ioBlocking) { block(this) } + +inline fun CoroutineScope.blockingAsync(crossinline block: suspend CoroutineScope.() -> T): Deferred = + this.async(CoroutineDispatchers.ioBlocking) { block(this) } \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CoroutineExtension.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CoroutineExtension.kt new file mode 100644 index 000000000..a89317a88 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CoroutineExtension.kt @@ -0,0 +1,10 @@ +package com.fireflysource.common.coroutine + +import com.fireflysource.common.sys.Result +import kotlinx.coroutines.Job +import kotlinx.coroutines.future.asCompletableFuture +import java.util.concurrent.CompletableFuture + +fun Job.asVoidFuture(): CompletableFuture { + return this.asCompletableFuture().thenCompose { Result.DONE } +} \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CoroutineLocal.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CoroutineLocal.kt new file mode 100644 index 000000000..7dacb8a4e --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/coroutine/CoroutineLocal.kt @@ -0,0 +1,160 @@ +package com.fireflysource.common.coroutine + +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Retain a value in the coroutine scope. + * + * @author Pengtao Qiu + */ +class CoroutineLocal { + + private val threadLocal = ThreadLocal() + + /** + * Convert a value to the coroutine context element. + * + * @param value A value runs through in the coroutine scope. + * @return The coroutine context element. + */ + fun asElement(value: T) = threadLocal.asContextElement(value) + + /** + * Get the value in the coroutine scope. + * + * @return The value in the coroutine scope. + */ + fun get(): T? = threadLocal.get() + + /** + * Set the value in the coroutine scope. + * + * @param value The value in the coroutine scope. + */ + fun set(value: T?) = threadLocal.set(value) +} + +/** + * Retain the attributes in the coroutine scope. + * + * @author Pengtao Qiu + */ +object CoroutineLocalContext { + + private val ctx: CoroutineLocal> by lazy { CoroutineLocal() } + + /** + * Convert the attributes to the coroutine context element. + * + * @param attributes The attributes run through in the coroutine scope. + * @return The coroutine context element. + */ + fun asElement(attributes: MutableMap): ThreadContextElement> = + ctx.asElement(HashMap(attributes)) + + /** + * Inherit parent coroutine context element, and merge it into current attributes. + * + * @param attributes The attributes run through in the coroutine scope. + * @return The coroutine context element. + */ + fun inheritParentElement(attributes: MutableMap? = null): ThreadContextElement> { + val newAttributes = HashMap() + val parentAttributes = getAttributes() + if (parentAttributes != null) { + newAttributes.putAll(parentAttributes) + } + if (attributes != null) { + newAttributes.putAll(attributes) + } + return ctx.asElement(newAttributes) + } + + /** + * Get the current attributes. + * + * @return The current attributes. + */ + fun getAttributes(): MutableMap? = ctx.get() + + /** + * Get an attribute in the current coroutine scope. + * + * @param name The name of attribute. + * @return An attribute in the current coroutine scope. + */ + inline fun getAttr(name: String): T? { + return getAttributes()?.get(name) as T? + } + + /** + * Get an attribute in the current coroutine scope, if the value is null return the default value. + * + * @param name The name of attribute. + * @param func Get the default value lazily. + * @return An attribute in the current coroutine scope, or the default value. + */ + inline fun getAttrOrDefault(name: String, crossinline func: (String) -> T): T { + return getAttributes()?.get(name) as T? ?: func(name) + } + + /** + * Set an attribute in the current coroutine scope. + * + * @param name The attribute's name. + * @param value The attribute's value. + * @return The old value in the current coroutine scope. + */ + inline fun setAttr(name: String, value: T): T? { + return getAttributes()?.put(name, value) as T? + } + + /** + * If the specified attribute name does not already associate with a value (or is mapped + * to null), attempts to compute its value using the given mapping + * function and enters it into this map unless null. + * + * @param name The attribute's name. + * @param func The mapping function. + * @return The value in the current coroutine scope. + */ + inline fun computeIfAbsent(name: String, crossinline func: (String) -> T): T? { + return getAttributes()?.computeIfAbsent(name) { func(it) } as T? + } +} + +inline fun CoroutineScope.inheritableAsync( + context: CoroutineContext = EmptyCoroutineContext, + attributes: MutableMap? = null, + start: CoroutineStart = CoroutineStart.DEFAULT, + crossinline block: suspend CoroutineScope.() -> T +): Deferred { + return this.async(context + CoroutineLocalContext.inheritParentElement(attributes), start) { block(this) } +} + +inline fun CoroutineScope.inheritableLaunch( + context: CoroutineContext = EmptyCoroutineContext, + attributes: MutableMap? = null, + start: CoroutineStart = CoroutineStart.DEFAULT, + crossinline block: suspend CoroutineScope.() -> Unit +): Job { + return this.launch(context + CoroutineLocalContext.inheritParentElement(attributes), start) { block(this) } +} + +/** + * Starts an asynchronous task waiting the result and inherits parent coroutine local attributes. + * + * @param context Additional to [CoroutineScope.coroutineContext] context of the coroutine. + * @param attributes The attributes merge into the parent coroutine context element. + * @param block The coroutine code block. + * @return The result. + */ +suspend inline fun withContextInheritable( + context: CoroutineContext = EmptyCoroutineContext, + attributes: MutableMap? = null, + crossinline block: suspend CoroutineScope.() -> T +): T { + return withContext(context + CoroutineLocalContext.inheritParentElement(attributes)) { block(this) } +} \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/exception/UnsupportedOperationException.java b/firefly-common/src/main/kotlin/com/fireflysource/common/exception/UnsupportedOperationException.java new file mode 100644 index 000000000..30c04d4f9 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/exception/UnsupportedOperationException.java @@ -0,0 +1,14 @@ +package com.fireflysource.common.exception; + +/** + * @author Pengtao Qiu + */ +public class UnsupportedOperationException extends RuntimeException { + + public UnsupportedOperationException() { + } + + public UnsupportedOperationException(String message) { + super(message); + } +} diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/io/BufferExtension.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/io/BufferExtension.kt new file mode 100644 index 000000000..e9c2c233d --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/io/BufferExtension.kt @@ -0,0 +1,33 @@ +package com.fireflysource.common.io + +import java.nio.ByteBuffer +import java.nio.charset.Charset + +fun ByteBuffer.append(buffer: ByteBuffer): ByteBuffer { + BufferUtils.append(this, buffer) + return this +} + +fun ByteBuffer.addCapacity(capacity: Int): ByteBuffer { + return BufferUtils.addCapacity(this, capacity) +} + +fun ByteBuffer.flipToFill(): Int = BufferUtils.flipToFill(this) + +fun ByteBuffer.flipToFlush(position: Int): ByteBuffer { + BufferUtils.flipToFlush(this, position) + return this +} + +fun ByteBuffer.copy(): ByteBuffer { + return if (this.hasRemaining()) BufferUtils.allocate(this.remaining()).append(this) + else BufferUtils.EMPTY_BUFFER +} + +fun String.toBuffer(): ByteBuffer { + return BufferUtils.toBuffer(this) +} + +fun String.toBuffer(charset: Charset): ByteBuffer { + return BufferUtils.toBuffer(this, charset) +} \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/io/Nio.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/io/Nio.kt new file mode 100644 index 000000000..d1a8d6072 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/io/Nio.kt @@ -0,0 +1,273 @@ +@file:Suppress("BlockingMethodInNonBlockingContext", "KDocUnresolvedReference") + +package com.fireflysource.common.io + +import com.fireflysource.common.coroutine.CoroutineDispatchers.ioBlockingThreadPool +import com.fireflysource.common.coroutine.blocking +import com.fireflysource.common.coroutine.blockingAsync +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.future.await +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import java.io.Closeable +import java.net.SocketAddress +import java.nio.ByteBuffer +import java.nio.channels.* +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.OpenOption +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +import java.util.concurrent.TimeUnit + +/** + * Performs [AsynchronousFileChannel.lock] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousFileChannel.lockAwait() = suspendCancellableCoroutine { cont -> + lock(cont, asyncIOHandler()) + closeOnCancel(cont) +} + +/** + * Performs [AsynchronousFileChannel.lock] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousFileChannel.lockAwait( + position: Long, + size: Long, + shared: Boolean +) = suspendCancellableCoroutine { cont -> + lock(position, size, shared, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +suspend fun T.useAwait(block: suspend (T) -> R): R { + try { + return block(this) + } catch (e: Throwable) { + throw e + } finally { + withContext(NonCancellable) { + this@useAwait?.closeAsync()?.join() + } + } +} + +suspend fun T.useAwait(block: suspend (T) -> R): R { + try { + return block(this) + } catch (e: Throwable) { + throw e + } finally { + withContext(NonCancellable) { + this@useAwait?.closeAsync()?.await() + } + } +} + +/** + * Close in the I/O blocking coroutine dispatcher + */ +fun Closeable.closeAsync() = blocking { + close() +} + +fun openFileChannelAsync(file: Path, vararg options: OpenOption) = blockingAsync { + AsynchronousFileChannel.open(file, setOf(*options), ioBlockingThreadPool) +} + +fun openFileChannelAsync(file: Path, options: Set) = blockingAsync { + AsynchronousFileChannel.open(file, options, ioBlockingThreadPool) +} + +fun listFilesAsync(dir: Path) = blockingAsync { + Files.list(dir) +} + +fun readFileLinesAsync(file: Path, charset: Charset = StandardCharsets.UTF_8) = blockingAsync { + Files.readAllLines(file, charset) +} + +fun readFileBytesAsync(file: Path) = blockingAsync { + Files.readAllBytes(file) +} + +fun deleteIfExistsAsync(file: Path) = blockingAsync { + Files.deleteIfExists(file) +} + +fun existsAsync(file: Path, vararg options: LinkOption) = blockingAsync { + Files.exists(file, *options) +} + +fun readAttributesAsync(file: Path, vararg options: LinkOption) = blockingAsync { + Files.readAttributes(file, BasicFileAttributes::class.java, *options) +} + +fun writeFileLinesAsync(file: Path, iterable: Iterable, vararg options: OpenOption) = blockingAsync { + Files.write(file, iterable, *options) +} + +fun writeFileBytesAsync(file: Path, byteArray: ByteArray, vararg options: OpenOption) = blockingAsync { + Files.write(file, byteArray, *options) +} + + + +/** + * Performs [AsynchronousFileChannel.read] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousFileChannel.readAwait( + buf: ByteBuffer, + position: Long +) = suspendCancellableCoroutine { cont -> + read(buf, position, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +/** + * Performs [AsynchronousFileChannel.write] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousFileChannel.writeAwait( + buf: ByteBuffer, + position: Long +) = suspendCancellableCoroutine { cont -> + write(buf, position, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +/** + * Performs [AsynchronousServerSocketChannel.accept] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousServerSocketChannel.acceptAwait() = + suspendCancellableCoroutine { cont -> + accept(cont, asyncIOHandler()) + closeOnCancel(cont) + } + +/** + * Performs [AsynchronousSocketChannel.connect] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousSocketChannel.connectAwait( + socketAddress: SocketAddress +) = suspendCancellableCoroutine { cont -> + connect(socketAddress, cont, AsyncVoidIOHandler) + closeOnCancel(cont) +} + +/** + * Performs [AsynchronousSocketChannel.read] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousSocketChannel.readAwait( + buf: ByteBuffer, + timeout: Long = 0L, + timeUnit: TimeUnit = TimeUnit.MILLISECONDS +) = suspendCancellableCoroutine { cont -> + read(buf, timeout, timeUnit, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +suspend fun AsynchronousSocketChannel.readAwait( + buffers: Array, + offset: Int, + length: Int, + timeout: Long = 0L, + timeUnit: TimeUnit = TimeUnit.MILLISECONDS +) = suspendCancellableCoroutine { cont -> + read(buffers, offset, length, timeout, timeUnit, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +/** + * Performs [AsynchronousSocketChannel.write] without blocking a thread and resumes when asynchronous operation completes. + * This suspending function is cancellable. + * If the [Job] of the current coroutine cancelled or completed while this suspending function is waiting, this function + * *closes the underlying channel* and immediately resumes with [CancellationException]. + */ +suspend fun AsynchronousSocketChannel.writeAwait( + buf: ByteBuffer, + timeout: Long = 0L, + timeUnit: TimeUnit = TimeUnit.MILLISECONDS +) = suspendCancellableCoroutine { cont -> + write(buf, timeout, timeUnit, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +suspend fun AsynchronousSocketChannel.writeAwait( + buffers: Array, + offset: Int, + length: Int, + timeout: Long = 0L, + timeUnit: TimeUnit = TimeUnit.MILLISECONDS +) = suspendCancellableCoroutine { cont -> + write(buffers, offset, length, timeout, timeUnit, cont, asyncIOHandler()) + closeOnCancel(cont) +} + +// ---------------- private details ---------------- + +private fun Channel.closeOnCancel(cont: CancellableContinuation<*>) { + cont.invokeOnCancellation { + try { + close() + } catch (ex: Throwable) { + // Specification says that it is Ok to call it any time, but reality is different, + // so we have just to ignore exception + } + } +} + +@Suppress("UNCHECKED_CAST") +private fun asyncIOHandler(): CompletionHandler> = + AsyncIOHandlerAny as CompletionHandler> + +private object AsyncIOHandlerAny : CompletionHandler> { + override fun completed(result: Any, cont: CancellableContinuation) { + cont.resumeWith(Result.success(result)) + } + + override fun failed(ex: Throwable, cont: CancellableContinuation) { + // just return if already cancelled and got an expected exception for that case + if (ex is AsynchronousCloseException && cont.isCancelled) { + return + } + cont.resumeWith(Result.failure(ex)) + } +} + +private object AsyncVoidIOHandler : CompletionHandler> { + override fun completed(result: Void?, cont: CancellableContinuation) { + cont.resumeWith(Result.success(Unit)) + } + + override fun failed(ex: Throwable, cont: CancellableContinuation) { + // just return if already cancelled and got an expected exception for that case + if (ex is AsynchronousCloseException && cont.isCancelled) { + return + } + cont.resumeWith(Result.failure(ex)) + } +} diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/reflect/KotlinNameResolver.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/reflect/KotlinNameResolver.kt new file mode 100644 index 000000000..1bcd93f72 --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/reflect/KotlinNameResolver.kt @@ -0,0 +1,51 @@ +package com.fireflysource.common.reflect + +import java.lang.reflect.Modifier + +/** + * Resolves name of java classes. + * + * @author Pengtao Qiu + */ +object KotlinNameResolver { + /** + * Get class name for function by the package of the function. + * + * @param func Get class name for the function. + */ + fun name(func: () -> Unit): String { + val name = func.javaClass.name + return when { + name.contains("Kt$") -> name.substringBefore("Kt$") + name.contains("$") -> name.substringBefore("$") + else -> name + } + } + + /** + * Get class name for java class (that usually represents kotlin class) + * + * @param forClass Get the name for the class. + */ + fun name(forClass: Class): String = unwrapCompanionClass(forClass).name + + + /** + * unwrap companion class to enclosing class given a Java Class + */ + private fun unwrapCompanionClass(clazz: Class): Class<*> { + if (clazz.enclosingClass != null) { + try { + val field = clazz.enclosingClass.getField(clazz.simpleName) + if (Modifier.isStatic(field.modifiers) && field.type == clazz) { + // && field.get(null) === obj + // the above might be safer but problematic with initialization order + return clazz.enclosingClass + } + } catch (e: Exception) { + //ok, it is not a companion object + } + } + return clazz + } +} \ No newline at end of file diff --git a/firefly-common/src/main/kotlin/com/fireflysource/common/slf4j/Slf4jExtension.kt b/firefly-common/src/main/kotlin/com/fireflysource/common/slf4j/Slf4jExtension.kt new file mode 100644 index 000000000..c166dc54a --- /dev/null +++ b/firefly-common/src/main/kotlin/com/fireflysource/common/slf4j/Slf4jExtension.kt @@ -0,0 +1,95 @@ +package com.fireflysource.common.slf4j + +import com.fireflysource.common.reflect.KotlinNameResolver +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * @author Pengtao Qiu + */ +object LazyLoggerKt { + fun getLogger(func: () -> Unit): Logger = LoggerFactory.getLogger(KotlinNameResolver.name(func)) + + fun getLogger(name: String): Logger = LoggerFactory.getLogger(name) + + fun getLogger(clazz: Class<*>): Logger = LoggerFactory.getLogger(clazz) +} + +/** + * Lazy add a log message if isTraceEnabled is true + */ +inline fun Logger.trace(crossinline msg: () -> Any?) { + if (isTraceEnabled) trace(toStringSafe(msg)) +} + +/** + * Lazy add a log message if isDebugEnabled is true + */ +inline fun Logger.debug(crossinline msg: () -> Any?) { + if (isDebugEnabled) debug(toStringSafe(msg)) +} + +/** + * Lazy add a log message if isInfoEnabled is true + */ +inline fun Logger.info(crossinline msg: () -> Any?) { + if (isInfoEnabled) info(toStringSafe(msg)) +} + +/** + * Lazy add a log message if isWarnEnabled is true + */ +inline fun Logger.warn(crossinline msg: () -> Any?) { + if (isWarnEnabled) warn(toStringSafe(msg)) +} + +/** + * Lazy add a log message if isErrorEnabled is true + */ +inline fun Logger.error(crossinline msg: () -> Any?) { + if (isErrorEnabled) error(toStringSafe(msg)) +} + +/** + * Lazy add a log message with throwable payload if isTraceEnabled is true + */ +inline fun Logger.trace(throwable: Throwable, crossinline msg: () -> Any?) { + if (isTraceEnabled) trace(toStringSafe(msg), throwable) +} + +/** + * Lazy add a log message with throwable payload if isDebugEnabled is true + */ +inline fun Logger.debug(throwable: Throwable, crossinline msg: () -> Any?) { + if (isDebugEnabled) debug(toStringSafe(msg), throwable) +} + +/** + * Lazy add a log message with throwable payload if isInfoEnabled is true + */ +inline fun Logger.info(throwable: Throwable, crossinline msg: () -> Any?) { + if (isInfoEnabled) info(toStringSafe(msg), throwable) +} + +/** + * Lazy add a log message with throwable payload if isWarnEnabled is true + */ +inline fun Logger.warn(throwable: Throwable, crossinline msg: () -> Any?) { + if (isWarnEnabled) warn(toStringSafe(msg), throwable) +} + +/** + * Lazy add a log message with throwable payload if isErrorEnabled is true + */ +inline fun Logger.error(throwable: Throwable, crossinline msg: () -> Any?) { + if (isErrorEnabled) error(toStringSafe(msg), throwable) +} + + +inline fun toStringSafe(crossinline msg: () -> Any?): String { + return try { + msg.invoke().toString() + } catch (e: Exception) { + "KtLogger: get message exception: $e" + } +} \ No newline at end of file diff --git a/firefly-common/src/main/resources/firefly_version.properties b/firefly-common/src/main/resources/firefly_version.properties new file mode 100644 index 000000000..f54db2138 --- /dev/null +++ b/firefly-common/src/main/resources/firefly_version.properties @@ -0,0 +1,2 @@ +firefly.version=${firefly.version} +github.url=${github.url} \ No newline at end of file diff --git a/firefly-common/src/test/java/com/fireflysource/common/actor/TestActor.java b/firefly-common/src/test/java/com/fireflysource/common/actor/TestActor.java new file mode 100644 index 000000000..ede25fdac --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/actor/TestActor.java @@ -0,0 +1,181 @@ +package com.fireflysource.common.actor; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestActor { + + @Test + @DisplayName("should purchase products successfully") + void test() throws Exception { + StoreActor store = new StoreActor(); + stock(store, "iPhone", 5200, 5); + stock(store, "h301x", 310, 20); + + List> results = new LinkedList<>(); + results.addAll(purchase(store, "h301x", 400, 10)); + results.addAll(purchase(store, "iPhone", 6500, 9)); + + StoreActor.CloseMessage closeMessage = new StoreActor.CloseMessage(); + results.add(closeMessage.todayAmount.thenApply(amount -> { + log("Today sales amount: " + amount); + return null; + })); + store.offer(closeMessage); + + CompletableFuture.allOf(results.stream().map(r -> r.handle((ignore, throwable) -> { + Optional.ofNullable(throwable).map(Throwable::getMessage).ifPresent(System.out::println); + return ignore; + })).toArray(CompletableFuture[]::new)).join(); + assertEquals(36500L, closeMessage.todayAmount.get()); + } + + private void stock(StoreActor store, String name, long price, int count) { + IntStream.range(0, count).parallel() + .forEach(i -> store.offer(new StoreActor.StockMessage(new StoreActor.Product(name, price)))); + } + + private List> purchase(StoreActor store, String name, long price, int count) { + return IntStream.range(0, count).parallel().boxed().map(i -> { + StoreActor.PurchaseMessage purchaseMessage = new StoreActor.PurchaseMessage(new StoreActor.Product(name, price)); + store.offer(purchaseMessage); + return purchaseMessage.result.thenAccept(ignore -> log("purchase " + name + " success.")); + }).collect(Collectors.toList()); + } + + public static class StoreActor extends AbstractAsyncActor { + + public enum MessageType { + PURCHASE, STOCK, CLOSE + } + + public interface Message { + MessageType getType(); + } + + public static class Product { + public final String name; + public final long price; + + public Product(String name, long price) { + this.name = name; + this.price = price; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Product product = (Product) o; + return name.equals(product.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + public static class PurchaseMessage implements Message { + + public final Product product; + public final CompletableFuture result = new CompletableFuture<>(); + + public PurchaseMessage(Product product) { + this.product = product; + } + + @Override + public MessageType getType() { + return MessageType.PURCHASE; + } + } + + public static class StockMessage implements Message { + public final Product product; + + public StockMessage(Product product) { + this.product = product; + } + + @Override + public MessageType getType() { + return MessageType.STOCK; + } + } + + public static class CloseMessage implements Message { + public final CompletableFuture todayAmount = new CompletableFuture<>(); + + @Override + public MessageType getType() { + return MessageType.CLOSE; + } + } + + private final Map> products = new HashMap<>(); + private long amount; + + @Override + public CompletableFuture onReceiveAsync(Message message) { + switch (message.getType()) { + case STOCK: + StockMessage stockMessage = (StockMessage) message; + return stock(stockMessage.product); + case PURCHASE: + PurchaseMessage purchaseMessage = (PurchaseMessage) message; + return purchase(purchaseMessage); + case CLOSE: + shutdown(); + CloseMessage closeMessage = (CloseMessage) message; + closeMessage.todayAmount.complete(amount); + return CompletableFuture.completedFuture(null); + default: + return CompletableFuture.completedFuture(null); + } + } + + private CompletableFuture stock(Product product) { + return CompletableFuture.runAsync(() -> { + BlockingTask.sleep(100); + products.computeIfAbsent(product.name, k -> new LinkedList<>()).offer(product); + log("stock " + product.name + " success."); + }); + } + + private CompletableFuture purchase(PurchaseMessage purchaseMessage) { + return CompletableFuture.runAsync(() -> { + BlockingTask.sleep(200); + Optional orderProduct = Optional.ofNullable(products.get(purchaseMessage.product.name)).map(Queue::poll); + if (orderProduct.isPresent()) { + amount += purchaseMessage.product.price; + purchaseMessage.result.complete(null); + } else { + purchaseMessage.result.completeExceptionally(new IllegalStateException("The product sells out")); + } + }); + } + + @Override + public void onDiscard(Message message) { + log("discard message: " + message.getType()); + if (message.getType() == MessageType.PURCHASE) { + PurchaseMessage purchaseMessage = (PurchaseMessage) message; + purchaseMessage.result.completeExceptionally(new IllegalStateException("The store is close")); + } + } + } + + public static void log(String text) { + System.out.println(LocalDateTime.now().format(ISO_LOCAL_DATE_TIME) + " " + Thread.currentThread().getName() + " -- " + text); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/bytecode/TestClassProxy.java b/firefly-common/src/test/java/com/fireflysource/common/bytecode/TestClassProxy.java new file mode 100644 index 000000000..82cec1381 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/bytecode/TestClassProxy.java @@ -0,0 +1,133 @@ +package com.fireflysource.common.bytecode; + + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.InvocationTargetException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class TestClassProxy { + + static Stream parametersProvider() { + return Stream.of(arguments(JavassistClassProxyFactory.INSTANCE)); + } + + @ParameterizedTest + @MethodSource("parametersProvider") + void test(ClassProxyFactory classProxyFactory) throws Throwable { + Fee origin = new Fee(); + + Fee fee = classProxyFactory.createProxy(origin, + (handler, originalInstance, args) -> { + System.out.println("intercept method 1: " + handler.method().getName() + "|" + originalInstance.getClass().getCanonicalName()); + if (handler.method().getName().equals("testInt")) { + args[0] = 1; + } + Object ret = handler.invoke(originalInstance, args); + System.out.println("intercept method 1 end..."); + if (handler.method().getName().equals("hello")) { + ret = ret + " intercept 1"; + } + return ret; + }, null); + System.out.println(fee.getClass().getCanonicalName()); + assertEquals("hello fee intercept 1", fee.hello()); + assertEquals(1, fee.testInt(25)); + + Fee fee2 = classProxyFactory.createProxy(fee, + (handler, originalInstance, args) -> { + System.out.println("intercept method 2: " + handler.method().getName() + "|" + originalInstance.getClass().getCanonicalName()); + if (handler.method().getName().equals("testInt")) { + args[0] = 2; + } + Object ret = handler.invoke(originalInstance, args); + System.out.println("intercept method 2 end..."); + + if (handler.method().getName().equals("hello")) { + ret = ret + " intercept 2"; + } + return ret; + }, null); + System.out.println(fee2.getClass().getCanonicalName()); + assertEquals("hello fee intercept 1 intercept 2", fee2.hello()); + assertEquals(1, fee.testInt(25)); + } + + @ParameterizedTest + @MethodSource("parametersProvider") + void testFilter(ClassProxyFactory classProxyFactory) throws Throwable { + Fee origin = new Fee(); + + Fee fee = classProxyFactory.createProxy(origin, + (handler, originalInstance, args) -> { + System.out.println("filter method 1: " + handler.method().getName() + "|" + originalInstance.getClass().getCanonicalName()); + if (handler.method().getName().equals("testInt")) { + args[0] = 1; + } + Object ret = handler.invoke(originalInstance, args); + System.out.println("filter method 1 end..."); + if (handler.method().getName().equals("hello")) { + ret = ret + " filter 1"; + } + return ret; + }, method -> !method.getName().equals("testInt")); + System.out.println(fee.getClass().getCanonicalName()); + assertEquals("hello fee filter 1", fee.hello()); + assertEquals(25, fee.testInt(25)); + } + + @ParameterizedTest + @MethodSource("parametersProvider") + void testNonJavaBean(ClassProxyFactory classProxyFactory) { + assertThrows(InvocationTargetException.class, () -> { + NonJavaBean x = new NonJavaBean("test"); + NonJavaBean y = classProxyFactory.createProxy(x, + (handler, originalInstance, args) -> "no java bean", + method -> method.getName().equals("getHello")); + System.out.println(y.getHello()); + }); + } + + public static class Fee { + protected void testProtected() { + } + + public void testVoid(String str, Long l) { + } + + public int testInt(int i) { + return i; + } + + public Void testParameters(String str, int i, Long l) { + return null; + } + + public String hello() { + return "hello fee"; + } + } + + public static class NonJavaBean { + String hello; + + public NonJavaBean(String hello) { + this.hello = hello; + } + + public String getHello() { + return hello; + } + + public void setHello(String hello) { + this.hello = hello; + } + } + +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/bytecode/TestProxyFactory.java b/firefly-common/src/test/java/com/fireflysource/common/bytecode/TestProxyFactory.java new file mode 100644 index 000000000..ea026367a --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/bytecode/TestProxyFactory.java @@ -0,0 +1,167 @@ +package com.fireflysource.common.bytecode; + + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Field; +import java.util.stream.Stream; + +import static com.fireflysource.common.reflection.ReflectionUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + + +/** + * @author Pengtao Qiu + */ +public class TestProxyFactory { + + static Stream parametersProvider() { + return Stream.of( + arguments(JavaReflectionProxyFactory.INSTANCE), + arguments(JavassistReflectionProxyFactory.INSTANCE) + ); + } + + @ParameterizedTest + @MethodSource("parametersProvider") + void testProxyMethod(ProxyFactory proxyFactory) throws Throwable { + Foo foo = new Foo(); + MethodProxy proxy = proxyFactory.getMethodProxy(Foo.class.getMethod("setProperty", String.class, boolean.class)); + assertNull(proxy.invoke(foo, "proxy foo", true)); + assertEquals("proxy foo", foo.getName()); + assertTrue(foo.isFailure()); + + proxy = proxyFactory.getMethodProxy(getGetterMethod(Foo.class, "name")); + assertEquals("proxy foo", proxy.invoke(foo)); + + proxy = proxyFactory.getMethodProxy(getGetterMethod(Foo.class, "failure")); + assertTrue((Boolean) proxy.invoke(foo)); + + proxy = proxyFactory.getMethodProxy(getSetterMethod(Foo.class, "price")); + assertNull(proxy.invoke(foo, 35.5)); + assertEquals(35.5, foo.getPrice()); + } + + @ParameterizedTest + @MethodSource("parametersProvider") + void testArray(ProxyFactory proxyFactory) { + int[] intArr = new int[5]; + Integer[] intArr2 = new Integer[10]; + + ArrayProxy intArrProxy = proxyFactory.getArrayProxy(intArr.getClass()); + ArrayProxy intArr2Proxy = proxyFactory.getArrayProxy(intArr2.getClass()); + + assertEquals(5, intArrProxy.size(intArr)); + assertEquals(10, intArr2Proxy.size(intArr2)); + + intArrProxy.set(intArr, 0, 33); + assertEquals(33, intArrProxy.get(intArr, 0)); + + intArr2Proxy.set(intArr2, intArr2.length - 1, 55); + assertEquals(55, intArr2Proxy.get(intArr2, 9)); + + intArrProxy.set(intArr, 1, 23); + assertEquals(23, intArrProxy.get(intArr, 1)); + + intArr2Proxy.set(intArr2, intArr2.length - 1, 65); + assertEquals(65, intArr2Proxy.get(intArr2, 9)); + } + + @ParameterizedTest + @MethodSource("parametersProvider") + void testProxyField(ProxyFactory proxyFactory) throws Throwable { + Foo foo = new Foo(); + Field num2 = Foo.class.getField("num2"); + Field info = Foo.class.getField("info"); + + FieldProxy proxyNum2 = proxyFactory.getFieldProxy(num2); + proxyNum2.set(foo, 30); + assertEquals(30, proxyNum2.get(foo)); + + FieldProxy proxyInfo = proxyFactory.getFieldProxy(info); + proxyInfo.set(foo, "test info 0"); + assertEquals("test info 0", proxyInfo.get(foo)); + + setProperty(foo, "name", "hello"); + assertEquals("hello", getProperty(foo, "name")); + + + Foo foo2 = new Foo(); + + proxyNum2 = proxyFactory.getFieldProxy(num2); + proxyNum2.set(foo2, 303); + assertEquals(303, proxyNum2.get(foo2)); + + proxyInfo = proxyFactory.getFieldProxy(info); + proxyInfo.set(foo2, "test info 03"); + assertEquals("test info 03", proxyInfo.get(foo2)); + } + + public static class Foo { + public String name; + public int num2; + public String info; + private boolean failure; + private int number; + private double price; + private String iPhone; + private boolean iPad; + + public String getiPhone() { + return iPhone; + } + + public void setiPhone(String iPhone) { + this.iPhone = iPhone; + } + + public boolean isiPad() { + return iPad; + } + + public void setiPad(boolean iPad) { + this.iPad = iPad; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public boolean isFailure() { + return failure; + } + + public void setFailure(boolean failure) { + this.failure = failure; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setProperty(String name, boolean failure) { + this.name = name; + this.failure = failure; + } + + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/codec/TestBase64.java b/firefly-common/src/test/java/com/fireflysource/common/codec/TestBase64.java new file mode 100644 index 000000000..9528b656e --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/codec/TestBase64.java @@ -0,0 +1,28 @@ +package com.fireflysource.common.codec; + +import com.fireflysource.common.codec.base64.Base64Utils; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestBase64 { + + @Test + void test() { + byte[] hello = "hello world".getBytes(StandardCharsets.UTF_8); + String base64 = Base64Utils.encodeToString(hello); + String src = new String(Base64Utils.decodeFromString(base64), StandardCharsets.UTF_8); + assertEquals("hello world", src); + } + + @Test + void testSafeUrl() { + byte[] safeUrl = "http://www.fireflysource.com/base64/test?id=测试".getBytes(StandardCharsets.UTF_8); + String base64 = Base64Utils.encodeToUrlSafeString(safeUrl); + System.out.println(base64); + String src = new String(Base64Utils.decodeFromUrlSafeString(base64), StandardCharsets.UTF_8); + assertEquals("http://www.fireflysource.com/base64/test?id=测试", src); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/collection/TestCollectionUtils.java b/firefly-common/src/test/java/com/fireflysource/common/collection/TestCollectionUtils.java new file mode 100644 index 000000000..cb13813f7 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/collection/TestCollectionUtils.java @@ -0,0 +1,70 @@ +package com.fireflysource.common.collection; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Set; + +import static com.fireflysource.common.collection.CollectionUtils.newHashSet; +import static org.junit.jupiter.api.Assertions.*; + +class TestCollectionUtils { + + @Test + void testIsEmpty() { + assertTrue(CollectionUtils.isEmpty(Collections.emptyMap())); + assertTrue(CollectionUtils.isEmpty(Collections.emptyList())); + assertTrue(CollectionUtils.isEmpty(new HashMap<>())); + Set map = null; + assertTrue(CollectionUtils.isEmpty(map)); + } + + @Test + void testIntersection() { + Set a = newHashSet(2, 3, 4); + Set b = newHashSet(3, 4, 5, 6); + Set result = CollectionUtils.intersect(a, b); + + assertEquals(3, a.size()); + assertEquals(4, b.size()); + assertEquals(newHashSet(3, 4), result); + } + + @Test + void testIntersectionEmpty() { + Set a = newHashSet(2, 3, 4); + Set b = newHashSet(5, 6); + Set result = CollectionUtils.intersect(a, b); + assertTrue(result.isEmpty()); + } + + @Test + void testHasIntersection() { + Set a = newHashSet(2, 3, 4); + Set b = newHashSet(3, 4, 5, 6); + assertTrue(CollectionUtils.hasIntersection(a, b)); + } + + @Test + void testNoIntersection() { + Set a = newHashSet(1, 2, 3, 4); + Set b = newHashSet(5, 6); + assertFalse(CollectionUtils.hasIntersection(a, b)); + assertFalse(CollectionUtils.hasIntersection(a, newHashSet())); + assertFalse(CollectionUtils.hasIntersection(newHashSet(), b)); + assertFalse(CollectionUtils.hasIntersection(newHashSet(), newHashSet())); + } + + @Test + void testUnion() { + Set a = newHashSet(2, 3, 4); + Set b = newHashSet(3, 4, 5, 6); + Set result = CollectionUtils.union(a, b); + assertEquals(newHashSet(2, 3, 4, 5, 6), result); + + result = CollectionUtils.union(a, newHashSet()); + assertEquals(a, result); + } + +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/collection/list/TestLazyList.java b/firefly-common/src/test/java/com/fireflysource/common/collection/list/TestLazyList.java new file mode 100644 index 000000000..3fed43392 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/collection/list/TestLazyList.java @@ -0,0 +1,1968 @@ +package com.fireflysource.common.collection.list; + +import com.fireflysource.common.collection.array.ArrayUtils; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + + +/** + * Tests for LazyList utility class. + */ +class TestLazyList { + private static final boolean STRICT = false; + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_NullInput_NullItem() { + Object list = LazyList.add(null, null); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + } + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_NullInput_NonListItem() { + String item = "a"; + Object list = LazyList.add(null, item); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + } + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_NullInput_LazyListItem() { + Object item = LazyList.add(null, "x"); + item = LazyList.add(item, "y"); + item = LazyList.add(item, "z"); + + Object list = LazyList.add(null, item); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + + Object val = LazyList.get(list, 0); + assertTrue(val instanceof List); + } + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_NonListInput() { + String input = "a"; + + Object list = LazyList.add(input, "b"); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(2, LazyList.size(list)); + } + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_LazyListInput() { + Object input = LazyList.add(null, "a"); + + Object list = LazyList.add(input, "b"); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + + list = LazyList.add(list, "c"); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_GenericListInput() { + List input = new ArrayList<>(); + input.add("a"); + + Object list = LazyList.add(input, "b"); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + + list = LazyList.add(list, "c"); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#add(Object, Object)} + */ + @Test + void testAddObjectObject_AddNull() { + Object list = null; + list = LazyList.add(list, null); + assertEquals(1, LazyList.size(list)); + assertNull(LazyList.get(list, 0)); + + list = "a"; + list = LazyList.add(list, null); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertNull(LazyList.get(list, 1)); + + list = LazyList.add(list, null); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertNull(LazyList.get(list, 1)); + assertNull(LazyList.get(list, 2)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NullInput_NullItem() { + Object list = LazyList.add(null, 0, null); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NullInput_NonListItem() { + String item = "a"; + Object list = LazyList.add(null, 0, item); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NullInput_NonListItem2() { + assumeTrue(STRICT); // Only run in STRICT mode. + + String item = "a"; + // Test branch of logic "index>0" + Object list = LazyList.add(null, 1, item); // Always throws exception? + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NullInput_LazyListItem() { + Object item = LazyList.add(null, "x"); + item = LazyList.add(item, "y"); + item = LazyList.add(item, "z"); + + Object list = LazyList.add(null, 0, item); + assertNotNull(list); + assertEquals(1, LazyList.size(list)); + + Object val = LazyList.get(list, 0); + assertTrue(val instanceof List); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NullInput_GenericListItem() { + List item = new ArrayList<>(); + item.add("a"); + + Object list = LazyList.add(null, 0, item); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NonListInput_NullItem() { + String input = "a"; + + Object list = LazyList.add(input, 0, null); + assertNotNull(list); + assertEquals(2, LazyList.size(list)); + assertNull(LazyList.get(list, 0)); + assertEquals("a", LazyList.get(list, 1)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_NonListInput_NonListItem() { + String input = "a"; + String item = "b"; + + Object list = LazyList.add(input, 0, item); + assertNotNull(list); + assertEquals(2, LazyList.size(list)); + assertEquals("b", LazyList.get(list, 0)); + assertEquals("a", LazyList.get(list, 1)); + } + + /** + * Test for {@link LazyList#add(Object, int, Object)} + */ + @Test + void testAddObjectIntObject_LazyListInput() { + Object list = LazyList.add(null, "c"); // [c] + list = LazyList.add(list, 0, "a"); // [a, c] + list = LazyList.add(list, 1, "b"); // [a, b, c] + list = LazyList.add(list, 3, "d"); // [a, b, c, d] + + assertEquals(4, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + assertEquals("d", LazyList.get(list, 3)); + } + + /** + * Test for {@link LazyList#addCollection(Object, Collection)} + */ + @Test + void testAddCollection_NullInput() { + Collection coll = Arrays.asList("a", "b", "c"); + + Object list = LazyList.addCollection(null, coll); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Test for {@link LazyList#addCollection(Object, Collection)} + */ + @Test + void testAddCollection_NonListInput() { + Collection coll = Arrays.asList("a", "b", "c"); + String input = "z"; + + Object list = LazyList.addCollection(input, coll); + assertTrue(list instanceof List); + assertEquals(4, LazyList.size(list)); + assertEquals("z", LazyList.get(list, 0)); + assertEquals("a", LazyList.get(list, 1)); + assertEquals("b", LazyList.get(list, 2)); + assertEquals("c", LazyList.get(list, 3)); + } + + /** + * Test for {@link LazyList#addCollection(Object, Collection)} + */ + @Test + void testAddCollection_LazyListInput() { + Collection coll = Arrays.asList("a", "b", "c"); + + Object input = LazyList.add(null, "x"); + input = LazyList.add(input, "y"); + input = LazyList.add(input, "z"); + + Object list = LazyList.addCollection(input, coll); + assertTrue(list instanceof List); + assertEquals(6, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + assertEquals("a", LazyList.get(list, 3)); + assertEquals("b", LazyList.get(list, 4)); + assertEquals("c", LazyList.get(list, 5)); + } + + /** + * Test for {@link LazyList#addCollection(Object, Collection)} + */ + @Test + void testAddCollection_GenricListInput() { + Collection coll = Arrays.asList("a", "b", "c"); + + List input = new ArrayList(); + input.add("x"); + input.add("y"); + input.add("z"); + + Object list = LazyList.addCollection(input, coll); + assertTrue(list instanceof List); + assertEquals(6, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + assertEquals("a", LazyList.get(list, 3)); + assertEquals("b", LazyList.get(list, 4)); + assertEquals("c", LazyList.get(list, 5)); + } + + /** + * Test for {@link LazyList#addCollection(Object, Collection)} + */ + @Test + void testAddCollection_Sequential() { + Collection coll = Arrays.asList("a", "b"); + + Object list = null; + list = LazyList.addCollection(list, coll); + list = LazyList.addCollection(list, coll); + + assertEquals(4, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("a", LazyList.get(list, 2)); + assertEquals("b", LazyList.get(list, 3)); + } + + /** + * Test for {@link LazyList#addCollection(Object, Collection)} + */ + @Test + void testAddCollection_GenericListInput() { + List l = new ArrayList<>(); + l.add("a"); + l.add("b"); + + Object list = null; + list = LazyList.addCollection(list, l); + list = LazyList.addCollection(list, l); + + assertEquals(4, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("a", LazyList.get(list, 2)); + assertEquals("b", LazyList.get(list, 3)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NullInput_NullArray() { + String[] arr = null; + Object list = LazyList.addArray(null, arr); + assertNull(list); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NullInput_EmptyArray() { + String[] arr = new String[0]; + Object list = LazyList.addArray(null, arr); + if (STRICT) { + assertNotNull(list); + assertTrue(list instanceof List); + } + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NullInput_SingleArray() { + String[] arr = new String[]{"a"}; + Object list = LazyList.addArray(null, arr); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NullInput_Array() { + String[] arr = new String[]{"a", "b", "c"}; + Object list = LazyList.addArray(null, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NonListInput_NullArray() { + String input = "z"; + String[] arr = null; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + assertEquals("z", LazyList.get(list, 0)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NonListInput_EmptyArray() { + String input = "z"; + String[] arr = new String[0]; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + assertEquals("z", LazyList.get(list, 0)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NonListInput_SingleArray() { + String input = "z"; + String[] arr = new String[]{"a"}; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(2, LazyList.size(list)); + assertEquals("z", LazyList.get(list, 0)); + assertEquals("a", LazyList.get(list, 1)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_NonListInput_Array() { + String input = "z"; + String[] arr = new String[]{"a", "b", "c"}; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(4, LazyList.size(list)); + assertEquals("z", LazyList.get(list, 0)); + assertEquals("a", LazyList.get(list, 1)); + assertEquals("b", LazyList.get(list, 2)); + assertEquals("c", LazyList.get(list, 3)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_LazyListInput_NullArray() { + Object input = LazyList.add(null, "x"); + input = LazyList.add(input, "y"); + input = LazyList.add(input, "z"); + + String[] arr = null; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_LazyListInput_EmptyArray() { + Object input = LazyList.add(null, "x"); + input = LazyList.add(input, "y"); + input = LazyList.add(input, "z"); + + String[] arr = new String[0]; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_LazyListInput_SingleArray() { + Object input = LazyList.add(null, "x"); + input = LazyList.add(input, "y"); + input = LazyList.add(input, "z"); + + String[] arr = new String[]{"a"}; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(4, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + assertEquals("a", LazyList.get(list, 3)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_LazyListInput_Array() { + Object input = LazyList.add(null, "x"); + input = LazyList.add(input, "y"); + input = LazyList.add(input, "z"); + + String[] arr = new String[]{"a", "b", "c"}; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(6, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + assertEquals("a", LazyList.get(list, 3)); + assertEquals("b", LazyList.get(list, 4)); + assertEquals("c", LazyList.get(list, 5)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_GenericListInput_NullArray() { + List input = new ArrayList(); + input.add("x"); + input.add("y"); + input.add("z"); + + String[] arr = null; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_GenericListInput_EmptyArray() { + List input = new ArrayList<>(); + input.add("x"); + input.add("y"); + input.add("z"); + + String[] arr = new String[0]; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_GenericListInput_SingleArray() { + List input = new ArrayList(); + input.add("x"); + input.add("y"); + input.add("z"); + + String[] arr = new String[]{"a"}; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(4, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + assertEquals("a", LazyList.get(list, 3)); + } + + /** + * Tests for {@link LazyList#addArray(Object, Object[])} + */ + @Test + void testAddArray_GenericListInput_Array() { + List input = new ArrayList<>(); + input.add("x"); + input.add("y"); + input.add("z"); + + String[] arr = new String[]{"a", "b", "c"}; + Object list = LazyList.addArray(input, arr); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(6, LazyList.size(list)); + assertEquals("x", LazyList.get(list, 0)); + assertEquals("y", LazyList.get(list, 1)); + assertEquals("z", LazyList.get(list, 2)); + assertEquals("a", LazyList.get(list, 3)); + assertEquals("b", LazyList.get(list, 4)); + assertEquals("c", LazyList.get(list, 5)); + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_NullInput() { + Object list = LazyList.ensureSize(null, 10); + assertNotNull(list); + assertTrue(list instanceof List); + // Not possible to test for List capacity value. + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_NonListInput() { + String input = "a"; + Object list = LazyList.ensureSize(input, 10); + assertNotNull(list); + assertTrue(list instanceof List); + // Not possible to test for List capacity value. + assertEquals(1, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + + Object list = LazyList.ensureSize(input, 10); + assertNotNull(list); + assertTrue(list instanceof List); + // Not possible to test for List capacity value. + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_GenericListInput() { + List input = new ArrayList<>(); + input.add("a"); + input.add("b"); + + Object list = LazyList.ensureSize(input, 10); + assertNotNull(list); + assertTrue(list instanceof List); + // Not possible to test for List capacity value. + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_GenericListInput_LinkedList() { + assumeTrue(STRICT); // Only run in STRICT mode. + + // Using LinkedList concrete type as LazyList internal + // implementation does not look for this specifically. + List input = new LinkedList(); + input.add("a"); + input.add("b"); + + Object list = LazyList.ensureSize(input, 10); + assertNotNull(list); + assertTrue(list instanceof List); + // Not possible to test for List capacity value. + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_Growth() { + List l = new ArrayList<>(); + l.add("a"); + l.add("b"); + l.add("c"); + + // NOTE: Testing for object equality might be viewed as + // fragile by most developers, however, for this + // specific implementation, we don't want the + // provided list to change if the size requirements + // have been met. + + // Trigger growth + Object ret = LazyList.ensureSize(l, 10); + assertNotSame(ret, l); + + // Growth not neeed. + ret = LazyList.ensureSize(l, 1); + assertSame(ret, l); + } + + /** + * Tests for {@link LazyList#ensureSize(Object, int)} + */ + @Test + void testEnsureSize_Growth_LinkedList() { + assumeTrue(STRICT); // Only run in STRICT mode. + + // Using LinkedList concrete type as LazyList internal + // implementation has not historically looked for this + // specifically. + List l = new LinkedList<>(); + l.add("a"); + l.add("b"); + l.add("c"); + + // NOTE: Testing for object equality might be viewed as + // fragile by most developers, however, for this + // specific implementation, we don't want the + // provided list to change if the size requirements + // have been met. + + // Trigger growth + Object ret = LazyList.ensureSize(l, 10); + assertTrue(ret != l); + + // Growth not neeed. + ret = LazyList.ensureSize(l, 1); + assertTrue(ret == l); + } + + /** + * Test for {@link LazyList#remove(Object, Object)} + */ + @Test + void testRemoveObjectObject_NullInput() { + Object input = null; + + assertNull(LazyList.remove(input, null)); + assertNull(LazyList.remove(input, "a")); + assertNull(LazyList.remove(input, new ArrayList<>())); + assertNull(LazyList.remove(input, Integer.valueOf(42))); + } + + /** + * Test for {@link LazyList#remove(Object, Object)} + */ + @Test + void testRemoveObjectObject_NonListInput() { + String input = "a"; + + // Remove null item + Object list = LazyList.remove(input, null); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + + // Remove item that doesn't exist + list = LazyList.remove(input, "b"); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + + // Remove item that exists + list = LazyList.remove(input, "a"); + // should this be null? or an empty list? + assertNull(list); // nothing left in list + assertEquals(0, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#remove(Object, Object)} + */ + @Test + void testRemoveObjectObject_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + // Remove null item + Object list = LazyList.remove(input, null); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + + // Attempt to remove something that doesn't exist + list = LazyList.remove(input, "z"); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + + // Remove something that exists in input + list = LazyList.remove(input, "b"); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("c", LazyList.get(list, 1)); + } + + /** + * Test for {@link LazyList#remove(Object, Object)} + */ + @Test + void testRemoveObjectObject_GenericListInput() { + List input = new ArrayList<>(); + input.add("a"); + input.add("b"); + input.add("c"); + + // Remove null item + Object list = LazyList.remove(input, null); + assertNotNull(list); + assertTrue(list instanceof List); + assertTrue(input == list); + assertEquals(3, LazyList.size(list)); + + // Attempt to remove something that doesn't exist + list = LazyList.remove(input, "z"); + assertNotNull(list); + assertTrue(list instanceof List); + assertTrue(input == list); + assertEquals(3, LazyList.size(list)); + + // Remove something that exists in input + list = LazyList.remove(input, "b"); + assertNotNull(list); + assertTrue(list instanceof List); + assertTrue(input == list); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("c", LazyList.get(list, 1)); + + // Try to remove the rest. + list = LazyList.remove(list, "a"); + list = LazyList.remove(list, "c"); + assertNull(list); + } + + /** + * Test for {@link LazyList#remove(Object, Object)} + */ + @Test + void testRemoveObjectObject_LinkedListInput() { + // Should be able to use any collection object. + List input = new LinkedList<>(); + input.add("a"); + input.add("b"); + input.add("c"); + + // Remove null item + Object list = LazyList.remove(input, null); + assertNotNull(list); + assertTrue(list instanceof List); + assertTrue(input == list); + assertEquals(3, LazyList.size(list)); + + // Attempt to remove something that doesn't exist + list = LazyList.remove(input, "z"); + assertNotNull(list); + assertTrue(list instanceof List); + assertTrue(input == list); + assertEquals(3, LazyList.size(list)); + + // Remove something that exists in input + list = LazyList.remove(input, "b"); + assertNotNull(list); + assertTrue(list instanceof List); + assertTrue(input == list); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("c", LazyList.get(list, 1)); + } + + /** + * Tests for {@link LazyList#remove(Object, int)} + */ + @Test + void testRemoveObjectInt_NullInput() { + Object input = null; + + assertNull(LazyList.remove(input, 0)); + assertNull(LazyList.remove(input, 2)); + assertNull(LazyList.remove(input, -2)); + } + + /** + * Tests for {@link LazyList#remove(Object, int)} + */ + @Test + void testRemoveObjectInt_NonListInput() { + String input = "a"; + + // Invalid index + Object list = LazyList.remove(input, 1); + assertNotNull(list); + if (STRICT) { + assertTrue(list instanceof List); + } + assertEquals(1, LazyList.size(list)); + + // Valid index + list = LazyList.remove(input, 0); + // should this be null? or an empty list? + assertNull(list); // nothing left in list + assertEquals(0, LazyList.size(list)); + } + + /** + * Tests for {@link LazyList#remove(Object, int)} + */ + @Test + void testRemoveObjectInt_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + Object list = null; + + if (STRICT) { + // Invalid index + // Shouldn't cause a IndexOutOfBoundsException as this is not the + // same behavior you experience in testRemoveObjectInt_NonListInput + // and + // testRemoveObjectInt_NullInput when using invalid indexes. + list = LazyList.remove(input, 5); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + } + + // Valid index + list = LazyList.remove(input, 1); // remove the 'b' + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("c", LazyList.get(list, 1)); + } + + /** + * Tests for {@link LazyList#remove(Object, int)} + */ + @Test + void testRemoveObjectInt_GenericListInput() { + List input = new ArrayList(); + input.add("a"); + input.add("b"); + input.add("c"); + + Object list = null; + + if (STRICT) { + // Invalid index + // Shouldn't cause a IndexOutOfBoundsException as this is not the + // same behavior you experience in testRemoveObjectInt_NonListInput + // and + // testRemoveObjectInt_NullInput when using invalid indexes. + list = LazyList.remove(input, 5); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + } + + // Valid index + list = LazyList.remove(input, 1); // remove the 'b' + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(2, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("c", LazyList.get(list, 1)); + + // Remove the rest + list = LazyList.remove(list, 0); // the 'a' + list = LazyList.remove(list, 0); // the 'c' + assertNull(list); + } + + /** + * Test for {@link LazyList#getList(Object)} + */ + @Test + void testGetListObject_NullInput() { + Object input = null; + + Object list = LazyList.getList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(0, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#getList(Object)} + */ + @Test + void testGetListObject_NonListInput() { + String input = "a"; + + Object list = LazyList.getList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + } + + /** + * Test for {@link LazyList#getList(Object)} + */ + @Test + void testGetListObject_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + Object list = LazyList.getList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Test for {@link LazyList#getList(Object)} + */ + @Test + void testGetListObject_GenericListInput() { + List input = new ArrayList(); + input.add("a"); + input.add("b"); + input.add("c"); + + Object list = LazyList.getList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Test for {@link LazyList#getList(Object)} + */ + @Test + void testGetListObject_LinkedListInput() { + List input = new LinkedList(); + input.add("a"); + input.add("b"); + input.add("c"); + + Object list = LazyList.getList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Test for {@link LazyList#getList(Object)} + */ + @Test + void testGetListObject_NullForEmpty() { + assertNull(LazyList.getList(null, true)); + assertNotNull(LazyList.getList(null, false)); + } + + /** + * Tests for {@link LazyList#toStringArray(Object)} + */ + @Test + @SuppressWarnings("unchecked") + void testToStringArray() { + assertEquals(0, LazyList.toStringArray(null).length); + + assertEquals(1, LazyList.toStringArray("a").length); + assertEquals("a", LazyList.toStringArray("a")[0]); + + @SuppressWarnings("rawtypes") + ArrayList l = new ArrayList(); + l.add("a"); + l.add(null); + l.add(new Integer(2)); + String[] a = LazyList.toStringArray(l); + + assertEquals(3, a.length); + assertEquals("a", a[0]); + assertEquals(null, a[1]); + assertEquals("2", a[2]); + + } + + /** + * Tests for {@link LazyList#toArray(Object, Class)} + */ + @Test + void testToArray_NullInput_Object() { + Object input = null; + + Object arr = LazyList.toArray(input, Object.class); + assertNotNull(arr); + assertTrue(arr.getClass().isArray()); + } + + /** + * Tests for {@link LazyList#toArray(Object, Class)} + */ + @Test + void testToArray_NullInput_String() { + String input = null; + + Object arr = LazyList.toArray(input, String.class); + assertNotNull(arr); + assertTrue(arr.getClass().isArray()); + assertTrue(arr instanceof String[]); + } + + /** + * Tests for {@link LazyList#toArray(Object, Class)} + */ + @Test + void testToArray_NonListInput() { + String input = "a"; + + Object arr = LazyList.toArray(input, String.class); + assertNotNull(arr); + assertTrue(arr.getClass().isArray()); + assertTrue(arr instanceof String[]); + + String[] strs = (String[]) arr; + assertEquals(1, strs.length); + assertEquals("a", strs[0]); + } + + /** + * Tests for {@link LazyList#toArray(Object, Class)} + */ + @Test + void testToArray_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + Object arr = LazyList.toArray(input, String.class); + assertNotNull(arr); + assertTrue(arr.getClass().isArray()); + assertTrue(arr instanceof String[]); + + String[] strs = (String[]) arr; + assertEquals(3, strs.length); + assertEquals("a", strs[0]); + assertEquals("b", strs[1]); + assertEquals("c", strs[2]); + } + + /** + * Tests for {@link LazyList#toArray(Object, Class)} + */ + @Test + void testToArray_LazyListInput_Primitives() { + Object input = LazyList.add(null, 22); + input = LazyList.add(input, 333); + input = LazyList.add(input, 4444); + input = LazyList.add(input, 55555); + + Object arr = LazyList.toArray(input, int.class); + assertNotNull(arr); + assertTrue(arr.getClass().isArray()); + assertTrue(arr instanceof int[]); + + int[] nums = (int[]) arr; + assertEquals(4, nums.length); + assertEquals(22, nums[0]); + assertEquals(333, nums[1]); + assertEquals(4444, nums[2]); + assertEquals(55555, nums[3]); + } + + /** + * Tests for {@link LazyList#toArray(Object, Class)} + */ + @Test + void testToArray_GenericListInput() { + List input = new ArrayList(); + input.add("a"); + input.add("b"); + input.add("c"); + + Object arr = LazyList.toArray(input, String.class); + assertNotNull(arr); + assertTrue(arr.getClass().isArray()); + assertTrue(arr instanceof String[]); + + String[] strs = (String[]) arr; + assertEquals(3, strs.length); + assertEquals("a", strs[0]); + assertEquals("b", strs[1]); + assertEquals("c", strs[2]); + } + + /** + * Tests for {@link LazyList#size(Object)} + */ + @Test + void testSize_NullInput() { + assertEquals(0, LazyList.size(null)); + } + + /** + * Tests for {@link LazyList#size(Object)} + */ + @Test + void testSize_NonListInput() { + String input = "a"; + assertEquals(1, LazyList.size(input)); + } + + /** + * Tests for {@link LazyList#size(Object)} + */ + @Test + void testSize_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + + assertEquals(2, LazyList.size(input)); + + input = LazyList.add(input, "c"); + + assertEquals(3, LazyList.size(input)); + } + + /** + * Tests for {@link LazyList#size(Object)} + */ + @Test + void testSize_GenericListInput() { + List input = new ArrayList(); + + assertEquals(0, LazyList.size(input)); + + input.add("a"); + input.add("b"); + + assertEquals(2, LazyList.size(input)); + + input.add("c"); + + assertEquals(3, LazyList.size(input)); + } + + /** + * Tests for bad input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_OutOfBounds_NullInput() { + assertThrows(IndexOutOfBoundsException.class, () -> { + LazyList.get(null, 0); // Should Fail due to null input + }); + } + + /** + * Tests for bad input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_OutOfBounds_NonListInput() { + assertThrows(IndexOutOfBoundsException.class, () -> { + String input = "a"; + LazyList.get(input, 1); // Should Fail + }); + } + + /** + * Tests for bad input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_OutOfBounds_LazyListInput() { + assertThrows(IndexOutOfBoundsException.class, () -> { + Object input = LazyList.add(null, "a"); + LazyList.get(input, 1); // Should Fail + }); + } + + /** + * Tests for bad input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_OutOfBounds_GenericListInput() { + assertThrows(IndexOutOfBoundsException.class, () -> { + List input = new ArrayList<>(); + input.add("a"); + LazyList.get(input, 1); // Should Fail + }); + } + + /** + * Tests for non-list input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_NonListInput() { + String input = "a"; + assertEquals("a", LazyList.get(input, 0)); + } + + /** + * Tests for list input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_LazyListInput() { + Object input = LazyList.add(null, "a"); + assertEquals("a", LazyList.get(input, 0)); + } + + /** + * Tests for list input on {@link LazyList#get(Object, int)} + */ + @Test + void testGet_GenericListInput() { + List input = new ArrayList<>(); + input.add("a"); + assertEquals("a", LazyList.get(input, 0)); + + List uris = new ArrayList<>(); + uris.add(URI.create("http://www.abc.org/")); + uris.add(URI.create("http://www.cde.ort/firefly/")); + uris.add(URI.create("http://www.fgh.com/firefly/")); + uris.add(URI.create("http://www.fireflysource.com/")); + + // Make sure that Generics pass through the 'get' routine safely. + // We should be able to call this without casting the result to URI + URI eclipseUri = LazyList.get(uris, 3); + assertEquals("http://www.fireflysource.com/", eclipseUri.toASCIIString()); + } + + /** + * Tests for {@link LazyList#contains(Object, Object)} + */ + @Test + void testContains_NullInput() { + assertFalse(LazyList.contains(null, "z")); + } + + /** + * Tests for {@link LazyList#contains(Object, Object)} + */ + @Test + void testContains_NonListInput() { + String input = "a"; + assertFalse(LazyList.contains(input, "z")); + assertTrue(LazyList.contains(input, "a")); + } + + /** + * Tests for {@link LazyList#contains(Object, Object)} + */ + @Test + void testContains_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + assertFalse(LazyList.contains(input, "z")); + assertTrue(LazyList.contains(input, "a")); + assertTrue(LazyList.contains(input, "b")); + } + + /** + * Tests for {@link LazyList#contains(Object, Object)} + */ + @Test + void testContains_GenericListInput() { + List input = new ArrayList<>(); + input.add("a"); + input.add("b"); + input.add("c"); + + assertFalse(LazyList.contains(input, "z")); + assertTrue(LazyList.contains(input, "a")); + assertTrue(LazyList.contains(input, "b")); + } + + /** + * Tests for {@link LazyList#clone(Object)} + */ + @Test + void testClone_NullInput() { + Object input = null; + + Object list = LazyList.clone(input); + assertNull(list); + } + + /** + * Tests for {@link LazyList#clone(Object)} + */ + @Test + void testClone_NonListInput() { + String input = "a"; + + Object list = LazyList.clone(input); + assertNotNull(list); + assertSame(input, list); + } + + /** + * Tests for {@link LazyList#clone(Object)} + */ + @Test + void testClone_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + Object list = LazyList.clone(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertNotSame(input, list); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#clone(Object)} + */ + @Test + void testClone_GenericListInput() { + List input = new ArrayList<>(); + input.add("a"); + input.add("b"); + input.add("c"); + + // decorate the .clone(Object) method to return + // the same generic object element type + Object list = LazyList.clone(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertNotSame(input, list); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link LazyList#toString(Object)} + */ + @Test + void testToString_NullInput() { + Object input = null; + assertEquals("[]", LazyList.toString(input)); + } + + /** + * Tests for {@link LazyList#toString(Object)} + */ + @Test + void testToString_NonListInput() { + String input = "a"; + assertEquals("[a]", LazyList.toString(input)); + } + + /** + * Tests for {@link LazyList#toString(Object)} + */ + @Test + void testToString_LazyListInput() { + Object input = LazyList.add(null, "a"); + + assertEquals("[a]", LazyList.toString(input)); + + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + assertEquals("[a, b, c]", LazyList.toString(input)); + } + + /** + * Tests for {@link LazyList#toString(Object)} + */ + @Test + void testToString_GenericListInput() { + List input = new ArrayList(); + input.add("a"); + + assertEquals("[a]", LazyList.toString(input)); + + input.add("b"); + input.add("c"); + + assertEquals("[a, b, c]", LazyList.toString(input)); + } + + /** + * Tests for {@link LazyList#iterator(Object)} + */ + @Test + void testIterator_NullInput() { + Iterator iter = LazyList.iterator(null); + assertNotNull(iter); + assertFalse(iter.hasNext()); + } + + /** + * Tests for {@link LazyList#iterator(Object)} + */ + @Test + void testIterator_NonListInput() { + String input = "a"; + + Iterator iter = LazyList.iterator(input); + assertNotNull(iter); + assertTrue(iter.hasNext()); + assertEquals("a", iter.next()); + assertFalse(iter.hasNext()); + } + + /** + * Tests for {@link LazyList#iterator(Object)} + */ + @Test + void testIterator_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + Iterator iter = LazyList.iterator(input); + assertNotNull(iter); + assertTrue(iter.hasNext()); + assertEquals("a", iter.next()); + assertEquals("b", iter.next()); + assertEquals("c", iter.next()); + assertFalse(iter.hasNext()); + } + + /** + * Tests for {@link LazyList#iterator(Object)} + */ + @Test + void testIterator_GenericListInput() { + List input = new ArrayList(); + input.add("a"); + input.add("b"); + input.add("c"); + + Iterator iter = LazyList.iterator(input); + assertNotNull(iter); + assertTrue(iter.hasNext()); + assertEquals("a", iter.next()); + assertEquals("b", iter.next()); + assertEquals("c", iter.next()); + assertFalse(iter.hasNext()); + } + + /** + * Tests for {@link LazyList#listIterator(Object)} + */ + @Test + void testListIterator_NullInput() { + ListIterator iter = LazyList.listIterator(null); + assertNotNull(iter); + assertFalse(iter.hasNext()); + assertFalse(iter.hasPrevious()); + } + + /** + * Tests for {@link LazyList#listIterator(Object)} + */ + @Test + void testListIterator_NonListInput() { + String input = "a"; + + ListIterator iter = LazyList.listIterator(input); + assertNotNull(iter); + assertTrue(iter.hasNext()); + assertFalse(iter.hasPrevious()); + assertEquals("a", iter.next()); + assertFalse(iter.hasNext()); + assertTrue(iter.hasPrevious()); + } + + /** + * Tests for {@link LazyList#listIterator(Object)} + */ + @Test + void testListIterator_LazyListInput() { + Object input = LazyList.add(null, "a"); + input = LazyList.add(input, "b"); + input = LazyList.add(input, "c"); + + ListIterator iter = LazyList.listIterator(input); + assertNotNull(iter); + assertTrue(iter.hasNext()); + assertFalse(iter.hasPrevious()); + assertEquals("a", iter.next()); + assertEquals("b", iter.next()); + assertEquals("c", iter.next()); + assertFalse(iter.hasNext()); + assertTrue(iter.hasPrevious()); + assertEquals("c", iter.previous()); + assertEquals("b", iter.previous()); + assertEquals("a", iter.previous()); + assertFalse(iter.hasPrevious()); + } + + /** + * Tests for {@link LazyList#listIterator(Object)} + */ + @Test + void testListIterator_GenericListInput() { + List input = new ArrayList(); + input.add("a"); + input.add("b"); + input.add("c"); + + ListIterator iter = LazyList.listIterator(input); + assertNotNull(iter); + assertTrue(iter.hasNext()); + assertFalse(iter.hasPrevious()); + assertEquals("a", iter.next()); + assertEquals("b", iter.next()); + assertEquals("c", iter.next()); + assertFalse(iter.hasNext()); + assertTrue(iter.hasPrevious()); + assertEquals("c", iter.previous()); + assertEquals("b", iter.previous()); + assertEquals("a", iter.previous()); + assertFalse(iter.hasPrevious()); + } + + /** + * Tests for {@link ArrayUtils#asMutableList(Object[])} + */ + @Test + void testArray2List_NullInput() { + Object[] input = null; + Object list = ArrayUtils.asMutableList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(0, LazyList.size(list)); + } + + /** + * Tests for {@link ArrayUtils#asMutableList(Object[])} + */ + @Test + void testArray2List_EmptyInput() { + String[] input = new String[0]; + + Object list = ArrayUtils.asMutableList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(0, LazyList.size(list)); + } + + /** + * Tests for {@link ArrayUtils#asMutableList(Object[])} + */ + @Test + void testArray2List_SingleInput() { + String[] input = new String[]{"a"}; + + Object list = ArrayUtils.asMutableList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(1, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + } + + /** + * Tests for {@link ArrayUtils#asMutableList(Object[])} + */ + @Test + void testArray2List_MultiInput() { + String[] input = new String[]{"a", "b", "c"}; + + Object list = ArrayUtils.asMutableList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link ArrayUtils#asMutableList(Object[])} + */ + @Test + void testArray2List_GenericsInput() { + String[] input = new String[]{"a", "b", "c"}; + + // Test the Generics definitions for array2List + List list = ArrayUtils.asMutableList(input); + assertNotNull(list); + assertTrue(list instanceof List); + assertEquals(3, LazyList.size(list)); + assertEquals("a", LazyList.get(list, 0)); + assertEquals("b", LazyList.get(list, 1)); + assertEquals("c", LazyList.get(list, 2)); + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_NullInput_NullItem() { + Object[] input = null; + + Object[] arr = ArrayUtils.addToArray(input, null, Object.class); + assertNotNull(arr); + if (STRICT) { + // Adding null item to array should result in nothing added? + assertEquals(0, arr.length); + } else { + assertEquals(1, arr.length); + } + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_NullNullNull() { + // NPE if item && type are both null. + assumeTrue(STRICT); + + // Harsh test case. + Object[] input = null; + + Object[] arr = ArrayUtils.addToArray(input, null, null); + assertNotNull(arr); + if (STRICT) { + // Adding null item to array should result in nothing added? + assertEquals(0, arr.length); + } else { + assertEquals(1, arr.length); + } + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_NullInput_SimpleItem() { + Object[] input = null; + + Object[] arr = ArrayUtils.addToArray(input, "a", String.class); + assertNotNull(arr); + assertEquals(1, arr.length); + assertEquals("a", arr[0]); + + // Same test, but with an undefined type + arr = ArrayUtils.addToArray(input, "b", null); + assertNotNull(arr); + assertEquals(1, arr.length); + assertEquals("b", arr[0]); + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_EmptyInput_NullItem() { + String[] input = new String[0]; + + String[] arr = ArrayUtils.addToArray(input, null, Object.class); + assertNotNull(arr); + if (STRICT) { + // Adding null item to array should result in nothing added? + assertEquals(0, arr.length); + } else { + assertEquals(1, arr.length); + } + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_EmptyInput_SimpleItem() { + String[] input = new String[0]; + + String[] arr = ArrayUtils.addToArray(input, "a", String.class); + assertNotNull(arr); + assertEquals(1, arr.length); + assertEquals("a", arr[0]); + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_SingleInput_NullItem() { + String[] input = new String[]{"z"}; + + String[] arr = ArrayUtils.addToArray(input, null, Object.class); + assertNotNull(arr); + if (STRICT) { + // Should a null item be added to an array? + assertEquals(1, arr.length); + } else { + assertEquals(2, arr.length); + assertEquals("z", arr[0]); + assertEquals(null, arr[1]); + } + } + + /** + * Tests for {@link ArrayUtils#addToArray(Object[], Object, Class)} + */ + @Test + void testAddToArray_SingleInput_SimpleItem() { + String[] input = new String[]{"z"}; + + String[] arr = ArrayUtils.addToArray(input, "a", String.class); + assertNotNull(arr); + assertEquals(2, arr.length); + assertEquals("z", arr[0]); + assertEquals("a", arr[1]); + } + + /** + * Tests for {@link ArrayUtils#removeFromArray(Object[], Object)} + */ + @Test + void testRemoveFromArray_NullInput_NullItem() { + Object[] input = null; + + Object[] arr = ArrayUtils.removeFromArray(input, null); + assertNull(arr); + } + + /** + * Tests for {@link ArrayUtils#removeFromArray(Object[], Object)} + */ + @Test + void testRemoveFromArray_NullInput_SimpleItem() { + Object[] input = null; + + Object[] arr = ArrayUtils.removeFromArray(input, "a"); + assertNull(arr); + } + + /** + * Tests for {@link ArrayUtils#removeFromArray(Object[], Object)} + */ + @Test + void testRemoveFromArray_EmptyInput_NullItem() { + String[] input = new String[0]; + + String[] arr = ArrayUtils.removeFromArray(input, null); + assertNotNull(arr); + assertEquals(0, arr.length); + } + + /** + * Tests for {@link ArrayUtils#removeFromArray(Object[], Object)} + */ + @Test + void testRemoveFromArray_EmptyInput_SimpleItem() { + String[] input = new String[0]; + + String[] arr = ArrayUtils.removeFromArray(input, "a"); + assertNotNull(arr); + assertEquals(0, arr.length); + } + + /** + * Tests for {@link ArrayUtils#removeFromArray(Object[], Object)} + */ + @Test + void testRemoveFromArray_SingleInput() { + String[] input = new String[]{"a"}; + + String[] arr = ArrayUtils.removeFromArray(input, null); + assertNotNull(arr); + assertEquals(1, arr.length); + assertEquals("a", arr[0]); + + // Remove actual item + arr = ArrayUtils.removeFromArray(input, "a"); + assertNotNull(arr); + assertEquals(0, arr.length); + } + + /** + * Tests for {@link ArrayUtils#removeFromArray(Object[], Object)} + */ + @Test + void testRemoveFromArray_MultiInput() { + String[] input = new String[]{"a", "b", "c"}; + + String[] arr = ArrayUtils.removeFromArray(input, null); + assertNotNull(arr); + assertEquals(3, arr.length); + assertEquals("a", arr[0]); + assertEquals("b", arr[1]); + assertEquals("c", arr[2]); + + // Remove an actual item + arr = ArrayUtils.removeFromArray(input, "b"); + assertNotNull(arr); + assertEquals(2, arr.length); + assertEquals("a", arr[0]); + assertEquals("c", arr[1]); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/collection/map/MultiMapTest.java b/firefly-common/src/test/java/com/fireflysource/common/collection/map/MultiMapTest.java new file mode 100644 index 000000000..be81b0a34 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/collection/map/MultiMapTest.java @@ -0,0 +1,467 @@ +package com.fireflysource.common.collection.map; + + +import com.fireflysource.common.collection.list.LazyList; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class MultiMapTest { + + @Test + void testPut() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + } + + @Test + void testPutNullString() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + String val = null; + + mm.put(key, val); + assertMapSize(mm, 1); + assertNullValues(mm, key); + } + + @Test + void testPutNullList() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + List vals = null; + + mm.put(key, vals); + assertMapSize(mm, 1); + assertNullValues(mm, key); + } + + @Test + void testPutReplace() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + Object ret; + + ret = mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + assertNull(ret); + Object orig = mm.get(key); + + // Now replace it + ret = mm.put(key, "jar"); + assertMapSize(mm, 1); + assertValues(mm, key, "jar"); + assertEquals(orig, ret); + } + + @Test + void testPutValuesList() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + List input = new ArrayList<>(); + input.add("gzip"); + input.add("jar"); + input.add("pack200"); + + mm.putValues(key, input); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + } + + @Test + void testPutValuesStringArray() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + String[] input = {"gzip", "jar", "pack200"}; + mm.putValues(key, input); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + } + + @Test + void testPutValuesVarArgs() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + mm.putValues(key, "gzip", "jar", "pack200"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + } + + @Test + void testAdd() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + + // Add to the key + mm.add(key, "jar"); + mm.add(key, "pack200"); + + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + } + + @Test + void testAddValuesList() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + + // Add to the key + List extras = new ArrayList<>(); + extras.add("jar"); + extras.add("pack200"); + extras.add("zip"); + mm.addValues(key, extras); + + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200", "zip"); + } + + @Test + void testAddValuesListEmpty() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + + // Add to the key + List extras = new ArrayList<>(); + mm.addValues(key, extras); + + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + } + + @Test + void testAddValuesStringArray() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + + // Add to the key + String[] extras = {"jar", "pack200", "zip"}; + mm.addValues(key, extras); + + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200", "zip"); + } + + @Test + void testAddValuesStringArrayEmpty() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.put(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + + // Add to the key + String[] extras = new String[0]; + mm.addValues(key, extras); + + assertMapSize(mm, 1); + assertValues(mm, key, "gzip"); + } + + @Test + void testRemoveValue() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.putValues(key, "gzip", "jar", "pack200"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + + // Remove a value + mm.removeValue(key, "jar"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "pack200"); + + } + + @Test + void testRemoveValueInvalidItem() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.putValues(key, "gzip", "jar", "pack200"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + + // Remove a value that isn't there + mm.removeValue(key, "msi"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + } + + @Test + void testRemoveValueAllItems() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.putValues(key, "gzip", "jar", "pack200"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "jar", "pack200"); + + // Remove a value + mm.removeValue(key, "jar"); + assertMapSize(mm, 1); + assertValues(mm, key, "gzip", "pack200"); + + // Remove another value + mm.removeValue(key, "gzip"); + assertMapSize(mm, 1); + assertValues(mm, key, "pack200"); + + // Remove last value + mm.removeValue(key, "pack200"); + assertMapSize(mm, 0); // should be empty now + } + + @Test + void testRemoveValueFromEmpty() { + MultiMap mm = new MultiMap<>(); + + String key = "formats"; + + // Setup the key + mm.putValues(key, new String[0]); + assertMapSize(mm, 1); + assertEmptyValues(mm, key); + + // Remove a value that isn't in the underlying values + mm.removeValue(key, "jar"); + assertMapSize(mm, 1); + assertEmptyValues(mm, key); + } + + @Test + void testPutAllMap() { + MultiMap mm = new MultiMap<>(); + + assertMapSize(mm, 0); // Shouldn't have anything yet. + + Map input = new HashMap<>(); + input.put("food", "apple"); + input.put("color", "red"); + input.put("amount", "bushel"); + + mm.putAllValues(input); + + assertMapSize(mm, 3); + assertValues(mm, "food", "apple"); + assertValues(mm, "color", "red"); + assertValues(mm, "amount", "bushel"); + } + + @Test + void testPutAllMultiMapSimple() { + MultiMap mm = new MultiMap<>(); + + assertMapSize(mm, 0); // Shouldn't have anything yet. + + MultiMap input = new MultiMap<>(); + input.put("food", "apple"); + input.put("color", "red"); + input.put("amount", "bushel"); + + mm.putAll(input); + + assertMapSize(mm, 3); + assertValues(mm, "food", "apple"); + assertValues(mm, "color", "red"); + assertValues(mm, "amount", "bushel"); + } + + @Test + void testPutAllMultiMapComplex() { + MultiMap mm = new MultiMap<>(); + + assertMapSize(mm, 0); // Shouldn't have anything yet. + + MultiMap input = new MultiMap<>(); + input.putValues("food", "apple", "cherry", "raspberry"); + input.put("color", "red"); + input.putValues("amount", "bushel", "pint"); + + mm.putAll(input); + + assertMapSize(mm, 3); + assertValues(mm, "food", "apple", "cherry", "raspberry"); + assertValues(mm, "color", "red"); + assertValues(mm, "amount", "bushel", "pint"); + } + + @Test + void testToStringArrayMap() { + MultiMap mm = new MultiMap<>(); + mm.putValues("food", "apple", "cherry", "raspberry"); + mm.put("color", "red"); + mm.putValues("amount", "bushel", "pint"); + + assertMapSize(mm, 3); + + Map sam = mm.toStringArrayMap(); + assertEquals(3, sam.size()); + + assertArray("toStringArrayMap(food)", sam.get("food"), "apple", "cherry", "raspberry"); + assertArray("toStringArrayMap(color)", sam.get("color"), "red"); + assertArray("toStringArrayMap(amount)", sam.get("amount"), "bushel", "pint"); + } + + @Test + void testToString() { + MultiMap mm = new MultiMap<>(); + mm.put("color", "red"); + assertEquals("{color=red}", mm.toString()); + + mm.putValues("food", "apple", "cherry", "raspberry"); + assertEquals("{color=red, food=[apple, cherry, raspberry]}", mm.toString()); + } + @Test + void testClear() { + MultiMap mm = new MultiMap<>(); + mm.putValues("food", "apple", "cherry", "raspberry"); + mm.put("color", "red"); + mm.putValues("amount", "bushel", "pint"); + + assertMapSize(mm, 3); + + mm.clear(); + + assertMapSize(mm, 0); + } + + @Test + void testContainsKey() { + MultiMap mm = new MultiMap<>(); + mm.putValues("food", "apple", "cherry", "raspberry"); + mm.put("color", "red"); + mm.putValues("amount", "bushel", "pint"); + + assertTrue(mm.containsKey("color")); + assertFalse(mm.containsKey("nutrition")); + } + + @Test + void testContainsSimpleValue() { + MultiMap mm = new MultiMap<>(); + mm.putValues("food", "apple", "cherry", "raspberry"); + mm.put("color", "red"); + mm.putValues("amount", "bushel", "pint"); + + assertTrue(mm.containsSimpleValue("red")); + assertFalse(mm.containsValue("nutrition")); + } + + @Test + void testContainsValue() { + MultiMap mm = new MultiMap<>(); + mm.putValues("food", "apple", "cherry", "raspberry"); + mm.put("color", "red"); + mm.putValues("amount", "bushel", "pint"); + + List acr = new ArrayList<>(); + acr.add("apple"); + acr.add("cherry"); + acr.add("raspberry"); + assertTrue(mm.containsValue(acr)); + assertFalse(mm.containsValue("nutrition")); + } + + @Test + void testContainsValue_LazyList() { + MultiMap mm = new MultiMap<>(); + mm.putValues("food", "apple", "cherry", "raspberry"); + mm.put("color", "red"); + mm.putValues("amount", "bushel", "pint"); + + Object list = LazyList.add(null, "bushel"); + list = LazyList.add(list, "pint"); + + assertTrue(mm.containsValue(list)); + } + + private void assertArray(String prefix, Object[] actualValues, Object... expectedValues) { + assertEquals(expectedValues.length, actualValues.length); + int len = actualValues.length; + for (int i = 0; i < len; i++) { + assertEquals(expectedValues[i], actualValues[i]); + } + } + + private void assertValues(MultiMap mm, String key, Object... expectedValues) { + List values = mm.getValues(key); + assertEquals(expectedValues.length, values.size()); + int len = expectedValues.length; + for (int i = 0; i < len; i++) { + if (expectedValues[i] == null) { + assertNull(values.get(i)); + } else { + assertEquals(expectedValues[i], values.get(i)); + } + } + } + + private void assertNullValues(MultiMap mm, String key) { + List values = mm.getValues(key); + assertNull(values); + } + + private void assertEmptyValues(MultiMap mm, String key) { + List values = mm.getValues(key); + assertEquals(0, LazyList.size(values)); + } + + private void assertMapSize(MultiMap mm, int expectedSize) { + assertEquals(expectedSize, mm.size()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/collection/trie/TestTrie.java b/firefly-common/src/test/java/com/fireflysource/common/collection/trie/TestTrie.java new file mode 100644 index 000000000..ff80f693a --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/collection/trie/TestTrie.java @@ -0,0 +1,36 @@ +package com.fireflysource.common.collection.trie; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestTrie { + + @ParameterizedTest + @ValueSource(strings = {"TreeTrie", "ArrayTernaryTrie", "ArrayTrie"}) + void test(String type) { + Trie trie; + switch (type) { + case "TreeTrie": + trie = new TreeTrie<>(); + break; + case "ArrayTernaryTrie": + trie = new ArrayTernaryTrie<>(500); + break; + case "ArrayTrie": + trie = new ArrayTrie<>(500); + break; + default: + trie = new TreeTrie<>(); + } + + trie.put("com.firefly.foo.bar"); + trie.put("com.firefly.foo"); + + assertEquals(2, trie.keySet().size()); + assertEquals("com.firefly.foo", trie.getBest("com.firefly.foo.Test")); + assertEquals("com.firefly.foo.bar", trie.getBest("com.firefly.foo.bar.Hello")); + } + +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/AtomicBiIntegerTest.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/AtomicBiIntegerTest.java new file mode 100644 index 000000000..572173f1a --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/AtomicBiIntegerTest.java @@ -0,0 +1,81 @@ +package com.fireflysource.common.concurrent; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AtomicBiIntegerTest { + + @Test + void testBitOperations() { + long encoded; + + encoded = AtomicBiInteger.encode(0, 0); + assertEquals(0, AtomicBiInteger.getHi(encoded)); + assertEquals(0, AtomicBiInteger.getLo(encoded)); + + encoded = AtomicBiInteger.encode(1, 2); + assertEquals(1, AtomicBiInteger.getHi(encoded)); + assertEquals(2, AtomicBiInteger.getLo(encoded)); + + encoded = AtomicBiInteger.encode(Integer.MAX_VALUE, -1); + assertEquals(Integer.MAX_VALUE, AtomicBiInteger.getHi(encoded)); + assertEquals(-1, AtomicBiInteger.getLo(encoded)); + encoded = AtomicBiInteger.encodeLo(encoded, 42); + assertEquals(Integer.MAX_VALUE, AtomicBiInteger.getHi(encoded)); + assertEquals(42, AtomicBiInteger.getLo(encoded)); + + encoded = AtomicBiInteger.encode(-1, Integer.MAX_VALUE); + assertEquals(-1, AtomicBiInteger.getHi(encoded)); + assertEquals(Integer.MAX_VALUE, AtomicBiInteger.getLo(encoded)); + encoded = AtomicBiInteger.encodeHi(encoded, 42); + assertEquals(42, AtomicBiInteger.getHi(encoded)); + assertEquals(Integer.MAX_VALUE, AtomicBiInteger.getLo(encoded)); + + encoded = AtomicBiInteger.encode(Integer.MIN_VALUE, 1); + assertEquals(Integer.MIN_VALUE, AtomicBiInteger.getHi(encoded)); + assertEquals(1, AtomicBiInteger.getLo(encoded)); + encoded = AtomicBiInteger.encodeLo(encoded, Integer.MAX_VALUE); + assertEquals(Integer.MIN_VALUE, AtomicBiInteger.getHi(encoded)); + assertEquals(Integer.MAX_VALUE, AtomicBiInteger.getLo(encoded)); + + encoded = AtomicBiInteger.encode(1, Integer.MIN_VALUE); + assertEquals(1, AtomicBiInteger.getHi(encoded)); + assertEquals(Integer.MIN_VALUE, AtomicBiInteger.getLo(encoded)); + encoded = AtomicBiInteger.encodeHi(encoded, Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, AtomicBiInteger.getHi(encoded)); + assertEquals(Integer.MIN_VALUE, AtomicBiInteger.getLo(encoded)); + } + + @Test + void testSet() { + AtomicBiInteger abi = new AtomicBiInteger(); + assertEquals(0, abi.getHi()); + assertEquals(0, abi.getLo()); + + abi.getAndSetHi(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, abi.getHi()); + assertEquals(0, abi.getLo()); + + abi.getAndSetLo(Integer.MIN_VALUE); + assertEquals(Integer.MAX_VALUE, abi.getHi()); + assertEquals(Integer.MIN_VALUE, abi.getLo()); + } + + @Test + void testCompareAndSet() { + AtomicBiInteger abi = new AtomicBiInteger(); + assertEquals(0, abi.getHi()); + assertEquals(0, abi.getLo()); + + assertFalse(abi.compareAndSetHi(1, 42)); + assertTrue(abi.compareAndSetHi(0, 42)); + assertEquals(42, abi.getHi()); + assertEquals(0, abi.getLo()); + + assertFalse(abi.compareAndSetLo(1, -42)); + assertTrue(abi.compareAndSetLo(0, -42)); + assertEquals(42, abi.getHi()); + assertEquals(-42, abi.getLo()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/AutoLockTest.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/AutoLockTest.java new file mode 100644 index 000000000..4a18a29e5 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/AutoLockTest.java @@ -0,0 +1,93 @@ +package com.fireflysource.common.concurrent; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AutoLockTest { + + @Test + public void testLocked() { + AutoLock lock = new AutoLock(); + assertFalse(lock.isLocked()); + + try (AutoLock ignored = lock.lock()) { + assertTrue(lock.isLocked()); + } finally { + assertFalse(lock.isLocked()); + } + + assertFalse(lock.isLocked()); + } + + @Test + public void testLockedException() { + AutoLock lock = new AutoLock(); + assertFalse(lock.isLocked()); + + try (AutoLock ignored = lock.lock()) { + assertTrue(lock.isLocked()); + throw new Exception(); + } catch (Exception e) { + assertFalse(lock.isLocked()); + } finally { + assertFalse(lock.isLocked()); + } + + assertFalse(lock.isLocked()); + } + + @Test + public void testContend() throws Exception { + AutoLock lock = new AutoLock(); + + final CountDownLatch held0 = new CountDownLatch(1); + final CountDownLatch hold0 = new CountDownLatch(1); + + Thread thread0 = new Thread(() -> + { + try (AutoLock ignored = lock.lock()) { + held0.countDown(); + hold0.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread0.start(); + held0.await(); + + assertTrue(lock.isLocked()); + + final CountDownLatch held1 = new CountDownLatch(1); + final CountDownLatch hold1 = new CountDownLatch(1); + Thread thread1 = new Thread(() -> + { + try (AutoLock ignored = lock.lock()) { + held1.countDown(); + hold1.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread1.start(); + // thread1 will be spinning here + assertFalse(held1.await(100, TimeUnit.MILLISECONDS)); + + // Let thread0 complete + hold0.countDown(); + thread0.join(); + + // thread1 can progress + held1.await(); + + // let thread1 complete + hold1.countDown(); + thread1.join(); + + assertFalse(lock.isLocked()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/IteratingCallbackTest.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/IteratingCallbackTest.java new file mode 100644 index 000000000..a11743c75 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/IteratingCallbackTest.java @@ -0,0 +1,250 @@ +package com.fireflysource.common.concurrent; + +import com.fireflysource.common.sys.Result; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +public class IteratingCallbackTest { + private ScheduledExecutorService scheduler; + + @BeforeEach + public void prepare() { + scheduler = Executors.newScheduledThreadPool(4); + } + + @AfterEach + public void dispose() { + ExecutorServiceUtils.shutdownAndAwaitTermination(scheduler, 1, TimeUnit.SECONDS); + } + + @Test + public void testNonWaitingProcess() throws Exception { + TestCB cb = new TestCB() { + int i = 10; + + @Override + protected Action process() { + processed++; + if (i-- > 1) { + accept(Result.SUCCESS); // fake a completed IO operation + return Action.SCHEDULED; + } + return Action.SUCCEEDED; + } + }; + + cb.iterate(); + assertTrue(cb.waitForComplete()); + assertEquals(10, cb.processed); + } + + @Test + public void testWaitingProcess() throws Exception { + TestCB cb = new TestCB() { + int i = 4; + + @Override + protected Action process() { + processed++; + if (i-- > 1) { + scheduler.schedule(successTask, 50, TimeUnit.MILLISECONDS); + return Action.SCHEDULED; + } + return Action.SUCCEEDED; + } + }; + + cb.iterate(); + + assertTrue(cb.waitForComplete()); + + assertEquals(4, cb.processed); + } + + @Test + public void testWaitingProcessSpuriousIterate() throws Exception { + final TestCB cb = new TestCB() { + int i = 4; + + @Override + protected Action process() { + processed++; + if (i-- > 1) { + scheduler.schedule(successTask, 50, TimeUnit.MILLISECONDS); + return Action.SCHEDULED; + } + return Action.SUCCEEDED; + } + }; + + cb.iterate(); + scheduler.schedule(new Runnable() { + @Override + public void run() { + cb.iterate(); + if (!cb.isSucceeded()) + scheduler.schedule(this, 50, TimeUnit.MILLISECONDS); + } + }, 49, TimeUnit.MILLISECONDS); + + assertTrue(cb.waitForComplete()); + + assertEquals(4, cb.processed); + } + + @Test + public void testNonWaitingProcessFailure() throws Exception { + TestCB cb = new TestCB() { + int i = 10; + + @Override + protected Action process() { + processed++; + if (i-- > 1) { + if (i > 5) + accept(Result.SUCCESS); // fake a completed IO operation + else + accept(Result.createFailedResult(new Exception("testing"))); + return Action.SCHEDULED; + } + return Action.SUCCEEDED; + } + }; + + cb.iterate(); + assertFalse(cb.waitForComplete()); + assertEquals(5, cb.processed); + } + + @Test + public void testWaitingProcessFailure() throws Exception { + TestCB cb = new TestCB() { + int i = 4; + + @Override + protected Action process() { + processed++; + if (i-- > 1) { + scheduler.schedule(i > 2 ? successTask : failTask, 50, TimeUnit.MILLISECONDS); + return Action.SCHEDULED; + } + return Action.SUCCEEDED; + } + }; + + cb.iterate(); + + assertFalse(cb.waitForComplete()); + assertEquals(2, cb.processed); + } + + @Test + public void testIdleWaiting() throws Exception { + final CountDownLatch idle = new CountDownLatch(1); + + TestCB cb = new TestCB() { + int i = 5; + + @Override + protected Action process() { + processed++; + + switch (i--) { + case 5: + accept(Result.SUCCESS); + return Action.SCHEDULED; + + case 4: + scheduler.schedule(successTask, 5, TimeUnit.MILLISECONDS); + return Action.SCHEDULED; + + case 3: + scheduler.schedule(idle::countDown, 5, TimeUnit.MILLISECONDS); + return Action.IDLE; + + case 2: + accept(Result.SUCCESS); + return Action.SCHEDULED; + + case 1: + scheduler.schedule(successTask, 5, TimeUnit.MILLISECONDS); + return Action.SCHEDULED; + + case 0: + return Action.SUCCEEDED; + + default: + throw new IllegalStateException(); + } + } + }; + + cb.iterate(); + idle.await(10, TimeUnit.SECONDS); + assertTrue(cb.isIdle()); + + cb.iterate(); + assertTrue(cb.waitForComplete()); + assertEquals(6, cb.processed); + } + + @Test + public void testCloseDuringProcessingReturningScheduled() throws Exception { + testCloseDuringProcessing(IteratingCallback.Action.SCHEDULED); + } + + @Test + public void testCloseDuringProcessingReturningSucceeded() throws Exception { + testCloseDuringProcessing(IteratingCallback.Action.SUCCEEDED); + } + + private void testCloseDuringProcessing(final IteratingCallback.Action action) throws Exception { + final CountDownLatch failureLatch = new CountDownLatch(1); + IteratingCallback callback = new IteratingCallback() { + @Override + protected Action process() throws Exception { + close(); + return action; + } + + @Override + protected void onCompleteFailure(Throwable cause) { + failureLatch.countDown(); + } + }; + + callback.iterate(); + + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + } + + private abstract static class TestCB extends IteratingCallback { + protected Runnable successTask = () -> accept(Result.SUCCESS); + protected Runnable failTask = () -> accept(Result.createFailedResult(new Exception("testing failure"))); + protected CountDownLatch completed = new CountDownLatch(1); + protected int processed = 0; + + @Override + protected void onCompleteSuccess() { + completed.countDown(); + } + + @Override + public void onCompleteFailure(Throwable x) { + completed.countDown(); + } + + boolean waitForComplete() throws InterruptedException { + completed.await(10, TimeUnit.SECONDS); + return isSucceeded(); + } + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestAtomics.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestAtomics.java new file mode 100644 index 000000000..7cb0f1583 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestAtomics.java @@ -0,0 +1,36 @@ +package com.fireflysource.common.concurrent; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * @author Pengtao Qiu + */ +class TestAtomics { + + @Test + void testGetAndDecrement() { + int init = 10; + int min = 5; + AtomicInteger integer = new AtomicInteger(init); + + for (int i = init; i > 0; i--) { + Atomics.getAndDecrement(integer, min); + } + assertEquals(min, integer.get()); + } + + @Test + void testGetAndIncrement() { + int max = 10; + AtomicInteger integer = new AtomicInteger(0); + for (int i = 0; i < 20; i++) { + Atomics.getAndIncrement(integer, max); + } + assertEquals(max, integer.get()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestCompletableFutures.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestCompletableFutures.java new file mode 100644 index 000000000..b8009a767 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestCompletableFutures.java @@ -0,0 +1,76 @@ +package com.fireflysource.common.concurrent; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Pengtao Qiu + */ +public class TestCompletableFutures { + + @Test + @DisplayName("should retry operation successfully") + void testRetrySuccessfully() throws Exception { + AtomicInteger i = new AtomicInteger(4); + CompletableFuture future = CompletableFutures.retry(3, () -> { + System.out.println("execute count: " + i.get()); + if (i.decrementAndGet() > 0) { + return CompletableFutures.failedFuture(new IllegalStateException("error")); + } else { + return CompletableFuture.completedFuture("ok"); + } + }, (e, c) -> System.out.println("start to retry: " + c)); + String str = future.get(); + assertEquals("ok", str); + assertEquals(0, i.get()); + } + + @Test + @DisplayName("should retry operation failure") + void testRetryFailure() { + AtomicInteger i = new AtomicInteger(4); + CompletableFuture future = CompletableFutures.retry(2, () -> { + System.out.println("execute count: " + i.get()); + if (i.decrementAndGet() > 0) { + return CompletableFutures.failedFuture(new IllegalStateException("error")); + } else { + return CompletableFuture.completedFuture("ok"); + } + }, (e, c) -> System.out.println("start to retry: " + c)); + + assertThrows(ExecutionException.class, future::get); + } + + @Test + @DisplayName("should do finally always") + void testDoFinally() throws Exception { + AtomicReference ref = new AtomicReference<>(); + CompletableFuture future = CompletableFuture.supplyAsync(() -> "OK"); + CompletableFuture result = CompletableFutures.doFinally(future, (value, ex) -> CompletableFuture.runAsync(() -> { + String msg = "do finally. " + value; + System.out.println(msg); + ref.set(msg); + })); + assertEquals("OK", result.get()); + assertEquals("do finally. OK", ref.get()); + + CompletableFuture failure = CompletableFuture.supplyAsync(() -> { + throw new IllegalStateException("Failure"); + }); + CompletableFuture failureResult = CompletableFutures.doFinally(failure, (value, ex) -> CompletableFuture.runAsync(() -> { + String msg = "do finally. " + ex.getMessage(); + System.out.println(msg); + ref.set(msg); + })); + assertThrows(ExecutionException.class, failureResult::get); + assertEquals("do finally. java.lang.IllegalStateException: Failure", ref.get()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestExecutorServiceUtils.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestExecutorServiceUtils.java new file mode 100644 index 000000000..020212f23 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestExecutorServiceUtils.java @@ -0,0 +1,40 @@ +package com.fireflysource.common.concurrent; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.fireflysource.common.concurrent.ExecutorServiceUtils.shutdownAndAwaitTermination; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Pengtao Qiu + */ +class TestExecutorServiceUtils { + + @Test + void testShutdownAndAwaitTermination() { + int threadNum = 2; + int count = 10; + long taskTime = 500L; + long maxTime = taskTime * count / threadNum; + + ExecutorService pool = Executors.newFixedThreadPool(threadNum); + AtomicInteger maxTask = new AtomicInteger(count); + for (int i = 0; i < count; i++) { + pool.submit(() -> { + try { + Thread.sleep(taskTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("task complete. " + maxTask.getAndDecrement()); + }); + } + shutdownAndAwaitTermination(pool, maxTime + 100, TimeUnit.MILLISECONDS); + assertEquals(0, maxTask.get()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestSingleThreadExecutorService.java b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestSingleThreadExecutorService.java new file mode 100644 index 000000000..0e25e5223 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/concurrent/TestSingleThreadExecutorService.java @@ -0,0 +1,28 @@ +package com.fireflysource.common.concurrent; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestSingleThreadExecutorService { + + @Test + void test() { + ExecutorService executorService = new SingleThreadExecutorService(1024 * 16); + executorService.execute(() -> { + try { + System.out.println("start to execute."); + Thread.sleep(1000L); + System.out.println("execute success."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + ExecutorServiceUtils.shutdownAndAwaitTermination(executorService, 3, TimeUnit.SECONDS); + assertTrue(executorService.isShutdown()); + assertTrue(executorService.isTerminated()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/io/TestBufferUtils.java b/firefly-common/src/test/java/com/fireflysource/common/io/TestBufferUtils.java new file mode 100644 index 000000000..771ea64c5 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/io/TestBufferUtils.java @@ -0,0 +1,422 @@ +package com.fireflysource.common.io; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Pengtao Qiu + */ +class TestBufferUtils { + + @Test + void testToArray() { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putInt(6); + buffer.flip(); + + byte[] bytes = BufferUtils.toArray(buffer); + assertNotSame(bytes, buffer.array()); + assertEquals(6, ByteBuffer.wrap(bytes).getInt()); + + buffer = ByteBuffer.allocateDirect(4); + buffer.putInt(3); + buffer.flip(); + bytes = BufferUtils.toArray(buffer); + assertEquals(3, ByteBuffer.wrap(bytes).getInt()); + } + + @Test + void testCollectionToArray() { + int count = 10; + List list = new LinkedList<>(); + for (int i = 0; i < count; i++) { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putInt(i); + buffer.flip(); + list.add(buffer); + } + + byte[] bytes = BufferUtils.toArray(list); + assertEquals(count * 4, bytes.length); + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + for (int i = 0; i < count; i++) { + int x = buffer.getInt(); + assertEquals(i, x); + } + } + + @Test + void testToInt() { + ByteBuffer[] buf = { + BufferUtils.toBuffer("0"), + BufferUtils.toBuffer(" 42 "), + BufferUtils.toBuffer(" 43abc"), + BufferUtils.toBuffer("-44"), + BufferUtils.toBuffer(" - 45;"), + BufferUtils.toBuffer("-2147483648"), + BufferUtils.toBuffer("2147483647"), + }; + + int[] val = { + 0, 42, 43, -44, -45, -2147483648, 2147483647 + }; + + for (int i = 0; i < buf.length; i++) + assertEquals(val[i], BufferUtils.toInt(buf[i]), "t" + i); + } + + @Test + void testPutInt() { + int[] val = { + 0, 42, 43, -44, -45, Integer.MIN_VALUE, Integer.MAX_VALUE + }; + + String[] str = { + "0", "42", "43", "-44", "-45", "" + Integer.MIN_VALUE, "" + Integer.MAX_VALUE + }; + + ByteBuffer buffer = ByteBuffer.allocate(24); + + for (int i = 0; i < val.length; i++) { + BufferUtils.clearToFill(buffer); + BufferUtils.putDecInt(buffer, val[i]); + BufferUtils.flipToFlush(buffer, 0); + assertEquals(str[i], BufferUtils.toString(buffer), "t" + i); + } + } + + @Test + void testPutLong() { + long[] val = { + 0L, 42L, 43L, -44L, -45L, Long.MIN_VALUE, Long.MAX_VALUE + }; + + String[] str = { + "0", "42", "43", "-44", "-45", "" + Long.MIN_VALUE, "" + Long.MAX_VALUE + }; + + ByteBuffer buffer = ByteBuffer.allocate(50); + + for (int i = 0; i < val.length; i++) { + BufferUtils.clearToFill(buffer); + BufferUtils.putDecLong(buffer, val[i]); + BufferUtils.flipToFlush(buffer, 0); + assertEquals(str[i], BufferUtils.toString(buffer), "t" + i); + } + } + + @Test + void testPutHexInt() { + int[] val = { + 0, 42, 43, -44, -45, -2147483648, 2147483647 + }; + + String[] str = { + "0", "2A", "2B", "-2C", "-2D", "-80000000", "7FFFFFFF" + }; + + ByteBuffer buffer = ByteBuffer.allocate(50); + + for (int i = 0; i < val.length; i++) { + BufferUtils.clearToFill(buffer); + BufferUtils.putHexInt(buffer, val[i]); + BufferUtils.flipToFlush(buffer, 0); + assertEquals(str[i], BufferUtils.toString(buffer), "t" + i); + } + } + + @Test + void testPut() { + ByteBuffer to = BufferUtils.allocate(10); + ByteBuffer from = BufferUtils.toBuffer("12345"); + + BufferUtils.clear(to); + assertEquals(5, BufferUtils.append(to, from)); + assertTrue(BufferUtils.isEmpty(from)); + assertEquals("12345", BufferUtils.toString(to)); + + from = BufferUtils.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5, BufferUtils.append(to, from)); + assertEquals(2, from.remaining()); + assertEquals("1234567890", BufferUtils.toString(to)); + + from = BufferUtils.toBuffer("1234"); + to = BufferUtils.allocate(from.remaining() * 2); + assertEquals(0, to.position()); + assertEquals(0, to.limit()); + assertEquals(0, from.position()); + assertEquals(4, from.limit()); + + BufferUtils.append(to, from); + assertEquals(0, to.position()); + assertEquals(4, to.limit()); + assertEquals(4, from.position()); + assertEquals(4, from.limit()); + + to.get(); + from = BufferUtils.toBuffer("1234"); + BufferUtils.append(to, from); + assertEquals(1, to.position()); + assertEquals(8, to.limit()); + } + + @Test + void testPutBuffer() { + ByteBuffer from = BufferUtils.toBuffer("hello"); + ByteBuffer to = BufferUtils.allocate(20); + int pos = BufferUtils.flipToFill(to); + System.out.println("from: " + from.remaining()); + System.out.println("to: " + to.remaining()); + assertEquals(5, from.remaining()); + assertEquals(20, to.remaining()); + + int len = BufferUtils.put(from, to); + System.out.println("len: " + len); + System.out.println("from: " + from.remaining()); + System.out.println("to: " + to.remaining()); + assertEquals(0, from.remaining()); + assertEquals(15, to.remaining()); + assertEquals(5, len); + + BufferUtils.flipToFlush(to, pos); + String str = BufferUtils.toString(to); + assertEquals("hello", str); + } + + + @Test + void testAppend() { + ByteBuffer to = BufferUtils.allocate(8); + ByteBuffer from = BufferUtils.toBuffer("12345"); + + BufferUtils.append(to, from.array(), 0, 3); + assertEquals("123", BufferUtils.toString(to)); + BufferUtils.append(to, from.array(), 3, 2); + assertEquals("12345", BufferUtils.toString(to)); + + assertThrows(BufferOverflowException.class, () -> BufferUtils.append(to, from.array(), 0, 5)); + } + + + @Test + void testPutDirect() { + ByteBuffer to = BufferUtils.allocateDirect(10); + ByteBuffer from = BufferUtils.toBuffer("12345"); + + BufferUtils.clear(to); + assertEquals(5, BufferUtils.append(to, from)); + assertTrue(BufferUtils.isEmpty(from)); + assertEquals("12345", BufferUtils.toString(to)); + + from = BufferUtils.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5, BufferUtils.append(to, from)); + assertEquals(2, from.remaining()); + assertEquals("1234567890", BufferUtils.toString(to)); + } + + @Test + void testToBuffer_Array() { + byte[] arr = new byte[128]; + Arrays.fill(arr, (byte) 0x44); + ByteBuffer buf = BufferUtils.toBuffer(arr); + + int count = 0; + while (buf.remaining() > 0) { + byte b = buf.get(); + assertEquals(b, 0x44); + count++; + } + + assertEquals(arr.length, count, "Count of bytes"); + } + + @Test + void testToBuffer_ArrayOffsetLength() { + byte[] arr = new byte[128]; + Arrays.fill(arr, (byte) 0xFF); // fill whole thing with FF + int offset = 10; + int length = 100; + Arrays.fill(arr, offset, offset + length, (byte) 0x77); // fill partial with 0x77 + ByteBuffer buf = BufferUtils.toBuffer(arr, offset, length); + + int count = 0; + while (buf.remaining() > 0) { + byte b = buf.get(); + assertEquals(b, 0x77); + count++; + } + + assertEquals(length, count, "Count of bytes"); + } + + @Test + void testWriteToWithBufferThatDoesNotExposeArrayAndSmallContent() throws IOException { + int capacity = BufferUtils.TEMP_BUFFER_SIZE / 4; + testWriteToWithBufferThatDoesNotExposeArray(capacity); + } + + @Test + void testWriteToWithBufferThatDoesNotExposeArrayAndContentLengthMatchingTempBufferSize() throws IOException { + int capacity = BufferUtils.TEMP_BUFFER_SIZE; + testWriteToWithBufferThatDoesNotExposeArray(capacity); + } + + @Test + void testWriteToWithBufferThatDoesNotExposeArrayAndContentSlightlyBiggerThanTwoTimesTempBufferSize() + throws + IOException { + int capacity = BufferUtils.TEMP_BUFFER_SIZE * 2 + 1024; + testWriteToWithBufferThatDoesNotExposeArray(capacity); + } + + + @Test + void testEnsureCapacity() { + ByteBuffer b = BufferUtils.toBuffer("Goodbye Cruel World"); + assertSame(b, BufferUtils.ensureCapacity(b, 0)); + assertSame(b, BufferUtils.ensureCapacity(b, 10)); + assertSame(b, BufferUtils.ensureCapacity(b, b.capacity())); + + + ByteBuffer b1 = BufferUtils.ensureCapacity(b, 64); + assertNotSame(b, b1); + assertEquals(64, b1.capacity()); + assertEquals("Goodbye Cruel World", BufferUtils.toString(b1)); + + b1.position(8); + b1.limit(13); + assertEquals("Cruel", BufferUtils.toString(b1)); + ByteBuffer b2 = b1.slice(); + assertEquals("Cruel", BufferUtils.toString(b2)); + System.err.println(BufferUtils.toDetailString(b2)); + assertEquals(8, b2.arrayOffset()); + assertEquals(5, b2.capacity()); + + assertSame(b2, BufferUtils.ensureCapacity(b2, 5)); + + ByteBuffer b3 = BufferUtils.ensureCapacity(b2, 64); + assertNotSame(b2, b3); + assertEquals(64, b3.capacity()); + assertEquals("Cruel", BufferUtils.toString(b3)); + assertEquals(0, b3.arrayOffset()); + + assertEquals(19, b.remaining()); + b.position(19); + ByteBuffer b4 = BufferUtils.ensureCapacity(b, b.position() + 20); + BufferUtils.flipToFill(b4); + b4.position(b.position()); + + assertEquals(20, b4.remaining()); + assertEquals(19, b4.position()); + assertEquals(39, b4.capacity()); + + BufferUtils.flipToFill(b); + ByteBuffer b5 = BufferUtils.allocateDirect(19); + assertEquals(0, b5.remaining()); + assertEquals(19, b.remaining()); + + BufferUtils.append(b5, b); + assertEquals(0, b.remaining()); + assertEquals(19, b5.remaining()); + BufferUtils.flipToFill(b); + assertEquals(19, b.remaining()); + + ByteBuffer b6 = BufferUtils.ensureCapacity(b5, 20); + assertEquals(19, b6.remaining()); + BufferUtils.flipToFill(b6); + assertEquals(1, b6.remaining()); + } + + @Test + void testAddCapacity() { + Arrays.asList(BufferUtils.allocateDirect(19), BufferUtils.allocate(19)).forEach(b -> { + BufferUtils.flipToFill(b); + b.put(BufferUtils.toBuffer("Goodbye Cruel World")); + + assertEquals(19, b.position()); + assertEquals(0, b.remaining()); + + ByteBuffer b1 = BufferUtils.addCapacity(b, 20); + assertEquals(20, b1.remaining()); + assertEquals(19, b1.position()); + assertEquals(39, b1.capacity()); + assertEquals(39, b1.limit()); + BufferUtils.flipToFlush(b1, 0); + assertEquals("Goodbye Cruel World", BufferUtils.toString(b1).trim()); + }); + + } + + @Test + void testToDetail_WithDEL() { + ByteBuffer b = ByteBuffer.allocate(40); + b.putChar('a').putChar('b').putChar('c'); + b.put((byte) 0x7F); + b.putChar('x').putChar('y').putChar('z'); + b.flip(); + String result = BufferUtils.toDetailString(b); + assertTrue(result.contains("\\x7f")); + } + + @Test + @DisplayName("should merge buffers successfully") + void testMergeBuffers() { + List buffers = new LinkedList<>(); + for (int i = 0; i < 10; i++) { + ByteBuffer buf = BufferUtils.allocate(16); + int pos = BufferUtils.flipToFill(buf); + for (int j = 0; j < 4; j++) { + int e = i * 4 + j; + buf.putInt(e); + } + BufferUtils.flipToFlush(buf, pos); + buffers.add(buf); + } + + ByteBuffer newBuffer = BufferUtils.merge(buffers); + buffers.forEach(buf -> assertEquals(0, buf.remaining())); + assertEquals(160, newBuffer.remaining()); + for (int i = 0; i < 40; i++) { + assertEquals(i, newBuffer.getInt()); + } + } + + @Test + @DisplayName("should convert buffers to string successfully") + void testBuffersToString() { + List buffers = Arrays.asList( + BufferUtils.toBuffer("hello ", StandardCharsets.UTF_8), + BufferUtils.toBuffer("测试 ", StandardCharsets.UTF_8), + BufferUtils.toBuffer("ok ", StandardCharsets.UTF_8)); + String str = BufferUtils.toString(buffers, StandardCharsets.UTF_8); + assertEquals("hello 测试 ok ", str); + } + + + private void testWriteToWithBufferThatDoesNotExposeArray(int capacity) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] bytes = new byte[capacity]; + ThreadLocalRandom.current().nextBytes(bytes); + ByteBuffer buffer = BufferUtils.allocate(capacity); + BufferUtils.append(buffer, bytes, 0, capacity); + BufferUtils.writeTo(buffer.asReadOnlyBuffer(), out); + assertArrayEquals(bytes, out.toByteArray()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/object/TestTypeUtils.java b/firefly-common/src/test/java/com/fireflysource/common/object/TestTypeUtils.java new file mode 100644 index 000000000..7961914b8 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/object/TestTypeUtils.java @@ -0,0 +1,119 @@ +package com.fireflysource.common.object; + + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TestTypeUtils { + @Test + void convertHexDigitTest() { + assertEquals((byte) 0, TypeUtils.convertHexDigit((byte) '0')); + assertEquals((byte) 9, TypeUtils.convertHexDigit((byte) '9')); + assertEquals((byte) 10, TypeUtils.convertHexDigit((byte) 'a')); + assertEquals((byte) 10, TypeUtils.convertHexDigit((byte) 'A')); + assertEquals((byte) 15, TypeUtils.convertHexDigit((byte) 'f')); + assertEquals((byte) 15, TypeUtils.convertHexDigit((byte) 'F')); + + assertEquals(0, TypeUtils.convertHexDigit((int) '0')); + assertEquals(9, TypeUtils.convertHexDigit((int) '9')); + assertEquals(10, TypeUtils.convertHexDigit((int) 'a')); + assertEquals(10, TypeUtils.convertHexDigit((int) 'A')); + assertEquals(15, TypeUtils.convertHexDigit((int) 'f')); + assertEquals(15, TypeUtils.convertHexDigit((int) 'F')); + } + + @Test + void testToHexInt() throws Exception { + StringBuilder b = new StringBuilder(); + + b.setLength(0); + TypeUtils.toHex(0, b); + assertEquals("00000000", b.toString()); + + b.setLength(0); + TypeUtils.toHex(Integer.MAX_VALUE, b); + assertEquals("7FFFFFFF", b.toString()); + + b.setLength(0); + TypeUtils.toHex(Integer.MIN_VALUE, b); + assertEquals("80000000", b.toString()); + + b.setLength(0); + TypeUtils.toHex(0x12345678, b); + assertEquals("12345678", b.toString()); + + b.setLength(0); + TypeUtils.toHex(0x9abcdef0, b); + assertEquals("9ABCDEF0", b.toString()); + } + + @Test + void testToHexLong() throws Exception { + StringBuilder b = new StringBuilder(); + + b.setLength(0); + TypeUtils.toHex((long) 0, b); + assertEquals("0000000000000000", b.toString()); + + b.setLength(0); + TypeUtils.toHex(Long.MAX_VALUE, b); + assertEquals("7FFFFFFFFFFFFFFF", b.toString()); + + b.setLength(0); + TypeUtils.toHex(Long.MIN_VALUE, b); + assertEquals("8000000000000000", b.toString()); + + b.setLength(0); + TypeUtils.toHex(0x123456789abcdef0L, b); + assertEquals("123456789ABCDEF0", b.toString()); + } + + @Test + void testIsTrue() { + assertTrue(TypeUtils.isTrue(Boolean.TRUE)); + assertTrue(TypeUtils.isTrue(true)); + assertTrue(TypeUtils.isTrue("true")); + assertTrue(TypeUtils.isTrue(new Object() { + @Override + public String toString() { + return "true"; + } + })); + + assertFalse(TypeUtils.isTrue(Boolean.FALSE)); + assertFalse(TypeUtils.isTrue(false)); + assertFalse(TypeUtils.isTrue("false")); + assertFalse(TypeUtils.isTrue("blargle")); + assertFalse(TypeUtils.isTrue(new Object() { + @Override + public String toString() { + return "false"; + } + })); + } + + @Test + void testIsFalse() { + assertTrue(TypeUtils.isFalse(Boolean.FALSE)); + assertTrue(TypeUtils.isFalse(false)); + assertTrue(TypeUtils.isFalse("false")); + assertTrue(TypeUtils.isFalse(new Object() { + @Override + public String toString() { + return "false"; + } + })); + + assertFalse(TypeUtils.isFalse(Boolean.TRUE)); + assertFalse(TypeUtils.isFalse(true)); + assertFalse(TypeUtils.isFalse("true")); + assertFalse(TypeUtils.isFalse("blargle")); + assertFalse(TypeUtils.isFalse(new Object() { + @Override + public String toString() { + return "true"; + } + })); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/ref/TestCleaner.java b/firefly-common/src/test/java/com/fireflysource/common/ref/TestCleaner.java new file mode 100644 index 000000000..933bbfb62 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/ref/TestCleaner.java @@ -0,0 +1,55 @@ +package com.fireflysource.common.ref; + +import com.fireflysource.common.lifecycle.ShutdownTasks; +import org.junit.jupiter.api.Test; + +public class TestCleaner { + + private Cleaner cleaner = Cleaner.create(); + + public static class AutoCloseableResource { + private final AutoCloseable closeable; + + public AutoCloseableResource(Cleaner cleaner, AutoCloseable closeable) { + this.closeable = closeable; + cleaner.register(this, () -> { + try { + closeable.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + } + + public static class Foo implements AutoCloseable { + + private final String text; + + public Foo(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @Override + public void close() throws Exception { + System.out.println("Clean resource: " + text); + } + } + + @Test + void test() throws InterruptedException { + ShutdownTasks.register(() -> System.out.println("exit process")); + + Foo foo = new Foo("Foo fu"); + AutoCloseableResource resource = new AutoCloseableResource(cleaner, foo); + System.out.println(foo.getText()); + resource = null; + + System.gc(); + Thread.sleep(3000); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/reflection/TestReflectUtils.java b/firefly-common/src/test/java/com/fireflysource/common/reflection/TestReflectUtils.java new file mode 100644 index 000000000..624440e19 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/reflection/TestReflectUtils.java @@ -0,0 +1,141 @@ +package com.fireflysource.common.reflection; + + +import org.junit.jupiter.api.Test; + +import static com.fireflysource.common.reflection.ReflectionUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TestReflectUtils { + + @Test + void testGetterAndSetterMethod() { + assertEquals("getName", getGetterMethod(Foo.class, "name").getName()); + assertEquals("isFailure", getGetterMethod(Foo.class, "failure").getName()); + + assertEquals("setName", getSetterMethod(Foo.class, "name").getName()); + assertEquals("setFailure", getSetterMethod(Foo.class, "failure").getName()); + + assertEquals("setiPad", getSetterMethod(Foo.class, "iPad").getName()); + assertEquals("setiPhone", getSetterMethod(Foo.class, "iPhone").getName()); + + assertEquals("isiPad", getGetterMethod(Foo.class, "iPad").getName()); + assertEquals("getiPhone", getGetterMethod(Foo.class, "iPhone").getName()); + } + + @Test + void testGetAndSet() throws Throwable { + Foo foo = new Foo(); + set(foo, "price", 4.44); + set(foo, "failure", true); + set(foo, "name", "foo hello"); + + assertEquals(4.44, get(foo, "price")); + assertTrue((Boolean) get(foo, "failure")); + assertEquals("foo hello", get(foo, "name")); + } + + @Test + void testCopy() { + Foo foo = new Foo(); + foo.setName("hello foo"); + foo.setPrice(3.3); + foo.setNumber(40); + + Foo2 foo2 = new Foo2(); + foo2.setName("hello foo2"); + + copy(foo2, foo); + assertEquals("hello foo2", foo.getName()); + assertEquals(40, foo.getNumber()); + } + + public static class Foo2 { + private String name; + private Integer number; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + } + + public static class Foo { + public String name; + public int num2; + public String info; + private boolean failure; + private int number; + private double price; + private String iPhone; + private boolean iPad; + + public String getiPhone() { + return iPhone; + } + + public void setiPhone(String iPhone) { + this.iPhone = iPhone; + } + + public boolean isiPad() { + return iPad; + } + + public void setiPad(boolean iPad) { + this.iPad = iPad; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public boolean isFailure() { + return failure; + } + + public void setFailure(boolean failure) { + this.failure = failure; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setProperty(String name, boolean failure) { + this.name = name; + this.failure = failure; + } + + } + +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/string/SearchPatternTest.java b/firefly-common/src/test/java/com/fireflysource/common/string/SearchPatternTest.java new file mode 100644 index 000000000..faf424e60 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/string/SearchPatternTest.java @@ -0,0 +1,188 @@ +package com.fireflysource.common.string; + +import com.fireflysource.common.io.BufferUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SearchPatternTest { + + @Test + public void testBasicSearch() { + byte[] p1 = "truth".getBytes(StandardCharsets.US_ASCII); + byte[] p2 = "evident".getBytes(StandardCharsets.US_ASCII); + byte[] p3 = "we".getBytes(StandardCharsets.US_ASCII); + byte[] d = "we hold these truths to be self evident".getBytes(StandardCharsets.US_ASCII); + + // Testing Compiled Pattern p1 "truth" + SearchPattern sp1 = SearchPattern.compile(p1); + assertEquals(14, sp1.match(d, 0, d.length)); + assertEquals(14, sp1.match(d, 14, p1.length)); + assertEquals(14, sp1.match(d, 14, p1.length + 1)); + assertEquals(-1, sp1.match(d, 14, p1.length - 1)); + assertEquals(-1, sp1.match(d, 15, d.length - 15)); + + // Testing Compiled Pattern p2 "evident" + SearchPattern sp2 = SearchPattern.compile(p2); + assertEquals(32, sp2.match(d, 0, d.length)); + assertEquals(32, sp2.match(d, 32, p2.length)); + assertEquals(32, sp2.match(d, 32, p2.length)); + assertEquals(-1, sp2.match(d, 32, p2.length - 1)); + assertEquals(-1, sp2.match(d, 33, d.length - 33)); + + // Testing Compiled Pattern p3 "evident" + SearchPattern sp3 = SearchPattern.compile(p3); + assertEquals(0, sp3.match(d, 0, d.length)); + assertEquals(0, sp3.match(d, 0, p3.length)); + assertEquals(0, sp3.match(d, 0, p3.length + 1)); + assertEquals(-1, sp3.match(d, 0, p3.length - 1)); + assertEquals(-1, sp3.match(d, 1, d.length - 1)); + } + + @Test + public void testDoubleMatch() { + byte[] p = "violent".getBytes(StandardCharsets.US_ASCII); + byte[] d = "These violent delights have violent ends.".getBytes(StandardCharsets.US_ASCII); + SearchPattern sp = SearchPattern.compile(p); + assertEquals(6, sp.match(d, 0, d.length)); + assertEquals(-1, sp.match(d, 6, p.length - 1)); + assertEquals(28, sp.match(d, 7, d.length - 7)); + assertEquals(28, sp.match(d, 28, d.length - 28)); + assertEquals(-1, sp.match(d, 29, d.length - 29)); + } + + @Test + public void testSearchInBinary() { + byte[] random = new byte[8192]; + ThreadLocalRandom.current().nextBytes(random); + // Arrays.fill(random,(byte)-67); + String preamble = "Blah blah blah"; + String epilogue = "The End! Blah Blah Blah"; + + ByteBuffer data = BufferUtils.allocate(preamble.length() + random.length + epilogue.length()); + BufferUtils.append(data, BufferUtils.toBuffer(preamble)); + BufferUtils.append(data, ByteBuffer.wrap(random)); + BufferUtils.append(data, BufferUtils.toBuffer(epilogue)); + + SearchPattern sp = SearchPattern.compile("The End!"); + + assertEquals(preamble.length() + random.length, sp.match(data.array(), data.arrayOffset() + data.position(), data.remaining())); + } + + @Test + public void testSearchBinaryKey() { + byte[] random = new byte[8192]; + ThreadLocalRandom.current().nextBytes(random); + byte[] key = new byte[64]; + ThreadLocalRandom.current().nextBytes(key); + + ByteBuffer data = BufferUtils.allocate(random.length + key.length); + BufferUtils.append(data, ByteBuffer.wrap(random)); + BufferUtils.append(data, ByteBuffer.wrap(key)); + SearchPattern sp = SearchPattern.compile(key); + + assertEquals(random.length, sp.match(data.array(), data.arrayOffset() + data.position(), data.remaining())); + } + + @Test + public void testAlmostMatch() { + byte[] p = "violent".getBytes(StandardCharsets.US_ASCII); + byte[] d = "vio lent violen v iolent violin vioviolenlent viiolent".getBytes(StandardCharsets.US_ASCII); + SearchPattern sp = SearchPattern.compile(p); + assertEquals(-1, sp.match(d, 0, d.length)); + } + + @Test + public void testOddSizedPatterns() { + // Test Large Pattern + byte[] p = "pneumonoultramicroscopicsilicovolcanoconiosis".getBytes(StandardCharsets.US_ASCII); + byte[] d = "pneumon".getBytes(StandardCharsets.US_ASCII); + SearchPattern sp = SearchPattern.compile(p); + assertEquals(-1, sp.match(d, 0, d.length)); + + // Test Single Character Pattern + p = "s".getBytes(StandardCharsets.US_ASCII); + d = "the cake is a lie".getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(10, sp.match(d, 0, d.length)); + } + + @Test + public void testEndsWith() { + byte[] p = "pneumonoultramicroscopicsilicovolcanoconiosis".getBytes(StandardCharsets.US_ASCII); + byte[] d = "pneumonoultrami".getBytes(StandardCharsets.US_ASCII); + SearchPattern sp = SearchPattern.compile(p); + assertEquals(15, sp.endsWith(d, 0, d.length)); + + p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmno".getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(0, sp.match(d, 0, d.length)); + assertEquals(-1, sp.match(d, 1, d.length - 1)); + assertEquals(15, sp.endsWith(d, 0, d.length)); + + p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + d = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(0, sp.match(d, 0, d.length)); + assertEquals(26, sp.match(d, 1, d.length - 1)); + assertEquals(26, sp.endsWith(d, 0, d.length)); + + //test no match + p = "hello world".getBytes(StandardCharsets.US_ASCII); + d = "there is definitely no match in here".getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(0, sp.endsWith(d, 0, d.length)); + } + + @Test + public void testStartsWithNoOffset() { + testStartsWith(""); + } + + @Test + public void testStartsWithOffset() { + testStartsWith("abcdef"); + } + + private void testStartsWith(String offset) { + byte[] p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + byte[] d = (offset + "ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz").getBytes(StandardCharsets.US_ASCII); + SearchPattern sp = SearchPattern.compile(p); + assertEquals(18 + offset.length(), sp.match(d, offset.length(), d.length - offset.length())); + assertEquals(-1, sp.match(d, offset.length() + 19, d.length - 19 - offset.length())); + assertEquals(26, sp.startsWith(d, offset.length(), d.length - offset.length(), 8)); + + p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + d = (offset + "ijklmnopqrstuvwxyNOMATCH").getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(0, sp.startsWith(d, offset.length(), d.length - offset.length(), 8)); + + p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + d = (offset + "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz").getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(26, sp.startsWith(d, offset.length(), d.length - offset.length(), 0)); + + //test no match + p = "hello world".getBytes(StandardCharsets.US_ASCII); + d = (offset + "there is definitely no match in here").getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(0, sp.startsWith(d, offset.length(), d.length - offset.length(), 0)); + + //test large pattern small buffer + p = "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.US_ASCII); + d = (offset + "mnopqrs").getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(19, sp.startsWith(d, offset.length(), d.length - offset.length(), 12)); + + //partial pattern + p = "abcdef".getBytes(StandardCharsets.US_ASCII); + d = (offset + "cde").getBytes(StandardCharsets.US_ASCII); + sp = SearchPattern.compile(p); + assertEquals(5, sp.startsWith(d, offset.length(), d.length - offset.length(), 2)); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/string/TestPattern.java b/firefly-common/src/test/java/com/fireflysource/common/string/TestPattern.java new file mode 100644 index 000000000..3cc6aa3af --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/string/TestPattern.java @@ -0,0 +1,85 @@ +package com.fireflysource.common.string; + + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class TestPattern { + + @Test + @DisplayName("should match pattern successfully.") + void testPattern() { + Pattern p = Pattern.compile("?ddaaad?", "?"); + assertEquals("", p.match("ddaaad")[1]); + assertEquals("xwww", p.match("ddaaadxwww")[1]); + assertEquals("xwww", p.match("addaaadxwww")[1]); + assertEquals("a", p.match("addaaadxwww")[0]); + assertEquals("a", p.match("addaaad")[0]); + assertNull(p.match("orange")); + + p = Pattern.compile("?", "?"); + assertEquals("orange", p.match("orange")[0]); + + p = Pattern.compile("??????", "?"); + assertEquals("orange", p.match("orange")[0]); + assertEquals(1, p.match("orange").length); + + p = Pattern.compile("org", "?"); + assertNull(p.match("orange")); + assertEquals(0, p.match("org").length); + + p = Pattern.compile("?org", "?"); + assertEquals("", p.match("org")[0]); + assertEquals("aass", p.match("aassorg")[0]); + assertEquals(1, p.match("ssorg").length); + + p = Pattern.compile("org?", "?"); + assertEquals("", p.match("org")[0]); + assertEquals("aaa", p.match("orgaaa")[0]); + assertEquals(1, p.match("orgaaa").length); + + p = Pattern.compile("www.?.com?", "?"); + assertEquals("fireflysource", p.match("www.fireflysource.com")[0]); + assertEquals("", p.match("www.fireflysource.com")[1]); + assertEquals("/cn/", p.match("www.fireflysource.com/cn/")[1]); + assertEquals(2, p.match("www.fireflysource.com/cn/").length); + assertNull(p.match("orange")); + + p = Pattern.compile("www.?.com/?/app", "?"); + assertNull(p.match("orange")); + assertEquals(2, p.match("www.fireflysource.com/cn/app").length); + assertEquals("fireflysource", p.match("www.fireflysource.com/cn/app")[0]); + assertEquals("cn", p.match("www.fireflysource.com/cn/app")[1]); + + p = Pattern.compile("?www.?.com/?/app", "?"); + assertNull(p.match("orange")); + assertEquals(3, p.match("www.fireflysource.com/cn/app").length); + assertEquals("", p.match("www.fireflysource.com/cn/app")[0]); + assertEquals("fireflysource", p.match("www.fireflysource.com/cn/app")[1]); + assertEquals("cn", p.match("www.fireflysource.com/cn/app")[2]); + assertEquals("http://", p.match("http://www.fireflysource.com/cn/app")[0]); + + p = Pattern.compile("?www.?.com/?/app?", "?"); + assertNull(p.match("orange")); + assertEquals(4, p.match("www.fireflysource.com/cn/app").length); + assertEquals("", p.match("www.fireflysource.com/cn/app")[0]); + assertEquals("fireflysource", p.match("www.fireflysource.com/cn/app")[1]); + assertEquals("cn", p.match("www.fireflysource.com/cn/app")[2]); + assertEquals("http://", p.match("http://www.fireflysource.com/cn/app")[0]); + assertEquals("", p.match("http://www.fireflysource.com/cn/app")[3]); + assertEquals("/1334", p.match("http://www.fireflysource.com/cn/app/1334")[3]); + + p = Pattern.compile("abc*abc", "*"); + assertEquals("", p.match("abcabcabc")[0]); + + p = Pattern.compile("aa*aa", "*"); + assertEquals("", p.match("aaaaa")[0]); + + p = Pattern.compile("*.mustache", "*"); + assertNull(p.match("IO.class")); + } + +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/string/TestQuotedStringTokenizer.java b/firefly-common/src/test/java/com/fireflysource/common/string/TestQuotedStringTokenizer.java new file mode 100644 index 000000000..8ca1f1590 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/string/TestQuotedStringTokenizer.java @@ -0,0 +1,160 @@ +package com.fireflysource.common.string; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + + +class TestQuotedStringTokenizer { + /* + * Test for String nextToken() + */ + @Test + void testTokenizer0() { + QuotedStringTokenizer tok = new QuotedStringTokenizer("abc\n\"d\\\"'\"\n'p\\',y'\nz"); + checkTok(tok, false, false); + } + + /* + * Test for String nextToken() + */ + @Test + void testTokenizer1() { + QuotedStringTokenizer tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,"); + checkTok(tok, false, false); + } + + /* + * Test for String nextToken() + */ + @Test + void testTokenizer2() { + QuotedStringTokenizer tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", false); + checkTok(tok, false, false); + + tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", true); + checkTok(tok, true, false); + } + + /* + * Test for String nextToken() + */ + @Test + void testTokenizer3() { + QuotedStringTokenizer tok; + + tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", false, false); + checkTok(tok, false, false); + + tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", false, true); + checkTok(tok, false, true); + + tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", true, false); + checkTok(tok, true, false); + + tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", true, true); + checkTok(tok, true, true); + } + + @Test + void testQuote() { + StringBuilder buf = new StringBuilder(); + + buf.setLength(0); + QuotedStringTokenizer.quote(buf, "abc \n efg"); + assertEquals("\"abc \\n efg\"", buf.toString()); + + buf.setLength(0); + QuotedStringTokenizer.quote(buf, "abcefg"); + assertEquals("\"abcefg\"", buf.toString()); + + buf.setLength(0); + QuotedStringTokenizer.quote(buf, "abcefg\""); + assertEquals("\"abcefg\\\"\"", buf.toString()); + + } + + /* + * Test for String nextToken() + */ + @Test + void testTokenizer4() { + QuotedStringTokenizer tok = new QuotedStringTokenizer("abc'def,ghi'jkl", ","); + tok.setSingle(false); + assertEquals("abc'def", tok.nextToken()); + assertEquals("ghi'jkl", tok.nextToken()); + tok = new QuotedStringTokenizer("abc'def,ghi'jkl", ","); + tok.setSingle(true); + assertEquals("abcdef,ghijkl", tok.nextToken()); + } + + private void checkTok(QuotedStringTokenizer tok, boolean delim, boolean quotes) { + assertTrue(tok.hasMoreElements()); + assertTrue(tok.hasMoreTokens()); + assertEquals("abc", tok.nextToken()); + if (delim) + assertEquals(",", tok.nextToken()); + if (delim) + assertEquals(" ", tok.nextToken()); + + assertEquals(quotes ? "\"d\\\"'\"" : "d\"'", tok.nextElement()); + if (delim) + assertEquals(",", tok.nextToken()); + assertEquals(quotes ? "'p\\',y'" : "p',y", tok.nextToken()); + if (delim) + assertEquals(" ", tok.nextToken()); + assertEquals("z", tok.nextToken()); + assertFalse(tok.hasMoreTokens()); + } + + /* + * Test for String quote(String, String) + */ + @Test + void testQuoteIfNeeded() { + assertEquals("abc", QuotedStringTokenizer.quoteIfNeeded("abc", " ,")); + assertEquals("\"a c\"", QuotedStringTokenizer.quoteIfNeeded("a c", " ,")); + assertEquals("\"a'c\"", QuotedStringTokenizer.quoteIfNeeded("a'c", " ,")); + assertEquals("\"a\\n\\r\\t\"", QuotedStringTokenizer.quote("a\n\r\t")); + assertEquals("\"\\u0000\\u001f\"", QuotedStringTokenizer.quote("\u0000\u001f")); + } + + @Test + void testUnquote() { + assertEquals("abc", QuotedStringTokenizer.unquote("abc")); + assertEquals("a\"c", QuotedStringTokenizer.unquote("\"a\\\"c\"")); + assertEquals("a'c", QuotedStringTokenizer.unquote("\"a'c\"")); + assertEquals("a\n\r\t", QuotedStringTokenizer.unquote("\"a\\n\\r\\t\"")); + assertEquals("\u0000\u001f ", QuotedStringTokenizer.unquote("\"\u0000\u001f\u0020\"")); + assertEquals("\u0000\u001f ", QuotedStringTokenizer.unquote("\"\u0000\u001f\u0020\"")); + assertEquals("ab\u001ec", QuotedStringTokenizer.unquote("ab\u001ec")); + assertEquals("ab\u001ec", QuotedStringTokenizer.unquote("\"ab\u001ec\"")); + } + + @Test + void testUnquoteOnly() { + assertEquals("abc", QuotedStringTokenizer.unquoteOnly("abc")); + assertEquals("a\"c", QuotedStringTokenizer.unquoteOnly("\"a\\\"c\"")); + assertEquals("a'c", QuotedStringTokenizer.unquoteOnly("\"a'c\"")); + assertEquals("a\\n\\r\\t", QuotedStringTokenizer.unquoteOnly("\"a\\\\n\\\\r\\\\t\"")); + assertEquals("ba\\uXXXXaaa", QuotedStringTokenizer.unquoteOnly("\"ba\\\\uXXXXaaa\"")); + } + + /** + * When encountering a Content-Disposition line during a multi-part mime + * file upload, the filename="..." field can contain '\' characters that do + * not belong to a proper escaping sequence, this tests + * QuotedStringTokenizer to ensure that it preserves those slashes for where + * they cannot be escaped. + */ + @Test + void testNextTokenOnContentDisposition() { + String content_disposition = "form-data; name=\"fileup\"; filename=\"Taken on Aug 22 \\ 2012.jpg\""; + + QuotedStringTokenizer tok = new QuotedStringTokenizer(content_disposition, ";", false, true); + + assertEquals("form-data", tok.nextToken().trim()); + assertEquals("name=\"fileup\"", tok.nextToken().trim()); + assertEquals("filename=\"Taken on Aug 22 \\ 2012.jpg\"", tok.nextToken().trim()); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/string/TestStringUtils.java b/firefly-common/src/test/java/com/fireflysource/common/string/TestStringUtils.java new file mode 100644 index 000000000..08b943ad9 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/string/TestStringUtils.java @@ -0,0 +1,348 @@ +package com.fireflysource.common.string; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class TestStringUtils { + + static final String WHITESPACE; + static final String NON_WHITESPACE; + static final String HARD_SPACE; + static final String TRIMMABLE; + static final String NON_TRIMMABLE; + + static { + String ws = ""; + String nws = ""; + final String hs = String.valueOf(((char) 160)); + String tr = ""; + String ntr = ""; + for (int i = 0; i < Character.MAX_VALUE; i++) { + if (Character.isWhitespace((char) i)) { + ws += String.valueOf((char) i); + if (i > 32) { + ntr += String.valueOf((char) i); + } + } else if (i < 40) { + nws += String.valueOf((char) i); + } + } + for (int i = 0; i <= 32; i++) { + tr += String.valueOf((char) i); + } + WHITESPACE = ws; + NON_WHITESPACE = nws; + HARD_SPACE = hs; + TRIMMABLE = tr; + NON_TRIMMABLE = ntr; + } + + + @Test + void testSplit() { + String byteRangeSet = "500-"; + String[] byteRangeSets = StringUtils.split(byteRangeSet, ','); + System.out.println(Arrays.toString(byteRangeSets)); + assertEquals(byteRangeSets.length, 1); + + byteRangeSet = "500-,"; + byteRangeSets = StringUtils.split(byteRangeSet, ','); + System.out.println(Arrays.toString(byteRangeSets)); + assertEquals(byteRangeSets.length, 1); + + byteRangeSet = ",500-,"; + byteRangeSets = StringUtils.split(byteRangeSet, ','); + System.out.println(Arrays.toString(byteRangeSets)); + assertEquals(byteRangeSets.length, 1); + + byteRangeSet = ",500-,"; + byteRangeSets = StringUtils.split(byteRangeSet, ","); + System.out.println(Arrays.toString(byteRangeSets)); + assertEquals(byteRangeSets.length, 1); + + byteRangeSet = ",500-"; + byteRangeSets = StringUtils.split(byteRangeSet, ','); + System.out.println(Arrays.toString(byteRangeSets)); + assertEquals(byteRangeSets.length, 1); + + byteRangeSet = "500-700,601-999,"; + byteRangeSets = StringUtils.split(byteRangeSet, ','); + assertEquals(byteRangeSets.length, 2); + + byteRangeSet = "500-700,,601-999,"; + byteRangeSets = StringUtils.split(byteRangeSet, ','); + assertEquals(byteRangeSets.length, 2); + + String tmp = "hello#$world#%test#$eee"; + String[] tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); + System.out.println(Arrays.toString(tmps)); + assertEquals(tmps.length, 3); + + tmp = "hello#$"; + tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); + System.out.println(Arrays.toString(tmps)); + assertEquals(tmps.length, 1); + + tmp = "#$hello#$"; + tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); + System.out.println(Arrays.toString(tmps)); + assertEquals(tmps.length, 1); + + tmp = "#$hello"; + tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); + System.out.println(Arrays.toString(tmps)); + assertEquals(tmps.length, 1); + + tmp = "#$hello#$world#$"; + tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); + System.out.println(Arrays.toString(tmps)); + assertEquals(tmps.length, 2); + + tmp = "#$hello#$#$world#$"; + tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); + System.out.println(Arrays.toString(tmps)); + assertEquals(tmps.length, 2); + + } + + @Test + void testSplit_String() { + assertNull(StringUtils.split(null)); + assertEquals(0, StringUtils.split("").length); + + String str = "a b .c"; + String[] res = StringUtils.split(str); + assertEquals(3, res.length); + assertEquals("a", res[0]); + assertEquals("b", res[1]); + assertEquals(".c", res[2]); + + str = " a "; + res = StringUtils.split(str); + assertEquals(1, res.length); + assertEquals("a", res[0]); + + str = "a" + WHITESPACE + "b" + NON_WHITESPACE + "c"; + res = StringUtils.split(str); + assertEquals(2, res.length); + assertEquals("a", res[0]); + assertEquals("b" + NON_WHITESPACE + "c", res[1]); + } + + @Test + void testSplit_StringChar() { + assertNull(StringUtils.split(null, '.')); + assertEquals(0, StringUtils.split("", '.').length); + + String str = "a.b.. c"; + String[] res = StringUtils.split(str, '.'); + assertEquals(3, res.length); + assertEquals("a", res[0]); + assertEquals("b", res[1]); + assertEquals(" c", res[2]); + + str = ".a."; + res = StringUtils.split(str, '.'); + assertEquals(1, res.length); + assertEquals("a", res[0]); + + str = "a b c"; + res = StringUtils.split(str, ' '); + assertEquals(3, res.length); + assertEquals("a", res[0]); + assertEquals("b", res[1]); + assertEquals("c", res[2]); + } + + @Test + void testSplit_StringString_StringStringInt() { + assertNull(StringUtils.split(null, ".")); + assertNull(StringUtils.split(null, ".", 3)); + + assertEquals(0, StringUtils.split("", ".").length); + assertEquals(0, StringUtils.split("", ".", 3).length); + + innerTestSplit('.', ".", ' '); + innerTestSplit('.', ".", ','); + innerTestSplit('.', ".,", 'x'); + for (int i = 0; i < WHITESPACE.length(); i++) { + for (int j = 0; j < NON_WHITESPACE.length(); j++) { + innerTestSplit(WHITESPACE.charAt(i), null, NON_WHITESPACE.charAt(j)); + innerTestSplit(WHITESPACE.charAt(i), String.valueOf(WHITESPACE.charAt(i)), NON_WHITESPACE.charAt(j)); + } + } + + String[] results; + final String[] expectedResults = {"ab", "de fg"}; + results = StringUtils.split("ab de fg", null, 2); + assertEquals(expectedResults.length, results.length); + for (int i = 0; i < expectedResults.length; i++) { + assertEquals(expectedResults[i], results[i]); + } + + final String[] expectedResults2 = {"ab", "cd:ef"}; + results = StringUtils.split("ab:cd:ef", ":", 2); + assertEquals(expectedResults2.length, results.length); + for (int i = 0; i < expectedResults2.length; i++) { + assertEquals(expectedResults2[i], results[i]); + } + } + + private void innerTestSplit(final char separator, final String sepStr, final char noMatch) { + final String msg = "Failed on separator hex(" + Integer.toHexString(separator) + + "), noMatch hex(" + Integer.toHexString(noMatch) + "), sepStr(" + sepStr + ")"; + + final String str = "a" + separator + "b" + separator + separator + noMatch + "c"; + String[] res; + // (str, sepStr) + res = StringUtils.split(str, sepStr); + assertEquals(3, res.length, msg); + assertEquals("a", res[0]); + assertEquals("b", res[1]); + assertEquals(noMatch + "c", res[2]); + + final String str2 = separator + "a" + separator; + res = StringUtils.split(str2, sepStr); + assertEquals(1, res.length, msg); + assertEquals("a", res[0], msg); + + res = StringUtils.split(str, sepStr, -1); + assertEquals(3, res.length, msg); + assertEquals("a", res[0], msg); + assertEquals("b", res[1], msg); + assertEquals(noMatch + "c", res[2], msg); + + res = StringUtils.split(str, sepStr, 0); + assertEquals(3, res.length, msg); + assertEquals("a", res[0], msg); + assertEquals("b", res[1], msg); + assertEquals(noMatch + "c", res[2], msg); + + res = StringUtils.split(str, sepStr, 1); + assertEquals(1, res.length, msg); + assertEquals(str, res[0], msg); + + res = StringUtils.split(str, sepStr, 2); + assertEquals(2, res.length, msg); + assertEquals("a", res[0], msg); + assertEquals(str.substring(2), res[1], msg); + } + + @Test + void testSplitByWholeString_StringStringBoolean() { + assertArrayEquals(null, StringUtils.splitByWholeSeparator(null, ".")); + + assertEquals(0, StringUtils.splitByWholeSeparator("", ".").length); + + final String stringToSplitOnNulls = "ab de fg"; + final String[] splitOnNullExpectedResults = {"ab", "de", "fg"}; + + final String[] splitOnNullResults = StringUtils.splitByWholeSeparator(stringToSplitOnNulls, null); + assertEquals(splitOnNullExpectedResults.length, splitOnNullResults.length); + for (int i = 0; i < splitOnNullExpectedResults.length; i += 1) { + assertEquals(splitOnNullExpectedResults[i], splitOnNullResults[i]); + } + + final String stringToSplitOnCharactersAndString = "abstemiouslyaeiouyabstemiously"; + + final String[] splitOnStringExpectedResults = {"abstemiously", "abstemiously"}; + final String[] splitOnStringResults = StringUtils.splitByWholeSeparator(stringToSplitOnCharactersAndString, "aeiouy"); + assertEquals(splitOnStringExpectedResults.length, splitOnStringResults.length); + for (int i = 0; i < splitOnStringExpectedResults.length; i += 1) { + assertEquals(splitOnStringExpectedResults[i], splitOnStringResults[i]); + } + + final String[] splitWithMultipleSeparatorExpectedResults = {"ab", "cd", "ef"}; + final String[] splitWithMultipleSeparator = StringUtils.splitByWholeSeparator("ab:cd::ef", ":"); + assertEquals(splitWithMultipleSeparatorExpectedResults.length, splitWithMultipleSeparator.length); + for (int i = 0; i < splitWithMultipleSeparatorExpectedResults.length; i++) { + assertEquals(splitWithMultipleSeparatorExpectedResults[i], splitWithMultipleSeparator[i]); + } + } + + @Test + void testSplitByWholeString_StringStringBooleanInt() { + assertArrayEquals(null, StringUtils.splitByWholeSeparator(null, ".", 3)); + + assertEquals(0, StringUtils.splitByWholeSeparator("", ".", 3).length); + + final String stringToSplitOnNulls = "ab de fg"; + final String[] splitOnNullExpectedResults = {"ab", "de fg"}; + //String[] splitOnNullExpectedResults = { "ab", "de" } ; + + final String[] splitOnNullResults = StringUtils.splitByWholeSeparator(stringToSplitOnNulls, null, 2); + assertEquals(splitOnNullExpectedResults.length, splitOnNullResults.length); + for (int i = 0; i < splitOnNullExpectedResults.length; i += 1) { + assertEquals(splitOnNullExpectedResults[i], splitOnNullResults[i]); + } + + final String stringToSplitOnCharactersAndString = "abstemiouslyaeiouyabstemiouslyaeiouyabstemiously"; + + final String[] splitOnStringExpectedResults = {"abstemiously", "abstemiouslyaeiouyabstemiously"}; + //String[] splitOnStringExpectedResults = { "abstemiously", "abstemiously" } ; + final String[] splitOnStringResults = StringUtils.splitByWholeSeparator(stringToSplitOnCharactersAndString, "aeiouy", 2); + assertEquals(splitOnStringExpectedResults.length, splitOnStringResults.length); + for (int i = 0; i < splitOnStringExpectedResults.length; i++) { + assertEquals(splitOnStringExpectedResults[i], splitOnStringResults[i]); + } + } + + @Test + void testHasText() { + String str = "\r\n\t\t"; + assertTrue(StringUtils.hasLength(str)); + assertFalse(StringUtils.hasText(str)); + str = null; + assertFalse(StringUtils.hasText(str)); + } + + @Test + void testReplace() { + String str = "hello ${t1} and ${t2} s"; + Map map = new HashMap<>(); + map.put("t1", "foo"); + map.put("t2", "bar"); + String ret = StringUtils.replace(str, map); + assertEquals(ret, "hello foo and bar s"); + + map = new HashMap<>(); + map.put("t1", "foo"); + map.put("t2", "${dddd}"); + ret = StringUtils.replace(str, map); + assertEquals(ret, "hello foo and ${dddd} s"); + + map = new HashMap<>(); + map.put("t1", null); + map.put("t2", "${dddd}"); + ret = StringUtils.replace(str, map); + assertEquals(ret, "hello null and ${dddd} s"); + + map = new HashMap<>(); + map.put("t1", 33); + map.put("t2", 42L); + ret = StringUtils.replace(str, map); + assertEquals(ret, "hello 33 and 42 s"); + } + + @Test + void testReplace2() { + String str2 = "hello {{{{} and {} mm"; + String ret2 = StringUtils.replace(str2, "foo", "bar"); + assertEquals(ret2, "hello {{{foo and bar mm"); + + ret2 = StringUtils.replace(str2, "foo"); + assertEquals(ret2, "hello {{{foo and {} mm"); + + ret2 = StringUtils.replace(str2, "foo", "bar", "foo2"); + assertEquals(ret2, "hello {{{foo and bar mm"); + + ret2 = StringUtils.replace(str2, 12, 23L, 33); + assertEquals(ret2, "hello {{{12 and 23 mm"); + } + +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/string/Utf8StringBuilderInvalidUtfTest.java b/firefly-common/src/test/java/com/fireflysource/common/string/Utf8StringBuilderInvalidUtfTest.java new file mode 100644 index 000000000..61ab94ace --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/string/Utf8StringBuilderInvalidUtfTest.java @@ -0,0 +1,27 @@ +package com.fireflysource.common.string; + + +import com.fireflysource.common.object.TypeUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test various invalid UTF8 byte sequences. + */ +class Utf8StringBuilderInvalidUtfTest { + + @ParameterizedTest + @ValueSource(strings = {"c0af", "EDA080", "f08080af", "f8808080af", "e080af", "F4908080", "fbbfbfbfbf", "10FFFF", + "CeBaE1BdB9Cf83CeBcCeB5EdA080656469746564", "da07", "d807", "EDA087"}) + void testInvalidUTF8(String hex) { + byte[] bytes = TypeUtils.fromHexString(hex); + System.out.printf("Utf8StringBuilderInvalidUtfTest (%s)%n", TypeUtils.toHexString(bytes)); + + assertThrows(Utf8Appendable.NotUtf8Exception.class, () -> { + Utf8StringBuilder buffer = new Utf8StringBuilder(); + buffer.append(bytes, 0, bytes.length); + }); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/string/Utf8StringBuilderTest.java b/firefly-common/src/test/java/com/fireflysource/common/string/Utf8StringBuilderTest.java new file mode 100644 index 000000000..b9e99dca4 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/string/Utf8StringBuilderTest.java @@ -0,0 +1,107 @@ +package com.fireflysource.common.string; + +import com.fireflysource.common.object.TypeUtils; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + + +class Utf8StringBuilderTest { + + @Test + void testFastFail_1() { + byte[] part1 = TypeUtils.fromHexString("cebae1bdb9cf83cebcceb5"); + byte[] part2 = TypeUtils.fromHexString("f4908080"); // INVALID + // Here for test tracking reasons, not needed to satisfy test + // byte[] part3 = TypeUtil.fromHexString("656469746564"); + + Utf8StringBuilder buffer = new Utf8StringBuilder(); + // Part 1 is valid + buffer.append(part1, 0, part1.length); + try { + // Part 2 is invalid + buffer.append(part2, 0, part2.length); + fail("Should have thrown a NotUtf8Exception"); + } catch (Utf8Appendable.NotUtf8Exception e) { + // expected path + } + } + + @Test + void testFastFail_2() { + byte[] part1 = TypeUtils.fromHexString("cebae1bdb9cf83cebcceb5f4"); + byte[] part2 = TypeUtils.fromHexString("90"); // INVALID + // Here for test search/tracking reasons, not needed to satisfy test + // byte[] part3 = TypeUtil.fromHexString("8080656469746564"); + + Utf8StringBuilder buffer = new Utf8StringBuilder(); + // Part 1 is valid + buffer.append(part1, 0, part1.length); + try { + // Part 2 is invalid + buffer.append(part2, 0, part2.length); + fail("Should have thrown a NotUtf8Exception"); + } catch (Utf8Appendable.NotUtf8Exception e) { + // expected path + } + } + + @Test + void testUtfStringBuilder() { + String source = "abcd012345\n\r\u0000\u00a4\u10fb\ufffdfirefly"; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + Utf8StringBuilder buffer = new Utf8StringBuilder(); + for (byte aByte : bytes) + buffer.append(aByte); + assertEquals(source, buffer.toString()); + assertTrue(buffer.toString().endsWith("firefly")); + } + + @Test + void testShort() { + assertThrows(IllegalArgumentException.class, () -> { + String source = "abc\u10fb"; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + Utf8StringBuilder buffer = new Utf8StringBuilder(); + for (int i = 0; i < bytes.length - 1; i++) { + buffer.append(bytes[i]); + } + buffer.toString(); + }); + } + + @Test + void testLong() { + String source = "abcXX"; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + bytes[3] = (byte) 0xc0; + bytes[4] = (byte) 0x00; + + Utf8StringBuilder buffer = new Utf8StringBuilder(); + try { + for (byte aByte : bytes) { + buffer.append(aByte); + } + fail("Should have resulted in an Utf8Appendable.NotUtf8Exception"); + } catch (Utf8Appendable.NotUtf8Exception e) { + // expected path + } + assertEquals("abc\ufffd", buffer.toString()); + } + + @Test + void testUTF32codes() { + String source = "\uD842\uDF9F"; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + + String jvmcheck = new String(bytes, 0, bytes.length, StandardCharsets.UTF_8); + assertEquals(source, jvmcheck); + + Utf8StringBuilder buffer = new Utf8StringBuilder(); + buffer.append(bytes, 0, bytes.length); + String result = buffer.toString(); + assertEquals(source, result); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/sys/TestJavaVersion.java b/firefly-common/src/test/java/com/fireflysource/common/sys/TestJavaVersion.java new file mode 100644 index 000000000..7df2fb450 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/sys/TestJavaVersion.java @@ -0,0 +1,13 @@ +package com.fireflysource.common.sys; + +import org.junit.jupiter.api.Test; + +public class TestJavaVersion { + + @Test + void test() { + System.out.println(JavaVersion.VERSION.getVersion()); + System.out.println(JavaVersion.VERSION.getPlatform()); + System.out.println(System.getProperty("java.version")); + } +} diff --git a/firefly-common/src/test/java/com/fireflysource/common/sys/TestProjectVersion.java b/firefly-common/src/test/java/com/fireflysource/common/sys/TestProjectVersion.java new file mode 100644 index 000000000..f2b9602c2 --- /dev/null +++ b/firefly-common/src/test/java/com/fireflysource/common/sys/TestProjectVersion.java @@ -0,0 +1,17 @@ +package com.fireflysource.common.sys; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class TestProjectVersion { + + @Test + @DisplayName("should generate project logo") + void testLogo() { + String logo = ProjectVersion.logo(); + System.out.println(logo); + assertNotNull(logo); + } +} diff --git a/firefly-common/src/test/java/test/utils/SplitPerformance.java b/firefly-common/src/test/java/test/utils/SplitPerformance.java deleted file mode 100644 index 5c74c9a58..000000000 --- a/firefly-common/src/test/java/test/utils/SplitPerformance.java +++ /dev/null @@ -1,32 +0,0 @@ -package test.utils; - -import com.firefly.utils.StringUtils; - -public class SplitPerformance { - - private static final int TIMES = 50000; - - /** - * @param args - */ - public static void main(String[] args) { - // TODO Auto-generated method stub - String str = "fdsf@dsfsdf"; - long start = System.currentTimeMillis(); - String [] strs = null; - for (int i = 0; i < TIMES; i++) { - strs = str.split("@"); - - } - long end = System.currentTimeMillis(); - System.out.println("String split [" + (end - start) + "ms] " + strs[0] + strs[1]); - - start = System.currentTimeMillis(); - for (int i = 0; i < TIMES; i++) { - strs = StringUtils.split(str, "@"); - } - end = System.currentTimeMillis(); - System.out.println("StringUtils split [" + (end - start) + "ms] " + strs[0] + strs[1]); - } - -} diff --git a/firefly-common/src/test/java/test/utils/SumJavaCode.java b/firefly-common/src/test/java/test/utils/SumJavaCode.java deleted file mode 100644 index be29850e3..000000000 --- a/firefly-common/src/test/java/test/utils/SumJavaCode.java +++ /dev/null @@ -1,128 +0,0 @@ -package test.utils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; - -public class SumJavaCode { - private long normalLines = 0; // 空行 - private long commentLines = 0; // 注释行 - private long whiteLines = 0; // 代码行 - - public static void main(String[] args) { - System.out.println(args[0]); - SumJavaCode sjc = new SumJavaCode(); - File f = new File(args[0] + "/firefly-project/firefly-common"); - System.out.println(f.getName()); - sjc.treeFile(f); - System.out.println("空行:" + sjc.getWhiteLines()); - System.out.println("注释行:" + sjc.getCommentLines()); - System.out.println("代码行:" + sjc.getNormalLines()); - - sjc = new SumJavaCode(); - f = new File(args[0] + "/firefly-project/firefly"); - System.out.println(f.getName()); - sjc.treeFile(f); - System.out.println("空行:" + sjc.getWhiteLines()); - System.out.println("注释行:" + sjc.getCommentLines()); - System.out.println("代码行:" + sjc.getNormalLines()); - - sjc = new SumJavaCode(); - f = new File(args[0] + "/firefly-project/firefly-nettool"); - System.out.println(f.getName()); - sjc.treeFile(f); - System.out.println("空行:" + sjc.getWhiteLines()); - System.out.println("注释行:" + sjc.getCommentLines()); - System.out.println("代码行:" + sjc.getNormalLines()); - - } - - /** - * 查找出一个目录下所有的.java文件 - * - * @param f - * 要查找的目录 - */ - private void treeFile(File f) { - f.listFiles(new FileFilter() { - @Override - public boolean accept(File file) { - if (!file.isDirectory()) { - if (file.getName().matches(".*\\.java$")) { - sumCode(file); - } - } else { - treeFile(file); - } - return false; - } - }); - } - - /** - * 计算一个.java文件中的代码行,空行,注释行 - * - * @param file - * - * 要计算的.java文件 - */ - private void sumCode(File file) { - BufferedReader br = null; - boolean comment = false; - try { - br = new BufferedReader(new FileReader(file)); - String line = ""; - try { - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.matches("^[\\s&&[^\\n]]*$")) { - whiteLines++; - } else if (line.startsWith("/*") && !line.endsWith("*/")) { - commentLines++; - comment = true; - } else if (true == comment) { - commentLines++; - if (line.endsWith("*/")) { - comment = false; - } - } else if (line.startsWith("//")) { - commentLines++; - } else { - normalLines++; - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - } finally { - if (br != null) { - try { - br.close(); - br = null; - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - } - - public long getNormalLines() { - return normalLines; - } - - public long getCommentLines() { - return commentLines; - } - - public long getWhiteLines() { - return whiteLines; - } - - -} diff --git a/firefly-common/src/test/java/test/utils/TestConvertUtils.java b/firefly-common/src/test/java/test/utils/TestConvertUtils.java deleted file mode 100644 index b030d87a3..000000000 --- a/firefly-common/src/test/java/test/utils/TestConvertUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -package test.utils; - -import java.lang.reflect.Method; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import static org.hamcrest.Matchers.*; -import org.junit.Assert; -import org.junit.Test; -import com.firefly.utils.ConvertUtils; -import com.firefly.utils.log.LogFactory; - -public class TestConvertUtils { - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - public void testConvertArray() throws Exception { - Collection collection = new ArrayList(); - collection.add("arr1"); - collection.add("arr2"); - Method method = TestConvertUtils.class.getMethod("setArray", - String[].class); - Object obj = ConvertUtils.convert(collection, - method.getParameterTypes()[0]); - Integer ret = (Integer)method.invoke(this, obj); - Assert.assertThat(ret, is(2)); - } - - @Test - public void testAutoConvertLong() { - Long x = ConvertUtils.convert("10000000000", ""); - Assert.assertThat(x, is(10000000000L)); - - x = ConvertUtils.convert("10000000000", long.class); - Assert.assertThat(x, is(10000000000L)); - - x = ConvertUtils.convert("10000000000", "long"); - Assert.assertThat(x, is(10000000000L)); - } - - public int setArray(String[] arr) { -// log.debug(Arrays.toString(arr)); - return arr.length; - } - - public static void main(String[] args) throws URISyntaxException { - System.out.println(LogFactory.class.getClassLoader().getResource("firefly-log.properties").toURI()); - } -} diff --git a/firefly-common/src/test/java/test/utils/TestReflectUtils.java b/firefly-common/src/test/java/test/utils/TestReflectUtils.java deleted file mode 100644 index add53f411..000000000 --- a/firefly-common/src/test/java/test/utils/TestReflectUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -package test.utils; - -import org.junit.Assert; -import org.junit.Test; -import com.firefly.utils.ReflectUtils; -import static org.hamcrest.Matchers.*; - -public class TestReflectUtils { - - @Test - public void testGetterMethod() { - Assert.assertThat(ReflectUtils.getGetterMethod(Foo.class, "name") - .getName(), is("getName")); - Assert.assertThat(ReflectUtils.getGetterMethod(Foo.class, "failure") - .getName(), is("isFailure")); - } - - public static void main(String[] args) { - System.out.println(ReflectUtils.getGetterMethod(Foo.class, "name").getName()); - } - - public static class Foo { - private boolean failure; - private String name; - - - public boolean isFailure() { - return failure; - } - - public void setFailure(boolean failure) { - this.failure = failure; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - } - -} diff --git a/firefly-common/src/test/java/test/utils/TestStringUtils.java b/firefly-common/src/test/java/test/utils/TestStringUtils.java deleted file mode 100644 index 3e4a9fbb0..000000000 --- a/firefly-common/src/test/java/test/utils/TestStringUtils.java +++ /dev/null @@ -1,168 +0,0 @@ -package test.utils; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.utils.StringUtils; -import static org.hamcrest.Matchers.*; - -public class TestStringUtils { - - @Test - public void testSplit() { - String byteRangeSet = "500-"; - String[] byteRangeSets = StringUtils.split(byteRangeSet, ','); - System.out.println(Arrays.toString(byteRangeSets)); - Assert.assertThat(byteRangeSets.length, is(1)); - - byteRangeSet = "500-,"; - byteRangeSets = StringUtils.split(byteRangeSet, ','); - System.out.println(Arrays.toString(byteRangeSets)); - Assert.assertThat(byteRangeSets.length, is(1)); - - byteRangeSet = ",500-,"; - byteRangeSets = StringUtils.split(byteRangeSet, ','); - System.out.println(Arrays.toString(byteRangeSets)); - Assert.assertThat(byteRangeSets.length, is(1)); - - byteRangeSet = ",500-,"; - byteRangeSets = StringUtils.split(byteRangeSet, ","); - System.out.println(Arrays.toString(byteRangeSets)); - Assert.assertThat(byteRangeSets.length, is(1)); - - byteRangeSet = ",500-"; - byteRangeSets = StringUtils.split(byteRangeSet, ','); - System.out.println(Arrays.toString(byteRangeSets)); - Assert.assertThat(byteRangeSets.length, is(1)); - - byteRangeSet = "500-700,601-999,"; - byteRangeSets = StringUtils.split(byteRangeSet, ','); - Assert.assertThat(byteRangeSets.length, is(2)); - - byteRangeSet = "500-700,,601-999,"; - byteRangeSets = StringUtils.split(byteRangeSet, ','); - Assert.assertThat(byteRangeSets.length, is(2)); - - String tmp = "hello#$world#%test#$eee"; - String[] tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); - System.out.println(Arrays.toString(tmps)); - Assert.assertThat(tmps.length, is(3)); - - tmp = "hello#$"; - tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); - System.out.println(Arrays.toString(tmps)); - Assert.assertThat(tmps.length, is(1)); - - tmp = "#$hello#$"; - tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); - System.out.println(Arrays.toString(tmps)); - Assert.assertThat(tmps.length, is(1)); - - tmp = "#$hello"; - tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); - System.out.println(Arrays.toString(tmps)); - Assert.assertThat(tmps.length, is(1)); - - tmp = "#$hello#$world#$"; - tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); - System.out.println(Arrays.toString(tmps)); - Assert.assertThat(tmps.length, is(2)); - - tmp = "#$hello#$#$world#$"; - tmps = StringUtils.splitByWholeSeparator(tmp, "#$"); - System.out.println(Arrays.toString(tmps)); - Assert.assertThat(tmps.length, is(2)); - - } - - @Test - public void testHasText() { - String str = "\r\n\t\t"; - Assert.assertThat(StringUtils.hasLength(str), is(true)); - Assert.assertThat(StringUtils.hasText(str), is(false)); - str = null; - Assert.assertThat(StringUtils.hasText(str), is(false)); - } - - @Test - public void testReplace() { - String str = "hello ${t1} and ${t2} s"; - Map map = new HashMap(); - map.put("t1", "foo"); - map.put("t2", "bar"); - String ret = StringUtils.replace(str, map); - Assert.assertThat(ret, is("hello foo and bar s")); - - map = new HashMap(); - map.put("t1", "foo"); - map.put("t2", "${dddd}"); - ret = StringUtils.replace(str, map); - Assert.assertThat(ret, is("hello foo and ${dddd} s")); - - map = new HashMap(); - map.put("t1", null); - map.put("t2", "${dddd}"); - ret = StringUtils.replace(str, map); - Assert.assertThat(ret, is("hello null and ${dddd} s")); - - map = new HashMap(); - map.put("t1", 33); - map.put("t2", 42L); - ret = StringUtils.replace(str, map); - Assert.assertThat(ret, is("hello 33 and 42 s")); - } - - @Test - public void testReplace2() { - String str2 = "hello {{{{} and {} mm"; - String ret2 = StringUtils.replace(str2, "foo", "bar"); - Assert.assertThat(ret2, is("hello {{{foo and bar mm")); - - ret2 = StringUtils.replace(str2, "foo"); - Assert.assertThat(ret2, is("hello {{{foo and {} mm")); - - ret2 = StringUtils.replace(str2, "foo", "bar", "foo2"); - Assert.assertThat(ret2, is("hello {{{foo and bar mm")); - - ret2 = StringUtils.replace(str2, 12, 23L, 33); - Assert.assertThat(ret2, is("hello {{{12 and 23 mm")); - } - - public static void main(String[] args) { - String str = "hello ${t1} and ${t2}"; - Map map = new HashMap(); - map.put("t1", "foo"); - map.put("t2", "bar"); - String ret = StringUtils.replace(str, map); - System.out.println(ret); - - map = new HashMap(); - map.put("t1", "foo"); - map.put("t2", "${dddd}"); - ret = StringUtils.replace(str, map); - System.out.println(ret); - - map = new HashMap(); - map.put("t1", "foo"); - map.put("t2", null); - ret = StringUtils.replace(str, map); - System.out.println(ret); - - String str2 = "hello {{{{} and {} mm"; - String ret2 = StringUtils.replace(str2, "foo", "bar"); - System.out.println(ret2); - - ret2 = StringUtils.replace(str2, "foo"); - System.out.println(ret2); - - ret2 = StringUtils.replace(str2, "foo", "bar", "foo2"); - System.out.println(ret2); - - String r = "-500"; - System.out.println(StringUtils.split(r, '-')[0] + "|" + StringUtils.split(r, '-').length); - } -} diff --git a/firefly-common/src/test/java/test/utils/TestVerifyUtils.java b/firefly-common/src/test/java/test/utils/TestVerifyUtils.java deleted file mode 100644 index 29c8c549d..000000000 --- a/firefly-common/src/test/java/test/utils/TestVerifyUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package test.utils; - -import org.junit.Assert; -import org.junit.Test; -import com.firefly.utils.VerifyUtils; -import static org.hamcrest.Matchers.*; - -public class TestVerifyUtils { - - @Test - public void testIsEmpty() { - String str = "\r\n\t\t"; - Assert.assertThat(VerifyUtils.isEmpty(str), is(true)); - str = null; - Assert.assertThat(VerifyUtils.isEmpty(str), is(true)); - } - - @Test - public void testIsFloat() { - Assert.assertThat(VerifyUtils.isFloat("3f"), is(true)); - Assert.assertThat(VerifyUtils.isFloat("3.3f"), is(true)); - Assert.assertThat(VerifyUtils.isFloat("-.33f"), is(true)); - Assert.assertThat(VerifyUtils.isFloat("-33.00f"), is(true)); - Assert.assertThat(VerifyUtils.isFloat("-33.00"), is(false)); - Assert.assertThat(VerifyUtils.isFloat("ddfffe33"), is(false)); - } - - @Test - public void testIsInteger() { - Assert.assertThat(VerifyUtils.isInteger("30"), is(true)); - Assert.assertThat(VerifyUtils.isInteger("-30"), is(true)); - Assert.assertThat(VerifyUtils.isInteger("122312123121"), is(false)); - Assert.assertThat(VerifyUtils.isInteger("1223121sss1"), is(false)); - } - - @Test - public void testIsLong() { - Assert.assertThat(VerifyUtils.isLong("30"), is(false)); - Assert.assertThat(VerifyUtils.isLong("-30"), is(false)); - Assert.assertThat(VerifyUtils.isLong("122312123121"), is(true)); - Assert.assertThat(VerifyUtils.isLong("-122312123121"), is(true)); - Assert.assertThat(VerifyUtils.isLong("-122l"), is(true)); - Assert.assertThat(VerifyUtils.isLong("122L"), is(true)); - Assert.assertThat(VerifyUtils.isLong("1223121sss1"), is(false)); - } - - @Test - public void testIsNumeric() { - Assert.assertThat(VerifyUtils.isNumeric("13422224343"), is(true)); - Assert.assertThat(VerifyUtils.isNumeric(""), is(false)); - Assert.assertThat(VerifyUtils.isNumeric("134"), is(true)); - Assert.assertThat(VerifyUtils.isNumeric("-13433"), is(true)); - Assert.assertThat(VerifyUtils.isNumeric("134dfdfsfdf"), is(false)); - } - - @Test - public void testIsDouble() { - Assert.assertThat(VerifyUtils.isDouble("-30.2222"), is(true)); - Assert.assertThat(VerifyUtils.isDouble("30.2222"), is(true)); - Assert.assertThat(VerifyUtils.isDouble("30"), is(false)); - Assert.assertThat(VerifyUtils.isDouble("30ss"), is(false)); - Assert.assertThat(VerifyUtils.isDouble(""), is(false)); - Assert.assertThat(VerifyUtils.isDouble(".33"), is(true)); - Assert.assertThat(VerifyUtils.isDouble("33."), is(true)); - } -} diff --git a/firefly-common/src/test/java/test/utils/codec/CodecDemo.java b/firefly-common/src/test/java/test/utils/codec/CodecDemo.java deleted file mode 100644 index 5e33abf88..000000000 --- a/firefly-common/src/test/java/test/utils/codec/CodecDemo.java +++ /dev/null @@ -1,23 +0,0 @@ -package test.utils.codec; - -import com.firefly.utils.codec.Base64; - -public class CodecDemo { - - /** - * @param args - */ - public static void main(String[] args) { - StringBuilder strBuilder = new StringBuilder(100); - for (int i = 0; i < 100; i++) { - strBuilder.append(i).append("+"); - } - - String ret = Base64.encodeToString(strBuilder.toString().getBytes(), false); - System.out.println(ret); - - byte[] b = Base64.decode(ret); - System.out.println(new String(b)); - } - -} diff --git a/firefly-common/src/test/java/test/utils/collection/TestConcurrentLRUHashMap.java b/firefly-common/src/test/java/test/utils/collection/TestConcurrentLRUHashMap.java deleted file mode 100644 index 38925d383..000000000 --- a/firefly-common/src/test/java/test/utils/collection/TestConcurrentLRUHashMap.java +++ /dev/null @@ -1,38 +0,0 @@ -package test.utils.collection; - -import org.junit.Assert; -import org.junit.Test; - -import static org.hamcrest.Matchers.*; -import com.firefly.utils.collection.ConcurrentLRUHashMap; -import com.firefly.utils.collection.LRUMapEventListener; - -public class TestConcurrentLRUHashMap { - - @Test - public void test() { - LRUMapEventListener listener = new LRUMapEventListener(){ - - @Override - public void eliminated(Object key, Object value) { - Assert.assertThat((String)key, is("a2")); - Assert.assertThat((String)value, is("hello2")); - } - - @Override - public Object getNull(Object key) { - Assert.assertThat((String)key, is("a2")); - return key + " is null"; - }}; - ConcurrentLRUHashMap map = new ConcurrentLRUHashMap(3, 0.75f, 1, listener); - map.put("a1", "hello1"); - map.put("a2", "hello2"); - map.put("a3", "hello3"); - map.get("a1"); - map.put("a4", "hello4"); - - Assert.assertThat(map.get("a1"), is("hello1")); - Assert.assertThat(map.get("a2"), is("a2 is null")); - } - -} diff --git a/firefly-common/src/test/java/test/utils/collection/TestHashedArrayTree.java b/firefly-common/src/test/java/test/utils/collection/TestHashedArrayTree.java deleted file mode 100644 index a53cfaf96..000000000 --- a/firefly-common/src/test/java/test/utils/collection/TestHashedArrayTree.java +++ /dev/null @@ -1,19 +0,0 @@ -package test.utils.collection; - -import org.junit.Assert; -import org.junit.Test; - -import static org.hamcrest.Matchers.*; -import com.firefly.utils.collection.HashedArrayTree; - -public class TestHashedArrayTree { - @Test - public void test() { - HashedArrayTree arr = new HashedArrayTree(); - for (int i = 0; i < 50; i++) { - arr.add(i); - } - Assert.assertThat(arr.size(), is(50)); - } - -} diff --git a/firefly-common/src/test/java/test/utils/io/FileUtilsExample.java b/firefly-common/src/test/java/test/utils/io/FileUtilsExample.java deleted file mode 100644 index 381857b22..000000000 --- a/firefly-common/src/test/java/test/utils/io/FileUtilsExample.java +++ /dev/null @@ -1,25 +0,0 @@ -package test.utils.io; - -import java.io.File; -import java.io.IOException; - -import com.firefly.utils.io.FileUtils; -import com.firefly.utils.io.LineReaderHandler; - -public class FileUtilsExample { - - public static void main(String[] args) throws IOException { - File parent = new File("/Users/qiupengtao/Documents"); - FileUtils.read(new File(parent, "dev_note"), new LineReaderHandler() { - @Override - public void readline(String text, int num) { - System.out.println(num + "\t" + text); - } - }, "utf-8"); - - long ret = FileUtils.copy(new File(parent, "dev_note"), new File(parent, "dev_note.bak")); - System.out.println("copy length: " + ret); - - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/ArrayObj.java b/firefly-common/src/test/java/test/utils/json/ArrayObj.java deleted file mode 100644 index 603a49da6..000000000 --- a/firefly-common/src/test/java/test/utils/json/ArrayObj.java +++ /dev/null @@ -1,32 +0,0 @@ -package test.utils.json; - -public class ArrayObj { - private Integer[] numbers; - private long[][] map; - private User[] users; - - public long[][] getMap() { - return map; - } - - public void setMap(long[][] map) { - this.map = map; - } - - public Integer[] getNumbers() { - return numbers; - } - - public void setNumbers(Integer[] numbers) { - this.numbers = numbers; - } - - public User[] getUsers() { - return users; - } - - public void setUsers(User[] users) { - this.users = users; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/Book.java b/firefly-common/src/test/java/test/utils/json/Book.java deleted file mode 100644 index 6d4f1edae..000000000 --- a/firefly-common/src/test/java/test/utils/json/Book.java +++ /dev/null @@ -1,59 +0,0 @@ -package test.utils.json; - -import java.util.List; - -import com.firefly.utils.json.annotation.Transient; - -public class Book { - @Transient - private String text, title; - private double price; - private Boolean sell; - public String author; - private Integer id; - public int publishingId; - public transient Object extInfo; - public List simpleObjs; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - @Transient - public Boolean getSell() { - return sell; - } - - public void setSell(Boolean sell) { - this.sell = sell; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public double getPrice() { - return price; - } - - public void setPrice(double price) { - this.price = price; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/BookDemo.java b/firefly-common/src/test/java/test/utils/json/BookDemo.java deleted file mode 100644 index 420675084..000000000 --- a/firefly-common/src/test/java/test/utils/json/BookDemo.java +++ /dev/null @@ -1,116 +0,0 @@ -package test.utils.json; - -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - -import com.firefly.utils.json.Json; - -public class BookDemo { - public static void main1(String[] args) throws Throwable { - Field field = Book.class.getField("simpleObjs"); - Type type = field.getGenericType(); - System.out.println(field); - System.out.println(type); - System.out.println(type instanceof ParameterizedType); - } - - public static void main(String[] args) { - Book book = new Book(); - book.setPrice(10.0); - book.setId(331); - book.setText("very good"); - book.setSell(true); - book.setTitle("gook book"); - book.publishingId = 44342; - book.author = "Gould"; - String str = Json.toJson(book); - System.out.println(str); - - Book book2 = Json.toObject(str, Book.class); - System.out.println(book2.getId()); - System.out.println(book2.getPrice()); - System.out.println(book2.publishingId); - System.out.println(book2.author); - - } - - public static void main2(String[] args) { - Book book = new Book(); - book.setPrice(10.0); - book.setId(331); - book.setText("very good"); - book.setSell(true); - book.setTitle("gook book"); - System.out.println(Json.toJson(book)); - - TestBook t = new TestBook(); - t.setObj(new Object()); - t.setBook(book); - System.out.println("t: " + Json.toJson(t)); - - t = new TestBook(); - t.setObj(book); - t.setBook(book); - System.out.println("t: " + Json.toJson(t)); - t.setObj(new Object()); - System.out.println("t: " + Json.toJson(t)); - t.setObj(book); - System.out.println("t: " + Json.toJson(t)); - - TestBook2 t2 = new TestBook2(); - t2.setObj(book); - t2.setBook(null); - System.out.println("t2: " + Json.toJson(t2)); - t2.setObj(book); - System.out.println("t2: " + Json.toJson(t2)); - - TestBook2 t3 = new TestBook2(); - t3.setObj(book); - t3.setBook(book); - System.out.println(Json.toJson(t3)); - } - - public static class TestBook { - private Object obj; - private Book book; - - public Object getObj() { - return obj; - } - - public void setObj(Object obj) { - this.obj = obj; - } - - public Book getBook() { - return book; - } - - public void setBook(Book book) { - this.book = book; - } - } - - public static class TestBook2 { - private T obj; - private Book book; - - public T getObj() { - return obj; - } - - public void setObj(T obj) { - this.obj = obj; - } - - public Book getBook() { - return book; - } - - public void setBook(Book book) { - this.book = book; - } - - } -} diff --git a/firefly-common/src/test/java/test/utils/json/CharTypes.java b/firefly-common/src/test/java/test/utils/json/CharTypes.java deleted file mode 100644 index 7041356c4..000000000 --- a/firefly-common/src/test/java/test/utils/json/CharTypes.java +++ /dev/null @@ -1,109 +0,0 @@ -package test.utils.json; - -public class CharTypes { - private final static boolean[] specicalFlags_doubleQuotes = new boolean[((int) '\\' + 1)]; - private final static char[] replaceChars = new char[((int) '\\' + 1)]; - static { - specicalFlags_doubleQuotes['\b'] = true; - specicalFlags_doubleQuotes['\n'] = true; - specicalFlags_doubleQuotes['\t'] = true; - specicalFlags_doubleQuotes['\f'] = true; - specicalFlags_doubleQuotes['\r'] = true; - specicalFlags_doubleQuotes['\"'] = true; - specicalFlags_doubleQuotes['\\'] = true; - specicalFlags_doubleQuotes['/'] = true; - - replaceChars['\b'] = 'b'; - replaceChars['\n'] = 'n'; - replaceChars['\t'] = 't'; - replaceChars['\f'] = 'f'; - replaceChars['\r'] = 'r'; - replaceChars['\"'] = '"'; - replaceChars['\''] = '\''; - replaceChars['\\'] = '\\'; - replaceChars['/'] = '/'; - - } - - public static String replaceSpecicalFlags(String s) { - StringBuilder sb = new StringBuilder(s.length() + 10); - char[] cs = s.toCharArray(); - for (char ch : cs) { - if (isSpecicalFlags(ch)) { - sb.append('\\'); - sb.append(replaceChar(ch)); - } else { - sb.append(ch); - } - } - return sb.toString(); - } - - public static String replaceSpecicalFlags2(String s) { - StringBuilder sb = new StringBuilder(s.length() + 10); - char[] cs = s.toCharArray(); - for (char c : cs) { - switch (c) { - case '"': - sb.append("\\\""); - break; - case '\b': - sb.append("\\b"); - break; - case '\n': - sb.append("\\n"); - break; - case '\t': - sb.append("\\t"); - break; - case '\f': - sb.append("\\f"); - break; - case '\r': - sb.append("\\r"); - break; - case '\\': - sb.append("\\\\"); - break; - default: - sb.append(c); - } - } - return sb.toString(); - } - - public static boolean isSpecicalFlags(char ch) { - return ch < specicalFlags_doubleQuotes.length - && specicalFlags_doubleQuotes[ch]; - } - - public static char replaceChar(char ch) { - return replaceChars[(int) ch]; - } - - public static void main(String[] args) { - // System.out.println(replaceChars.length); -// for (char c : replaceChars) { -// if ((int) c != 0) -// System.out.print((int) c + " "); -// } - String s = "dfdf\t"; - String r = null; - long start = System.currentTimeMillis(); - for(int i = 0; i < 100000; i++) { - r = replaceSpecicalFlags(s); - } - long end = System.currentTimeMillis(); - System.out.println(end - start); - System.out.println(r); - - start = System.currentTimeMillis(); - for(int i = 0; i < 100000; i++) { - r = replaceSpecicalFlags2(s); - } - end = System.currentTimeMillis(); - System.out.println(end - start); - System.out.println(r); - - } -} diff --git a/firefly-common/src/test/java/test/utils/json/CollectionObj.java b/firefly-common/src/test/java/test/utils/json/CollectionObj.java deleted file mode 100644 index 091477613..000000000 --- a/firefly-common/src/test/java/test/utils/json/CollectionObj.java +++ /dev/null @@ -1,17 +0,0 @@ -package test.utils.json; - -import java.util.LinkedList; -import java.util.List; - -public class CollectionObj { - private List> list; - - public List> getList() { - return list; - } - - public void setList(List> list) { - this.list = list; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/DateObj.java b/firefly-common/src/test/java/test/utils/json/DateObj.java deleted file mode 100644 index 48166378d..000000000 --- a/firefly-common/src/test/java/test/utils/json/DateObj.java +++ /dev/null @@ -1,25 +0,0 @@ -package test.utils.json; - -import java.util.Date; - -public class DateObj { - private Date date; - private byte[] byteArr; - - public byte[] getByteArr() { - return byteArr; - } - - public void setByteArr(byte[] byteArr) { - this.byteArr = byteArr; - } - - public Date getDate() { - return date; - } - - public void setDate(Date date) { - this.date = date; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/Group.java b/firefly-common/src/test/java/test/utils/json/Group.java deleted file mode 100644 index feda56a1c..000000000 --- a/firefly-common/src/test/java/test/utils/json/Group.java +++ /dev/null @@ -1,43 +0,0 @@ -package test.utils.json; - -import java.util.ArrayList; -import java.util.List; - -public class Group { - private Long id; - private String name; - private List users = new ArrayList(); - private String[] types; - - public String[] getTypes() { - return types; - } - - public void setTypes(String[] types) { - this.types = types; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } -} diff --git a/firefly-common/src/test/java/test/utils/json/JsonDemo.java b/firefly-common/src/test/java/test/utils/json/JsonDemo.java deleted file mode 100644 index cef39b6d9..000000000 --- a/firefly-common/src/test/java/test/utils/json/JsonDemo.java +++ /dev/null @@ -1,40 +0,0 @@ -package test.utils.json; - -import com.firefly.utils.json.Json; - -public class JsonDemo { - - /** - * @param args - */ - public static void main(String[] args) { - Group group = new Group(); - group.setId(0L); - group.setName("admin"); - group.setTypes(new String[] { "typeA", "typeA", "typeA", "typeA", - "typeA", "typeA", "typeA", "typeA", "typeA", "typeA" }); - - User guestUser = new User(); - guestUser.setId(2L); - guestUser.setName("guest"); - - User rootUser = new User(); - rootUser.setId(3L); - rootUser.setName("root"); - - group.getUsers().add(guestUser); - group.getUsers().add(rootUser); - - String jsonString = Json.toJson(group); - long start = System.currentTimeMillis(); - for (int i = 0; i < 1000 * 1000; i++) { - jsonString = Json.toJson(group); - } - long end = System.currentTimeMillis(); - System.out.println(end - start); - System.out.println(jsonString); - // System.out.println(Json.toJson(group)); - - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/MapObj.java b/firefly-common/src/test/java/test/utils/json/MapObj.java deleted file mode 100644 index 8b5e68945..000000000 --- a/firefly-common/src/test/java/test/utils/json/MapObj.java +++ /dev/null @@ -1,45 +0,0 @@ -package test.utils.json; - -import java.util.List; -import java.util.Map; - -public class MapObj { - private Map map; - private Map userMap; - private Map bookMap; - private Map>> map2; - public Map map3; - - public Map getMap() { - return map; - } - - public void setMap(Map map) { - this.map = map; - } - - public Map getUserMap() { - return userMap; - } - - public void setUserMap(Map userMap) { - this.userMap = userMap; - } - - public Map getBookMap() { - return bookMap; - } - - public void setBookMap(Map bookMap) { - this.bookMap = bookMap; - } - - public Map>> getMap2() { - return map2; - } - - public void setMap2(Map>> map2) { - this.map2 = map2; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/Node.java b/firefly-common/src/test/java/test/utils/json/Node.java deleted file mode 100644 index cafdc2a96..000000000 --- a/firefly-common/src/test/java/test/utils/json/Node.java +++ /dev/null @@ -1,101 +0,0 @@ -package test.utils.json; - -import java.util.Date; -import java.util.Map; - -import com.firefly.utils.json.annotation.CircularReferenceCheck; - -@CircularReferenceCheck -public class Node { - private int id; - private String text; - private boolean flag; - private Node node; - private Date timestamp; - private char sex; - private int[] rig; - private boolean[] rbool; - private Long[] rlong; - private Map map; - - public Long[] getRlong() { - return rlong; - } - - public void setRlong(Long[] rlong) { - this.rlong = rlong; - } - - public boolean[] getRbool() { - return rbool; - } - - public void setRbool(boolean[] rbool) { - this.rbool = rbool; - } - - public Map getMap() { - return map; - } - - public void setMap(Map map) { - this.map = map; - } - - public int[] getRig() { - return rig; - } - - public void setRig(int[] rig) { - this.rig = rig; - } - - public char getSex() { - return sex; - } - - public void setSex(char sex) { - this.sex = sex; - } - - public Date getTimestamp() { - return timestamp; - } - - public void setTimestamp(Date timestamp) { - this.timestamp = timestamp; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public Node getNode() { - return node; - } - - public void setNode(Node node) { - this.node = node; - } - - public boolean isFlag() { - return flag; - } - - public void setFlag(boolean flag) { - this.flag = flag; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/NodeDemo.java b/firefly-common/src/test/java/test/utils/json/NodeDemo.java deleted file mode 100644 index e71343b80..000000000 --- a/firefly-common/src/test/java/test/utils/json/NodeDemo.java +++ /dev/null @@ -1,48 +0,0 @@ -package test.utils.json; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import com.firefly.utils.json.Json; - -public class NodeDemo { - - /** - * @param args - */ - public static void main(String[] args) { - Map map = new HashMap(); - Node node = new Node(); - Node node2 = new Node(); - - node.setNode(node2); - node.setId(33); - node.setTimestamp(new Date()); - node.setSex('e'); - node.setText("dfs\t"); - int[] rig = new int[]{1,2,3}; - node.setRig(rig); - node.setRbool(new boolean[]{true, true, false}); - node.setRlong(new Long[]{}); - - map.put("hello", "world"); - map.put("node2", node2); - node.setMap(map); - - node2.setNode(node); - node2.setId(13); - node2.setSex('f'); - node2.setFlag(true); - node2.setText("\n\"\b"); - node2.setRlong(new Long[]{33L, 44L, 55L}); - - System.out.println(Json.toJson(node)); -// System.out.println(Json.toJson(node2)); -// StringWriter writer = new StringWriter(); -// Json.toJson(node, writer); -// System.out.println(writer.toString()); - - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/Profile.java b/firefly-common/src/test/java/test/utils/json/Profile.java deleted file mode 100644 index a1700724c..000000000 --- a/firefly-common/src/test/java/test/utils/json/Profile.java +++ /dev/null @@ -1,99 +0,0 @@ -package test.utils.json; - -import java.util.Map; - -import com.firefly.utils.json.Json; - -public class Profile { - private int readbookcount; //已看过的图书数量 - private Map readbooktype; //已看过的图书类型 包括"原创图书","出版图书","杂志"三个子类 - private int bookcollect; //用户收藏的图书数量 - private int notecount; //用户做的笔记是多少条 - private int noteshare; //分享笔记 - private int bookshare; //分享图书 - private int screenshotshare; //分享截图的数量 - private int totalreadtime; //总共合计阅读多长时间 - private int[] timeintervalreadtime; //每个时段阅读的时间分布 - - public int getReadbookcount() { - return readbookcount; - } - - public void setReadbookcount(int readbookcount) { - this.readbookcount = readbookcount; - } - - public int getBookcollect() { - return bookcollect; - } - - public void setBookcollect(int bookcollect) { - this.bookcollect = bookcollect; - } - - public int getNotecount() { - return notecount; - } - - public void setNotecount(int notecount) { - this.notecount = notecount; - } - - public int getNoteshare() { - return noteshare; - } - - public void setNoteshare(int noteshare) { - this.noteshare = noteshare; - } - - public int getBookshare() { - return bookshare; - } - - public void setBookshare(int bookshare) { - this.bookshare = bookshare; - } - - public int getScreenshotshare() { - return screenshotshare; - } - - public void setScreenshotshare(int screenshotshare) { - this.screenshotshare = screenshotshare; - } - - public int getTotalreadtime() { - return totalreadtime; - } - - public void setTotalreadtime(int totalreadtime) { - this.totalreadtime = totalreadtime; - } - - public int[] getTimeintervalreadtime() { - return timeintervalreadtime; - } - - public void setTimeintervalreadtime(int[] timeintervalreadtime) { - this.timeintervalreadtime = timeintervalreadtime; - } - - public Map getReadbooktype() { - return readbooktype; - } - - public void setReadbooktype(Map readbooktype) { - this.readbooktype = readbooktype; - } - - public static void main(String[] args) { -// String json = "{\"totalreadtime\":5,\"notecount\":27,\"timeintervalreadtime\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,0,0,0,0,0,0],\"bookcollect\":0,\"screenshotshare\":0,\"readbooktype\":{\"测试\":1},\"bookshare\":0,\"readbookcount\":0,\"noteshare\":0}"; - String json = "{\"totalreadtime\":5,\"notecount\":27,\"timeintervalreadtime\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,0,0,0,0,0,0],\"bookcollect\":0,\"screenshotshare\":0,\"readbooktype\":null,\"bookshare\":0,\"readbookcount\":0,\"noteshare\":0}"; - Profile p = Json.toObject(json, Profile.class); - System.out.println(p.getTotalreadtime()); - System.out.println(p.getTimeintervalreadtime().length); -// System.out.println(p.getReadbooktype().get("测试")); - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/SimpleObj.java b/firefly-common/src/test/java/test/utils/json/SimpleObj.java deleted file mode 100644 index 341e7239e..000000000 --- a/firefly-common/src/test/java/test/utils/json/SimpleObj.java +++ /dev/null @@ -1,95 +0,0 @@ -package test.utils.json; - -public class SimpleObj { - private int number; - private int age; - private int id; - private String name; - private Long date; - private Short type; - private float weight; - private Double height; - private SimpleObj contact1; - private SimpleObj contact2; - - public SimpleObj getContact1() { - return contact1; - } - - public void setContact1(SimpleObj contact1) { - this.contact1 = contact1; - } - - public SimpleObj getContact2() { - return contact2; - } - - public void setContact2(SimpleObj contact2) { - this.contact2 = contact2; - } - - public float getWeight() { - return weight; - } - - public void setWeight(float weight) { - this.weight = weight; - } - - public Double getHeight() { - return height; - } - - public void setHeight(Double height) { - this.height = height; - } - - public Short getType() { - return type; - } - - public void setType(Short type) { - this.type = type; - } - - public int getNumber() { - return number; - } - - public void setNumber(int number) { - this.number = number; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getDate() { - return date; - } - - public void setDate(Long date) { - this.date = date; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/SimpleObj2.java b/firefly-common/src/test/java/test/utils/json/SimpleObj2.java deleted file mode 100644 index 083b0e127..000000000 --- a/firefly-common/src/test/java/test/utils/json/SimpleObj2.java +++ /dev/null @@ -1,50 +0,0 @@ -package test.utils.json; - -public class SimpleObj2 { - private Integer id; - private User user; - private Book book; - private char sex; - private char[] symbol; - - public char[] getSymbol() { - return symbol; - } - - public void setSymbol(char[] symbol) { - this.symbol = symbol; - } - - public char getSex() { - return sex; - } - - public void setSex(char sex) { - this.sex = sex; - } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - public Book getBook() { - return book; - } - - public void setBook(Book book) { - this.book = book; - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/TreeNode.java b/firefly-common/src/test/java/test/utils/json/TreeNode.java deleted file mode 100644 index 8491d41bf..000000000 --- a/firefly-common/src/test/java/test/utils/json/TreeNode.java +++ /dev/null @@ -1,75 +0,0 @@ -package test.utils.json; - -import java.util.LinkedList; -import java.util.List; - -import com.firefly.utils.json.Json; - -public class TreeNode { - private transient TreeNode parent; - private List children; - private Integer id; - private String name; - - public TreeNode getParent() { - return parent; - } - - public void setParent(TreeNode parent) { - this.parent = parent; - } - - public List getChildren() { - return children; - } - - public void setChildren(List children) { - this.children = children; - } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public static void main(String[] args) { - TreeNode root = new TreeNode(); - root.setId(0); - root.setName("root"); - - List children = new LinkedList(); - for (int i = 1; i < 10; i++) { - TreeNode node = new TreeNode(); - node.setId(i); - node.setName("children_" + i); - node.setParent(root); - List children2 = new LinkedList(); - for (int j = 11; j < 22; j++) { - TreeNode node2 = new TreeNode(); - node2.setId(j); - node2.setName("children_" + j); - node2.setParent(node); - node2.setChildren(new LinkedList()); - children2.add(node2); - } - node.setChildren(children2); - children.add(node); - } - - root.setChildren(children); - - System.out.println(Json.toJson(root)); - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/User.java b/firefly-common/src/test/java/test/utils/json/User.java deleted file mode 100644 index 789887d72..000000000 --- a/firefly-common/src/test/java/test/utils/json/User.java +++ /dev/null @@ -1,22 +0,0 @@ -package test.utils.json; - -public class User { - private Long id; - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/firefly-common/src/test/java/test/utils/json/compiler/TestCompiler.java b/firefly-common/src/test/java/test/utils/json/compiler/TestCompiler.java deleted file mode 100644 index 170362ade..000000000 --- a/firefly-common/src/test/java/test/utils/json/compiler/TestCompiler.java +++ /dev/null @@ -1,70 +0,0 @@ -package test.utils.json.compiler; - -import static org.hamcrest.Matchers.is; - -import java.util.ArrayList; -import java.util.LinkedList; - -import org.junit.Assert; -import org.junit.Test; - -import test.utils.json.CollectionObj; -import test.utils.json.Group; -import test.utils.json.SimpleObj; - -import com.firefly.utils.json.Parser; -import com.firefly.utils.json.compiler.DecodeCompiler; -import com.firefly.utils.json.compiler.EncodeCompiler; -import com.firefly.utils.json.parser.CollectionParser; -import com.firefly.utils.json.support.ParserMetaInfo; -import com.firefly.utils.json.support.SerializerMetaInfo; - -public class TestCompiler { - - @Test - public void test() { - SerializerMetaInfo[] s = EncodeCompiler.compile(Group.class); - ParserMetaInfo[] p = DecodeCompiler.compile(Group.class); - - for (int i = 0; i < p.length; i++) { - Assert.assertThat(p[i].getPropertyNameString(), is(s[i].getPropertyNameString())); - System.out.println(p[i].getPropertyNameString()); - } - } - - @Test - public void test2() { - ParserMetaInfo[] pp = DecodeCompiler.compile(CollectionObj.class); - ParserMetaInfo p = pp[0]; - Assert.assertThat(p.getType().getName(), is(ArrayList.class.getName())); - - Parser parser = p.getParser(); - if(parser instanceof CollectionParser) { - CollectionParser colp = (CollectionParser) parser; - p = colp.getElementMetaInfo(); - Assert.assertThat(p.getType().getName(), is(LinkedList.class.getName())); - } - - parser = p.getParser(); - if(parser instanceof CollectionParser) { - CollectionParser colp = (CollectionParser) parser; - p = colp.getElementMetaInfo(); - Assert.assertThat(p.getType().getName(), is(SimpleObj.class.getName())); - } - } - - public static void main(String[] args) { - ParserMetaInfo[] pp = DecodeCompiler.compile(CollectionObj.class); - ParserMetaInfo p = pp[0]; - test(p); - } - - public static void test(ParserMetaInfo p) { - System.out.println(p.getType()); - Parser parser = p.getParser(); - if(parser instanceof CollectionParser) { - CollectionParser colp = (CollectionParser) parser; - test(colp.getElementMetaInfo()); - } - } -} diff --git a/firefly-common/src/test/java/test/utils/json/github/Image.java b/firefly-common/src/test/java/test/utils/json/github/Image.java deleted file mode 100644 index e5db344db..000000000 --- a/firefly-common/src/test/java/test/utils/json/github/Image.java +++ /dev/null @@ -1,54 +0,0 @@ -package test.utils.json.github; - -public class Image { - - private String uri; - - private String title; - - private int width; - - private int height; - - private Size size; - - public String getUri() { - return uri; - } - - public void setUri(String uri) { - this.uri = uri; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getWidth() { - return width; - } - - public void setWidth(int width) { - this.width = width; - } - - public int getHeight() { - return height; - } - - public void setHeight(int height) { - this.height = height; - } - - public Size getSize() { - return size; - } - - public void setSize(Size Size) { - this.size = Size; - } -} diff --git a/firefly-common/src/test/java/test/utils/json/github/Media.java b/firefly-common/src/test/java/test/utils/json/github/Media.java deleted file mode 100644 index 09f4aac3d..000000000 --- a/firefly-common/src/test/java/test/utils/json/github/Media.java +++ /dev/null @@ -1,116 +0,0 @@ -package test.utils.json.github; - -import java.util.List; - -public class Media { - - private String uri; - - private String title; - - private int width; - - private int height; - - private String format; - - private long duration; - - private long size; - - private int bitrate; - - private List persons; - - private Player player; - - private String copyright; - - public String getUri() { - return uri; - } - - public void setUri(String uri) { - this.uri = uri; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getWidth() { - return width; - } - - public void setWidth(int width) { - this.width = width; - } - - public int getHeight() { - return height; - } - - public void setHeight(int height) { - this.height = height; - } - - public String getFormat() { - return format; - } - - public void setFormat(String format) { - this.format = format; - } - - public long getDuration() { - return duration; - } - - public void setDuration(long duration) { - this.duration = duration; - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - - public int getBitrate() { - return bitrate; - } - - public void setBitrate(int bitrate) { - this.bitrate = bitrate; - } - - public List getPersons() { - return persons; - } - - public void setPersons(List persons) { - this.persons = persons; - } - - public Player getPlayer() { - return player; - } - - public void setPlayer(Player player) { - this.player = player; - } - - public String getCopyright() { - return copyright; - } - - public void setCopyright(String copyright) { - this.copyright = copyright; - } -} diff --git a/firefly-common/src/test/java/test/utils/json/github/MediaContent.java b/firefly-common/src/test/java/test/utils/json/github/MediaContent.java deleted file mode 100644 index 361e8fae0..000000000 --- a/firefly-common/src/test/java/test/utils/json/github/MediaContent.java +++ /dev/null @@ -1,90 +0,0 @@ -package test.utils.json.github; - -import java.util.Arrays; -import java.util.List; - -import com.firefly.utils.json.Json; - -public class MediaContent { - - private List images; - - private Media media; - - public static MediaContent createRecord() { - MediaContent record = new MediaContent(); - Media media = new Media(); - media.setUri("http://javaone.com/keynote.mpg"); - media.setTitle("Javaone Keynote"); - media.setWidth(640); - media.setHeight(480); - media.setFormat("video/mpg4"); - media.setDuration(18000000); - media.setSize(58982400); - media.setBitrate(262144); - media.setPersons(Arrays.asList(new String[]{"Bill Gates", "Steve Jobs"})); - media.setPlayer(Player.JAVA); - media.setCopyright(null); - - record.setMedia(media); - - Image image1 = new Image(); - image1.setUri("http://javaone.com/keynote_large.jpg"); - image1.setTitle("Javaone Keynote"); - image1.setWidth(1024); - image1.setHeight(768); - image1.setSize(Size.LARGE); - - Image image2 = new Image(); - image2.setUri("http://javaone.com/keynote_small.jpg"); - image2.setTitle("Javaone Keynote"); - image2.setWidth(320); - image2.setHeight(240); - image2.setSize(Size.SMALL); - record.setImages(Arrays.asList(image1, image2)); - return record; - } - - public static void main(String[] args) throws Throwable { -// System.out.println(list.getClass().getGenericInterfaces()); - - - MediaContent record = createRecord(); - String json = Json.toJson(record); - System.out.println(json); - System.out.println("==========================================="); - MediaContent r = Json.toObject(json, MediaContent.class); - System.out.println(r.getMedia().getPlayer()); - System.out.println(r.getImages().size()); - System.out.println(r.getMedia().getCopyright()); - System.out.println(r.getMedia().getFormat()); - System.out.println(r.getImages().get(1).getWidth()); - System.out.println(r.getImages().get(1).getSize()); - System.out.println(r.getImages().get(0).getUri()); -// -// Image image1 = new Image(); -// image1.setUri("http://javaone.com/keynote_large.jpg"); -// image1.setTitle("Javaone Keynote"); -// image1.setWidth(1024); -// image1.setHeight(768); -// image1.setSize(Size.LARGE); -// System.out.println(Json.toJson(image1)); -// Performance.run(20000, record); - } - - public List getImages() { - return images; - } - - public void setImages(List images) { - this.images = images; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } -} diff --git a/firefly-common/src/test/java/test/utils/json/github/MediaRecord.java b/firefly-common/src/test/java/test/utils/json/github/MediaRecord.java deleted file mode 100644 index 96eea3e49..000000000 --- a/firefly-common/src/test/java/test/utils/json/github/MediaRecord.java +++ /dev/null @@ -1,114 +0,0 @@ -package test.utils.json.github; - -public class MediaRecord { - - private String value1; - - private String value2; - - private int value3; - - private int value4; - - private String value5; - - private long value6; - - private long value7; - - private int value8; - - private String[] value9; - - private Player value10; - - private String value11; - - public String getValue1() { - return value1; - } - - public void setValue1(String value1) { - this.value1 = value1; - } - - public String getValue2() { - return value2; - } - - public void setValue2(String value2) { - this.value2 = value2; - } - - public int getValue3() { - return value3; - } - - public void setValue3(int value3) { - this.value3 = value3; - } - - public int getValue4() { - return value4; - } - - public void setValue4(int value4) { - this.value4 = value4; - } - - public String getValue5() { - return value5; - } - - public void setValue5(String value5) { - this.value5 = value5; - } - - public long getValue6() { - return value6; - } - - public void setValue6(long value6) { - this.value6 = value6; - } - - public long getValue7() { - return value7; - } - - public void setValue7(long value7) { - this.value7 = value7; - } - - public int getValue8() { - return value8; - } - - public void setValue8(int value8) { - this.value8 = value8; - } - - public String[] getValue9() { - return value9; - } - - public void setValue9(String[] value9) { - this.value9 = value9; - } - - public Player getValue10() { - return value10; - } - - public void setValue10(Player value10) { - this.value10 = value10; - } - - public String getValue11() { - return value11; - } - - public void setValue11(String value11) { - this.value11 = value11; - } -} diff --git a/firefly-common/src/test/java/test/utils/json/github/Player.java b/firefly-common/src/test/java/test/utils/json/github/Player.java deleted file mode 100644 index e0af631e2..000000000 --- a/firefly-common/src/test/java/test/utils/json/github/Player.java +++ /dev/null @@ -1,6 +0,0 @@ -package test.utils.json.github; - -public enum Player { - - JAVA, FLASH -} diff --git a/firefly-common/src/test/java/test/utils/json/github/Size.java b/firefly-common/src/test/java/test/utils/json/github/Size.java deleted file mode 100644 index ed63af852..000000000 --- a/firefly-common/src/test/java/test/utils/json/github/Size.java +++ /dev/null @@ -1,6 +0,0 @@ -package test.utils.json.github; - -public enum Size { - - SMALL, LARGE -} diff --git a/firefly-common/src/test/java/test/utils/json/parser/TestParser.java b/firefly-common/src/test/java/test/utils/json/parser/TestParser.java deleted file mode 100644 index 93e7d59ac..000000000 --- a/firefly-common/src/test/java/test/utils/json/parser/TestParser.java +++ /dev/null @@ -1,320 +0,0 @@ -package test.utils.json.parser; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.junit.Assert; -import org.junit.Test; - -import test.utils.json.ArrayObj; -import test.utils.json.Book; -import test.utils.json.CollectionObj; -import test.utils.json.DateObj; -import test.utils.json.MapObj; -import test.utils.json.Profile; -import test.utils.json.SimpleObj; -import test.utils.json.SimpleObj2; -import test.utils.json.User; -import test.utils.json.github.MediaContent; -import test.utils.json.github.Player; -import test.utils.json.github.Size; - -import com.firefly.utils.json.Json; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class TestParser { - @Test - public void testStr() { - SimpleObj i = new SimpleObj(); - i.setName("PengtaoQiu\nAlvin\nhttp://fireflysource.com"); - String jsonStr = Json.toJson(i); - System.out.println(jsonStr); - SimpleObj i2 = Json.toObject(jsonStr, SimpleObj.class); - Assert.assertThat(i2.getName(), is("PengtaoQiu\nAlvin\nhttp://fireflysource.com")); - } - - @Test - public void test() { - SimpleObj i = new SimpleObj(); - i.setAge(10); - i.setId(33442); - i.setNumber(30); - i.setName("PengtaoQiu\nAlvin"); - i.setType((short)-33); - i.setWeight(55.47f); - i.setHeight(170.5); - String jsonStr = Json.toJson(i); - - SimpleObj i2 = Json.toObject(jsonStr, SimpleObj.class); - Assert.assertThat(i2.getAge(), is(10)); - Assert.assertThat(i2.getId(), is(33442)); - Assert.assertThat(i2.getNumber(), is(30)); - Assert.assertThat(i2.getDate(), is(0L)); - Assert.assertThat(i2.getName(), is("PengtaoQiu\nAlvin")); - Assert.assertThat(i2.getType(), is((short)-33)); - Assert.assertThat(i2.getHeight(), is(170.5)); - Assert.assertThat(i2.getWeight(), is(55.47f)); - } - - @Test - public void test2() { - SimpleObj i = new SimpleObj(); - i.setAge(10); - i.setId(33442); - i.setNumber(30); - i.setName("PengtaoQiu\nAlvin"); - - SimpleObj i2 = new SimpleObj(); - i2.setAge(20); - i2.setId(12341); - i2.setNumber(33); - i2.setName("Tom"); - i.setContact1(i2); - String jsonStr = Json.toJson(i); - - SimpleObj temp = Json.toObject(jsonStr, SimpleObj.class); - Assert.assertThat(temp.getId(), is(33442)); - Assert.assertThat(temp.getContact1().getId(), is(12341)); - Assert.assertThat(temp.getContact1().getName(), is("Tom")); - Assert.assertThat(temp.getContact1().getAge(), is(20)); - Assert.assertThat(temp.getContact2(), nullValue()); - } - - @Test - public void test3() { - String jsonStr = "{\"id\":33442,\"date\":null,\"add1\":{}, \"add2\":{}, \"add3\":{}, \"add4\":{}, \"add5\":null,\"add6\":\"sdfsdf\",\"contact2\":{}, \"number\":30,\"height\":null,\"name\":\"PengtaoQiu\nAlvin\",\"type\":null,\"weight\":40.3}"; - SimpleObj temp = Json.toObject(jsonStr, SimpleObj.class); - Assert.assertThat(temp.getName(), is("PengtaoQiu\nAlvin")); - Assert.assertThat(temp.getId(), is(33442)); - Assert.assertThat(temp.getWeight(), is(40.3F)); - } - - @Test - public void test4() { - SimpleObj2 so2 = new SimpleObj2(); - so2.setId(334); - - User user = new User(); - user.setId(2434L); - user.setName("Pengtao"); - so2.setUser(user); - - Book book = new Book(); - book.setId(23424); - book.setPrice(3.4); - book.setSell(true); - book.setText("cccccccc"); - book.setTitle("ddddd"); - so2.setBook(book); - - String jsonStr = Json.toJson(so2); - - SimpleObj2 temp = Json.toObject(jsonStr, SimpleObj2.class); - Assert.assertThat(temp.getBook().getPrice(), is(3.4)); - Assert.assertThat(temp.getBook().getTitle(), nullValue()); - Assert.assertThat(temp.getId(), is(334)); - } - - @Test - public void test5() { - List> list = new LinkedList>(); - - LinkedList list1 = new LinkedList(); - for (int j = 0; j < 10; j++) { - SimpleObj i = new SimpleObj(); - i.setAge(10); - i.setId(33442 + j); - i.setNumber(30); - i.setName("PengtaoQiu\nAlvin"); - - SimpleObj i2 = new SimpleObj(); - i2.setAge(20); - i2.setId(12341); - i2.setNumber(33); - i2.setName("Tom"); - i.setContact1(i2); - list1.add(i); - } - list.add(list1); - - list1 = new LinkedList(); - for (int j = 0; j < 10; j++) { - SimpleObj i = new SimpleObj(); - i.setAge(10); - i.setId(1000 + j); - i.setNumber(30); - i.setName("PengtaoQiu\nAlvin"); - - SimpleObj i2 = new SimpleObj(); - i2.setAge(20); - i2.setId(12341); - i2.setNumber(33); - i2.setName("Tom"); - i.setContact1(i2); - list1.add(i); - } - list.add(list1); - - CollectionObj o = new CollectionObj(); - o.setList(list); - String json = Json.toJson(o); - - CollectionObj o2 = Json.toObject(json, CollectionObj.class); - Assert.assertThat(o2.getList().size(), is(2)); - Assert.assertThat(o2.getList().get(0).size(), is(10)); - Assert.assertThat(o2.getList().get(0).get(1).getId(), is(33443)); - Assert.assertThat(o2.getList().get(1).get(1).getId(), is(1001)); - } - - @Test - public void test6() { - MediaContent record = MediaContent.createRecord(); - String json = Json.toJson(record); - - MediaContent r = Json.toObject(json, MediaContent.class); - Assert.assertThat(r.getMedia().getPlayer(), is(Player.JAVA)); - Assert.assertThat(r.getImages().size(), is(2)); - Assert.assertThat(r.getImages().get(0).getSize(), is(Size.LARGE)); - Assert.assertThat(r.getImages().get(0).getHeight(), is(768)); - } - - @Test - public void test7() { - ArrayObj obj = new ArrayObj(); - Integer[] i = new Integer[]{2,3,4,5,6,332}; - obj.setNumbers(i); - - long[][] map = new long[][]{{3L, 44L, 55L}, {24, 324, 3}}; - obj.setMap(map); - - List users = new ArrayList(); - for (int j = 0; j < 3; j++) { - User user = new User(); - user.setId((long)j); - user.setName("user" + j); - users.add(user); - } - obj.setUsers(users.toArray(new User[0])); - - String json = Json.toJson(obj); - - ArrayObj obj2 = Json.toObject(json, ArrayObj.class); - Assert.assertThat(obj2.getNumbers()[3], is(5)); - Assert.assertThat(obj2.getNumbers().length, is(6)); - Assert.assertThat(obj2.getMap().length, is(2)); - Assert.assertThat(obj2.getMap()[0][1], is(44L)); - Assert.assertThat(obj2.getUsers().length, is(3)); - Assert.assertThat(obj2.getUsers()[0].getId(), is(0L)); - Assert.assertThat(obj2.getUsers()[1].getName(), is("user1")); - } - - @Test - public void test8() { - List users = new ArrayList(); - for (int j = 0; j < 3; j++) { - User user = new User(); - user.setId((long)j); - user.setName("user" + j); - users.add(user); - } - User[] u = users.toArray(new User[0]); - String json = Json.toJson(u); - - User[] u2 = Json.toObject(json, User[].class); - Assert.assertThat(u2.length, is(3)); - Assert.assertThat(u2[0].getId(), is(0L)); - Assert.assertThat(u2[1].getName(), is("user1")); - } - - @Test - public void test9() { - MapObj m = new MapObj(); - Map map = new HashMap(); - map.put("a1", 40); - m.setMap(map); - - Map userMap = new HashMap(); - List users = new ArrayList(); - for (int j = 0; j < 3; j++) { - User user = new User(); - user.setId((long)j); - user.setName("user" + j); - users.add(user); - } - User[] u = users.toArray(new User[0]); - userMap.put("user1", u); - - users = new ArrayList(); - for (int j = 10; j < 12; j++) { - User user = new User(); - user.setId((long)j); - user.setName("user_b" + j); - users.add(user); - } - u = users.toArray(new User[0]); - userMap.put("user2", u); - m.setUserMap(userMap); - - Map map3 = new HashMap(); - map3.put("m31", new int[]{3,4,5,6}); - map3.put("m32", new int[]{7,8,9}); - m.map3 = map3; - - String json = Json.toJson(m); - - MapObj m2 = Json.toObject(json, MapObj.class); - Assert.assertThat(m2.getMap().get("a1"), is(40)); - Assert.assertThat(m.getUserMap().get("user1").length, is(3)); - Assert.assertThat(m.getUserMap().get("user2").length, is(2)); - Assert.assertThat(m.getUserMap().get("user2")[0].getName(), is("user_b10")); - Assert.assertThat(m2.map3.get("m31")[3], is(6)); - } - - @Test - public void test10() throws Throwable { - DateObj obj = new DateObj(); - obj.setDate(new Date()); - - StringBuilder strBuilder = new StringBuilder(100); - for (int i = 0; i < 100; i++) { - strBuilder.append(i).append("+"); - } - obj.setByteArr(strBuilder.toString().getBytes("utf-8")); - - String json = Json.toJson(obj); - - DateObj obj2 = Json.toObject(json, DateObj.class); - System.out.println(SafeSimpleDateFormat.defaultDateFormat.format(obj2.getDate())); - Assert.assertThat(new String(obj2.getByteArr(), "utf-8"), is("0+1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17+18+19+20+21+22+23+24+25+26+27+28+29+30+31+32+33+34+35+36+37+38+39+40+41+42+43+44+45+46+47+48+49+50+51+52+53+54+55+56+57+58+59+60+61+62+63+64+65+66+67+68+69+70+71+72+73+74+75+76+77+78+79+80+81+82+83+84+85+86+87+88+89+90+91+92+93+94+95+96+97+98+99+")); - } - - @Test - public void test11() { - SimpleObj2 obj = new SimpleObj2(); - obj.setSex('c'); - obj.setSymbol("测试一下".toCharArray()); - - String json = Json.toJson(obj); - SimpleObj2 obj2 = Json.toObject(json, SimpleObj2.class); - Assert.assertThat(obj2.getSex(), is('c')); - Assert.assertThat(new String(obj2.getSymbol()), is("测试一下")); - } - - @Test - public void test12() { - String json = "{\"totalreadtime\":5,\"notecount\":27,\"timeintervalreadtime\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,0,0,0,0,0,0],\"bookcollect\":0,\"screenshotshare\":0,\"readbooktype\":{\"测试\":1,\"测试一下\":23},\"bookshare\":0,\"readbookcount\":0,\"noteshare\":0}"; - Profile p = Json.toObject(json, Profile.class); - Assert.assertThat(p.getTotalreadtime(), is(5)); - Assert.assertThat(p.getNotecount(), is(27)); - Assert.assertThat(p.getTimeintervalreadtime().length, is(24)); - Assert.assertThat(p.getReadbooktype().get("测试一下"), is(23)); - } - -} diff --git a/firefly-common/src/test/java/test/utils/json/reader/TestReader.java b/firefly-common/src/test/java/test/utils/json/reader/TestReader.java deleted file mode 100644 index ad401c46e..000000000 --- a/firefly-common/src/test/java/test/utils/json/reader/TestReader.java +++ /dev/null @@ -1,376 +0,0 @@ -package test.utils.json.reader; - -import java.util.Arrays; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.utils.json.support.JsonStringReader; - -import static org.hamcrest.Matchers.*; - -public class TestReader { - @Test - public void testReadAndSkipBlank() { - JsonStringReader reader = new JsonStringReader(" tt".trim()); - Assert.assertThat(reader.readAndSkipBlank(), is('t')); - Assert.assertThat(reader.position(), is(1)); - } - - @Test - public void testReadField() { - JsonStringReader reader = new JsonStringReader(" \"testField\":"); - char[] t1 = "test".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.get(reader.position()), is(':')); - } - - @Test - public void testReadField2() { - JsonStringReader reader = new JsonStringReader(" \"testField\" :"); - char[] t1 = "testField".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(ret, nullValue()); - Assert.assertThat(reader.isColon(), is(true)); - } - - @Test - public void testReadField3() { - JsonStringReader reader = new JsonStringReader(" \"testField\":"); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - } - - @Test - public void testReadDouble() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": 3332.44 }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readDouble(), is(3332.44)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadDouble2() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": -17.44320 , \"testField2\": \" -334\" }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readDouble(), is(-17.44320)); - Assert.assertThat(reader.isComma(), is(true)); - - ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField2")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readInt(), is(-334)); - Assert.assertThat(reader.isObjectEnd(), is(true)); - } - - @Test - public void testReadFloat() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": \" -17.44320\" , \"testField2\": \" -334\" }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readFloat(), is(-17.44320F)); - Assert.assertThat(reader.isComma(), is(true)); - - ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField2")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readInt(), is(-334)); - Assert.assertThat(reader.isObjectEnd(), is(true)); - } - - @Test - public void testReadInt() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": 333 }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readInt(), is(333)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadInt2() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": \" -333\" , \"testField2\": \" -334\" }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readInt(), is(-333)); - Assert.assertThat(reader.isComma(), is(true)); - - ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField2")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readInt(), is(-334)); - Assert.assertThat(reader.isObjectEnd(), is(true)); - } - - @Test - public void testReadInt3() { - JsonStringReader reader = new JsonStringReader(" \" -333\" "); - Assert.assertThat(reader.readInt(), is(-333)); - } - - @Test - public void testReadInt4() { - JsonStringReader reader = new JsonStringReader(" -333 "); - Assert.assertThat(reader.readInt(), is(-333)); - } - - @Test - public void testReadInt5() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": null }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readInt(), is(0)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadLong() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": -3334}"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readLong(), is(-3334L)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadString() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": \"ddsfseee\", \"testField2\": \"dddfd\"}"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readString(), is("ddsfseee")); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is(',')); - - ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField2")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readString(), is("dddfd")); - ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadString2() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": \"dds\\\"fseee\", \"testField2\": \"d\\nddfd\"}"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - String s = reader.readString(); - System.out.println(s); - Assert.assertThat(s, is("dds\"fseee")); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is(',')); - - ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField2")); - Assert.assertThat(reader.isColon(), is(true)); - s = reader.readString(); - System.out.println(s); - Assert.assertThat(s, is("d\nddfd")); - ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadString3() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": null, \"testField2\": \"d\\nddfd\"}"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - String s = reader.readString(); - Assert.assertThat(s, nullValue()); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is(',')); - } - - @Test - public void testIsNull() { - JsonStringReader reader = new JsonStringReader(" { \"testField\": null , \"testField2\": \"d\\nddfd\"}"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.isNull(), is(true)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is(',')); - - ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField2")); - Assert.assertThat(reader.isColon(), is(true)); - String s = reader.readString(); - System.out.println(s); - Assert.assertThat(s, is("d\nddfd")); - ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testIsNull2() { - JsonStringReader reader = new JsonStringReader(" null ".trim()); - Assert.assertThat(reader.isNull(), is(true)); - } - - @Test - public void testIsNull3() { - JsonStringReader reader = new JsonStringReader(" nul"); - Assert.assertThat(reader.isNull(), is(false)); - } - - @Test - public void testReadBoolean() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": true}"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readBoolean(), is(true)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadBoolean2() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": false }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readBoolean(), is(false)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadBoolean3() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": \"true\" }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readBoolean(), is(true)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadBoolean4() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": \"false\" }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readBoolean(), is(false)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testReadBoolean5() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": null }"); - Assert.assertThat(reader.isObject(), is(true)); - char[] t1 = "dsffsfsf".toCharArray(); - char[] ret = reader.readField(t1); - Assert.assertThat(new String(ret), is("testField")); - Assert.assertThat(reader.isColon(), is(true)); - Assert.assertThat(reader.readBoolean(), is(false)); - char ch = reader.readAndSkipBlank(); - Assert.assertThat(ch, is('}')); - } - - @Test - public void testSkipValue() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": null, \"ssdd\" : \"sdf\\\"sdfsdf\" }"); - Assert.assertThat(reader.isObject(), is(true)); - Assert.assertThat(Arrays.equals("testField".toCharArray(), reader.readChars()), is(true)); - Assert.assertThat(reader.isColon(), is(true)); - reader.skipValue(); - - Assert.assertThat(reader.isComma(), is(true)); - Assert.assertThat(Arrays.equals("ssdd".toCharArray(), reader.readChars()), is(true)); - Assert.assertThat(reader.isColon(), is(true)); - reader.skipValue(); - Assert.assertThat(reader.isObjectEnd(), is(true)); - } - - @Test - public void testSkipValue2() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": [ [[2 , 3],[3]], [[3,4]] ], \"ssdd\" : \"sdf\\\"sdfsdf\" }"); - Assert.assertThat(reader.isObject(), is(true)); - Assert.assertThat(Arrays.equals("testField".toCharArray(), reader.readChars()), is(true)); - Assert.assertThat(reader.isColon(), is(true)); - reader.skipValue(); - - Assert.assertThat(reader.isComma(), is(true)); - Assert.assertThat(Arrays.equals("ssdd".toCharArray(), reader.readChars()), is(true)); - Assert.assertThat(reader.isColon(), is(true)); - reader.skipValue(); - Assert.assertThat(reader.isObjectEnd(), is(true)); - } - - @Test - public void testSkipValue3() { - JsonStringReader reader = new JsonStringReader("{ \"testField\": [ [[{} , {\"t1\" : { \"t2\": {\"t3\" : [\"332f\", \"dsfdsf\\\"sd\"] } } }],[]], [[3,4]] ], \"ssdd\" : \"sdf\\\"sdfsdf\" }"); - Assert.assertThat(reader.isObject(), is(true)); - Assert.assertThat(Arrays.equals("testField".toCharArray(), reader.readChars()), is(true)); - Assert.assertThat(reader.isColon(), is(true)); - reader.skipValue(); - - Assert.assertThat(reader.isComma(), is(true)); - Assert.assertThat(Arrays.equals("ssdd".toCharArray(), reader.readChars()), is(true)); - Assert.assertThat(reader.isColon(), is(true)); - reader.skipValue(); - Assert.assertThat(reader.isObjectEnd(), is(true)); - } - - public static void main(String[] args) { - JsonStringReader reader = new JsonStringReader("{ \"testField\": [ [[{} , {\"t1\" : { \"t2\": {\"t3\" : [\"332f\", \"dsfdsf\\\"sd\"] } } }],[]], [[3,4]] ], \"ssdd\" : \"sdf\\\"sdfsdf\" }"); - reader.isObject(); - reader.readChars(); - reader.isColon(); - reader.skipValue(); - System.out.println(reader.isComma()); - } - -} diff --git a/firefly-common/src/test/java/test/utils/log/LogDemo.java b/firefly-common/src/test/java/test/utils/log/LogDemo.java deleted file mode 100644 index af0ce19c4..000000000 --- a/firefly-common/src/test/java/test/utils/log/LogDemo.java +++ /dev/null @@ -1,77 +0,0 @@ -package test.utils.log; - -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class LogDemo { - - private static final Log log = LogFactory.getInstance().getLog( - "firefly-common"); - private static final Log log2 = LogFactory.getInstance().getLog( - "test-TRACE"); - private static final Log log3 = LogFactory.getInstance().getLog( - "test-DEBUG"); - private static final Log log4 = LogFactory.getInstance().getLog( - "test-ERROR"); - private static final Log log5 = LogFactory.getInstance() - .getLog("test-WARN"); - private static final Log logConsole = LogFactory.getInstance().getLog( - "test-console"); - private static final Log defaultLog = LogFactory.getInstance().getLog( - "firefly-system"); - - /** - * @param args - */ - public static void main(String[] args) { - try { - log.info("test {} aa {}", "log1", 2); - log.info("test {} bb {}", "log1", 2); - log.info("test {} cc {}", "log1", 2); - log.debug("cccc"); - log.warn("warn hello"); - - log2.trace("test trace"); - log2.trace("log2 {} dfdfdf", 3, 5); - log2.debug("cccc"); - - log3.debug("log3", "dfd"); - log3.info("ccccddd"); - - log4.error("log4"); - log4.warn("ccc"); - - log5.warn("log5 {} {}", "warn"); - log5.error("log5 {}", "error"); - log5.trace("ccsc"); - - logConsole.info("test {} console", "hello"); - logConsole.debug("ccc"); - logConsole.warn("dsfsf {} cccc", 33); - - defaultLog.debug("default log debug"); - defaultLog.info("default log info"); - - try { - test3(); - } catch (Throwable t) { - log4.error("test exception", t); - } - } finally { - LogFactory.getInstance().shutdown(); - } - } - - public static void test1() { - throw new RuntimeException(); - } - - public static void test2() { - test1(); - } - - public static void test3() { - test2(); - } - -} diff --git a/firefly-common/src/test/java/test/utils/pattern/TestPattern.java b/firefly-common/src/test/java/test/utils/pattern/TestPattern.java deleted file mode 100644 index bf377d155..000000000 --- a/firefly-common/src/test/java/test/utils/pattern/TestPattern.java +++ /dev/null @@ -1,113 +0,0 @@ -package test.utils.pattern; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.utils.pattern.Pattern; - -public class TestPattern { - @Test - public void testPattern() { - Pattern p = Pattern.compile("?ddaaad?", "?"); - Assert.assertThat(p.match("ddaaad")[1], is("")); - Assert.assertThat(p.match("ddaaadxwww")[1], is("xwww")); - Assert.assertThat(p.match("addaaadxwww")[1], is("xwww")); - Assert.assertThat(p.match("addaaadxwww")[0], is("a")); - Assert.assertThat(p.match("addaaad")[0], is("a")); - Assert.assertThat(p.match("orange"), nullValue()); - - p = Pattern.compile("?", "?"); - Assert.assertThat(p.match("orange")[0], is("orange")); - - p = Pattern.compile("??????", "?"); - Assert.assertThat(p.match("orange")[0], is("orange")); - Assert.assertThat(p.match("orange").length, is(1)); - - p = Pattern.compile("org", "?"); - Assert.assertThat(p.match("orange"), nullValue()); - Assert.assertThat(p.match("org").length, is(0)); - - p = Pattern.compile("?org", "?"); - Assert.assertThat(p.match("org")[0], is("")); - Assert.assertThat(p.match("aassorg")[0], is("aass")); - Assert.assertThat(p.match("ssorg").length, is(1)); - - p = Pattern.compile("org?", "?"); - Assert.assertThat(p.match("org")[0], is("")); - Assert.assertThat(p.match("orgaaa")[0], is("aaa")); - Assert.assertThat(p.match("orgaaa").length, is(1)); - - p = Pattern.compile("www.?.com?", "?"); - Assert.assertThat(p.match("www.fireflysource.com")[0], is("fireflysource")); - Assert.assertThat(p.match("www.fireflysource.com")[1], is("")); - Assert.assertThat(p.match("www.fireflysource.com/cn/")[1], is("/cn/")); - Assert.assertThat(p.match("www.fireflysource.com/cn/").length, is(2)); - Assert.assertThat(p.match("orange"), nullValue()); - - p = Pattern.compile("www.?.com/?/app", "?"); - Assert.assertThat(p.match("orange"), nullValue()); - Assert.assertThat(p.match("www.fireflysource.com/cn/app").length, is(2)); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[0], is("fireflysource")); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[1], is("cn")); - - p = Pattern.compile("?www.?.com/?/app", "?"); - Assert.assertThat(p.match("orange"), nullValue()); - Assert.assertThat(p.match("www.fireflysource.com/cn/app").length, is(3)); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[0], is("")); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[1], is("fireflysource")); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[2], is("cn")); - Assert.assertThat(p.match("http://www.fireflysource.com/cn/app")[0], is("http://")); - - p = Pattern.compile("?www.?.com/?/app?", "?"); - Assert.assertThat(p.match("orange"), nullValue()); - Assert.assertThat(p.match("www.fireflysource.com/cn/app").length, is(4)); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[0], is("")); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[1], is("fireflysource")); - Assert.assertThat(p.match("www.fireflysource.com/cn/app")[2], is("cn")); - Assert.assertThat(p.match("http://www.fireflysource.com/cn/app")[0], is("http://")); - Assert.assertThat(p.match("http://www.fireflysource.com/cn/app")[3], is("")); - Assert.assertThat(p.match("http://www.fireflysource.com/cn/app/1334")[3], is("/1334")); - - p = Pattern.compile("abc*abc", "*"); - Assert.assertThat(p.match("abcabcabc")[0], is("")); - - p = Pattern.compile("aa*aa", "*"); - Assert.assertThat(p.match("aaaaa")[0], is("")); - } - - public static void main(String[] args) { - int times = 10000000; - - java.util.regex.Pattern p0 = java.util.regex.Pattern.compile("(.*)www.(.*).com/(.*)/app(.*)"); - java.util.regex.Pattern p1 = java.util.regex.Pattern.compile("(.*?)www.(.*?).com/(.*?)/app(.*?)"); - Pattern p2 = Pattern.compile("?www.?.com/?/app?", "?"); - - // 贪婪匹配 - long start = System.currentTimeMillis(); - for (int i = 0; i < times; i++) { - java.util.regex.Matcher m = p0.matcher("http://www.fireflysource.com/cn/app/1334"); - m.matches(); - m.group(1); - } - System.out.println("regex greedy: " + (System.currentTimeMillis() - start)); - - // 勉强匹配 - start = System.currentTimeMillis(); - for (int i = 0; i < times; i++) { - java.util.regex.Matcher m = p1.matcher("http://www.fireflysource.com/cn/app/1334"); - m.matches(); - m.group(1); - } - System.out.println("regex reluctant: " + (System.currentTimeMillis() - start)); - - // 简单匹配 - start = System.currentTimeMillis(); - for (int i = 0; i < times; i++) { - p2.match("http://www.fireflysource.com/cn/app/1334"); - } - System.out.println("simple: " + (System.currentTimeMillis() - start)); - } -} diff --git a/firefly-common/src/test/java/test/utils/time/TimerWheelExample.java b/firefly-common/src/test/java/test/utils/time/TimerWheelExample.java deleted file mode 100644 index 48e197d48..000000000 --- a/firefly-common/src/test/java/test/utils/time/TimerWheelExample.java +++ /dev/null @@ -1,67 +0,0 @@ -package test.utils.time; - -import com.firefly.utils.time.HashTimeWheel; -import com.firefly.utils.time.TimeProvider; - -public class TimerWheelExample { - public void test() { - final HashTimeWheel t = new HashTimeWheel(); - t.setMaxTimers(5); - t.setInterval(100); - t.start(); - - try { - Thread.sleep(130L); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - t.add(400, new Runnable() { - private long start = System.currentTimeMillis(); - - @Override - public void run() { - long end = System.currentTimeMillis(); - System.out.println("t1: " + (end - start)); - } - }); - - t.add(900, new Runnable() { - private long start = System.currentTimeMillis(); - - @Override - public void run() { - long end = System.currentTimeMillis(); - System.out.println("t1: " + (end - start)); - } - }); - - t.add(2500, new Runnable() { - private long start = System.currentTimeMillis(); - - @Override - public void run() { - long end = System.currentTimeMillis(); - System.out.println("t2: " + (end - start)); - t.add(1200, new Runnable() { - @Override - public void run() { - long end = System.currentTimeMillis(); - System.out.println("t2: " + (end - start)); - } - }); - } - }); - } - - public static void main(String[] args) throws InterruptedException { - new TimerWheelExample().test(); - TimeProvider t = new TimeProvider(1000L); - t.start(); - - Thread.sleep(1000L); - long start = t.currentTimeMillis(); - Thread.sleep(5000L); - System.out.println("TimeProvider: " + (t.currentTimeMillis() - start)); - } -} diff --git a/firefly-common/src/test/kotlin/com/fireflysource/common/concurrent/TestCompletableFuture.kt b/firefly-common/src/test/kotlin/com/fireflysource/common/concurrent/TestCompletableFuture.kt new file mode 100644 index 000000000..1196798db --- /dev/null +++ b/firefly-common/src/test/kotlin/com/fireflysource/common/concurrent/TestCompletableFuture.kt @@ -0,0 +1,28 @@ +package com.fireflysource.common.concurrent + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.util.concurrent.CompletableFuture + +class TestCompletableFuture { + + @Test + @DisplayName("should receive the exception message.") + fun testException() { + CompletableFuture + .runAsync { throw IllegalStateException("test_error") } + .exceptionallyAccept { assertEquals("test_error", it.cause?.message) } + .get() + } + + @Test + @DisplayName("should exceptionally compose successfully.") + fun testExceptionCompose() { + val value = CompletableFuture + .supplyAsync { throw IllegalStateException("test_error") } + .exceptionallyCompose { CompletableFuture.supplyAsync { "ok" } } + .get() + assertEquals("ok", value) + } +} \ No newline at end of file diff --git a/firefly-common/src/test/kotlin/com/fireflysource/common/coroutine/TestChannelExtension.kt b/firefly-common/src/test/kotlin/com/fireflysource/common/coroutine/TestChannelExtension.kt new file mode 100644 index 000000000..4b9182c3e --- /dev/null +++ b/firefly-common/src/test/kotlin/com/fireflysource/common/coroutine/TestChannelExtension.kt @@ -0,0 +1,38 @@ +package com.fireflysource.common.coroutine + +import kotlinx.coroutines.channels.Channel +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * @author Pengtao Qiu + */ +class TestChannelExtension { + + @Test + @DisplayName("should poll all elements successfully.") + fun testPollAll() { + val channel = Channel(Channel.UNLIMITED) + repeat(3) { + channel.trySend(1) + } + val list = mutableListOf() + channel.consumeAll { + list.add(it) + } + assertEquals(3, list.size) + } + + @Test + @DisplayName("should clear elements successfully.") + fun testClear() { + val channel = Channel(Channel.UNLIMITED) + repeat(3) { + channel.trySend(1) + } + channel.clear() + assertNull(channel.tryReceive().getOrNull()) + } +} \ No newline at end of file diff --git a/firefly-common/src/test/kotlin/com/fireflysource/common/coroutine/TestCoroutineLocal.kt b/firefly-common/src/test/kotlin/com/fireflysource/common/coroutine/TestCoroutineLocal.kt new file mode 100644 index 000000000..e5d4175d9 --- /dev/null +++ b/firefly-common/src/test/kotlin/com/fireflysource/common/coroutine/TestCoroutineLocal.kt @@ -0,0 +1,110 @@ +package com.fireflysource.common.coroutine + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicInteger + +/** + * @author Pengtao Qiu + */ + +private val dispatchExecutor: ExecutorService = ThreadPoolExecutor( + 2, 2, + 0L, TimeUnit.MILLISECONDS, + ArrayBlockingQueue(20), + Executors.defaultThreadFactory() +) + +class TestCoroutineLocal { + private val ctx = CoroutineLocalContext + + @Test + @DisplayName("should get the coroutine local value across the many coroutines.") + fun test(): Unit = runTest { + val dispatcher: CoroutineDispatcher = dispatchExecutor.asCoroutineDispatcher() + val key = "index" + val jobs = List(5) { i -> + async(dispatcher + ctx.asElement(mutableMapOf(key to i))) { + testAttr(key, i) + withTimeout(2000) { + assertEquals(i, ctx.getAttr(key)) + ctx.computeIfAbsent("key33") { 33 } + ctx.setAttr("newKey", i) + testLocalAttr(key, i) + assertEquals(i, ctx.getAttr(key)) + } + } + } + + jobs.forEach { + it.join() + } + + } + + private suspend fun testAttr(key: String, expect: Int) { + event { println("hello") }.join() + assertEquals(expect, ctx.getAttr(key)) + } + + private suspend fun testLocalAttr(key: String, expect: Int) = withContextInheritable { + assertEquals(33, ctx.getAttr("key33")) + assertEquals(expect, ctx.getAttr("newKey")) + println("beforeSuspend ${ctx.getAttributes()}. context: $coroutineContext") + inheritableLaunch(attributes = mutableMapOf("d1" to 200)) { + ctx.setAttr("c1", 100) + assertEquals(100, ctx.getAttr("c1")) + assertEquals(expect, ctx.getAttr(key)) + assertEquals(33, ctx.getAttr("key33")) + assertEquals(expect, ctx.getAttr("newKey")) + assertEquals("OK", ctx.getAttrOrDefault("keyX") { "OK" }) + println("inner fun. context: $coroutineContext") + assertEquals(200, ctx.getAttr("d1")) + }.join() + + val old = inheritableAsync { + val old = ctx.setAttr("c1", 200) + assertEquals(200, ctx.getAttr("c1")) + assertEquals(33, ctx.getAttr("key33")) + old + } + assertNull(old.await()) + + println("afterSuspend ${ctx.getAttributes()}") + assertNull(ctx.getAttr("d1")) + assertNull(ctx.getAttr("c1")) + } + + @Test + @DisplayName("should cancel the channel") + fun testCancelChannel(): Unit = runTest { + val count = AtomicInteger() + val channel = Channel() + val job = computeAsync { + println("job context: ${Thread.currentThread().name}") + while (true) { + val i = channel.receive() + println("test: $i") + count.incrementAndGet() + } + } + + compute { + (1..10).forEach { + delay(100) + channel.trySend(it) + } + } + + delay(500) + job.cancel() + job.join() + println("end") + assertTrue(count.get() < 10) + } +} \ No newline at end of file diff --git a/firefly-common/src/test/resources/firefly-log.properties b/firefly-common/src/test/resources/firefly-log.properties deleted file mode 100644 index 4525a36b9..000000000 --- a/firefly-common/src/test/resources/firefly-log.properties +++ /dev/null @@ -1,7 +0,0 @@ -firefly-common=${log.level},${log.path},console -test-TRACE=TRACE,${log.path},console -test-DEBUG=DEBUG,${log.path},console -test-ERROR=ERROR,${log.path},console -test-WARN=WARN,${log.path} -test-console=INFO,console -firefly-system=${log.level},${log.path},console \ No newline at end of file diff --git a/firefly-common/src/test/resources/testFile1 b/firefly-common/src/test/resources/testFile1 new file mode 100644 index 000000000..0bf7f8c4e --- /dev/null +++ b/firefly-common/src/test/resources/testFile1 @@ -0,0 +1,3 @@ +the line 1 +the line 2 +hello the end line \ No newline at end of file diff --git a/firefly-db/pom.xml b/firefly-db/pom.xml deleted file mode 100644 index b126c1afc..000000000 --- a/firefly-db/pom.xml +++ /dev/null @@ -1,126 +0,0 @@ - - 4.0.0 - - com.firefly - firefly-db - 1.0-SNAPSHOT - jar - - firefly-db - http://maven.apache.org - - - - com.firefly - firefly-db - UTF-8 - - - 4.8.1 - - - 1.8.0 - - - 0.9.1.2 - 1.3 - 5.1.18 - - - 1.0-SNAPSHOT - - - - - junit - junit - ${junit.version} - test - - - - - commons-beanutils - commons-beanutils - ${commons.beanutils.version} - - - - - commons-dbutils - commons-dbutils - ${dbutils.version} - - - c3p0 - c3p0 - ${c3p0.version} - - - mysql - mysql-connector-java - ${mysql.version} - - - - - com.firefly - firefly-common - ${firefly-common.version} - - - - - ${project.artifactId} - - - src/main/resources - true - - - - - true - src/main/resources - - - true - src/main/webapp - - **/*.xml - - - - - - maven-eclipse-plugin - 2.8 - - true - 2.0 - - **/*.xml - - - - - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - ${project.build.sourceEncoding} - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - ${project.build.sourceEncoding} - - - - - diff --git a/firefly-db/src/main/java/com/firefly/db/DBManager.java b/firefly-db/src/main/java/com/firefly/db/DBManager.java deleted file mode 100644 index 237e1f897..000000000 --- a/firefly-db/src/main/java/com/firefly/db/DBManager.java +++ /dev/null @@ -1,206 +0,0 @@ -package com.firefly.db; - -import java.io.InputStream; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.SQLException; -import java.util.Properties; - -import javax.sql.DataSource; - -import org.apache.commons.beanutils.BeanUtils; - -import com.firefly.db.exception.DBException; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -/** - *

数据库管理

- *

维护数据库连接池,提供获取、释放连接方法

- * - * @author 须俊杰 - * @version 1.0 2011-7-18 - */ -public class DBManager { - - private final static Log log = LogFactory.getInstance().getLog("firefly-system"); - // 线程本地变量,用于存放当前线程获取的数据库连接 - private final static ThreadLocal conns = new ThreadLocal(); - // 数据源 - private static DataSource dataSource; - // 是否开启数据库sql打印 - private static boolean show_sql = false; - // 是否开启事务控制 - private static boolean transaction = true; - - static { - initDataSource(null); - } - - /** - * 初始化连接池 - * @param dbProperties 数据库配置文件 - */ - private final static void initDataSource(Properties dbProperties) { - try { - if (dbProperties == null) { - dbProperties = new Properties(); - InputStream inputStream = DBManager.class.getResourceAsStream("/db.properties"); - dbProperties.load(inputStream); - inputStream.close(); - } - - Properties cp_props = new Properties(); - for (Object key : dbProperties.keySet()) { - String skey = (String) key; - if (skey.startsWith("jdbc.")) { - String name = skey.substring(5); - cp_props.put(name, dbProperties.getProperty(skey)); - if ("show_sql".equalsIgnoreCase(name)) { - show_sql = "true".equalsIgnoreCase(dbProperties - .getProperty(skey)); - } - if("transaction".equalsIgnoreCase(name)){ - transaction = "true".equalsIgnoreCase(dbProperties - .getProperty(skey)); - } - } - } - - dataSource = (DataSource) Class.forName( - cp_props.getProperty("datasource")).newInstance(); - log.info("Using DataSource : " + dataSource.getClass().getName()); - BeanUtils.populate(dataSource, cp_props); - - Connection conn = getConnection(); - DatabaseMetaData dmd = conn.getMetaData(); - log.info("Connected to " + dmd.getDatabaseProductName() + " " - + dmd.getDatabaseProductVersion()); - - closeConnection(); - } catch (Exception e) { - throw new DBException(e); - } - } - - /** - * 断开连接池 - */ - public final static void closeDataSource() { - try { - dataSource.getClass().getMethod("close").invoke(dataSource); - } catch (Exception e) { - log.error("Unabled to destroy DataSource!!! ", e); - } - } - - /** - * 关闭连接 - */ - public final static void closeConnection() { - Connection conn = conns.get(); - try { - if (conn != null && !conn.isClosed()) { - conn.close(); - } - } catch (SQLException e) { - log.error("Unabled to close connection!!! ", e); - } - conns.set(null); - } - - /** - *

获得一个数据库连接

- * 从数据库连接池中获取一个连接,将其保存在当前线程的“线程本地”变量中 - * - * @return 数据库连接 - * @throws SQLException 如果数据库连接获取失败 - */ - public final static Connection getConnection() throws SQLException { - Connection conn = conns.get(); - if (conn == null || conn.isClosed()) { - conn = dataSource.getConnection(); - conn.setAutoCommit(!transaction); - conns.set(conn); - } -// ComboPooledDataSource cpds = (ComboPooledDataSource) dataSource; -// log.info("datasourcename = " + cpds.getDataSourceName() -// + " ; NumConnections = " + cpds.getNumConnections() -// + " ; busy NumConnections = " + cpds.getNumBusyConnections()); - return (show_sql && !Proxy.isProxyClass(conn.getClass())) ? new _DebugConnection( - conn).getConnection() : conn; - } - - /** - * 事务提交 - */ - public final static void commit(){ - Connection conn = conns.get(); - try { - if (conn != null && !conn.isClosed()) { - conn.commit(); - } - } catch (SQLException e) { - log.error("Unabled to commit transaction!!! ", e); - } - } - - /** - * 事务回滚 - */ - public final static void rollback(){ - Connection conn = conns.get(); - try { - if (conn != null && !conn.isClosed()) { - conn.rollback(); - } - } catch (SQLException e) { - log.error("Unabled to rollback transaction!!! ", e); - } - } - - /** - * 用于跟踪执行的SQL语句 - * - * @author 须俊杰 - * @version 1.0 2011-10-20 - */ - static class _DebugConnection implements InvocationHandler { - - private final static Log log = LogFactory.getInstance().getLog("_DebugConnection"); - - private Connection conn = null; - - public _DebugConnection(Connection conn) { - this.conn = conn; - } - - /** - * 获取数据库连接. - * - * @return Connection 数据库连接 - */ - public Connection getConnection() { - return (Connection) Proxy.newProxyInstance(conn.getClass() - .getClassLoader(), conn.getClass().getInterfaces(), this); - } - - public Object invoke(Object proxy, Method m, Object[] args) - throws Throwable { - try { - String method = m.getName(); - if ("prepareStatement".equals(method) - || "createStatement".equals(method)) - log.info("[SQL] >>> " + args[0]); - return m.invoke(conn, args); - } catch (InvocationTargetException e) { - throw e.getTargetException(); - } - } - - } -} diff --git a/firefly-db/src/main/java/com/firefly/db/QueryHelper.java b/firefly-db/src/main/java/com/firefly/db/QueryHelper.java deleted file mode 100644 index 0cd4dbd15..000000000 --- a/firefly-db/src/main/java/com/firefly/db/QueryHelper.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.firefly.db; - -import java.math.BigInteger; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.dbutils.QueryRunner; -import org.apache.commons.dbutils.handlers.BeanHandler; -import org.apache.commons.dbutils.handlers.BeanListHandler; -import org.apache.commons.dbutils.handlers.ColumnListHandler; -import org.apache.commons.dbutils.handlers.ScalarHandler; - -/** - *

数据库查询助手

- * 提供数据库基本的增删改查操作 - * - * @author 须俊杰 - * @version 1.0 2011-6-22 - */ -@SuppressWarnings("unchecked") -public class QueryHelper { - - private final static QueryRunner _runner = new QueryRunner(); - - /** - * 返回单一列时用到的handler - */ - private final static ScalarHandler _scaleHandler = new ScalarHandler() { - @Override - public Object handle(ResultSet rs) throws SQLException { - Object obj = super.handle(rs); - if (obj instanceof BigInteger) - return ((BigInteger) obj).longValue(); - return obj; - } - }; - - private final static ColumnListHandler _columnListHandler = new ColumnListHandler(){ - @Override - protected Object handleRow(ResultSet rs) throws SQLException { - Object obj = super.handleRow(rs); - if(obj instanceof BigInteger) - return ((BigInteger)obj).longValue(); - return obj; - } - - }; - - /** - * 判断是否为原始类型 - * @param clazz - * @return - */ - private final static boolean _IsPrimitive(Class clazz) { - return clazz.isPrimitive() || PrimitiveClasses.contains(clazz); - } - - @SuppressWarnings("serial") - private final static List> PrimitiveClasses = new ArrayList>() { - { - add(Long.class); - add(Integer.class); - add(String.class); - add(java.util.Date.class); - add(java.sql.Date.class); - add(java.sql.Timestamp.class); - } - }; - - /** - * 获取数据库连接 - * - * @return 数据连接 - * @throws SQLException 如果获取数据库连接失败 - */ - private static Connection getConnection() throws SQLException { - return DBManager.getConnection(); - } - - /** - * 读取某个对象 - * @param beanClass 需要返回的对象类型 - * @param sql 数据库sql语句 - * @param params sql中的参数 - * @return 查询结果 - * @throws SQLException 数据库出错时抛出异常 - */ - public static T read(Class beanClass, String sql, Object... params) throws SQLException { - // 判断是否为原始类型,如果是就用_scaleHandler,否则根据beanClass参数生成一个BeanHandler - return (T) _runner.query(getConnection(), sql, - _IsPrimitive(beanClass) ? _scaleHandler - : new BeanHandler(beanClass), params); - } - - /** - * 读取对象列表 - * @param beanClass 需要返回的对象类型 - * @param sql 数据库sql语句 - * @param params sql中的参数 - * @return 查询结果(列表) - * @throws SQLException 数据库出错时抛出异常 - */ - @SuppressWarnings("rawtypes") - public static List query(Class beanClass, String sql, - Object... params) throws SQLException { - return (List) _runner.query(getConnection(), sql, - _IsPrimitive(beanClass) ? _columnListHandler - : new BeanListHandler(beanClass), params); - } - - /** - * 执行统计查询语句 - * @param sql 数据库sql语句 - * @param params sql中的参数 - * @return 统计结果(只返回一个数值) - * @throws SQLException 数据库出错时抛出异常 - */ - public static long stat(String sql, Object... params) throws SQLException { - Number num = (Number) _runner.query(getConnection(), sql, - _scaleHandler, params); - return (num != null) ? num.longValue() : -1; - } - - /** - * 执行INSERT/UPDATE/DELETE语句 - * @param sql 数据库sql语句 - * @param params sql中的参数 - * @return 成功:返回受影响的行数 - * @throws SQLException 修改数据库数据出错时抛出异常 - */ - public static int update(String sql, Object... params) throws SQLException { - return _runner.update(getConnection(), sql, params); - } -} diff --git a/firefly-db/src/main/java/com/firefly/db/exception/DBException.java b/firefly-db/src/main/java/com/firefly/db/exception/DBException.java deleted file mode 100644 index fd5bf0cf6..000000000 --- a/firefly-db/src/main/java/com/firefly/db/exception/DBException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.db.exception; - -public class DBException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public DBException(Throwable t) { - super(t); - } -} diff --git a/firefly-db/src/test/java/com/firefly/db/QueryHelperTest.java b/firefly-db/src/test/java/com/firefly/db/QueryHelperTest.java deleted file mode 100644 index 99202d266..000000000 --- a/firefly-db/src/test/java/com/firefly/db/QueryHelperTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.firefly.db; - -import java.sql.SQLException; -import java.util.List; - -import junit.framework.Assert; - -import org.junit.Test; - -public class QueryHelperTest { - - @Test - public void testUpdate(){ - Object[] params = new Object[]{"JJ","123"}; - try { - int result = QueryHelper.update("INSERT INTO user (name,password) values (?,?)", params); - Assert.assertEquals(1, result); - } catch (SQLException e) { - e.printStackTrace(); - } finally{ - DBManager.commit(); - } - } - - @Test - public void testRead(){ - Object[] params = new Object[]{3}; - try { - User user = QueryHelper.read(User.class, "SELECT id,name,password FROM user WHERE id = ?", params); - Assert.assertEquals("JJ", user.getName()); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - @Test - public void testQuery(){ - Object[] params = new Object[]{}; - try { - List list = QueryHelper.query(User.class, "SELECT id,name,password FROM user", params); - Assert.assertNotNull(list); - } catch (SQLException e) { - e.printStackTrace(); - } - } -} diff --git a/firefly-db/src/test/java/com/firefly/db/User.java b/firefly-db/src/test/java/com/firefly/db/User.java deleted file mode 100644 index 3f62f0b8b..000000000 --- a/firefly-db/src/test/java/com/firefly/db/User.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.db; - -public class User { - private Integer id; - private String name; - private String password; - public Integer getId() { - return id; - } - public void setId(Integer id) { - this.id = id; - } - public String getName() { - return name; - } - public void setName(String name) { - this.name = name; - } - public String getPassword() { - return password; - } - public void setPassword(String password) { - this.password = password; - } -} diff --git a/firefly-db/src/test/resources/db.properties b/firefly-db/src/test/resources/db.properties deleted file mode 100644 index ca4614ab5..000000000 --- a/firefly-db/src/test/resources/db.properties +++ /dev/null @@ -1,17 +0,0 @@ -# DataSource -jdbc.datasource=com.mchange.v2.c3p0.ComboPooledDataSource -jdbc.show_sql=false -jdbc.transaction=true - -# Database Configurations -jdbc.driverClass=com.mysql.jdbc.Driver -jdbc.jdbcUrl=jdbc:mysql://localhost:3306/fireflydb -jdbc.user=root -jdbc.password=root -jdbc.maxPoolSize=50 -jdbc.minPoolSize=4 -jdbc.initialPoolSize=4 -jdbc.acquireIncrement=2 -jdbc.maxStatements=1000 -jdbc.maxIdleTime=300 -jdbc.checkoutTimeout=120000 diff --git a/firefly-db/src/test/resources/firefly-log.properties b/firefly-db/src/test/resources/firefly-log.properties deleted file mode 100644 index 9590fe97a..000000000 --- a/firefly-db/src/test/resources/firefly-log.properties +++ /dev/null @@ -1 +0,0 @@ -firefly-system=INFO,console \ No newline at end of file diff --git a/firefly-example/pom.xml b/firefly-example/pom.xml new file mode 100644 index 000000000..31fa6d2d0 --- /dev/null +++ b/firefly-example/pom.xml @@ -0,0 +1,95 @@ + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 + + firefly-example + jar + + firefly-example + http://www.fireflysource.com + + + + com.fireflysource + firefly-net + + + + com.fireflysource + firefly-serialization + + + + com.fireflysource + firefly-slf4j + + + + + firefly-example + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.jks + **/*.ico + **/*.html + **/*.txt + + + **/*.xml + **/*.properties + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/test/resources + false + + **/*.jks + **/*.ico + **/*.html + **/*.txt + + + **/*.xml + **/*.properties + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/firefly-example/src/main/java/com/fireflysource/doc/FeignedExampleDoc.java b/firefly-example/src/main/java/com/fireflysource/doc/FeignedExampleDoc.java new file mode 100644 index 000000000..6be09192b --- /dev/null +++ b/firefly-example/src/main/java/com/fireflysource/doc/FeignedExampleDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedExampleDoc { +} diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientDemo.kt new file mode 100644 index 000000000..34900cfd5 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientDemo.kt @@ -0,0 +1,22 @@ +package com.fireflysource.example + +import com.fireflysource.fx +import kotlinx.coroutines.future.await + + +suspend fun main() { + // http://localhost:8080/hello-world + // http://nghttp2.org + + repeat(1) { + val response = fx.httpClient() + .get("http://nghttp2.org/") + .putQueryString("name", "PT_$it") + .upgradeHttp2() + .submit().await() + println("${response.status} ${response.reason}") + println(response.httpFields) + println(response.stringBody) + println() + } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientMultiPartUploadFileDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientMultiPartUploadFileDemo.kt new file mode 100644 index 000000000..fe0cfcc11 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientMultiPartUploadFileDemo.kt @@ -0,0 +1,32 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.common.annotation.NoArg +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.resourceFileBody +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.stringBody +import java.nio.file.StandardOpenOption + +@NoArg +data class Product(var id: String, var brand: String, var description: String) + +fun main() { + `$`.httpServer() + .router().post("/product/file-upload").handler { ctx -> + val id = ctx.getPart("id") + val brand = ctx.getPart("brand") + val description = ctx.getPart("description") + ctx.end(Product(id.stringBody, brand.stringBody, description.stringBody).toString()) + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().post("$url/product/file-upload") + .addPart("id", stringBody("x01"), null) + .addPart("brand", stringBody("Test"), null) + .addFilePart( + "description", "poem.txt", + resourceFileBody("files/poem.txt", StandardOpenOption.READ), + null + ) + .submit().thenAccept { response -> println(response) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientProxyDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientProxyDemo.kt new file mode 100644 index 000000000..572cf636d --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientProxyDemo.kt @@ -0,0 +1,28 @@ +package com.fireflysource.example + +import com.fireflysource.fx +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.ProxyConfig +import kotlinx.coroutines.future.await + +suspend fun main() { + val client = createProxyHttpClient("127.0.0.1", 1091) +// val host = "nghttp2.org" + val host = "www.google.com" + val response = client + .get("https://$host/") + .submit().await() + println("${response.status} ${response.reason}") + println(response.httpFields) + println(response.stringBody) +} + +fun createProxyHttpClient(host: String, port: Int): HttpClient { + val proxyConfig = ProxyConfig() + proxyConfig.host = host + proxyConfig.port = port + val httpConfig = HttpConfig() + httpConfig.proxyConfig = proxyConfig + return fx.createHttpClient(httpConfig) +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientWithoutConnectionPoolDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientWithoutConnectionPoolDemo.kt new file mode 100644 index 000000000..545b58dcb --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpClientWithoutConnectionPoolDemo.kt @@ -0,0 +1,24 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.common.io.useAwait +import com.fireflysource.net.http.client.impl.connectAsync +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await + +fun main() { + `$`.httpServer() + .router().get("/hello").handler { ctx -> ctx.end("Hello http! ") } + .listen("localhost", 8090) + + `$`.httpClient().connectAsync("http://localhost:8090") { connection -> + connection.useAwait { + repeat(3) { + val response = connection.get("/hello").submit().await() + println("connection ${connection.id} received: ${response.stringBody}") + delay(1000) + } + } + println("connection ${connection.id} closed. ${connection.isClosed}") + } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerCorsDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerCorsDemo.kt new file mode 100644 index 000000000..4ec7e3ff8 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerCorsDemo.kt @@ -0,0 +1,23 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.impl.router.handler.CorsConfig +import com.fireflysource.net.http.server.impl.router.handler.CorsHandler + +fun main() { + val corsConfig = CorsConfig("*.cors.test.com") + `$`.httpServer() + .router().path("*").handler(CorsHandler(corsConfig)) + .router().post("/cors-data-request/*") + .handler { it.end("success") } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().post("$url/cors-data-request/xxx") + .put(HttpHeader.ORIGIN, "hello.cors.test.com") + .put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_PLAIN_UTF_8.value) + .body("hello") + .submit().thenAccept { response -> println(response) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerDemo.kt new file mode 100644 index 000000000..fe3e735e8 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerDemo.kt @@ -0,0 +1,45 @@ +package com.fireflysource.example + +import com.fireflysource.net.http.server.HttpServerFactory +import com.fireflysource.net.http.server.impl.router.handler.CorsConfig +import com.fireflysource.net.http.server.impl.router.handler.CorsHandler +import com.fireflysource.net.http.server.impl.router.handler.FileHandler + +/* +Intel i5 1.4GHz 16GB macbook pro 13 + +wrk -t4 -c16 -d60s --latency http://localhost:9999/test +Running 1m test @ http://localhost:9999/test + 4 threads and 16 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 170.83us 34.94us 4.30ms 77.90% + Req/Sec 22.74k 0.99k 26.71k 81.82% + Latency Distribution + 50% 170.00us + 75% 189.00us + 90% 207.00us + 99% 252.00us + 5438229 requests in 1.00m, 456.39MB read +Requests/sec: 90486.34 +Transfer/sec: 7.59MB +*/ +fun main() { + val httpServer = HttpServerFactory.create() + val corsConfig = CorsConfig("*") + + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().paths(listOf("/favicon.ico", "/poem.html", "/poem.txt")) + .handler(FileHandler.createFileHandlerByResourcePath("files")) + .router().post("/cors-preflight/*").handler { + it.end( + """ + |{"status": "ok"} + """.trimMargin() + ) + } + .router().get("/test").handler { + it.end("Welcome") + } + .listen("localhost", 9999) +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerErrorHandlerDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerErrorHandlerDemo.kt new file mode 100644 index 000000000..d04c7ffe1 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerErrorHandlerDemo.kt @@ -0,0 +1,15 @@ +package com.fireflysource.example + +import com.fireflysource.`$` + +fun main() { + `$`.httpServer() + .router().post("/product").handler { + throw IllegalStateException("Create product exception") + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().post("$url/product/").submit() + .thenAccept { response -> println(response) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerErrorHandlerDemo2.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerErrorHandlerDemo2.kt new file mode 100644 index 000000000..4b7a853f3 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerErrorHandlerDemo2.kt @@ -0,0 +1,20 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.net.http.common.model.HttpStatus + +fun main() { + `$`.httpServer() + .onException { ctx, exception -> + ctx.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) + .end("The server exception. ${exception.message}") + } + .router().post("/product").handler { + throw IllegalStateException("Create product exception") + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().post("$url/product/").submit() + .thenAccept { response -> println(response) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerPathParamDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerPathParamDemo.kt new file mode 100644 index 000000000..ccf7ac46f --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerPathParamDemo.kt @@ -0,0 +1,26 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.net.http.common.model.HttpStatus.NOT_FOUND_404 + +fun main() { + `$`.httpServer() + .router().get("/product/:id").handler { ctx -> + when (val id = ctx.getPathParameter("id")) { + "1" -> ctx.end("Apple") + "2" -> ctx.end("Orange") + else -> ctx.setStatus(NOT_FOUND_404).end("The product $id not found.") + } + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().get("$url/product/1").submit() + .thenAccept { response -> println(response.stringBody) } + + `$`.httpClient().get("$url/product/2").submit() + .thenAccept { response -> println(response.stringBody) } + + `$`.httpClient().get("$url/product/3").submit() + .thenAccept { response -> println(response.stringBody) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerProxyDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerProxyDemo.kt new file mode 100644 index 000000000..e82b8e3fb --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerProxyDemo.kt @@ -0,0 +1,35 @@ +package com.fireflysource.example + +import com.fireflysource.fx +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.client.HttpClientResponse +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await + +suspend fun main() { + val proxy = fx.createHttpProxy() + proxy.listen("localhost", 1678) + + val client = createProxyHttpClient("localhost", 1678) + testHttpsProxy(client) + delay(1000) + testHttpProxy(client) +} + +suspend fun testHttpsProxy(client: HttpClient) { + val response = client.get("https://www.baidu.com/").submit().await() + printResponse(response) +} + +suspend fun testHttpProxy(client: HttpClient) { + val response = client.get("http://www.fireflysource.com/").submit().await() + printResponse(response) +} + +private fun printResponse(response: HttpClientResponse) { + println("${response.status} ${response.reason}") + println(response.httpFields) + println(response.stringBody) + println("-------------------------------------------") + println() +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRegexDemo1.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRegexDemo1.kt new file mode 100644 index 000000000..c4dbc6003 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRegexDemo1.kt @@ -0,0 +1,25 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.net.http.common.model.HttpMethod + +fun main() { + `$`.httpServer() + .router().method(HttpMethod.PUT).pathRegex("/product/(.*)/(.*)").handler { ctx -> + val type = ctx.getPathParameterByRegexGroup(1) + val id = ctx.getPathParameterByRegexGroup(2) + val product = ctx.stringBody + ctx.end("Put product success. id: $id, type: $type, product: $product") + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().put("$url/product/fruit/1").body("Apple").submit() + .thenAccept { response -> println(response.stringBody) } + + `$`.httpClient().put("$url/product/book/1").body("Tom and Jerry").submit() + .thenAccept { response -> println(response.stringBody) } + + `$`.httpClient().put("$url/product/book/2").body("The Three-Body Problem").submit() + .thenAccept { response -> println(response.stringBody) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRouterContextDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRouterContextDemo.kt new file mode 100644 index 000000000..c10607ce0 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRouterContextDemo.kt @@ -0,0 +1,16 @@ +package com.fireflysource.example + +import com.fireflysource.`$` + +fun main() { + `$`.httpServer() + .router().get("/").handler { ctx -> + ctx.attributes["router1"] = "Some one visits the /. " + ctx.write("Hello world! ").next() + } + .router().get("/").handler { ctx -> + val data = ctx.attributes["router1"] + ctx.end("The router data: $data") + } + .listen("localhost", 8090) +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRouterDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRouterDemo.kt new file mode 100644 index 000000000..7a9f32777 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRouterDemo.kt @@ -0,0 +1,10 @@ +package com.fireflysource.example + +import com.fireflysource.`$` + +fun main() { + `$`.httpServer() + .router().get("/").handler { ctx -> ctx.write("Hello world! ").next() } + .router().get("/").handler { ctx -> ctx.end("The router demo.") } + .listen("localhost", 8090) +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByAcceptTypeDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByAcceptTypeDemo.kt new file mode 100644 index 000000000..ca22014f7 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByAcceptTypeDemo.kt @@ -0,0 +1,29 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.serialization.SerializationServiceFactory.json + +fun main() { + `$`.httpServer() + .router().get("/product/:id").produces("text/plain") + .handler { ctx -> + ctx.end(Car("Benz", "Black").toString()) + } + .router().get("/product/:id").produces("application/json") + .handler { ctx -> + ctx.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .end(json.write(Car("Benz", "Black"))) + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().get("$url/product/3") + .put(HttpHeader.ACCEPT, "text/plain, application/json;q=0.9, */*;q=0.8") + .submit().thenAccept { response -> println("accept text; ${response.stringBody}") } + + `$`.httpClient().get("$url/product/3") + .put(HttpHeader.ACCEPT, "application/json, text/plain, */*;q=0.8") + .submit().thenAccept { response -> println("accept json; ${response.stringBody}") } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByContentTypeDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByContentTypeDemo.kt new file mode 100644 index 000000000..1b16aa21b --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByContentTypeDemo.kt @@ -0,0 +1,33 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.common.annotation.NoArg +import com.fireflysource.net.http.common.model.HttpField +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.serialization.SerializationServiceFactory.json +import com.fireflysource.serialization.impl.json.read + +@NoArg +data class Car(var name: String, var color: String) + +fun main() { + `$`.httpServer() + .router().put("/product/:id").consumes("*/json") + .handler { ctx -> + val id = ctx.getPathParameter("id") + val type = ctx.getPathParameter(0) + val car = json().read(ctx.stringBody) + + ctx.write("Update product. id: $id, type: $type. \r\n") + .end(car.toString()) + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient() + .put("$url/product/3") + .add(HttpField(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value)) + .body(json.write(Car("Benz", "Black"))) + .submit().thenAccept { response -> println(response.stringBody) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByMethodDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByMethodDemo.kt new file mode 100644 index 000000000..4adc8f356 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerRoutingByMethodDemo.kt @@ -0,0 +1,23 @@ +package com.fireflysource.example + +import com.fireflysource.`$` + +fun main() { + `$`.httpServer() + .router().get("/product/:id").handler { ctx -> + val id = ctx.getPathParameter("id") + ctx.end("Get the product $id") + } + .router().post("/product").handler { ctx -> + ctx.end("Create the product 1") + } + .router().put("/product/:id").handler { ctx -> + val id = ctx.getPathParameter("id") + ctx.end("Update the product $id") + } + .router().delete("/product/:id").handler { ctx -> + val id = ctx.getPathParameter("id") + ctx.end("Delete the product $id") + } + .listen("localhost", 8090) +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerStaticFileDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerStaticFileDemo.kt new file mode 100644 index 000000000..a6259b94b --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerStaticFileDemo.kt @@ -0,0 +1,13 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.server.impl.router.handler.FileHandler + +fun main() { + `$`.httpServer() + .router().method(HttpMethod.GET) + .paths(listOf("/favicon.ico", "/poem.html", "/poem.txt")) + .handler(FileHandler.createFileHandlerByResourcePath("files")) + .listen("localhost", 8090) +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerWildcardDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerWildcardDemo.kt new file mode 100644 index 000000000..bdd8dbbd4 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpServerWildcardDemo.kt @@ -0,0 +1,24 @@ +package com.fireflysource.example + +import com.fireflysource.`$` + +fun main() { + `$`.httpServer() + .router().put("/product/*/*").handler { ctx -> + val type = ctx.getPathParameter(0) + val id = ctx.getPathParameter(1) + val product = ctx.stringBody + ctx.end("Put product success. id: $id, type: $type, product: $product") + } + .listen("localhost", 8090) + + val url = "http://localhost:8090" + `$`.httpClient().put("$url/product/fruit/1").body("Apple").submit() + .thenAccept { response -> println(response.stringBody) } + + `$`.httpClient().put("$url/product/book/1").body("Tom and Jerry").submit() + .thenAccept { response -> println(response.stringBody) } + + `$`.httpClient().put("$url/product/book/2").body("The Three-Body Problem").submit() + .thenAccept { response -> println(response.stringBody) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/HttpsDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpsDemo.kt new file mode 100644 index 000000000..88957d51b --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/HttpsDemo.kt @@ -0,0 +1,13 @@ +package com.fireflysource.example + +import com.fireflysource.`$` + +fun main() { + `$`.httpServer() + .router().get("/").handler { ctx -> ctx.end("Hello https! ") } + .enableSecureConnection() + .listen("localhost", 8090) + + `$`.httpClient().get("https://localhost:8090/").submit() + .thenAccept { response -> println(response.stringBody) } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/TcpServerAndClientDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/TcpServerAndClientDemo.kt new file mode 100644 index 000000000..85d7d804a --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/TcpServerAndClientDemo.kt @@ -0,0 +1,44 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.BufferUtils.toBuffer +import com.fireflysource.common.io.useAwait +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.aio.connectAsync +import com.fireflysource.net.tcp.aio.onAcceptAsync +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.util.* + +fun main() { + `$`.tcpServer().onAcceptAsync { connection -> + launch { writeLoop("Server", connection) } + launch { readLoop(connection) } + }.listen("localhost", 8090) + + `$`.tcpClient().connectAsync("localhost", 8090) { connection -> + launch { writeLoop("Client", connection) } + launch { readLoop(connection) } + } +} + +private suspend fun readLoop(connection: TcpConnection) = connection.useAwait { + while (true) { + try { + val buffer = connection.read().await() + println(BufferUtils.toString(buffer)) + } catch (e: Exception) { + println("Connection closed.") + break + } + } +} + +private suspend fun writeLoop(data: String, connection: TcpConnection) = connection.useAwait { + (1..10).forEach { + connection.write(toBuffer("TCP ${data}. count: $it, time: ${Date()}")) + delay(1000) + } +} \ No newline at end of file diff --git a/firefly-example/src/main/kotlin/com/fireflysource/example/WebSocketServerDemo.kt b/firefly-example/src/main/kotlin/com/fireflysource/example/WebSocketServerDemo.kt new file mode 100644 index 000000000..b8057f0b0 --- /dev/null +++ b/firefly-example/src/main/kotlin/com/fireflysource/example/WebSocketServerDemo.kt @@ -0,0 +1,39 @@ +package com.fireflysource.example + +import com.fireflysource.`$` +import com.fireflysource.common.io.useAwait +import com.fireflysource.net.websocket.client.impl.connectAsync +import com.fireflysource.net.websocket.client.impl.onClientMessageAsync +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.frame.Frame +import com.fireflysource.net.websocket.common.frame.TextFrame +import com.fireflysource.net.websocket.server.impl.onAcceptAsync +import com.fireflysource.net.websocket.server.impl.onServerMessageAsync +import kotlinx.coroutines.delay +import java.util.* + +fun main() { + `$`.httpServer().websocket("/websocket/hello") + .onServerMessageAsync { frame, _ -> onMessage(frame) } + .onAcceptAsync { connection -> sendMessage("Server", connection) } + .listen("localhost", 8090) + + val url = "ws://localhost:8090" + `$`.httpClient().websocket("$url/websocket/hello") + .extensions(listOf("permessage-deflate")) + .onClientMessageAsync { frame, _ -> onMessage(frame) } + .connectAsync { connection -> sendMessage("Client", connection) } +} + +private suspend fun sendMessage(data: String, connection: WebSocketConnection) = connection.useAwait { + (1..10).forEach { + connection.sendText("WebSocket ${data}. count: $it, time: ${Date()}") + delay(1000) + } +} + +private fun onMessage(frame: Frame) { + if (frame is TextFrame) { + println(frame.payloadAsUTF8) + } +} \ No newline at end of file diff --git a/firefly-example/src/main/resources/files/favicon.ico b/firefly-example/src/main/resources/files/favicon.ico new file mode 100644 index 000000000..d888a0c47 Binary files /dev/null and b/firefly-example/src/main/resources/files/favicon.ico differ diff --git a/firefly-example/src/main/resources/files/poem.html b/firefly-example/src/main/resources/files/poem.html new file mode 100644 index 000000000..415dcaccf --- /dev/null +++ b/firefly-example/src/main/resources/files/poem.html @@ -0,0 +1,20 @@ + + + + + Poem + + +

家庭

+

+ 我独自在横跨过田地的路上行走,夕阳像一个守财奴似的,正藏起它的最后的金子。 + 白昼更加深沉地陷入黑暗之中,那已经收割了的孤独的田地, + 默默地躺在那里。 + 天空里突然升起了一个男孩童的尖锐的歌声。他穿过看不见的黑暗,留下他的歌声的辙痕跨过黄昏的静谧。 + 他的乡村的家坐落在荒凉的边上,在甘蔗田的后面,躲藏在香蕉树,瘦长的槟榔树,椰子树和深绿色的贾克果树的阴影里。 + 我在星光下独自走着的路上停留了一会,我看见黑沉沉的大地展开在我的面前,用她的手臂拥抱着无量数的家庭, + 在那些家庭里有着摇篮和床铺,母亲们的心和夜晚的灯,还有年轻轻的生命, + 他们满心快乐,却浑然不知这样的快乐对于世界的价值。 +

+ + \ No newline at end of file diff --git a/firefly-example/src/main/resources/files/poem.txt b/firefly-example/src/main/resources/files/poem.txt new file mode 100644 index 000000000..a9ccadd62 --- /dev/null +++ b/firefly-example/src/main/resources/files/poem.txt @@ -0,0 +1,22 @@ +偷睡眠的人 +谁从孩童的眼里把睡眠偷了去呢?我一定要知道。 +妈妈把她的水罐挟在腰间,走到近村汲水去了。 +这是正午的时候,孩童们游戏的时间已经过去了,池中的鸭子缄默无声。 +牧童躺在榕树的荫下睡着了。 +白鹤庄重而安静地立在檬果树边的泥泽里。 + +就在这个时候,偷睡眠的人跑来从孩童的两眼里捉住睡眠,便飞去了。 +当妈妈回来时,她看见孩童四肢着地地在屋里爬着。 +谁从孩童的眼里把睡眠偷了去呢?我一定要知道。我一定要找到她,把她锁起来。 +我一定要向那个黑洞里张望,在这个洞里,有一道小泉从圆的和有皱纹的石上滴下来。 +我一定要到醉花林中的沉寂的树影里搜寻,在这林中,鸽子在它们住的地方咕咕地叫着,仙女的脚环在繁星满天的静夜里叮当地响着。 +我要在黄昏时,向静静的萧萧的竹林里窥望,在这林中,萤火虫闪闪地耗费它们的光明,只要遇见一个人,我便要问他:“谁能告诉我偷睡眠者住在什么地方?” + +谁从孩童的眼里把睡眠偷了去呢?我一定要知道。 +只要我能捉住她,怕不会给她一顿好教训! +我要闯入她的巢穴,看她把所有偷来的睡眠藏在什么地方。 +我要把它都夺回来,带回家去。 +我要把她的双翼缚得紧紧的,把她放在河边,然后叫她拿一根芦苇在灯心草和睡莲间钓鱼为戏。 +黄昏,街上已经收了市,村里的孩童们都坐在妈妈的膝上时, +夜鸟便会讥讽地在她耳边说: +“你现在还想偷谁的睡眠呢?” \ No newline at end of file diff --git a/firefly-example/src/main/resources/firefly-log.xml b/firefly-example/src/main/resources/firefly-log.xml new file mode 100644 index 000000000..9d3dbfa8a --- /dev/null +++ b/firefly-example/src/main/resources/firefly-log.xml @@ -0,0 +1,11 @@ + + + + + firefly-system + INFO + ${log.path} + + + diff --git a/firefly-jni-helper/build.sh b/firefly-jni-helper/build.sh new file mode 100755 index 000000000..a8ee8bd33 --- /dev/null +++ b/firefly-jni-helper/build.sh @@ -0,0 +1,46 @@ +#!/bin/bash +PROJECT_HOME=$(cd "$(dirname "$0")" && pwd) +echo "project dir: $PROJECT_HOME" + +JNI_HELPER_EXAMPLE_HEADERS_DIR="$PROJECT_HOME/target/headers" +echo "JNI helper example headers dir: $JNI_HELPER_EXAMPLE_HEADERS_DIR" + +JNI_HELPER_SOURCES_DIR="$PROJECT_HOME/src/main/cpp/jni-helper" +echo "JNI helper sources dir: $JNI_HELPER_SOURCES_DIR" + + +# generate JNI header +cd "$PROJECT_HOME" && mvn clean compile +cp "$JNI_HELPER_EXAMPLE_HEADERS_DIR/"*.h "$JNI_HELPER_SOURCES_DIR/example" +echo "-- copy JNI example headers is complete" + +# build JNI example project +cd "$JNI_HELPER_SOURCES_DIR" && sh ./build.sh + + +JNI_HELPER_EXAMPLE_LIB_DIR="$JNI_HELPER_SOURCES_DIR/build-release/release" +echo "JNI helper example lib dir: $JNI_HELPER_EXAMPLE_LIB_DIR" + +if [ "$(uname)" == "Darwin" ];then + # Mac OS X 操作系统 + if [ -f "$JNI_HELPER_EXAMPLE_LIB_DIR/lib/libjni_helper_example.dylib" ]; then + cp "$JNI_HELPER_EXAMPLE_LIB_DIR/lib/libjni_helper_example.dylib" "$PROJECT_HOME/src/test/resources/lib/macos" + fi +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ];then + # GNU/Linux操作系统 + if [ -f "$JNI_HELPER_EXAMPLE_LIB_DIR/lib/libjni_helper_example.so" ]; then + cp "$JNI_HELPER_EXAMPLE_LIB_DIR/lib/libjni_helper_example.so" "$PROJECT_HOME/src/test/resources/lib/linux" + fi +elif [[ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" || "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]];then + # Windows NT操作系统 + if [ -f "$JNI_HELPER_EXAMPLE_LIB_DIR/bin/jni_helper_example.dll" ]; then + cp "$JNI_HELPER_EXAMPLE_LIB_DIR/bin/jni_helper_example.dll" "$PROJECT_HOME/src/test/resources/lib/windows/libjni_helper_example.dll" + fi +fi + +echo "-- build JNI example cpp project is complete" + +# run JNI test cases +cd "$PROJECT_HOME" && mvn test + + diff --git a/firefly-jni-helper/pom.xml b/firefly-jni-helper/pom.xml new file mode 100644 index 000000000..e5f8277a2 --- /dev/null +++ b/firefly-jni-helper/pom.xml @@ -0,0 +1,88 @@ + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 + + firefly-jni-helper + jar + + firefly-jni-helper + http://www.fireflysource.com + + + + com.fireflysource + firefly-common + + + + com.fireflysource + firefly-slf4j + test + + + + + firefly-jni-helper + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.xml + **/*.properties + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/test/resources + false + + **/*.jks + **/*.ico + **/*.html + **/*.txt + **/*.so + **/*.dylib + **/*.dll + + + **/*.xml + **/*.properties + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/.gitignore b/firefly-jni-helper/src/main/cpp/jni-helper/.gitignore new file mode 100644 index 000000000..c8d652170 --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/.gitignore @@ -0,0 +1,2 @@ +cmake-build-debug +build-release \ No newline at end of file diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/CMakeLists.txt b/firefly-jni-helper/src/main/cpp/jni-helper/CMakeLists.txt new file mode 100644 index 000000000..79afc4557 --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.16) +project(jni_helper) + +set(CMAKE_CXX_STANDARD 14) + +# set compiler level +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.") + #不管CACHE里有没有设置过CMAKE_BUILD_TYPE这个变量,都强制赋值这个值为RelWithDebInfo + set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) + + # 当使用cmake-gui的时候,设置构建级别的四个可选项 + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" + "MinSizeRel" "RelWithDebInfo") +endif () + +# set output directory +message(STATUS "project source dir: ${PROJECT_SOURCE_DIR}") +message(STATUS "bin dir: ${CMAKE_BINARY_DIR}") + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/debug/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/debug/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/debug/bin) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/release/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/release/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/release/bin) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/release/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/release/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/release/bin) + +# set java dependency +include(UseJava) +if (DEFINED ENV{JAVA_HOME}) + set(JAVA_HOME "$ENV{JAVA_HOME}") +else () + set(JAVA_HOME /Users/qiupengtao/Develop/jdk/hotspot/jdk8u292-b10/Contents/Home) +endif () +message(STATUS "JAVA_HOME variable is defined or set as '${JAVA_HOME}'") + +if (APPLE) + message(STATUS "os: MacOS") + set(JNI_INCLUDE_DIRS ${JAVA_HOME}/include ${JAVA_HOME}/include/darwin) + set(JNI_LIB_DIRS ${JAVA_HOME}/jre/lib) + add_compile_options(-fPIC) +elseif (WIN32) + message(STATUS "os: Windows") + set(JNI_INCLUDE_DIRS ${JAVA_HOME}/include ${JAVA_HOME}/include/win32 ${JAVA_HOME}/include/win32/bridge) + set(JNI_LIB_DIRS ${JAVA_HOME}/jre/lib) +elseif (UNIX) + message(STATUS "os: Unix like e.g. linux, bsd.") + set(JNI_INCLUDE_DIRS ${JAVA_HOME}/include ${JAVA_HOME}/include/linux) + set(JNI_LIB_DIRS ${JAVA_HOME}/jre/lib) + add_compile_options(-fPIC) +endif () + +message(STATUS "JNI include dirs: ${JNI_INCLUDE_DIRS}") +include_directories(${JNI_INCLUDE_DIRS}) +link_directories(${JNI_LIB_DIRS}) + +# set jni helper sources +set(JNI_HELPER_SOURCES JniHelper.hpp JniHelper.cpp) +add_library(jni_helper_static STATIC ${JNI_HELPER_SOURCES}) +add_library(jni_helper SHARED ${JNI_HELPER_SOURCES}) + +# set jni example sources +set(JNI_HELPER_EXAMPLE_SOURCES + example/com_fireflysource_jni_example_JniExample.h + example/JniExample.cpp) +add_library(jni_helper_example SHARED ${JNI_HELPER_EXAMPLE_SOURCES}) +target_link_libraries(jni_helper_example jni_helper_static) \ No newline at end of file diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/JniHelper.cpp b/firefly-jni-helper/src/main/cpp/jni-helper/JniHelper.cpp new file mode 100644 index 000000000..6f1233f95 --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/JniHelper.cpp @@ -0,0 +1,53 @@ +// +// Created by qiupengtao on 2021/7/9. +// +#include "JniHelper.hpp" +#include + +namespace com { +namespace fireflysource { +namespace jni { + +std::string javaStringToCppString(JNIEnv *env, jstring javaString) { + const char *cString = env->GetStringUTFChars(javaString, nullptr); + std::string result = cString; + env->ReleaseStringUTFChars(javaString, cString); + return result; +} + +jstring newJavaString(JNIEnv *env, const std::string &cppString) { + auto result_buffer = static_cast(std::malloc(cppString.size())); + std::strcpy(result_buffer, cppString.c_str()); + return env->NewStringUTF(result_buffer); +} + +LocalReference newJavaStringLocalReference(JNIEnv *env, const std::string &cppString) { + return LocalReference(env, cppString, newJavaString); +} + +namespace java { + +void println(JNIEnv *env, jstring javaString) { + // Get system class + jclass system = env->FindClass("java/lang/System"); + // Lookup the "out" field + jfieldID fid = env->GetStaticFieldID(system, "out", "Ljava/io/PrintStream;"); + jobject out = env->GetStaticObjectField(system, fid); + // Get PrintStream class + jclass printStream = env->FindClass("java/io/PrintStream"); + // Lookup printLn(String) + jmethodID printlnMethod = env->GetMethodID(printStream, "println", "(Ljava/lang/String;)V"); + env->CallVoidMethod(out, printlnMethod, javaString); +} + +void println(JNIEnv *env, const std::string &str) { + LocalReference javaStringRef = newJavaStringLocalReference(env, str); + jstring javaString = javaStringRef.get(); + println(env, javaString); +} + +} + +} +} +} \ No newline at end of file diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/JniHelper.hpp b/firefly-jni-helper/src/main/cpp/jni-helper/JniHelper.hpp new file mode 100644 index 000000000..de8944be4 --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/JniHelper.hpp @@ -0,0 +1,56 @@ +// +// Created by qiupengtao on 2021/7/9. +// + +#ifndef TEST_JNI_JNI_HELPER_JNIHELPER_HPP_ +#define TEST_JNI_JNI_HELPER_JNIHELPER_HPP_ +#include +#include + +namespace com { +namespace fireflysource { +namespace jni { + +template +class LocalReference { + public: + template + LocalReference(JNIEnv *env, + const CppType &cppParam, + JniType(*newJavaType)(JNIEnv *, const CppType &)) { + this->env = env; + this->javaObject = newJavaType(env, cppParam); + } + + ~LocalReference() { + env->DeleteLocalRef(javaObject); + }; + + JniType get() { + return javaObject; + } + + private: + JniType javaObject; + JNIEnv *env = nullptr; +}; + +std::string javaStringToCppString(JNIEnv *env, jstring javaString); + +jstring newJavaString(JNIEnv *env, const std::string &cppString); + +LocalReference newJavaStringLocalReference(JNIEnv *env, const std::string &cppString); + +namespace java { + +void println(JNIEnv *env, jstring javaString); + +void println(JNIEnv *env, const std::string &cppString); + +} + +} +} +} + +#endif //TEST_JNI_JNI_HELPER_JNIHELPER_HPP_ diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/build.sh b/firefly-jni-helper/src/main/cpp/jni-helper/build.sh new file mode 100755 index 000000000..2a99a0277 --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash +PROJECT_HOME=$(cd "$(dirname "$0")" && pwd) +echo "project dir: $PROJECT_HOME" +RELEASE_BUILD_DIR="$PROJECT_HOME/build-release" +echo "cmake release dir: $RELEASE_BUILD_DIR" + +if [ ! -d "$RELEASE_BUILD_DIR" ]; then + mkdir "$RELEASE_BUILD_DIR" +else + rm -rf "$RELEASE_BUILD_DIR" +fi + +echo "$(uname)" + +if [ "$(uname)" == "Darwin" ];then + echo "build on MacOS" + cmake -S "$PROJECT_HOME" -B "$RELEASE_BUILD_DIR" + cmake --build "$RELEASE_BUILD_DIR" --target clean + cmake --build "$RELEASE_BUILD_DIR" --target all + cd "$RELEASE_BUILD_DIR" && make +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ];then + echo "build on Linux" + cmake -S "$PROJECT_HOME" -B "$RELEASE_BUILD_DIR" + cmake --build "$RELEASE_BUILD_DIR" --target clean + cmake --build "$RELEASE_BUILD_DIR" --target all + cd "$RELEASE_BUILD_DIR" && make +elif [[ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" || "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]];then + echo "build on Windows" + cmake -S "$PROJECT_HOME" -B "$RELEASE_BUILD_DIR" + cmake --build "$RELEASE_BUILD_DIR" --target clean + cmake --build "$RELEASE_BUILD_DIR" --target ALL_BUILD + cd "$RELEASE_BUILD_DIR" && msbuild.exe ALL_BUILD.vcxproj -t:rebuild -p:Configuration=Release +fi \ No newline at end of file diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/example/JniExample.cpp b/firefly-jni-helper/src/main/cpp/jni-helper/example/JniExample.cpp new file mode 100644 index 000000000..c4c94bdd3 --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/example/JniExample.cpp @@ -0,0 +1,34 @@ +// +// Created by qiupengtao on 2021/7/12. +// +#include "com_fireflysource_jni_example_JniExample.h" +#include "../JniHelper.hpp" + +namespace com { +namespace fireflysource { +namespace jni { +namespace example { + +std::string sayHello(const std::string &str) { + return "Bonjour, " + str; +} + +} +} +} +} + +using namespace com::fireflysource::jni; + +JNIEXPORT jstring JNICALL Java_com_fireflysource_jni_example_JniExample_sayHello + (JNIEnv *env, jclass javaClass, jstring javaString) { + if (javaString == nullptr) { + return newJavaString(env, "param str cannot be null"); + } + + std::string param = javaStringToCppString(env, javaString); + std::string hello = example::sayHello(param); // Call native library + java::println(env, "call java method to print result: " + hello); + + return newJavaString(env, hello); +} \ No newline at end of file diff --git a/firefly-jni-helper/src/main/cpp/jni-helper/example/com_fireflysource_jni_example_JniExample.h b/firefly-jni-helper/src/main/cpp/jni-helper/example/com_fireflysource_jni_example_JniExample.h new file mode 100644 index 000000000..4fba3bdac --- /dev/null +++ b/firefly-jni-helper/src/main/cpp/jni-helper/example/com_fireflysource_jni_example_JniExample.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_fireflysource_jni_example_JniExample */ + +#ifndef _Included_com_fireflysource_jni_example_JniExample +#define _Included_com_fireflysource_jni_example_JniExample +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_fireflysource_jni_example_JniExample + * Method: sayHello + * Signature: (Ljava/lang/String;)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_com_fireflysource_jni_example_JniExample_sayHello + (JNIEnv *, jclass, jstring); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/firefly-jni-helper/src/main/java/com/fireflysource/doc/FeignedJniDoc.java b/firefly-jni-helper/src/main/java/com/fireflysource/doc/FeignedJniDoc.java new file mode 100644 index 000000000..ee78970d3 --- /dev/null +++ b/firefly-jni-helper/src/main/java/com/fireflysource/doc/FeignedJniDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedJniDoc { +} diff --git a/firefly-jni-helper/src/main/java/com/fireflysource/jni/example/JniExample.java b/firefly-jni-helper/src/main/java/com/fireflysource/jni/example/JniExample.java new file mode 100644 index 000000000..cf57b9a13 --- /dev/null +++ b/firefly-jni-helper/src/main/java/com/fireflysource/jni/example/JniExample.java @@ -0,0 +1,12 @@ +package com.fireflysource.jni.example; + +import com.fireflysource.common.jni.JniLibLoader; + +public class JniExample { + + static { + JniLibLoader.load("jni_helper_example"); + } + + public static native String sayHello(String s); +} diff --git a/firefly-jni-helper/src/test/java/com/fireflysource/jni/example/TestJniExample.java b/firefly-jni-helper/src/test/java/com/fireflysource/jni/example/TestJniExample.java new file mode 100644 index 000000000..750a5873a --- /dev/null +++ b/firefly-jni-helper/src/test/java/com/fireflysource/jni/example/TestJniExample.java @@ -0,0 +1,18 @@ +package com.fireflysource.jni.example; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestJniExample { + + @Test + @EnabledOnOs({OS.MAC, OS.LINUX, OS.WINDOWS}) + void testSayHello() { + String result = JniExample.sayHello("欢迎!"); + System.out.println(result); + assertEquals("Bonjour, 欢迎!", result); + } +} diff --git a/firefly-jni-helper/src/test/resources/firefly-log.xml b/firefly-jni-helper/src/test/resources/firefly-log.xml new file mode 100644 index 000000000..d139af581 --- /dev/null +++ b/firefly-jni-helper/src/test/resources/firefly-log.xml @@ -0,0 +1,18 @@ + + + + + firefly-system + INFO + ${log.path} + false + + + + firefly-monitor + INFO + ${log.path} + + + diff --git a/firefly-jni-helper/src/test/resources/lib/linux/libjni_helper_example.so b/firefly-jni-helper/src/test/resources/lib/linux/libjni_helper_example.so new file mode 100755 index 000000000..30e5e759c Binary files /dev/null and b/firefly-jni-helper/src/test/resources/lib/linux/libjni_helper_example.so differ diff --git a/firefly-jni-helper/src/test/resources/lib/macos/libjni_helper_example.dylib b/firefly-jni-helper/src/test/resources/lib/macos/libjni_helper_example.dylib new file mode 100755 index 000000000..4c16b964a Binary files /dev/null and b/firefly-jni-helper/src/test/resources/lib/macos/libjni_helper_example.dylib differ diff --git a/firefly-jni-helper/src/test/resources/lib/windows/libjni_helper_example.dll b/firefly-jni-helper/src/test/resources/lib/windows/libjni_helper_example.dll new file mode 100755 index 000000000..ae960fb93 Binary files /dev/null and b/firefly-jni-helper/src/test/resources/lib/windows/libjni_helper_example.dll differ diff --git a/firefly-net/pom.xml b/firefly-net/pom.xml new file mode 100644 index 000000000..cdf092e95 --- /dev/null +++ b/firefly-net/pom.xml @@ -0,0 +1,94 @@ + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 + + firefly-net + jar + + firefly-net + http://www.fireflysource.com + + + + com.fireflysource + firefly-common + + + + org.conscrypt + conscrypt-openjdk-uber + + + + org.openjsse + openjsse + + + + org.wildfly.openssl + wildfly-openssl + + + + com.fireflysource + firefly-slf4j + test + + + + + firefly-net + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.xml + **/*.properties + + + **/*.jks + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/test/resources + false + + **/*.xml + **/*.properties + + + **/*.ico + **/*.html + **/*.txt + **/*.MockMaker + + + + + diff --git a/firefly-net/src/main/java/com/fireflysource/$.java b/firefly-net/src/main/java/com/fireflysource/$.java new file mode 100644 index 000000000..11142a454 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/$.java @@ -0,0 +1,351 @@ +package com.fireflysource; + +import com.fireflysource.common.concurrent.CompletableFutures; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.CommonTcpChannelGroup; +import com.fireflysource.net.http.client.HttpClient; +import com.fireflysource.net.http.client.HttpClientFactory; +import com.fireflysource.net.http.common.HttpConfig; +import com.fireflysource.net.http.server.HttpProxy; +import com.fireflysource.net.http.server.HttpServer; +import com.fireflysource.net.http.server.HttpServerFactory; +import com.fireflysource.net.tcp.TcpClient; +import com.fireflysource.net.tcp.TcpClientFactory; +import com.fireflysource.net.tcp.TcpServer; +import com.fireflysource.net.tcp.TcpServerFactory; +import com.fireflysource.net.tcp.aio.TcpConfig; +import com.fireflysource.net.websocket.client.WebSocketClientConnectionBuilder; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * The Firefly functions start from here. + * + * @author Pengtao Qiu + */ +public interface $ { + + /** + * Get the HTTP client. It is singleton. The client uses the common tcp channel group. + * + * @return The HTTP client. + */ + static HttpClient httpClient() { + return CommonTcpChannelGroup.INSTANCE.getHttpClient(); + } + + /** + * Create a websocket client connection builder. + * + * @param url The websocket url. + * @return The websocket client connection builder. + */ + static WebSocketClientConnectionBuilder websocket(String url) { + return httpClient().websocket(url); + } + + /** + * Create a websocket client connection builder. + * + * @return The websocket client connection builder. + */ + static WebSocketClientConnectionBuilder websocket() { + return httpClient().websocket(); + } + + /** + * Create the HTTP client. The client uses the common tcp channel group. + * + * @param httpConfig The HTTP config. + * @return The HTTP client. + */ + static HttpClient httpClient(HttpConfig httpConfig) { + return CommonTcpChannelGroup.INSTANCE.createHttpClient(httpConfig); + } + + /** + * Create a new HTTP client. + * + * @return The HTTP client. + */ + static HttpClient createHttpClient() { + return HttpClientFactory.create(); + } + + /** + * Create a new HTTP client. + * + * @param httpConfig The HTTP config. + * @return The HTTP client. + */ + static HttpClient createHttpClient(HttpConfig httpConfig) { + return HttpClientFactory.create(httpConfig); + } + + /** + * Create the HTTP server. It uses the common tcp channel group. + * + * @return The HTTP server. + */ + static HttpServer httpServer() { + return CommonTcpChannelGroup.INSTANCE.createHttpServer(); + } + + /** + * Create the HTTP server. It uses the common tcp channel group. + * + * @param httpConfig The HTTP config. + * @return The HTTP server. + */ + static HttpServer httpServer(HttpConfig httpConfig) { + return CommonTcpChannelGroup.INSTANCE.createHttpServer(httpConfig); + } + + /** + * Create a new HTTP server. + * + * @return The HTTP server. + */ + static HttpServer createHttpServer() { + return HttpServerFactory.create(); + } + + /** + * Create a new HTTP server. + * + * @param httpConfig The HTTP config. + * @return The HTTP server. + */ + static HttpServer createHttpServer(HttpConfig httpConfig) { + return HttpServerFactory.create(httpConfig); + } + + /** + * Create a new HTTP proxy. + * + * @return The HTTP proxy. + */ + static HttpProxy createHttpProxy() { + return HttpServerFactory.createHttpProxy(); + } + + /** + * Create a new HTTP proxy. + * + * @param httpConfig The HTTP config. + * @return The HTTP proxy. + */ + static HttpProxy createHttpProxy(HttpConfig httpConfig) { + return HttpServerFactory.createHttpProxy(httpConfig); + } + + /** + * Create the TCP client. It uses the common tcp channel group. + * + * @return The TCP client. + */ + static TcpClient tcpClient() { + return CommonTcpChannelGroup.INSTANCE.createTcpClient(); + } + + /** + * Create the TCP client. It uses the common tcp channel group. + * + * @param tcpConfig The TCP config. + * @return The TCP client. + */ + static TcpClient tcpClient(TcpConfig tcpConfig) { + return CommonTcpChannelGroup.INSTANCE.createTcpClient(tcpConfig); + } + + /** + * Create a new TCP client. + * + * @return The TCP client. + */ + static TcpClient createTcpClient() { + return TcpClientFactory.create(); + } + + /** + * Create a new TCP client. + * + * @param tcpConfig The TCP config. + * @return The TCP client. + */ + static TcpClient createTcpClient(TcpConfig tcpConfig) { + return TcpClientFactory.create(tcpConfig); + } + + /** + * Create the TCP server. It uses the common tcp channel group. + * + * @return The TCP server. + */ + static TcpServer tcpServer() { + return CommonTcpChannelGroup.INSTANCE.createTcpServer(); + } + + /** + * Create the TCP server. It uses the common tcp channel group. + * + * @param tcpConfig The TCP config. + * @return The TCP server. + */ + static TcpServer tcpServer(TcpConfig tcpConfig) { + return CommonTcpChannelGroup.INSTANCE.createTcpServer(tcpConfig); + } + + /** + * Create a new TCP server. + * + * @return The TCP server. + */ + static TcpServer createTcpServer() { + return TcpServerFactory.create(); + } + + /** + * Create a new TCP server. + * + * @param tcpConfig The TCP config. + * @return The TCP server. + */ + static TcpServer createTcpServer(TcpConfig tcpConfig) { + return TcpServerFactory.create(tcpConfig); + } + + /** + * The logger functions. + */ + interface logger { + /** + * Create a lazy logger. + * + * @param name The logger name. + * @return The lazy logger. + */ + static LazyLogger create(String name) { + return LazyLogger.create(name); + } + + /** + * Create a lazy logger. + * + * @param clazz The class name as the logger name. + * @return The lazy logger. + */ + static LazyLogger create(Class clazz) { + return LazyLogger.create(clazz); + } + } + + /** + * The future functions. + */ + interface future { + + /** + * Done future. + * + * @return The done future. + */ + static CompletableFuture done() { + return Result.DONE; + } + + /** + * Done future. + * + * @param future The future. + */ + static void done(CompletableFuture future) { + Result.done(future); + } + + /** + * Create a failed future. + * + * @param t The exception. + * @param The future item type. + * @return The future. + */ + static CompletableFuture failedFuture(Throwable t) { + return CompletableFutures.failedFuture(t); + } + + /** + * Retry the async operation. + * + * @param retryCount The max retry times. + * @param supplier The async operation function. + * @param prepareRetry The callback before retries async operation. + * @param The future result type. + * @return The operation result future. + */ + static CompletableFuture retry(int retryCount, Supplier> supplier, BiConsumer prepareRetry) { + return CompletableFutures.retry(retryCount, supplier, prepareRetry); + } + + } + + /** + * The consumer functions. + */ + interface consumer { + + /** + * Discard the result. + * + * @param The result type. + * @return The consumer that discards the result. + */ + static Consumer> discard() { + return Result.discard(); + } + + /** + * Convert future to the result consumer. + * + * @param future The future. + * @param The result type. + * @return The result consumer. + */ + static Consumer> futureToConsumer(CompletableFuture future) { + return Result.futureToConsumer(future); + } + + /** + * The empty consumer. + * + * @param The consumer item type. + * @return The empty consumer. + */ + static Consumer emptyConsumer() { + return Result.emptyConsumer(); + } + + /** + * Create the failed result. + * + * @param t The exception. + * @return The failed result. + */ + static Result createFailedResult(Throwable t) { + return Result.createFailedResult(t); + } + + /** + * The success result. + * + * @return The success result. + */ + static Result success() { + return Result.SUCCESS; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/doc/FeignedNetDoc.java b/firefly-net/src/main/java/com/fireflysource/doc/FeignedNetDoc.java new file mode 100644 index 000000000..dfa811962 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/doc/FeignedNetDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedNetDoc { +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/AbstractConnection.java b/firefly-net/src/main/java/com/fireflysource/net/AbstractConnection.java new file mode 100644 index 000000000..f73d39244 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/AbstractConnection.java @@ -0,0 +1,89 @@ +package com.fireflysource.net; + +abstract public class AbstractConnection implements Connection { + + protected final int id; + protected final long openTime; + protected final long maxIdleTime; + protected volatile Object attachment; + protected long closeTime; + protected long lastReadTime; + protected long lastWrittenTime; + protected long readBytes; + protected long writtenBytes; + + public AbstractConnection(int id, long openTime, long maxIdleTime) { + this.id = id; + this.openTime = openTime; + this.maxIdleTime = maxIdleTime; + } + + @Override + public Object getAttachment() { + return attachment; + } + + @Override + public void setAttachment(Object attachment) { + this.attachment = attachment; + } + + @Override + public int getId() { + return id; + } + + @Override + public long getOpenTime() { + return openTime; + } + + @Override + public long getCloseTime() { + return closeTime; + } + + @Override + public long getLastReadTime() { + return lastReadTime; + } + + @Override + public long getLastWrittenTime() { + return lastWrittenTime; + } + + @Override + public long getReadBytes() { + return readBytes; + } + + @Override + public long getWrittenBytes() { + return writtenBytes; + } + + @Override + public long getLastActiveTime() { + return Math.max(lastReadTime, lastWrittenTime); + } + + @Override + public long getIdleTime() { + return System.currentTimeMillis() - getLastActiveTime(); + } + + @Override + public long getMaxIdleTime() { + return maxIdleTime; + } + + @Override + public long getDuration() { + if (isClosed()) { + return closeTime - openTime; + } else { + return System.currentTimeMillis() - openTime; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/Connection.java b/firefly-net/src/main/java/com/fireflysource/net/Connection.java new file mode 100644 index 000000000..3c4086eaf --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/Connection.java @@ -0,0 +1,50 @@ +package com.fireflysource.net; + +import com.fireflysource.common.io.AsyncCloseable; + +import java.net.InetSocketAddress; + +/** + * @author Pengtao Qiu + */ +public interface Connection extends AsyncCloseable { + + Object getAttachment(); + + void setAttachment(Object object); + + int getId(); + + long getOpenTime(); + + long getCloseTime(); + + long getDuration(); + + long getLastReadTime(); + + long getLastWrittenTime(); + + long getLastActiveTime(); + + long getReadBytes(); + + long getWrittenBytes(); + + long getIdleTime(); + + long getMaxIdleTime(); + + boolean isClosed(); + + InetSocketAddress getLocalAddress(); + + InetSocketAddress getRemoteAddress(); + + boolean isInvalid(); + + void setWriteTimeout(long timeout); + + void setReadTimeout(long timeout); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClient.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClient.java new file mode 100644 index 000000000..97f8e6db3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClient.java @@ -0,0 +1,59 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.common.lifecycle.LifeCycle; +import com.fireflysource.net.http.common.model.HttpURI; +import com.fireflysource.net.websocket.client.WebSocketClientConnectionBuilder; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @author Pengtao Qiu + */ +public interface HttpClient extends HttpClientRequestBuilderFactory, LifeCycle { + + /** + * Create a new HTTP client connection. + * + * @param httpURI The HTTP URI. + * @param supportedProtocols The supported application protocols. + * @return The new HTTP client connection. + */ + CompletableFuture createHttpClientConnection(HttpURI httpURI, List supportedProtocols); + + /** + * Create a new HTTP client connection. + * + * @param httpURI The HTTP URI. + * @return The new HTTP client connection. + */ + default CompletableFuture createHttpClientConnection(HttpURI httpURI) { + return createHttpClientConnection(httpURI, Collections.emptyList()); + } + + /** + * Create a new HTTP client connection. + * + * @param uri The HTTP URI. + * @return The new HTTP client connection. + */ + default CompletableFuture createHttpClientConnection(String uri) { + return createHttpClientConnection(new HttpURI(uri)); + } + + /** + * Create a websocket connection builder. + * + * @return The websocket connection builder. + */ + WebSocketClientConnectionBuilder websocket(); + + /** + * Create a websocket connection builder. + * + * @param url The websocket url. + * @return The websocket connection builder. + */ + WebSocketClientConnectionBuilder websocket(String url); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientConnection.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientConnection.java new file mode 100644 index 000000000..a72f1e118 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientConnection.java @@ -0,0 +1,25 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.HttpConnection; + +import java.util.concurrent.CompletableFuture; + +/** + * The HTTP connection manager creates the HTTP client connection. + * If the TLS is enabled, the connection uses the ALPN to decide the HTTP version, + * or else the connection uses the HTTP Upgrade mechanism to decide the HTTP version. + * The HTTP2 is the default preferred protocol. + * + * @author Pengtao Qiu + */ +public interface HttpClientConnection extends HttpConnection, HttpClientRequestBuilderFactory { + + /** + * Send HTTP request to the remote endpoint. + * + * @param request The HTTP request. + * @return The response future. + */ + CompletableFuture send(HttpClientRequest request); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientConnectionManager.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientConnectionManager.java new file mode 100644 index 000000000..536ec35d6 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientConnectionManager.java @@ -0,0 +1,46 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.common.lifecycle.LifeCycle; +import com.fireflysource.net.http.common.model.HttpURI; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * The HTTP client connection manager maintains the HTTP client connections. + * If it creates connection and negotiates the HTTP version is 2.0, and it maintains only one connection to send the request and receive the response. + * If the HTTP version is 1.1, it maintains the HTTP connections in a pool. + * + * @author Pengtao Qiu + */ +public interface HttpClientConnectionManager extends LifeCycle { + + /** + * Send HTTP request to the server using the connection pool. + * + * @param request The HTTP request. + * @return The HTTP response. + */ + CompletableFuture send(HttpClientRequest request); + + /** + * Create a new HTTP client connection. + * + * @param httpURI The server URI. + * @return The new HTTP client connection. + */ + default CompletableFuture createHttpClientConnection(HttpURI httpURI) { + return createHttpClientConnection(httpURI, Collections.emptyList()); + } + + /** + * Create a new HTTP client connection. + * + * @param httpURI The HTTP URI. + * @param supportedProtocols The supported application protocols. + * @return The new HTTP client connection. + */ + CompletableFuture createHttpClientConnection(HttpURI httpURI, List supportedProtocols); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentHandler.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentHandler.java new file mode 100644 index 000000000..493d0e0e0 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentHandler.java @@ -0,0 +1,9 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.content.handler.HttpContentHandler; + +/** + * @author Pengtao Qiu + */ +public interface HttpClientContentHandler extends HttpContentHandler { +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentHandlerFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentHandlerFactory.java new file mode 100644 index 000000000..8e28bcf1a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentHandlerFactory.java @@ -0,0 +1,31 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.client.impl.content.handler.ByteBufferContentHandler; +import com.fireflysource.net.http.client.impl.content.handler.FileContentHandler; +import com.fireflysource.net.http.client.impl.content.handler.StringContentHandler; + +import java.nio.file.OpenOption; +import java.nio.file.Path; + +abstract public class HttpClientContentHandlerFactory { + + public static HttpClientContentHandler bytesHandler() { + return new ByteBufferContentHandler(); + } + + public static HttpClientContentHandler bytesHandler(long maxRequestBodySize) { + return new ByteBufferContentHandler(maxRequestBodySize); + } + + public static HttpClientContentHandler stringHandler() { + return new StringContentHandler(); + } + + public static HttpClientContentHandler stringHandler(long maxRequestBodySize) { + return new StringContentHandler(maxRequestBodySize); + } + + public static HttpClientContentHandler fileHandler(Path path, OpenOption... openOptions) { + return new FileContentHandler(path, openOptions); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentProvider.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentProvider.java new file mode 100644 index 000000000..05d798ccc --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentProvider.java @@ -0,0 +1,26 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.content.provider.HttpContentProvider; + +import java.nio.ByteBuffer; + +/** + * @author Pengtao Qiu + */ +public interface HttpClientContentProvider extends HttpContentProvider { + + /** + * The content length. If the length is -1, the content is the data stream. + * + * @return The content length. + */ + long length(); + + /** + * Convert fixed length content to a ByteBuffer. If the content is the data stream, return an empty ByteBuffer. + * + * @return The ByteBuffer. + */ + ByteBuffer toByteBuffer(); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentProviderFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentProviderFactory.java new file mode 100644 index 000000000..be5bd74db --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientContentProviderFactory.java @@ -0,0 +1,54 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.client.impl.content.provider.ByteBufferContentProvider; +import com.fireflysource.net.http.client.impl.content.provider.FileContentProvider; +import com.fireflysource.net.http.client.impl.content.provider.StringContentProvider; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; + +abstract public class HttpClientContentProviderFactory { + + public static HttpClientContentProvider bytesBody(ByteBuffer buffer) { + return new ByteBufferContentProvider(buffer); + } + + public static HttpClientContentProvider stringBody(String string) { + return new StringContentProvider(string, StandardCharsets.UTF_8); + } + + public static HttpClientContentProvider stringBody(String string, Charset charset) { + return new StringContentProvider(string, charset); + } + + public static HttpClientContentProvider fileBody(Path path, OpenOption... openOptions) { + return new FileContentProvider(path, openOptions); + } + + public static HttpClientContentProvider fileBody(Path path, Set openOptions, long position, long length) { + return new FileContentProvider(path, openOptions, position, length); + } + + public static HttpClientContentProvider resourceFileBody(String resourcePath, OpenOption... openOptions) { + URL resource = FileContentProvider.class.getClassLoader().getResource(resourcePath); + try { + if (resource != null) { + URI uri = resource.toURI(); + return fileBody(Paths.get(uri), openOptions); + } else { + return null; + } + } catch (URISyntaxException e) { + return null; + } + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientFactory.java new file mode 100644 index 000000000..71d94379d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientFactory.java @@ -0,0 +1,15 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.client.impl.AsyncHttpClient; +import com.fireflysource.net.http.common.HttpConfig; + +abstract public class HttpClientFactory { + + public static HttpClient create(HttpConfig config) { + return new AsyncHttpClient(config); + } + + public static HttpClient create() { + return new AsyncHttpClient(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequest.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequest.java new file mode 100644 index 000000000..1c6e65f8d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequest.java @@ -0,0 +1,190 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.codec.UrlEncoded; +import com.fireflysource.net.http.common.model.Cookie; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpURI; +import com.fireflysource.net.http.common.model.HttpVersion; + +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +/** + * The HTTP client request. + * + * @author Pengtao Qiu + */ +public interface HttpClientRequest { + + /** + * Get the HTTP method. + * + * @return The HTTP method. + */ + String getMethod(); + + /** + * Set the HTTP method. + * + * @param method The HTTP method. + */ + void setMethod(String method); + + /** + * Get the HTTP URI. + * + * @return The HTTP URI. + */ + HttpURI getURI(); + + /** + * Set the HTTP URI. + * + * @param uri The HTTP URI. + */ + void setURI(HttpURI uri); + + /** + * Get the HTTP version. + * + * @return The HTTP version. + */ + HttpVersion getHttpVersion(); + + /** + * Set the HTTP version. + * + * @param httpVersion The HTTP version. + */ + void setHttpVersion(HttpVersion httpVersion); + + /** + * Get URL query strings. + * + * @return The URL query strings. + */ + UrlEncoded getQueryStrings(); + + /** + * Set URL query strings. + * + * @param queryStrings The URL query strings. + */ + void setQueryStrings(UrlEncoded queryStrings); + + /** + * Get the web form inputs. + * + * @return The web form inputs. + */ + UrlEncoded getFormInputs(); + + /** + * Set the web form inputs + * + * @param formInputs The web form inputs. + */ + void setFormInputs(UrlEncoded formInputs); + + /** + * Get the HTTP header fields. + * + * @return The HTTP header fields. + */ + HttpFields getHttpFields(); + + /** + * Set the HTTP header fields. + * + * @param httpFields The HTTP header fields. + */ + void setHttpFields(HttpFields httpFields); + + /** + * Get the HTTP cookies. + * + * @return The HTTP cookies. + */ + List getCookies(); + + /** + * Set the HTTP cookies. + * + * @param cookies The HTTP cookies. + */ + void setCookies(List cookies); + + /** + * Get the HTTP trailers. + * + * @return The HTTP trailers. + */ + Supplier getTrailerSupplier(); + + /** + * Set the HTTP trailers. + * + * @param trailerSupplier The HTTP trailers. + */ + void setTrailerSupplier(Supplier trailerSupplier); + + /** + * Set the content provider. When you submit the request, the HTTP client will send the data that read from the content provider. + * + * @param contentProvider When you submit the request, the HTTP client will send the data that read from the content provider. + */ + void setContentProvider(HttpClientContentProvider contentProvider); + + /** + * Get the content provider. + * + * @return the content provider. + */ + HttpClientContentProvider getContentProvider(); + + /** + * Set the HTTP content receiving handler. + * + * @param contentHandler The HTTP content receiving handler. When the HTTP client receives the HTTP body data, + * it will execute this action. It be executed many times. + */ + void setContentHandler(HttpClientContentHandler contentHandler); + + /** + * Get the HTTP content receiving callback. + * + * @return The HTTP content receiving callback. When the HTTP client receives the HTTP body data, + * it will execute this action. It will be executed many times. + */ + HttpClientContentHandler getContentHandler(); + + /** + * Set the HTTP2 settings. + * + * @param settings The HTTP2 settings. + */ + void setHttp2Settings(Map settings); + + /** + * Get the HTTP2 settings. + * + * @return The HTTP2 settings. + */ + Map getHttp2Settings(); + + /** + * Set the HTTP header response complete callback. + * + * @param headerComplete The HTTP header response complete callback. + */ + void setHeaderComplete(BiConsumer headerComplete); + + /** + * Get the HTTP header response complete callback. + * + * @return The HTTP header response complete callback. + */ + BiConsumer getHeaderComplete(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequestBuilder.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequestBuilder.java new file mode 100644 index 000000000..ab6602c05 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequestBuilder.java @@ -0,0 +1,295 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.model.Cookie; +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpHeader; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +/** + * The HTTP client request builder. + * + * @author Pengtao Qiu + */ +public interface HttpClientRequestBuilder { + /** + * Set the cookies. + * + * @param cookies The cookies. + * @return RequestBuilder + */ + HttpClientRequestBuilder cookies(List cookies); + + /** + * Put an HTTP field. It will replace the existed field. + * + * @param name The field name. + * @param list The field values. + * @return RequestBuilder + */ + HttpClientRequestBuilder put(String name, List list); + + /** + * Put an HTTP field. It will replace the existed field. + * + * @param header The field name. + * @param value The value. + * @return RequestBuilder + */ + HttpClientRequestBuilder put(HttpHeader header, String value); + + /** + * Put an HTTP field. It will replace the existed field. + * + * @param name The field name. + * @param value The value. + * @return RequestBuilder + */ + HttpClientRequestBuilder put(String name, String value); + + /** + * Put an HTTP field. It will replace the existed field. + * + * @param field The HTTP field. + * @return RequestBuilder + */ + HttpClientRequestBuilder put(HttpField field); + + /** + * Add some HTTP fields. + * + * @param fields The HTTP fields. + * @return RequestBuilder + */ + HttpClientRequestBuilder addAll(HttpFields fields); + + /** + * Add an HTTP field. + * + * @param field The HTTP field. + * @return RequestBuilder + */ + HttpClientRequestBuilder add(HttpField field); + + /** + * Add the comma separated values, but only if not already present. + * + * @param header The header to add the value(s) to. + * @param values The value(s). + * @return RequestBuilder + */ + HttpClientRequestBuilder addCsv(HttpHeader header, String... values); + + /** + * Add the comma separated values, but only if not already present. + * + * @param header The header to add the value(s) to. + * @param values The value(s). + * @return RequestBuilder + */ + HttpClientRequestBuilder addCsv(String header, String... values); + + /** + * Set the HTTP trailers. + * + * @param trailerSupplier The HTTP trailers. + * @return RequestBuilder + */ + HttpClientRequestBuilder trailerSupplier(Supplier trailerSupplier); + + /** + * Set the text HTTP body data. + * + * @param content The text HTTP body data. + * @return RequestBuilder + */ + HttpClientRequestBuilder body(String content); + + /** + * Set the text HTTP body data. + * + * @param content The text HTTP body data. + * @param charset THe charset of the text. + * @return RequestBuilder + */ + HttpClientRequestBuilder body(String content, Charset charset); + + /** + * Set the HTTP body data. When you submit the request, the data will be sent. + * + * @param buffer The HTTP body data. + * @return RequestBuilder + */ + HttpClientRequestBuilder body(ByteBuffer buffer); + + /** + * Set the content provider. When you submit the request, the HTTP client will send the data that read from the content provider. + * + * @param contentProvider When you submit the request, the HTTP client will send the data that read from the content provider. + * @return RequestBuilder + */ + HttpClientRequestBuilder contentProvider(HttpClientContentProvider contentProvider); + + /** + * Add a multi-part mime content. Such as a file. + * + * @param name The content name. + * @param content Content provider. + * @param fields Multi-part content header fields. + * @return RequestBuilder + */ + HttpClientRequestBuilder addPart(String name, HttpClientContentProvider content, HttpFields fields); + + /** + * Add a multi-part mime content. Such as a file. + * + * @param name The content name. + * @param fileName The content file name. + * @param content Content provider. + * @param fields Multi-part content header fields. + * @return RequestBuilder + */ + HttpClientRequestBuilder addFilePart(String name, String fileName, HttpClientContentProvider content, HttpFields fields); + + /** + * Add a web form input. + * + * @param name Input name. + * @param value Input value. + * @return RequestBuilder + */ + HttpClientRequestBuilder addFormInput(String name, String value); + + /** + * Add web form inputs. + * + * @param name Input name. + * @param values Input values. + * @return RequestBuilder + */ + HttpClientRequestBuilder addFormInputs(String name, List values); + + /** + * Put a web form input. + * + * @param name Input name. + * @param value Input value. + * @return RequestBuilder + */ + HttpClientRequestBuilder putFormInput(String name, String value); + + /** + * Put a web form inputs. + * + * @param name Input name. + * @param values Input values. + * @return RequestBuilder + */ + HttpClientRequestBuilder putFormInputs(String name, List values); + + /** + * Remove web form input. + * + * @param name Input name. + * @return RequestBuilder + */ + HttpClientRequestBuilder removeFormInput(String name); + + /** + * Add a value in an existed URL query strings. + * + * @param name The parameter name. + * @param value The value. + * @return RequestBuilder + */ + HttpClientRequestBuilder addQueryString(String name, String value); + + /** + * Add some values in an existed URL query strings. + * + * @param name The parameter name. + * @param values The parameter values. + * @return RequestBuilder + */ + HttpClientRequestBuilder addQueryStrings(String name, List values); + + /** + * Put a value in the URL query strings. + * + * @param name The parameter name. + * @param value The value. + * @return RequestBuilder + */ + HttpClientRequestBuilder putQueryString(String name, String value); + + /** + * Put a value in the URL query strings. + * + * @param name The parameter name. + * @param values The parameter values. + * @return RequestBuilder + */ + HttpClientRequestBuilder putQueryStrings(String name, List values); + + /** + * Remove a parameter in the URL query strings. + * + * @param name The parameter name. + * @return RequestBuilder + */ + HttpClientRequestBuilder removeQueryString(String name); + + /** + * Set the HTTP content receiving callback. + * + * @param contentHandler The HTTP content receiving callback. When the HTTP client receives the HTTP body data, + * it will execute this action. This action will be executed many times. + * @return RequestBuilder + */ + HttpClientRequestBuilder contentHandler(HttpClientContentHandler contentHandler); + + /** + * Set the HTTP2 settings. + * + * @param http2Settings The HTTP2 settings. + * @return RequestBuilder + */ + HttpClientRequestBuilder http2Settings(Map http2Settings); + + /** + * Try to upgrade HTTP2 protocol via the Upgrade header (h2c). + * + * @return RequestBuilder + */ + HttpClientRequestBuilder upgradeHttp2(); + + /** + * Set the HTTP header response complete callback. + * + * @param headerComplete The HTTP header response complete callback. + * @return RequestBuilder + */ + HttpClientRequestBuilder onHeaderComplete(BiConsumer headerComplete); + + /** + * Submit the HTTP request to the server using the connection pool. + * The HTTP client manages HTTP connection automatically. + * + * @return The HTTP response. + */ + CompletableFuture submit(); + + /** + * Get the current HTTP client request. + * + * @return The current HTTP client request. + */ + HttpClientRequest getHttpClientRequest(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequestBuilderFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequestBuilderFactory.java new file mode 100644 index 000000000..6e9b1a991 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientRequestBuilderFactory.java @@ -0,0 +1,110 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.exception.URISyntaxRuntimeException; +import com.fireflysource.net.http.common.model.HttpMethod; +import com.fireflysource.net.http.common.model.HttpURI; + +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Create a new HTTP client request builder. + */ +public interface HttpClientRequestBuilderFactory { + + /** + * Create a RequestBuilder with GET method and URL. + * + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder get(String url) { + return request(HttpMethod.GET, url); + } + + /** + * Create a RequestBuilder with POST method and URL. + * + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder post(String url) { + return request(HttpMethod.POST, url); + } + + /** + * Create a RequestBuilder with HEAD method and URL. + * + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder head(String url) { + return request(HttpMethod.HEAD, url); + } + + /** + * Create a RequestBuilder with PUT method and URL. + * + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder put(String url) { + return request(HttpMethod.PUT, url); + } + + /** + * Create a RequestBuilder with DELETE method and URL. + * + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder delete(String url) { + return request(HttpMethod.DELETE, url); + } + + /** + * Create a RequestBuilder with HTTP method and URL. + * + * @param method The HTTP method. + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder request(HttpMethod method, String url) { + return request(method.getValue(), url); + } + + /** + * Create a RequestBuilder with HTTP method and URL. + * + * @param method The HTTP method. + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder request(String method, String url) { + return request(method, new HttpURI(url)); + } + + /** + * Create a RequestBuilder with HTTP method and URL. + * + * @param method The HTTP method. + * @param url The request URL. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + default HttpClientRequestBuilder request(String method, URL url) { + try { + return request(method, new HttpURI(url.toURI())); + } catch (URISyntaxException e) { + throw new URISyntaxRuntimeException("URI syntax error", e); + } + } + + /** + * Create a RequestBuilder with HTTP method and URL. + * + * @param method The HTTP method. + * @param httpURI The HTTP URI. + * @return A new RequestBuilder that helps you to build an HTTP request. + */ + HttpClientRequestBuilder request(String method, HttpURI httpURI); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientResponse.java b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientResponse.java new file mode 100644 index 000000000..b007a5aac --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/client/HttpClientResponse.java @@ -0,0 +1,101 @@ +package com.fireflysource.net.http.client; + +import com.fireflysource.net.http.common.model.Cookie; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpVersion; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.function.Supplier; + +/** + * @author Pengtao Qiu + */ +public interface HttpClientResponse { + + /** + * Get the HTTP response status code. + * + * @return The HTTP response status code. + */ + int getStatus(); + + /** + * Get the textual description associated with the numeric status code. + * + * @return The textual description associated with the numeric status code. + */ + String getReason(); + + /** + * Get the HTTP version of the current HTTP connection. + * + * @return The HTTP version of the current HTTP connection. + */ + HttpVersion getHttpVersion(); + + /** + * Get the HTTP header fields. + * + * @return The HTTP header fields. + */ + HttpFields getHttpFields(); + + /** + * Get the cookies. + * + * @return The cookies. + */ + List getCookies(); + + /** + * Get the content length. + * + * @return The content length. + */ + long getContentLength(); + + /** + * Get the HTTP trailer fields. + * + * @return The HTTP trailer fields. + */ + Supplier getTrailerSupplier(); + + /** + * Get the HTTP body and convert it to the UTF-8 string. + * + * @return The HTTP body string. + */ + String getStringBody(); + + /** + * Get the HTTP body and convert the specified charset string. + * + * @param charset The charset of the HTTP body string. + * @return The HTTP body string. + */ + String getStringBody(Charset charset); + + /** + * Get the HTTP body raw binary data. + * + * @return The HTTP body raw binary data. + */ + List getBody(); + + /** + * Set the HTTP body content handler. + * + * @param contentHandler The HTTP body content handler. + */ + void setContentHandler(HttpClientContentHandler contentHandler); + + /** + * Get the HTTP body content handler. + * + * @return The HTTP body content handler. + */ + HttpClientContentHandler getContentHandler(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/HttpConfig.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/HttpConfig.java new file mode 100644 index 000000000..ada039e76 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/HttpConfig.java @@ -0,0 +1,251 @@ +package com.fireflysource.net.http.common; + +import com.fireflysource.common.coroutine.CoroutineDispatchers; +import com.fireflysource.net.tcp.TcpChannelGroup; +import com.fireflysource.net.tcp.secure.SecureEngineFactory; + +public class HttpConfig implements Cloneable { + + public static int DEFAULT_WINDOW_SIZE = 65535; + public static int DEFAULT_HEADER_TABLE_SIZE = 4096; + + // TCP config + private TcpChannelGroup tcpChannelGroup; + private boolean stopTcpChannelGroup = true; + private long timeout = 30; + private SecureEngineFactory secureEngineFactory; + + // HTTP common config + private int headerBufferSize = 4 * 1024; + private int contentBufferSize = 16 * 1024; + private int maxDynamicTableSize = DEFAULT_HEADER_TABLE_SIZE; + private int maxHeaderSize = 32 * 1024; + private int maxHeaderBlockFragment = 0; + private int initialStreamRecvWindow = 8 * 1024 * 1024; + private int maxConcurrentStreams = -1; + private int initialSessionRecvWindow = 16 * 1024 * 1024; + private long streamIdleTimeout = 0; + + // HTTP client config + private int clientRetryCount = CoroutineDispatchers.INSTANCE.getDefaultPoolSize(); + private int connectionPoolSize = CoroutineDispatchers.INSTANCE.getDefaultPoolSize(); + private long checkConnectionLiveInterval = 15; + private boolean autoGeneratedClientHttp1Headers = true; + private long waitResponse100ContinueTimeout = 5; + private ProxyConfig proxyConfig; + + // HTTP server config + private long maxUploadFileSize = 200 * 1024 * 1024; + private long maxRequestBodySize = (4 * 1024) + (200 * 1024 * 1024); + private int uploadFileSizeThreshold = 4 * 1024 * 1024; + + // HTTP proxy config + private long httpProxyBodySizeThreshold = 10 * 1024 * 1024; + + + public TcpChannelGroup getTcpChannelGroup() { + return tcpChannelGroup; + } + + public void setTcpChannelGroup(TcpChannelGroup tcpChannelGroup) { + this.tcpChannelGroup = tcpChannelGroup; + } + + public boolean isStopTcpChannelGroup() { + return stopTcpChannelGroup; + } + + public void setStopTcpChannelGroup(boolean stopTcpChannelGroup) { + this.stopTcpChannelGroup = stopTcpChannelGroup; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public int getConnectionPoolSize() { + return connectionPoolSize; + } + + public void setConnectionPoolSize(int connectionPoolSize) { + this.connectionPoolSize = Math.max(connectionPoolSize, 1); + } + + public long getCheckConnectionLiveInterval() { + return checkConnectionLiveInterval; + } + + public void setCheckConnectionLiveInterval(long checkConnectionLiveInterval) { + this.checkConnectionLiveInterval = checkConnectionLiveInterval; + } + + public int getHeaderBufferSize() { + return headerBufferSize; + } + + public void setHeaderBufferSize(int headerBufferSize) { + this.headerBufferSize = headerBufferSize; + } + + public int getContentBufferSize() { + return contentBufferSize; + } + + public void setContentBufferSize(int contentBufferSize) { + this.contentBufferSize = contentBufferSize; + } + + public SecureEngineFactory getSecureEngineFactory() { + return secureEngineFactory; + } + + public void setSecureEngineFactory(SecureEngineFactory secureEngineFactory) { + this.secureEngineFactory = secureEngineFactory; + } + + public int getMaxDynamicTableSize() { + return maxDynamicTableSize; + } + + public void setMaxDynamicTableSize(int maxDynamicTableSize) { + this.maxDynamicTableSize = maxDynamicTableSize; + } + + public int getMaxHeaderSize() { + return maxHeaderSize; + } + + public void setMaxHeaderSize(int maxHeaderSize) { + this.maxHeaderSize = maxHeaderSize; + } + + public int getMaxHeaderBlockFragment() { + return maxHeaderBlockFragment; + } + + public void setMaxHeaderBlockFragment(int maxHeaderBlockFragment) { + this.maxHeaderBlockFragment = maxHeaderBlockFragment; + } + + public int getInitialStreamRecvWindow() { + return initialStreamRecvWindow; + } + + public void setInitialStreamRecvWindow(int initialStreamRecvWindow) { + this.initialStreamRecvWindow = initialStreamRecvWindow; + } + + public int getMaxConcurrentStreams() { + return maxConcurrentStreams; + } + + public void setMaxConcurrentStreams(int maxConcurrentStreams) { + this.maxConcurrentStreams = maxConcurrentStreams; + } + + public int getInitialSessionRecvWindow() { + return initialSessionRecvWindow; + } + + public void setInitialSessionRecvWindow(int initialSessionRecvWindow) { + this.initialSessionRecvWindow = initialSessionRecvWindow; + } + + public long getStreamIdleTimeout() { + return streamIdleTimeout; + } + + public void setStreamIdleTimeout(long streamIdleTimeout) { + this.streamIdleTimeout = streamIdleTimeout; + } + + public long getMaxUploadFileSize() { + return maxUploadFileSize; + } + + public void setMaxUploadFileSize(long maxUploadFileSize) { + this.maxUploadFileSize = maxUploadFileSize; + } + + public long getMaxRequestBodySize() { + return maxRequestBodySize; + } + + public void setMaxRequestBodySize(long maxRequestBodySize) { + this.maxRequestBodySize = maxRequestBodySize; + } + + public int getUploadFileSizeThreshold() { + return uploadFileSizeThreshold; + } + + public void setUploadFileSizeThreshold(int uploadFileSizeThreshold) { + this.uploadFileSizeThreshold = uploadFileSizeThreshold; + } + + public long getWaitResponse100ContinueTimeout() { + return waitResponse100ContinueTimeout; + } + + public void setWaitResponse100ContinueTimeout(long waitResponse100ContinueTimeout) { + this.waitResponse100ContinueTimeout = waitResponse100ContinueTimeout; + } + + public ProxyConfig getProxyConfig() { + return proxyConfig; + } + + public void setProxyConfig(ProxyConfig proxyConfig) { + this.proxyConfig = proxyConfig; + } + + public boolean isAutoGeneratedClientHttp1Headers() { + return autoGeneratedClientHttp1Headers; + } + + public void setAutoGeneratedClientHttp1Headers(boolean autoGeneratedClientHttp1Headers) { + this.autoGeneratedClientHttp1Headers = autoGeneratedClientHttp1Headers; + } + + public int getClientRetryCount() { + return clientRetryCount; + } + + public void setClientRetryCount(int clientRetryCount) { + this.clientRetryCount = clientRetryCount; + } + + public long getHttpProxyBodySizeThreshold() { + return httpProxyBodySizeThreshold; + } + + public void setHttpProxyBodySizeThreshold(long httpProxyBodySizeThreshold) { + this.httpProxyBodySizeThreshold = httpProxyBodySizeThreshold; + } + + @Override + public String toString() { + return "{" + + "timeout=" + timeout + + ", clientRetryCount=" + clientRetryCount + + ", connectionPoolSize=" + connectionPoolSize + + ", checkConnectionLiveInterval=" + checkConnectionLiveInterval + + ", maxUploadFileSize=" + maxUploadFileSize + + ", maxRequestBodySize=" + maxRequestBodySize + + ", uploadFileSizeThreshold=" + uploadFileSizeThreshold + + '}'; + } + + @Override + public HttpConfig clone() throws CloneNotSupportedException { + HttpConfig config = (HttpConfig) super.clone(); + if (this.proxyConfig != null) { + config.proxyConfig = (ProxyConfig) this.proxyConfig.clone(); + } + return config; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/HttpConnection.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/HttpConnection.java new file mode 100644 index 000000000..afcb263b1 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/HttpConnection.java @@ -0,0 +1,27 @@ +package com.fireflysource.net.http.common; + +import com.fireflysource.net.Connection; +import com.fireflysource.net.http.common.model.HttpVersion; +import com.fireflysource.net.tcp.TcpCoroutineDispatcher; + +/** + * The HTTP connection. + * + * @author Pengtao Qiu + */ +public interface HttpConnection extends Connection, TcpCoroutineDispatcher { + + /** + * Get the HTTP version. + * + * @return The HTTP version. + */ + HttpVersion getHttpVersion(); + + /** + * If you enable the TLS protocol, it returns true. + * + * @return If you enable the TLS protocol, it returns true. + */ + boolean isSecureConnection(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/ProxyAuthentication.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/ProxyAuthentication.java new file mode 100644 index 000000000..b6dc4ae64 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/ProxyAuthentication.java @@ -0,0 +1,36 @@ +package com.fireflysource.net.http.common; + +public class ProxyAuthentication implements Cloneable { + + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "ProxyAuthentication{" + + "username=******'" + '\'' + + ", password='******" + '\'' + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/ProxyConfig.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/ProxyConfig.java new file mode 100644 index 000000000..985daf0b2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/ProxyConfig.java @@ -0,0 +1,60 @@ +package com.fireflysource.net.http.common; + +public class ProxyConfig implements Cloneable { + + private String protocol; + private String host; + private int port; + private ProxyAuthentication proxyAuthentication; + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public ProxyAuthentication getProxyAuthentication() { + return proxyAuthentication; + } + + public void setProxyAuthentication(ProxyAuthentication proxyAuthentication) { + this.proxyAuthentication = proxyAuthentication; + } + + @Override + public String toString() { + return "ProxyConfig{" + + "protocol='" + protocol + '\'' + + ", host='" + host + '\'' + + ", port=" + port + + ", proxyAuthentication=" + proxyAuthentication + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + ProxyConfig proxyConfig = (ProxyConfig) super.clone(); + if (this.proxyAuthentication != null) { + proxyConfig.proxyAuthentication = (ProxyAuthentication) this.proxyAuthentication.clone(); + } + return proxyConfig; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/TcpBasedHttpConnection.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/TcpBasedHttpConnection.java new file mode 100644 index 000000000..9af60e8ee --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/TcpBasedHttpConnection.java @@ -0,0 +1,18 @@ +package com.fireflysource.net.http.common; + +import com.fireflysource.net.tcp.TcpConnection; + +/** + * The TCP based HTTP connection. + * + * @author Pengtao Qiu + */ +public interface TcpBasedHttpConnection extends HttpConnection { + + /** + * Get the TCP connection. + * + * @return The TCP connection. + */ + TcpConnection getTcpConnection(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/ContentEncoded.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/ContentEncoded.java new file mode 100644 index 000000000..e3b79dd2c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/ContentEncoded.java @@ -0,0 +1,68 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.net.http.common.exception.NotSupportContentEncoding; +import com.fireflysource.net.http.common.model.ContentEncoding; + +import java.io.*; +import java.util.zip.*; + +abstract public class ContentEncoded { + + public static byte[] decode(byte[] content, ContentEncoding contentEncoding) throws IOException { + return decode(content, contentEncoding, 512); + } + + public static byte[] decode(byte[] content, ContentEncoding contentEncoding, int bufferSize) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[bufferSize]; + try (ByteArrayInputStream in = new ByteArrayInputStream(content); + InputStream decodingInputStream = createDecodingInputStream(in, contentEncoding, bufferSize) + ) { + while (true) { + int len = decodingInputStream.read(buffer); + if (len < 0) break; + + if (len > 0) { + out.write(buffer, 0, len); + } + } + } + return out.toByteArray(); + } + + public static byte[] encode(byte[] content, ContentEncoding contentEncoding) throws IOException { + return encode(content, contentEncoding, 512); + } + + public static byte[] encode(byte[] content, ContentEncoding contentEncoding, int bufferSize) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (OutputStream encodingOutputStream = createEncodingOutputStream(out, contentEncoding, bufferSize)) { + encodingOutputStream.write(content); + } + return out.toByteArray(); + } + + public static InputStream createDecodingInputStream(InputStream in, ContentEncoding contentEncoding, int bufferSize) throws IOException { + switch (contentEncoding) { + case GZIP: + return new GZIPInputStream(in, bufferSize); + case DEFLATE: + return new InflaterInputStream(in, new Inflater(), bufferSize); + default: + throw new NotSupportContentEncoding("Not support the content encoding"); + } + } + + public static OutputStream createEncodingOutputStream(OutputStream out, ContentEncoding contentEncoding, int bufferSize) throws IOException { + switch (contentEncoding) { + case GZIP: + return new GZIPOutputStream(out, bufferSize); + case DEFLATE: + return new DeflaterOutputStream(out, new Deflater(), bufferSize); + default: + throw new NotSupportContentEncoding("Not support the content encoding"); + } + } + + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/CookieGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/CookieGenerator.java new file mode 100644 index 000000000..e82bd4a12 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/CookieGenerator.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.http.common.model.Cookie; + +import java.util.List; + +abstract public class CookieGenerator { + + public static String generateCookies(List cookies) { + if (cookies == null) { + throw new IllegalArgumentException("the cookie list is null"); + } + + if (cookies.size() == 1) { + return generateCookie(cookies.get(0)); + } else if (cookies.size() > 1) { + StringBuilder str = new StringBuilder(); + + str.append(generateCookie(cookies.get(0))); + for (int i = 1; i < cookies.size(); i++) { + str.append(';').append(generateCookie(cookies.get(i))); + } + + return str.toString(); + } else { + throw new IllegalArgumentException("the cookie list size is 0"); + } + } + + public static String generateCookie(Cookie cookie) { + if (cookie == null) { + throw new IllegalArgumentException("the cookie is null"); + } else { + return cookie.getName() + '=' + cookie.getValue(); + } + } + + public static String generateSetCookie(Cookie cookie) { + if (cookie == null) { + throw new IllegalArgumentException("the cookie is null"); + } else { + StringBuilder str = new StringBuilder(); + + str.append(cookie.getName()).append('=').append(cookie.getValue()); + + if (StringUtils.hasText(cookie.getComment())) { + str.append(";Comment=").append(cookie.getComment()); + } + + if (StringUtils.hasText(cookie.getDomain())) { + str.append(";Domain=").append(cookie.getDomain()); + } + if (cookie.getMaxAge() >= 0) { + str.append(";Max-Age=").append(cookie.getMaxAge()); + } + + String path = !StringUtils.hasText(cookie.getPath()) ? "/" : cookie.getPath(); + str.append(";Path=").append(path); + + if (cookie.getSecure()) { + str.append(";Secure"); + } + + str.append(";Version=").append(cookie.getVersion()); + + return str.toString(); + } + } + + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/CookieParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/CookieParser.java new file mode 100644 index 000000000..4d7653af2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/CookieParser.java @@ -0,0 +1,75 @@ +package com.fireflysource.net.http.common.codec; + + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.http.common.model.Cookie; + +import java.util.ArrayList; +import java.util.List; + +abstract public class CookieParser { + + public static void parseCookies(String cookieStr, CookieParserCallback callback) { + if (!StringUtils.hasText(cookieStr)) { + throw new IllegalArgumentException("the cookie string is empty"); + } else { + String[] cookieKeyValues = StringUtils.split(cookieStr, ';'); + for (String cookieKeyValue : cookieKeyValues) { + String[] kv = StringUtils.split(cookieKeyValue, "=", 2); + if (kv != null) { + if (kv.length == 2) { + callback.cookie(kv[0].trim(), kv[1].trim()); + } else if (kv.length == 1) { + callback.cookie(kv[0].trim(), ""); + } else { + throw new IllegalStateException("the cookie string format error"); + } + } else { + throw new IllegalStateException("the cookie string format error"); + } + } + } + } + + public static Cookie parseSetCookie(String cookieStr) { + final Cookie cookie = new Cookie(); + parseCookies(cookieStr, (name, value) -> { + switch (name.toLowerCase()) { + case "comment": + cookie.setComment(value); + break; + case "domain": + cookie.setDomain(value); + break; + case "max-age": + cookie.setMaxAge(Integer.parseInt(value)); + break; + case "path": + cookie.setPath(value); + break; + case "secure": + cookie.setSecure(true); + break; + case "version": + cookie.setVersion(Integer.parseInt(value)); + break; + default: + cookie.setName(name); + cookie.setValue(value); + break; + } + }); + return cookie; + } + + public static List parseCookie(String cookieStr) { + final List list = new ArrayList<>(); + parseCookies(cookieStr, (name, value) -> list.add(new Cookie(name, value))); + return list; + } + + public interface CookieParserCallback { + void cookie(String name, String value); + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/DateGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/DateGenerator.java new file mode 100644 index 000000000..cfd42f235 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/DateGenerator.java @@ -0,0 +1,141 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.string.StringUtils; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * ThreadLocal Date formatters for HTTP style dates. + */ +public class DateGenerator { + static final String[] DAYS = {"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + static final String[] MONTHS = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "Jan"}; + private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT"); + private static final ThreadLocal DATE_GENERATOR = ThreadLocal.withInitial(DateGenerator::new); + public final static String JAN_01_1970 = DateGenerator.formatDate(0); + + static { + GMT_TIME_ZONE.setID("GMT"); + } + + private final StringBuilder buf = new StringBuilder(32); + private final GregorianCalendar gc = new GregorianCalendar(GMT_TIME_ZONE); + + /** + * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'" + * + * @param date the date in milliseconds + * @return the formatted date + */ + public static String formatDate(long date) { + return DATE_GENERATOR.get().doFormatDate(date); + } + + /** + * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies + * + * @param buf the buffer to put the formatted date into + * @param date the date in milliseconds + */ + public static void formatCookieDate(StringBuilder buf, long date) { + DATE_GENERATOR.get().doFormatCookieDate(buf, date); + } + + /** + * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies + * + * @param date the date in milliseconds + * @return the formatted date + */ + public static String formatCookieDate(long date) { + StringBuilder buf = new StringBuilder(28); + formatCookieDate(buf, date); + return buf.toString(); + } + + /** + * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'" + * + * @param date the date in milliseconds + * @return the formatted date + */ + public String doFormatDate(long date) { + buf.setLength(0); + gc.setTimeInMillis(date); + + int day_of_week = gc.get(Calendar.DAY_OF_WEEK); + int day_of_month = gc.get(Calendar.DAY_OF_MONTH); + int month = gc.get(Calendar.MONTH); + int year = gc.get(Calendar.YEAR); + int century = year / 100; + year = year % 100; + + int hours = gc.get(Calendar.HOUR_OF_DAY); + int minutes = gc.get(Calendar.MINUTE); + int seconds = gc.get(Calendar.SECOND); + + buf.append(DAYS[day_of_week]); + buf.append(','); + buf.append(' '); + StringUtils.append2digits(buf, day_of_month); + + buf.append(' '); + buf.append(MONTHS[month]); + buf.append(' '); + StringUtils.append2digits(buf, century); + StringUtils.append2digits(buf, year); + + buf.append(' '); + StringUtils.append2digits(buf, hours); + buf.append(':'); + StringUtils.append2digits(buf, minutes); + buf.append(':'); + StringUtils.append2digits(buf, seconds); + buf.append(" GMT"); + return buf.toString(); + } + + /** + * Format "EEE, dd-MMM-yy HH:mm:ss 'GMT'" for cookies + * + * @param buf the buffer to format the date into + * @param date the date in milliseconds + */ + public void doFormatCookieDate(StringBuilder buf, long date) { + gc.setTimeInMillis(date); + + int day_of_week = gc.get(Calendar.DAY_OF_WEEK); + int day_of_month = gc.get(Calendar.DAY_OF_MONTH); + int month = gc.get(Calendar.MONTH); + int year = gc.get(Calendar.YEAR); + year = year % 10000; + + int epoch = (int) ((date / 1000) % (60 * 60 * 24)); + int seconds = epoch % 60; + epoch = epoch / 60; + int minutes = epoch % 60; + int hours = epoch / 60; + + buf.append(DAYS[day_of_week]); + buf.append(','); + buf.append(' '); + StringUtils.append2digits(buf, day_of_month); + + buf.append('-'); + buf.append(MONTHS[month]); + buf.append('-'); + StringUtils.append2digits(buf, year / 100); + StringUtils.append2digits(buf, year % 100); + + buf.append(' '); + StringUtils.append2digits(buf, hours); + buf.append(':'); + StringUtils.append2digits(buf, minutes); + buf.append(':'); + StringUtils.append2digits(buf, seconds); + buf.append(" GMT"); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/DateParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/DateParser.java new file mode 100644 index 000000000..75e439c6b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/DateParser.java @@ -0,0 +1,62 @@ +package com.fireflysource.net.http.common.codec; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * ThreadLocal data parsers for HTTP style dates + */ +public class DateParser { + final static String[] DATE_RECEIVE_FMT = { + "EEE, dd MMM yyyy HH:mm:ss zzz", "EEE, dd-MMM-yy HH:mm:ss", + "EEE MMM dd HH:mm:ss yyyy", + + "EEE, dd MMM yyyy HH:mm:ss", "EEE dd MMM yyyy HH:mm:ss zzz", "EEE dd MMM yyyy HH:mm:ss", + "EEE MMM dd yyyy HH:mm:ss zzz", "EEE MMM dd yyyy HH:mm:ss", "EEE MMM-dd-yyyy HH:mm:ss zzz", + "EEE MMM-dd-yyyy HH:mm:ss", "dd MMM yyyy HH:mm:ss zzz", "dd MMM yyyy HH:mm:ss", "dd-MMM-yy HH:mm:ss zzz", + "dd-MMM-yy HH:mm:ss", "MMM dd HH:mm:ss yyyy zzz", "MMM dd HH:mm:ss yyyy", "EEE MMM dd HH:mm:ss yyyy zzz", + "EEE, MMM dd HH:mm:ss yyyy zzz", "EEE, MMM dd HH:mm:ss yyyy", "EEE, dd-MMM-yy HH:mm:ss zzz", + "EEE dd-MMM-yy HH:mm:ss zzz", "EEE dd-MMM-yy HH:mm:ss"}; + private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT"); + private static final ThreadLocal DATE_PARSER = ThreadLocal.withInitial(DateParser::new); + + static { + GMT_TIME_ZONE.setID("GMT"); + } + + final SimpleDateFormat[] DATE_RECEIVE = new SimpleDateFormat[DATE_RECEIVE_FMT.length]; + + public static long parseDate(String date) { + return DATE_PARSER.get().parse(date); + } + + private long parse(final String dateVal) { + for (int i = 0; i < DATE_RECEIVE.length; i++) { + if (DATE_RECEIVE[i] == null) { + DATE_RECEIVE[i] = new SimpleDateFormat(DATE_RECEIVE_FMT[i], Locale.US); + DATE_RECEIVE[i].setTimeZone(GMT_TIME_ZONE); + } + + try { + Date date = (Date) DATE_RECEIVE[i].parseObject(dateVal); + return date.getTime(); + } catch (Exception ignored) { + } + } + + if (dateVal.endsWith(" GMT")) { + final String val = dateVal.substring(0, dateVal.length() - 4); + + for (SimpleDateFormat element : DATE_RECEIVE) { + try { + Date date = (Date) element.parseObject(val); + return date.getTime(); + } catch (Exception ignored) { + } + } + } + return -1; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/Http1FieldPreEncoder.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/Http1FieldPreEncoder.java new file mode 100644 index 000000000..bc8416e77 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/Http1FieldPreEncoder.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpVersion; + +import java.util.Arrays; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class Http1FieldPreEncoder implements HttpFieldPreEncoder { + + @Override + public HttpVersion getHttpVersion() { + return HttpVersion.HTTP_1_0; + } + + @Override + public byte[] getEncodedField(HttpHeader header, String headerString, String value) { + if (header != null) { + int cbl = header.getBytesColonSpace().length; + byte[] bytes = Arrays.copyOf(header.getBytesColonSpace(), cbl + value.length() + 2); + System.arraycopy(value.getBytes(UTF_8), 0, bytes, cbl, value.length()); + bytes[bytes.length - 2] = (byte) '\r'; + bytes[bytes.length - 1] = (byte) '\n'; + return bytes; + } + + byte[] n = headerString.getBytes(UTF_8); + byte[] v = value.getBytes(UTF_8); + byte[] bytes = Arrays.copyOf(n, n.length + 2 + v.length + 2); + bytes[n.length] = (byte) ':'; + bytes[n.length] = (byte) ' '; + bytes[bytes.length - 2] = (byte) '\r'; + bytes[bytes.length - 1] = (byte) '\n'; + return bytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/HttpFieldPreEncoder.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/HttpFieldPreEncoder.java new file mode 100644 index 000000000..91d4b3d8f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/HttpFieldPreEncoder.java @@ -0,0 +1,21 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpVersion; + +/** + * Interface to pre-encode HttpFields. Used by {@link PreEncodedHttpField} + */ +public interface HttpFieldPreEncoder { + + /** + * The major version this encoder is for. Both HTTP/1.0 and HTTP/1.1 use the + * same field encoding, so the {@link HttpVersion#HTTP_1_0} should be return + * for all HTTP/1.x encodings. + * + * @return The major version this encoder is for. + */ + HttpVersion getHttpVersion(); + + byte[] getEncodedField(HttpHeader header, String headerString, String value); +} \ No newline at end of file diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/InclusiveByteRange.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/InclusiveByteRange.java new file mode 100644 index 000000000..bd8578617 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/InclusiveByteRange.java @@ -0,0 +1,224 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; + +import java.util.*; + +/** + * Byte range inclusive of end points. + * <PRE> + * <p> + * parses the following types of byte ranges: + * <p> + * bytes=100-499 + * bytes=-300 + * bytes=100- + * bytes=1-2,2-3,6-,-2 + * <p> + * given an entity length, converts range to string + * <p> + * bytes 100-499/500 + * + * </PRE> + * <p> + * Based on RFC2616 3.12, 14.16, 14.35.1, 14.35.2 + * <p> + * And yes the spec does strangely say that while 10-20, is bytes 10 to 20 and 10- is bytes 10 until the end that -20 IS NOT bytes 0-20, but the last 20 bytes of the content. + */ +public class InclusiveByteRange { + public static final LazyLogger LOG = SystemLogger.create(InclusiveByteRange.class); + + private long first; + private long last; + + public InclusiveByteRange(long first, long last) { + this.first = first; + this.last = last; + } + + public long getFirst() { + return first; + } + + public long getLast() { + return last; + } + + private void coalesce(InclusiveByteRange r) { + first = Math.min(first, r.first); + last = Math.max(last, r.last); + } + + private boolean overlaps(InclusiveByteRange range) { + return (range.first >= this.first && range.first <= this.last) || + (range.last >= this.first && range.last <= this.last) || + (range.first < this.first && range.last > this.last); + } + + public long getSize() { + return last - first + 1; + } + + public String toHeaderRangeString(long size) { + StringBuilder sb = new StringBuilder(40); + sb.append("bytes "); + sb.append(first); + sb.append('-'); + sb.append(last); + sb.append("/"); + sb.append(size); + return sb.toString(); + } + + @Override + public int hashCode() { + return (int) (first ^ last); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + + if (!(obj instanceof InclusiveByteRange)) + return false; + + return ((InclusiveByteRange) obj).first == this.first && + ((InclusiveByteRange) obj).last == this.last; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(60); + sb.append(first); + sb.append(":"); + sb.append(last); + return sb.toString(); + } + + public static List satisfiableRanges(List headers, long size) { + Iterator iterator = headers.iterator(); + Enumeration enumeration = new Enumeration() { + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public String nextElement() { + return iterator.next(); + } + }; + return satisfiableRanges(enumeration, size); + } + + /** + * @param headers Enumeration of Range header fields. + * @param size Size of the resource. + * @return List of satisfiable ranges + */ + public static List satisfiableRanges(Enumeration headers, long size) { + List ranges = null; + final long end = size - 1; + + // walk through all Range headers + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + StringTokenizer tok = new StringTokenizer(header, "=,", false); + String t = null; + try { + // read all byte ranges for this header + while (tok.hasMoreTokens()) { + try { + t = tok.nextToken().trim(); + if ("bytes".equals(t)) + continue; + + long first = -1; + long last = -1; + int dash = t.indexOf('-'); + if (dash < 0 || t.indexOf("-", dash + 1) >= 0) { + LOG.warn("Bad range format: {}", t); + break; + } + + if (dash > 0) + first = Long.parseLong(t.substring(0, dash).trim()); + if (dash < (t.length() - 1)) + last = Long.parseLong(t.substring(dash + 1).trim()); + + if (first == -1) { + if (last == -1) { + LOG.warn("Bad range format: {}", t); + break; + } + + if (last == 0) + continue; + + // This is a suffix range + first = Math.max(0, size - last); + last = end; + } else { + // Range starts after end + if (first >= size) + continue; + + if (last == -1) + last = end; + else if (last >= end) + last = end; + } + + if (last < first) { + LOG.warn("Bad range format: {}", t); + break; + } + + InclusiveByteRange range = new InclusiveByteRange(first, last); + if (ranges == null) + ranges = new ArrayList<>(); + + boolean coalesced = false; + for (Iterator i = ranges.listIterator(); i.hasNext(); ) { + InclusiveByteRange r = i.next(); + if (range.overlaps(r)) { + coalesced = true; + r.coalesce(range); + while (i.hasNext()) { + InclusiveByteRange r2 = i.next(); + + if (r2.overlaps(r)) { + r.coalesce(r2); + i.remove(); + } + } + } + } + + if (!coalesced) + ranges.add(range); + } catch (NumberFormatException e) { + LOG.warn("Bad range format: {}", t); + } + } + } catch (Exception e) { + LOG.warn("Bad range format: {}", t); + } + } + + return ranges; + } + + public static String to416HeaderRangeString(long size) { + StringBuilder sb = new StringBuilder(40); + sb.append("bytes */"); + sb.append(size); + return sb.toString(); + } +} + + + diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/MultiPartParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/MultiPartParser.java new file mode 100644 index 000000000..d9f85a89a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/MultiPartParser.java @@ -0,0 +1,631 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.SearchPattern; +import com.fireflysource.common.string.Utf8StringBuilder; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.exception.BadMessageException; +import com.fireflysource.net.http.common.model.HttpTokens; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; + +/** + * A parser for MultiPart content type. + * + * @see https://tools.ietf.org/html/rfc2046#section-5.1 + * @see https://tools.ietf.org/html/rfc2045 + */ +public class MultiPartParser { + public static final LazyLogger LOG = SystemLogger.create(MultiPartParser.class); + + // States + public enum FieldState { + FIELD, + IN_NAME, + AFTER_NAME, + VALUE, + IN_VALUE + } + + // States + public enum State { + PREAMBLE, + DELIMITER, + DELIMITER_PADDING, + DELIMITER_CLOSE, + BODY_PART, + FIRST_OCTETS, + OCTETS, + EPILOGUE, + END + } + + private static final EnumSet DELIMITER_STATES = EnumSet.of(State.DELIMITER, State.DELIMITER_CLOSE, State.DELIMITER_PADDING); + private static final int MAX_HEADER_LINE_LENGTH = 998; + + private final Handler handler; + private final SearchPattern delimiterSearch; + + private String fieldName; + private String fieldValue; + + private State state = State.PREAMBLE; + private FieldState fieldState = FieldState.FIELD; + private int partialBoundary = 2; // No CRLF if no preamble + private boolean cr; + private ByteBuffer patternBuffer; + + private final Utf8StringBuilder string = new Utf8StringBuilder(); + private int length; + + private int totalHeaderLineLength = -1; + + public MultiPartParser(Handler handler, String boundary) { + this.handler = handler; + + String delimiter = "\r\n--" + boundary; + patternBuffer = ByteBuffer.wrap(delimiter.getBytes(StandardCharsets.US_ASCII)); + delimiterSearch = SearchPattern.compile(patternBuffer.array()); + } + + public void reset() { + state = State.PREAMBLE; + fieldState = FieldState.FIELD; + partialBoundary = 2; // No CRLF if no preamble + } + + public Handler getHandler() { + return handler; + } + + public State getState() { + return state; + } + + public boolean isState(State state) { + return this.state == state; + } + + private static boolean hasNextByte(ByteBuffer buffer) { + return BufferUtils.hasContent(buffer); + } + + private HttpTokens.Token next(ByteBuffer buffer) { + byte ch = buffer.get(); + HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch]; + + switch (t.getType()) { + case CNTL: + throw new IllegalCharacterException(state, t, buffer); + + case LF: + cr = false; + break; + + case CR: + if (cr) + throw new BadMessageException("Bad EOL"); + + cr = true; + return null; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case HTAB: + case SPACE: + case OTEXT: + case COLON: + if (cr) + throw new BadMessageException("Bad EOL"); + break; + + default: + break; + } + + return t; + } + + private void setString(String s) { + string.reset(); + string.append(s); + length = s.length(); + } + + /* + * Mime Field strings are treated as UTF-8 as per https://tools.ietf.org/html/rfc7578#section-5.1 + */ + private String takeString() { + String s = string.toString(); + // trim trailing whitespace. + if (s.length() > length) + s = s.substring(0, length); + string.reset(); + length = -1; + return s; + } + + /** + * Parse until next Event. + * + * @param buffer the buffer to parse + * @param last whether this buffer contains last bit of content + * @return True if an RequestHandler method called and it returned true; + */ + public boolean parse(ByteBuffer buffer, boolean last) { + boolean handle = false; + while (!handle && BufferUtils.hasContent(buffer)) { + switch (state) { + case PREAMBLE: + parsePreamble(buffer); + continue; + + case DELIMITER: + case DELIMITER_PADDING: + case DELIMITER_CLOSE: + parseDelimiter(buffer); + continue; + + case BODY_PART: + handle = parseMimePartHeaders(buffer); + break; + + case FIRST_OCTETS: + case OCTETS: + handle = parseOctetContent(buffer); + break; + + case EPILOGUE: + BufferUtils.clear(buffer); + break; + + case END: + handle = true; + break; + + default: + throw new IllegalStateException(); + } + } + + if (last && BufferUtils.isEmpty(buffer)) { + if (state == State.EPILOGUE) { + state = State.END; + + if (LOG.isDebugEnabled()) + LOG.debug("messageComplete {}", this); + + return handler.messageComplete(); + } else { + if (LOG.isDebugEnabled()) + LOG.debug("earlyEOF {}", this); + + handler.earlyEOF(); + return true; + } + } + + return handle; + } + + private void parsePreamble(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parsePreamble({})", BufferUtils.toDetailString(buffer)); + + if (partialBoundary > 0) { + int partial = delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), partialBoundary); + if (partial > 0) { + if (partial == delimiterSearch.getLength()) { + buffer.position(buffer.position() + partial - partialBoundary); + partialBoundary = 0; + setState(State.DELIMITER); + return; + } + + partialBoundary = partial; + BufferUtils.clear(buffer); + return; + } + + partialBoundary = 0; + } + + int delimiter = delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + if (delimiter >= 0) { + buffer.position(delimiter - buffer.arrayOffset() + delimiterSearch.getLength()); + setState(State.DELIMITER); + return; + } + + partialBoundary = delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + BufferUtils.clear(buffer); + } + + private void parseDelimiter(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parseDelimiter({})", BufferUtils.toDetailString(buffer)); + + while (DELIMITER_STATES.contains(state) && hasNextByte(buffer)) { + HttpTokens.Token t = next(buffer); + if (t == null) + return; + + if (t.getType() == HttpTokens.Type.LF) { + setState(State.BODY_PART); + + if (LOG.isDebugEnabled()) + LOG.debug("startPart {}", this); + + handler.startPart(); + return; + } + + switch (state) { + case DELIMITER: + if (t.getChar() == '-') + setState(State.DELIMITER_CLOSE); + else + setState(State.DELIMITER_PADDING); + continue; + + case DELIMITER_CLOSE: + if (t.getChar() == '-') { + setState(State.EPILOGUE); + return; + } + setState(State.DELIMITER_PADDING); + continue; + + case DELIMITER_PADDING: + default: + } + } + } + + /* + * Parse the message headers and return true if the handler has signaled for a return + */ + protected boolean parseMimePartHeaders(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parseMimePartHeaders({})", BufferUtils.toDetailString(buffer)); + + // Process headers + while (state == State.BODY_PART && hasNextByte(buffer)) { + // process each character + HttpTokens.Token t = next(buffer); + if (t == null) + break; + + if (t.getType() != HttpTokens.Type.LF) + totalHeaderLineLength++; + + if (totalHeaderLineLength > MAX_HEADER_LINE_LENGTH) + throw new IllegalStateException("Header Line Exceeded Max Length"); + + switch (fieldState) { + case FIELD: + switch (t.getType()) { + case SPACE: + case HTAB: { + // Folded field value! + + if (fieldName == null) + throw new IllegalStateException("First field folded"); + + if (fieldValue == null) { + string.reset(); + length = 0; + } else { + setString(fieldValue); + string.append(' '); + length++; + fieldValue = null; + } + setState(FieldState.VALUE); + break; + } + + case LF: + handleField(); + setState(State.FIRST_OCTETS); + partialBoundary = 2; // CRLF is option for empty parts + + if (LOG.isDebugEnabled()) + LOG.debug("headerComplete {}", this); + + if (handler.headerComplete()) + return true; + break; + + case ALPHA: + case DIGIT: + case TCHAR: + // process previous header + handleField(); + + // New header + setState(FieldState.IN_NAME); + string.reset(); + string.append(t.getChar()); + length = 1; + + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case IN_NAME: + switch (t.getType()) { + case COLON: + fieldName = takeString(); + length = -1; + setState(FieldState.VALUE); + break; + + case SPACE: + // Ignore trailing whitespaces + setState(FieldState.AFTER_NAME); + break; + + case LF: { + if (LOG.isDebugEnabled()) + LOG.debug("Line Feed in Name {}", this); + + handleField(); + setState(FieldState.FIELD); + break; + } + + case ALPHA: + case DIGIT: + case TCHAR: + string.append(t.getChar()); + length = string.length(); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case AFTER_NAME: + switch (t.getType()) { + case COLON: + fieldName = takeString(); + length = -1; + setState(FieldState.VALUE); + break; + + case LF: + fieldName = takeString(); + string.reset(); + fieldValue = ""; + length = -1; + break; + + case SPACE: + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case VALUE: + switch (t.getType()) { + case LF: + string.reset(); + fieldValue = ""; + length = -1; + + setState(FieldState.FIELD); + break; + + case SPACE: + case HTAB: + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + case OTEXT: + string.append(t.getByte()); + length = string.length(); + setState(FieldState.IN_VALUE); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case IN_VALUE: + switch (t.getType()) { + case SPACE: + case HTAB: + string.append(' '); + break; + + case LF: + if (length > 0) { + fieldValue = takeString(); + length = -1; + totalHeaderLineLength = -1; + } + setState(FieldState.FIELD); + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + case OTEXT: + string.append(t.getByte()); + length = string.length(); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + default: + throw new IllegalStateException(state.toString()); + } + } + return false; + } + + private void handleField() { + if (LOG.isDebugEnabled()) + LOG.debug("parsedField: _fieldName={} _fieldValue={} {}", fieldName, fieldValue, this); + + if (fieldName != null && fieldValue != null) + handler.parsedField(fieldName, fieldValue); + fieldName = fieldValue = null; + } + + protected boolean parseOctetContent(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parseOctetContent({})", BufferUtils.toDetailString(buffer)); + + // Starts With + if (partialBoundary > 0) { + int partial = delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), partialBoundary); + if (partial > 0) { + if (partial == delimiterSearch.getLength()) { + buffer.position(buffer.position() + delimiterSearch.getLength() - partialBoundary); + setState(State.DELIMITER); + partialBoundary = 0; + + if (LOG.isDebugEnabled()) + LOG.debug("Content={}, Last={} {}", BufferUtils.toDetailString(BufferUtils.EMPTY_BUFFER), true, this); + + return handler.content(BufferUtils.EMPTY_BUFFER, true); + } + + partialBoundary = partial; + BufferUtils.clear(buffer); + return false; + } else { + // output up to _partialBoundary of the search pattern + ByteBuffer content = patternBuffer.slice(); + if (state == State.FIRST_OCTETS) { + setState(State.OCTETS); + content.position(2); + } + content.limit(partialBoundary); + partialBoundary = 0; + + if (LOG.isDebugEnabled()) + LOG.debug("Content={}, Last={} {}", BufferUtils.toDetailString(content), false, this); + + if (handler.content(content, false)) + return true; + } + } + + // Contains + int delimiter = delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + if (delimiter >= 0) { + ByteBuffer content = buffer.slice(); + content.limit(delimiter - buffer.arrayOffset() - buffer.position()); + + buffer.position(delimiter - buffer.arrayOffset() + delimiterSearch.getLength()); + setState(State.DELIMITER); + + if (LOG.isDebugEnabled()) + LOG.debug("Content={}, Last={} {}", BufferUtils.toDetailString(content), true, this); + + return handler.content(content, true); + } + + // Ends With + partialBoundary = delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + if (partialBoundary > 0) { + ByteBuffer content = buffer.slice(); + content.limit(content.limit() - partialBoundary); + + if (LOG.isDebugEnabled()) + LOG.debug("Content={}, Last={} {}", BufferUtils.toDetailString(content), false, this); + + BufferUtils.clear(buffer); + return handler.content(content, false); + } + + // There is normal content with no delimiter + ByteBuffer content = buffer.slice(); + + if (LOG.isDebugEnabled()) + LOG.debug("Content={}, Last={} {}", BufferUtils.toDetailString(content), false, this); + + BufferUtils.clear(buffer); + return handler.content(content, false); + } + + private void setState(State state) { + if (LOG.isDebugEnabled()) + LOG.debug("{} --> {}", this.state, state); + this.state = state; + } + + private void setState(FieldState state) { + if (LOG.isDebugEnabled()) + LOG.debug("{}:{} --> {}", this.state, fieldState, state); + fieldState = state; + } + + @Override + public String toString() { + return String.format("%s{s=%s}", getClass().getSimpleName(), state); + } + + /* + * Event Handler interface These methods return true if the caller should process the events so far received (eg return from parseNext and call + * HttpChannel.handle). If multiple callbacks are called in sequence (eg headerComplete then messageComplete) from the same point in the parsing then it is + * sufficient for the caller to process the events only once. + */ + public interface Handler { + default void startPart() { + } + + @SuppressWarnings("unused") + default void parsedField(String name, String value) { + } + + default boolean headerComplete() { + return false; + } + + @SuppressWarnings("unused") + default boolean content(ByteBuffer item, boolean last) { + return false; + } + + default boolean messageComplete() { + return false; + } + + default void earlyEOF() { + } + } + + @SuppressWarnings("serial") + private static class IllegalCharacterException extends BadMessageException { + private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer) { + super(400, String.format("Illegal character %s", token)); + if (LOG.isDebugEnabled()) + LOG.debug(String.format("Illegal character %s in state=%s for buffer %s", token, state, BufferUtils.toDetailString(buffer))); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/PreEncodedHttpField.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/PreEncodedHttpField.java new file mode 100644 index 000000000..e9553cf6a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/PreEncodedHttpField.java @@ -0,0 +1,46 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpVersion; +import com.fireflysource.net.http.common.v2.hpack.HpackFieldPreEncoder; + +import java.nio.ByteBuffer; +import java.util.ServiceLoader; + +/** + * Pre encoded HttpField. + *

A HttpField that will be cached and used many times can be created as + * a {@link PreEncodedHttpField}, which will use the {@link HttpFieldPreEncoder} + * instances discovered by the {@link ServiceLoader} to pre-encode the header + * for each version of HTTP in use. This will save garbage + * and CPU each time the field is encoded into a response. + *

+ */ +public class PreEncodedHttpField extends HttpField { + private final static HttpFieldPreEncoder[] ENCODERS = new HttpFieldPreEncoder[]{ + new HpackFieldPreEncoder(), + new Http1FieldPreEncoder()}; + + private final byte[][] encodedField = new byte[2][]; + + public PreEncodedHttpField(HttpHeader header, String name, String value) { + super(header, name, value); + + for (HttpFieldPreEncoder e : ENCODERS) { + encodedField[e.getHttpVersion() == HttpVersion.HTTP_2 ? 1 : 0] = e.getEncodedField(header, header.getValue(), value); + } + } + + public PreEncodedHttpField(HttpHeader header, String value) { + this(header, header.getValue(), value); + } + + public PreEncodedHttpField(String name, String value) { + this(null, name, value); + } + + public void putTo(ByteBuffer bufferInFillMode, HttpVersion version) { + bufferInFillMode.put(encodedField[version == HttpVersion.HTTP_2 ? 1 : 0]); + } +} \ No newline at end of file diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/URIUtils.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/URIUtils.java new file mode 100644 index 000000000..139680534 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/URIUtils.java @@ -0,0 +1,1123 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.string.Utf8Appendable; +import com.fireflysource.common.string.Utf8StringBuilder; +import com.fireflysource.net.http.common.model.HostPort; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * URI Utility methods. + *

+ * This class assists with the decoding and encoding or HTTP URI's. + * It differs from the java.net.URL class as it does not provide + * communications ability, but it does assist with query string + * formatting. + *

+ */ +@SuppressWarnings("unused") +public class URIUtils { + + public static final String SLASH = "/"; + public static final String HTTP = "http"; + public static final String HTTPS = "https"; + + // Use UTF-8 as per http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars + public static final Charset __CHARSET = StandardCharsets.UTF_8; + + private URIUtils() { + } + + /** + * Encode a URI path. + * This is the same encoding offered by URLEncoder, except that + * the '/' character is not encoded. + * + * @param path The path the encode + * @return The encoded path + */ + public static String encodePath(String path) { + if (path == null || path.length() == 0) + return path; + + StringBuilder buf = encodePath(null, path, 0); + return buf == null ? path : buf.toString(); + } + + /** + * Encode a URI path. + * + * @param path The path the encode + * @param buf StringBuilder to encode path into (or null) + * @return The StringBuilder or null if no substitutions required. + */ + public static StringBuilder encodePath(StringBuilder buf, String path) { + return encodePath(buf, path, 0); + } + + /** + * Encode a URI path. + * + * @param path The path the encode + * @param buf StringBuilder to encode path into (or null) + * @return The StringBuilder or null if no substitutions required. + */ + private static StringBuilder encodePath(StringBuilder buf, String path, int offset) { + byte[] bytes = null; + if (buf == null) { + loop: + for (int i = offset; i < path.length(); i++) { + char c = path.charAt(i); + switch (c) { + case '%': + case '?': + case ';': + case '#': + case '"': + case '\'': + case '<': + case '>': + case ' ': + case '[': + case '\\': + case ']': + case '^': + case '`': + case '{': + case '|': + case '}': + buf = new StringBuilder(path.length() * 2); + break loop; + default: + if (c > 127) { + bytes = path.getBytes(URIUtils.__CHARSET); + buf = new StringBuilder(path.length() * 2); + break loop; + } + } + } + if (buf == null) + return null; + } + + int i; + + loop: + for (i = offset; i < path.length(); i++) { + char c = path.charAt(i); + switch (c) { + case '%': + buf.append("%25"); + continue; + case '?': + buf.append("%3F"); + continue; + case ';': + buf.append("%3B"); + continue; + case '#': + buf.append("%23"); + continue; + case '"': + buf.append("%22"); + continue; + case '\'': + buf.append("%27"); + continue; + case '<': + buf.append("%3C"); + continue; + case '>': + buf.append("%3E"); + continue; + case ' ': + buf.append("%20"); + continue; + case '[': + buf.append("%5B"); + continue; + case '\\': + buf.append("%5C"); + continue; + case ']': + buf.append("%5D"); + continue; + case '^': + buf.append("%5E"); + continue; + case '`': + buf.append("%60"); + continue; + case '{': + buf.append("%7B"); + continue; + case '|': + buf.append("%7C"); + continue; + case '}': + buf.append("%7D"); + continue; + + default: + if (c > 127) { + bytes = path.getBytes(URIUtils.__CHARSET); + break loop; + } + buf.append(c); + } + } + + if (bytes != null) { + for (; i < bytes.length; i++) { + byte c = bytes[i]; + switch (c) { + case '%': + buf.append("%25"); + continue; + case '?': + buf.append("%3F"); + continue; + case ';': + buf.append("%3B"); + continue; + case '#': + buf.append("%23"); + continue; + case '"': + buf.append("%22"); + continue; + case '\'': + buf.append("%27"); + continue; + case '<': + buf.append("%3C"); + continue; + case '>': + buf.append("%3E"); + continue; + case ' ': + buf.append("%20"); + continue; + case '[': + buf.append("%5B"); + continue; + case '\\': + buf.append("%5C"); + continue; + case ']': + buf.append("%5D"); + continue; + case '^': + buf.append("%5E"); + continue; + case '`': + buf.append("%60"); + continue; + case '{': + buf.append("%7B"); + continue; + case '|': + buf.append("%7C"); + continue; + case '}': + buf.append("%7D"); + continue; + default: + if (c < 0) { + buf.append('%'); + TypeUtils.toHex(c, buf); + } else + buf.append((char) c); + } + } + } + + return buf; + } + + /** + * Encode a raw URI String and convert any raw spaces to + * their "%20" equivalent. + * + * @param str input raw string + * @return output with spaces converted to "%20" + */ + public static String encodeSpaces(String str) { + return StringUtils.replaceStr(str, " ", "%20"); + } + + /** + * Encode a raw String and convert any specific characters to their URI encoded equivalent. + * + * @param str input raw string + * @param charsToEncode the list of raw characters that need to be encoded (if encountered) + * @return output with specified characters encoded. + */ + @SuppressWarnings("Duplicates") + public static String encodeSpecific(String str, String charsToEncode) { + if ((str == null) || (str.length() == 0)) + return null; + + if ((charsToEncode == null) || (charsToEncode.length() == 0)) + return str; + + char[] find = charsToEncode.toCharArray(); + int len = str.length(); + StringBuilder ret = new StringBuilder((int) (len * 0.20d)); + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + boolean escaped = false; + for (char f : find) { + if (c == f) { + escaped = true; + ret.append('%'); + int d = 0xf & ((0xF0 & c) >> 4); + ret.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + d = 0xf & c; + ret.append((char) ((d > 9 ? ('A' - 10) : '0') + d)); + break; + } + } + if (!escaped) { + ret.append(c); + } + } + return ret.toString(); + } + + /** + * Decode a raw String and convert any specific URI encoded sequences into characters. + * + * @param str input raw string + * @param charsToDecode the list of raw characters that need to be decoded (if encountered), leaving all other encoded sequences alone. + * @return output with specified characters decoded. + */ + @SuppressWarnings("Duplicates") + public static String decodeSpecific(String str, String charsToDecode) { + if ((str == null) || (str.length() == 0)) + return null; + + if ((charsToDecode == null) || (charsToDecode.length() == 0)) + return str; + + int idx = str.indexOf('%'); + if (idx == -1) { + // no hits + return str; + } + + char[] find = charsToDecode.toCharArray(); + int len = str.length(); + Utf8StringBuilder ret = new Utf8StringBuilder(len); + ret.append(str, 0, idx); + + for (int i = idx; i < len; i++) { + char c = str.charAt(i); + switch (c) { + case '%': + if ((i + 2) < len) { + char u = str.charAt(i + 1); + char l = str.charAt(i + 2); + char result = (char) (0xff & (TypeUtils.convertHexDigit(u) * 16 + TypeUtils.convertHexDigit(l))); + boolean decoded = false; + for (char f : find) { + if (f == result) { + ret.append(result); + decoded = true; + break; + } + } + if (decoded) { + i += 2; + } else { + ret.append(c); + } + } else { + throw new IllegalArgumentException("Bad URI % encoding"); + } + break; + default: + ret.append(c); + break; + } + } + return ret.toString(); + } + + /** + * Encode a URI path. + * + * @param path The path the encode + * @param buf StringBuilder to encode path into (or null) + * @param encode String of characters to encode. % is always encoded. + * @return The StringBuilder or null if no substitutions required. + */ + public static StringBuilder encodeString(StringBuilder buf, + String path, + String encode) { + if (buf == null) { + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '%' || encode.indexOf(c) >= 0) { + buf = new StringBuilder(path.length() << 1); + break; + } + } + if (buf == null) + return null; + } + + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '%' || encode.indexOf(c) >= 0) { + buf.append('%'); + StringUtils.append(buf, (byte) (0xff & c), 16); + } else + buf.append(c); + } + + return buf; + } + + /* Decode a URI path and strip parameters + */ + public static String decodePath(String path) { + return decodePath(path, 0, path.length()); + } + + /* Decode a URI path and strip parameters of UTF-8 path + */ + public static String decodePath(String path, int offset, int length) { + try { + Utf8StringBuilder builder = null; + int end = offset + length; + for (int i = offset; i < end; i++) { + char c = path.charAt(i); + switch (c) { + case '%': + if (builder == null) { + builder = new Utf8StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + + // lenient percent decoding + if (i >= end) { + // [LENIENT] a percent sign at end of string. + builder.append('%'); + i = end; + } else if (end > (i + 1)) { + char type = path.charAt(i + 1); + if (type == 'u') { + // We have a possible (deprecated) microsoft unicode code point "%u####" + // - not recommended to use as it's limited to 2 bytes. + if ((i + 5) >= end) { + // [LENIENT] we have a partial "%u####" at the end of a string. + builder.append(path, i, (end - i)); + i = end; + } else { + // this seems wrong, as we are casting to a char, but that's the known + // limitation of this deprecated encoding (only 2 bytes allowed) + if (StringUtils.isHex(path, i + 2, 4)) { + builder.append((char) (0xffff & TypeUtils.parseInt(path, i + 2, 4, 16))); + i += 5; + } else { + // [LENIENT] copy the "%u" as-is. + builder.append(path, i, 2); + i += 1; + } + } + } else if (end > (i + 2)) { + // we have a possible "%##" encoding + if (StringUtils.isHex(path, i + 1, 2)) { + builder.append((byte) TypeUtils.parseInt(path, i + 1, 2, 16)); + i += 2; + } else { + builder.append(path, i, 3); + i += 2; + } + } else { + // [LENIENT] incomplete "%##" sequence at end of string + builder.append(path, i, (end - i)); + i = end; + } + } else { + // [LENIENT] the "%" at the end of the string + builder.append(path, i, (end - i)); + i = end; + } + + break; + + case ';': + if (builder == null) { + builder = new Utf8StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + + while (++i < end) { + if (path.charAt(i) == '/') { + builder.append('/'); + break; + } + } + + break; + + default: + if (builder != null) + builder.append(c); + break; + } + } + + if (builder != null) + return builder.toString(); + if (offset == 0 && length == path.length()) + return path; + return path.substring(offset, end); + } catch (Utf8Appendable.NotUtf8Exception e) { + return decodeISO88591Path(path, offset, length); + } + } + + /* Decode a URI path and strip parameters of ISO-8859-1 path + */ + private static String decodeISO88591Path(String path, int offset, int length) { + StringBuilder builder = null; + int end = offset + length; + for (int i = offset; i < end; i++) { + char c = path.charAt(i); + switch (c) { + case '%': + if (builder == null) { + builder = new StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + if ((i + 2) < end) { + char u = path.charAt(i + 1); + if (u == 'u') { + // TODO this is wrong. This is a codepoint not a char + builder.append((char) (0xffff & TypeUtils.parseInt(path, i + 2, 4, 16))); + i += 5; + } else { + builder.append((byte) (0xff & (TypeUtils.convertHexDigit(u) * 16 + TypeUtils.convertHexDigit(path.charAt(i + 2))))); + i += 2; + } + } else { + throw new IllegalArgumentException(); + } + + break; + + case ';': + if (builder == null) { + builder = new StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + while (++i < end) { + if (path.charAt(i) == '/') { + builder.append('/'); + break; + } + } + break; + + default: + if (builder != null) + builder.append(c); + break; + } + } + + if (builder != null) + return builder.toString(); + if (offset == 0 && length == path.length()) + return path; + return path.substring(offset, end); + } + + /** + * Add two encoded URI path segments. + * Handles null and empty paths, path and query params + * (eg ?a=b or ;JSESSIONID=xxx) and avoids duplicate '/' + * + * @param p1 URI path segment (should be encoded) + * @param p2 URI path segment (should be encoded) + * @return Legally combined path segments. + */ + public static String addEncodedPaths(String p1, String p2) { + if (p1 == null || p1.length() == 0) { + if (p1 != null && p2 == null) + return p1; + return p2; + } + if (p2 == null || p2.length() == 0) + return p1; + + int split = p1.indexOf(';'); + if (split < 0) + split = p1.indexOf('?'); + if (split == 0) + return p2 + p1; + if (split < 0) + split = p1.length(); + + StringBuilder buf = new StringBuilder(p1.length() + p2.length() + 2); + buf.append(p1); + + if (buf.charAt(split - 1) == '/') { + if (p2.startsWith(URIUtils.SLASH)) { + buf.deleteCharAt(split - 1); + buf.insert(split - 1, p2); + } else + buf.insert(split, p2); + } else { + if (p2.startsWith(URIUtils.SLASH)) + buf.insert(split, p2); + else { + buf.insert(split, '/'); + buf.insert(split + 1, p2); + } + } + + return buf.toString(); + } + + /** + * Add two Decoded URI path segments. + * Handles null and empty paths. Path and query params (eg ?a=b or + * ;JSESSIONID=xxx) are not handled + * + * @param p1 URI path segment (should be decoded) + * @param p2 URI path segment (should be decoded) + * @return Legally combined path segments. + */ + public static String addPaths(String p1, String p2) { + if (p1 == null || p1.length() == 0) { + if (p1 != null && p2 == null) + return p1; + return p2; + } + if (p2 == null || p2.length() == 0) + return p1; + + boolean p1EndsWithSlash = p1.endsWith(SLASH); + boolean p2StartsWithSlash = p2.startsWith(SLASH); + + if (p1EndsWithSlash && p2StartsWithSlash) { + if (p2.length() == 1) + return p1; + if (p1.length() == 1) + return p2; + } + + StringBuilder buf = new StringBuilder(p1.length() + p2.length() + 2); + buf.append(p1); + + if (p1.endsWith(SLASH)) { + if (p2.startsWith(SLASH)) + buf.setLength(buf.length() - 1); + } else { + if (!p2.startsWith(SLASH)) + buf.append(SLASH); + } + buf.append(p2); + + return buf.toString(); + } + + /** + * Return the parent Path. + * Treat a URI like a directory path and return the parent directory. + * + * @param p the path to return a parent reference to + * @return the parent path of the URI + */ + public static String parentPath(String p) { + if (p == null || URIUtils.SLASH.equals(p)) + return null; + int slash = p.lastIndexOf('/', p.length() - 2); + if (slash >= 0) + return p.substring(0, slash + 1); + return null; + } + + /** + * Convert a decoded path to a canonical form. + *

+ * All instances of "." and ".." are factored out. + *

+ *

+ * Null is returned if the path tries to .. above its root. + *

+ * + * @param path the path to convert, decoded, with path separators '/' and no queries. + * @return the canonical path, or null if path traversal above root. + */ + public static String canonicalPath(String path) { + if (path == null || path.isEmpty()) + return path; + + boolean slash = true; + int end = path.length(); + int i = 0; + + loop: + while (i < end) { + char c = path.charAt(i); + switch (c) { + case '/': + slash = true; + break; + + case '.': + if (slash) + break loop; + slash = false; + break; + + default: + slash = false; + } + + i++; + } + + if (i == end) + return path; + + StringBuilder canonical = new StringBuilder(path.length()); + canonical.append(path, 0, i); + + int dots = 1; + i++; + while (i <= end) { + char c = i < end ? path.charAt(i) : '\0'; + switch (c) { + case '\0': + case '/': + switch (dots) { + case 0: + if (c != '\0') + canonical.append(c); + break; + + case 1: + break; + + case 2: + if (canonical.length() < 2) + return null; + canonical.setLength(canonical.length() - 1); + canonical.setLength(canonical.lastIndexOf("/") + 1); + break; + + default: + while (dots-- > 0) { + canonical.append('.'); + } + if (c != '\0') + canonical.append(c); + } + + slash = true; + dots = 0; + break; + + case '.': + if (dots > 0) + dots++; + else if (slash) + dots = 1; + else + canonical.append('.'); + slash = false; + break; + + default: + while (dots-- > 0) { + canonical.append('.'); + } + canonical.append(c); + dots = 0; + slash = false; + } + + i++; + } + return canonical.toString(); + } + + /** + * Convert a path to a cananonical form. + *

+ * All instances of "." and ".." are factored out. + *

+ *

+ * Null is returned if the path tries to .. above its root. + *

+ * + * @param path the path to convert (expects URI/URL form, encoded, and with path separators '/') + * @return the canonical path, or null if path traversal above root. + */ + public static String canonicalEncodedPath(String path) { + if (path == null || path.isEmpty()) + return path; + + boolean slash = true; + int end = path.length(); + int i = 0; + + loop: + while (i < end) { + char c = path.charAt(i); + switch (c) { + case '/': + slash = true; + break; + + case '.': + if (slash) + break loop; + slash = false; + break; + + case '?': + return path; + + default: + slash = false; + } + + i++; + } + + if (i == end) + return path; + + StringBuilder canonical = new StringBuilder(path.length()); + canonical.append(path, 0, i); + + int dots = 1; + i++; + while (i <= end) { + char c = i < end ? path.charAt(i) : '\0'; + switch (c) { + case '\0': + case '/': + case '?': + switch (dots) { + case 0: + if (c != '\0') + canonical.append(c); + break; + + case 1: + if (c == '?') + canonical.append(c); + break; + + case 2: + if (canonical.length() < 2) + return null; + canonical.setLength(canonical.length() - 1); + canonical.setLength(canonical.lastIndexOf("/") + 1); + if (c == '?') + canonical.append(c); + break; + default: + while (dots-- > 0) { + canonical.append('.'); + } + if (c != '\0') + canonical.append(c); + } + + slash = true; + dots = 0; + break; + + case '.': + if (dots > 0) + dots++; + else if (slash) + dots = 1; + else + canonical.append('.'); + slash = false; + break; + + default: + while (dots-- > 0) { + canonical.append('.'); + } + canonical.append(c); + dots = 0; + slash = false; + } + + i++; + } + return canonical.toString(); + } + + /** + * Convert a path to a compact form. + * All instances of "//" and "///" etc. are factored out to single "/" + * + * @param path the path to compact + * @return the compacted path + */ + public static String compactPath(String path) { + if (path == null || path.length() == 0) + return path; + + int state = 0; + int end = path.length(); + int i = 0; + + loop: + while (i < end) { + char c = path.charAt(i); + switch (c) { + case '?': + return path; + case '/': + state++; + if (state == 2) + break loop; + break; + default: + state = 0; + } + i++; + } + + if (state < 2) + return path; + + StringBuilder buf = new StringBuilder(path.length()); + buf.append(path, 0, i); + + loop2: + while (i < end) { + char c = path.charAt(i); + switch (c) { + case '?': + buf.append(path, i, end); + break loop2; + case '/': + if (state++ == 0) + buf.append(c); + break; + default: + state = 0; + buf.append(c); + } + i++; + } + + return buf.toString(); + } + + /** + * @param uri URI + * @return True if the uri has a scheme + */ + public static boolean hasScheme(String uri) { + for (int i = 0; i < uri.length(); i++) { + char c = uri.charAt(i); + if (c == ':') { + return true; + } + if (!(c >= 'a' && c <= 'z' || + c >= 'A' && c <= 'Z' || + (i > 0 && (c >= '0' && c <= '9' || c == '.' || c == '+' || c == '-')))) { + break; + } + } + return false; + } + + /** + * Create a new URI from the arguments, handling IPv6 host encoding and default ports + * + * @param scheme the URI scheme + * @param server the URI server + * @param port the URI port + * @param path the URI path + * @param query the URI query + * @return A String URI + */ + public static String newURI(String scheme, String server, int port, String path, String query) { + StringBuilder builder = newURIBuilder(scheme, server, port); + builder.append(path); + if (query != null && query.length() > 0) + builder.append('?').append(query); + return builder.toString(); + } + + /** + * Create a new URI StringBuilder from the arguments, handling IPv6 host encoding and default ports + * + * @param scheme the URI scheme + * @param server the URI server + * @param port the URI port + * @return a StringBuilder containing URI prefix + */ + public static StringBuilder newURIBuilder(String scheme, String server, int port) { + StringBuilder builder = new StringBuilder(); + appendSchemeHostPort(builder, scheme, server, port); + return builder; + } + + /** + * Append scheme, host and port URI prefix, handling IPv6 address encoding and default ports + * + * @param url StringBuilder to append to + * @param scheme the URI scheme + * @param server the URI server + * @param port the URI port + */ + public static void appendSchemeHostPort(StringBuilder url, String scheme, String server, int port) { + url.append(scheme).append("://").append(HostPort.normalizeHost(server)); + + if (port > 0) { + switch (scheme) { + case "http": + if (port != 80) + url.append(':').append(port); + break; + + case "https": + if (port != 443) + url.append(':').append(port); + break; + + default: + url.append(':').append(port); + } + } + } + + public static boolean equalsIgnoreEncodings(String uriA, String uriB) { + int lenA = uriA.length(); + int lenB = uriB.length(); + int a = 0; + int b = 0; + + while (a < lenA && b < lenB) { + int oa = uriA.charAt(a++); + int ca = oa; + if (ca == '%') { + ca = lenientPercentDecode(uriA, a); + if (ca == (-1)) { + ca = '%'; + } else { + a += 2; + } + } + + int ob = uriB.charAt(b++); + int cb = ob; + if (cb == '%') { + cb = lenientPercentDecode(uriB, b); + if (cb == (-1)) { + cb = '%'; + } else { + b += 2; + } + } + + // Don't match on encoded slash + if (ca == '/' && oa != ob) + return false; + + if (ca != cb) + return false; + } + return a == lenA && b == lenB; + } + + private static int lenientPercentDecode(String str, int offset) { + if (offset >= str.length()) + return -1; + + if (StringUtils.isHex(str, offset, 2)) { + return TypeUtils.parseInt(str, offset, 2, 16); + } else { + return -1; + } + } + + public static boolean equalsIgnoreEncodings(URI uriA, URI uriB) { + if (uriA.equals(uriB)) + return true; + + if (uriA.getScheme() == null) { + if (uriB.getScheme() != null) + return false; + } else if (!uriA.getScheme().equalsIgnoreCase(uriB.getScheme())) + return false; + + if ("jar".equalsIgnoreCase(uriA.getScheme())) { + // at this point we know that both uri's are "jar:" + URI uriAssp = URI.create(uriA.getSchemeSpecificPart()); + URI uriBssp = URI.create(uriB.getSchemeSpecificPart()); + return equalsIgnoreEncodings(uriAssp, uriBssp); + } + + if (uriA.getAuthority() == null) { + if (uriB.getAuthority() != null) + return false; + } else if (!uriA.getAuthority().equals(uriB.getAuthority())) + return false; + + return equalsIgnoreEncodings(uriA.getPath(), uriB.getPath()); + } + + /** + * @param uri A URI to add the path to + * @param path A decoded path element + * @return URI with path added. + */ + public static URI addPath(URI uri, String path) { + String base = uri.toASCIIString(); + StringBuilder buf = new StringBuilder(base.length() + path.length() * 3); + buf.append(base); + if (buf.charAt(base.length() - 1) != '/') + buf.append('/'); + + int offset = path.charAt(0) == '/' ? 1 : 0; + encodePath(buf, path, offset); + + return URI.create(buf.toString()); + } + + public static URI getJarSource(URI uri) { + try { + if (!"jar".equals(uri.getScheme())) + return uri; + // Get SSP (retaining encoded form) + String s = uri.getRawSchemeSpecificPart(); + int bangSlash = s.indexOf("!/"); + if (bangSlash >= 0) + s = s.substring(0, bangSlash); + return new URI(s); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public static String getJarSource(String uri) { + if (!uri.startsWith("jar:")) + return uri; + int bangSlash = uri.indexOf("!/"); + return (bangSlash >= 0) ? uri.substring(4, bangSlash) : uri.substring(4); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/UrlEncoded.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/UrlEncoded.java new file mode 100644 index 000000000..ebd09cebb --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/codec/UrlEncoded.java @@ -0,0 +1,805 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.collection.map.MultiMap; +import com.fireflysource.common.io.ByteArrayOutputStream2; +import com.fireflysource.common.io.IO; +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.Utf8Appendable; +import com.fireflysource.common.string.Utf8StringBuilder; +import com.fireflysource.common.sys.SystemLogger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import static com.fireflysource.common.object.TypeUtils.convertHexDigit; + +/** + * Handles coding of MIME "x-www-form-urlencoded". + *

+ * This class handles the encoding and decoding for either the query string of a + * URL or the _content of a POST HTTP request. + *

+ * Notes + *

+ * The UTF-8 charset is assumed, unless otherwise defined by either passing a + * parameter or setting the "org.firefly.utils.UrlEncoding.charset" System + * property. + *

+ *

+ * The hashtable either contains String single values, vectors of String or + * arrays of Strings. + *

+ * + * @see java.net.URLEncoder + */ +@SuppressWarnings("serial") +public class UrlEncoded extends MultiMap implements Cloneable { + + public static final Charset ENCODING; + private static final String __ISO_8859_1 = "iso-8859-1"; + private static final String __UTF8 = "utf-8"; + private static final String __UTF16 = "utf-16"; + private static LazyLogger LOG = SystemLogger.create(UrlEncoded.class); + + static { + Charset encoding; + try { + String charset = System.getProperty("com.firefly.codec.http2.encode.UrlEncoding.charset"); + encoding = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset); + } catch (Exception e) { + LOG.warn("get charset exception", e); + encoding = StandardCharsets.UTF_8; + } + ENCODING = encoding; + } + + public UrlEncoded(UrlEncoded url) { + super(url); + } + + public UrlEncoded() { + } + + public UrlEncoded(String query) { + decodeTo(query, this, ENCODING); + } + + /** + * Encode MultiMap with % encoding. + * + * @param map the map to encode + * @param charset the charset to use for encoding (uses default encoding if null) + * @param equalsForNullValue if True, then an '=' is always used, even + * for parameters without a value. e.g. "blah?a=&b=&c=". + * @return the MultiMap as a string encoded with % encodings. + */ + public static String encode(MultiMap map, Charset charset, boolean equalsForNullValue) { + if (charset == null) + charset = ENCODING; + + StringBuilder result = new StringBuilder(128); + + boolean delim = false; + for (Map.Entry> entry : map.entrySet()) { + String key = entry.getKey(); + List list = entry.getValue(); + int s = list.size(); + + if (delim) { + result.append('&'); + } + + if (s == 0) { + result.append(encodeString(key, charset)); + if (equalsForNullValue) + result.append('='); + } else { + for (int i = 0; i < s; i++) { + if (i > 0) + result.append('&'); + String val = list.get(i); + result.append(encodeString(key, charset)); + + if (val != null) { + if (val.length() > 0) { + result.append('='); + result.append(encodeString(val, charset)); + } else if (equalsForNullValue) + result.append('='); + } else if (equalsForNullValue) + result.append('='); + } + } + delim = true; + } + return result.toString(); + } + + /** + * Decoded parameters to Map. + * + * @param content the string containing the encoded parameters + * @param map the MultiMap to put parsed query parameters into + * @param charset the charset to use for decoding + */ + public static void decodeTo(String content, MultiMap map, String charset) { + decodeTo(content, map, charset == null ? null : Charset.forName(charset)); + } + + /** + * Decoded parameters to Map. + * + * @param content the string containing the encoded parameters + * @param map the MultiMap to put parsed query parameters into + * @param charset the charset to use for decoding + */ + public static void decodeTo(String content, MultiMap map, Charset charset) { + if (charset == null) + charset = ENCODING; + + if (charset == StandardCharsets.UTF_8) { + decodeUtf8To(content, 0, content.length(), map); + return; + } + + String key = null; + String value = null; + int mark = -1; + boolean encoded = false; + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + switch (c) { + case '&': + int l = i - mark - 1; + value = l == 0 ? "" : + (encoded ? decodeString(content, mark + 1, l, charset) : content.substring(mark + 1, i)); + mark = i; + encoded = false; + if (key != null) { + map.add(key, value); + } else if (value != null && value.length() > 0) { + map.add(value, ""); + } + key = null; + value = null; + break; + case '=': + if (key != null) + break; + key = encoded ? decodeString(content, mark + 1, i - mark - 1, charset) : content.substring(mark + 1, i); + mark = i; + encoded = false; + break; + case '+': + encoded = true; + break; + case '%': + encoded = true; + break; + } + } + + if (key != null) { + int l = content.length() - mark - 1; + value = l == 0 ? "" : (encoded ? decodeString(content, mark + 1, l, charset) : content.substring(mark + 1)); + map.add(key, value); + } else if (mark < content.length()) { + key = encoded + ? decodeString(content, mark + 1, content.length() - mark - 1, charset) + : content.substring(mark + 1); + if (key != null && key.length() > 0) { + map.add(key, ""); + } + } + + } + + public static void decodeUtf8To(String query, MultiMap map) { + decodeUtf8To(query, 0, query.length(), map); + } + + /** + * Decoded parameters to Map. + * + * @param query the string containing the encoded parameters + * @param offset the offset within raw to decode from + * @param length the length of the section to decode + * @param map the {@link MultiMap} to populate + */ + public static void decodeUtf8To(String query, int offset, int length, MultiMap map) { + Utf8StringBuilder buffer = new Utf8StringBuilder(); + + String key = null; + String value; + + int end = offset + length; + for (int i = offset; i < end; i++) { + char c = query.charAt(i); + switch (c) { + case '&': + value = buffer.toReplacedString(); + buffer.reset(); + if (key != null) { + map.add(key, value); + } else if (value != null && value.length() > 0) { + map.add(value, ""); + } + key = null; + value = null; + break; + + case '=': + if (key != null) { + buffer.append(c); + break; + } + key = buffer.toReplacedString(); + buffer.reset(); + break; + + case '+': + buffer.append((byte) ' '); + break; + + case '%': + if (i + 2 < end) { + char hi = query.charAt(++i); + char lo = query.charAt(++i); + buffer.append(decodeHexByte(hi, lo)); + } else { + throw new Utf8Appendable.NotUtf8Exception("Incomplete % encoding"); + } + break; + + default: + buffer.append(c); + break; + } + } + + if (key != null) { + value = buffer.toReplacedString(); + buffer.reset(); + map.add(key, value); + } else if (buffer.length() > 0) { + map.add(buffer.toReplacedString(), ""); + } + + } + + /** + * Decoded parameters to MultiMap, using ISO8859-1 encodings. + * + * @param in InputSteam to read + * @param map MultiMap to add parameters to + * @param maxLength maximum length of form to read + * @param maxKeys maximum number of keys to read or -1 for no limit + * @throws IOException if unable to decode inputstream as ISO8859-1 + */ + public static void decode88591To(InputStream in, MultiMap map, int maxLength, int maxKeys) + throws IOException { + + StringBuilder buffer = new StringBuilder(); + String key = null; + String value; + + int b; + + int totalLength = 0; + while ((b = in.read()) >= 0) { + switch ((char) b) { + case '&': + value = buffer.length() == 0 ? "" : buffer.toString(); + buffer.setLength(0); + if (key != null) { + map.add(key, value); + } else if (value.length() > 0) { + map.add(value, ""); + } + key = null; + value = null; + if (maxKeys > 0 && map.size() > maxKeys) + throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); + break; + + case '=': + if (key != null) { + buffer.append((char) b); + break; + } + key = buffer.toString(); + buffer.setLength(0); + break; + + case '+': + buffer.append(' '); + break; + + case '%': + int code0 = in.read(); + int code1 = in.read(); + buffer.append(decodeHexChar(code0, code1)); + break; + + default: + buffer.append((char) b); + break; + } + if (maxLength >= 0 && (++totalLength > maxLength)) + throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); + } + + if (key != null) { + value = buffer.length() == 0 ? "" : buffer.toString(); + buffer.setLength(0); + map.add(key, value); + } else if (buffer.length() > 0) { + map.add(buffer.toString(), ""); + } + + } + + /** + * Decoded parameters to Map. + * + * @param in InputSteam to read + * @param map MultiMap to add parameters to + * @param maxLength maximum form length to decode + * @param maxKeys the maximum number of keys to read or -1 for no limit + * @throws IOException if unable to decode input stream + */ + public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException { + + Utf8StringBuilder buffer = new Utf8StringBuilder(); + String key = null; + String value; + + int b; + + int totalLength = 0; + while ((b = in.read()) >= 0) { + switch ((char) b) { + case '&': + value = buffer.toReplacedString(); + buffer.reset(); + if (key != null) { + map.add(key, value); + } else if (value != null && value.length() > 0) { + map.add(value, ""); + } + key = null; + value = null; + if (maxKeys > 0 && map.size() > maxKeys) + throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); + break; + + case '=': + if (key != null) { + buffer.append((byte) b); + break; + } + key = buffer.toReplacedString(); + buffer.reset(); + break; + + case '+': + buffer.append((byte) ' '); + break; + + case '%': + char code0 = (char) in.read(); + char code1 = (char) in.read(); + buffer.append(decodeHexByte(code0, code1)); + break; + + default: + buffer.append((byte) b); + break; + } + if (maxLength >= 0 && (++totalLength > maxLength)) + throw new IllegalStateException("Form is too large"); + } + + if (key != null) { + value = buffer.toReplacedString(); + buffer.reset(); + map.add(key, value); + } else if (buffer.length() > 0) { + map.add(buffer.toReplacedString(), ""); + } + + } + + public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException { + InputStreamReader input = new InputStreamReader(in, StandardCharsets.UTF_16); + StringWriter buf = new StringWriter(8192); + IO.copy(input, buf, maxLength); + + // implement maxKeys + decodeTo(buf.getBuffer().toString(), map, StandardCharsets.UTF_16); + } + + /** + * Decoded parameters to Map. + * + * @param in the stream containing the encoded parameters + * @param map the MultiMap to decode into + * @param charset the charset to use for decoding + * @param maxLength the maximum length of the form to decode + * @param maxKeys the maximum number of keys to decode + * @throws IOException if unable to decode input stream + */ + public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength, int maxKeys) throws IOException { + if (charset == null) { + if (ENCODING.equals(StandardCharsets.UTF_8)) + decodeUtf8To(in, map, maxLength, maxKeys); + else + decodeTo(in, map, ENCODING, maxLength, maxKeys); + } else if (__UTF8.equalsIgnoreCase(charset)) + decodeUtf8To(in, map, maxLength, maxKeys); + else if (__ISO_8859_1.equalsIgnoreCase(charset)) + decode88591To(in, map, maxLength, maxKeys); + else if (__UTF16.equalsIgnoreCase(charset)) + decodeUtf16To(in, map, maxLength, maxKeys); + else + decodeTo(in, map, Charset.forName(charset), maxLength, maxKeys); + } + + /** + * Decoded parameters to Map. + * + * @param in the stream containing the encoded parameters + * @param map the MultiMap to decode into + * @param charset the charset to use for decoding + * @param maxLength the maximum length of the form to decode + * @param maxKeys the maximum number of keys to decode + * @throws IOException if unable to decode input stream + */ + public static void decodeTo(InputStream in, MultiMap map, Charset charset, int maxLength, int maxKeys) throws IOException { + //no charset present, use the configured default + if (charset == null) + charset = ENCODING; + + if (StandardCharsets.UTF_8.equals(charset)) { + decodeUtf8To(in, map, maxLength, maxKeys); + return; + } + + if (StandardCharsets.ISO_8859_1.equals(charset)) { + decode88591To(in, map, maxLength, maxKeys); + return; + } + + // Should be all 2 byte encodings + if (StandardCharsets.UTF_16.equals(charset)) { + decodeUtf16To(in, map, maxLength, maxKeys); + return; + } + + + String key = null; + String value; + + int c; + + int totalLength = 0; + + try (ByteArrayOutputStream2 output = new ByteArrayOutputStream2()) { + int size = 0; + + while ((c = in.read()) > 0) { + switch ((char) c) { + case '&': + size = output.size(); + value = size == 0 ? "" : output.toString(charset); + output.setCount(0); + if (key != null) { + map.add(key, value); + } else if (value != null && value.length() > 0) { + map.add(value, ""); + } + key = null; + if (maxKeys > 0 && map.size() > maxKeys) + throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); + break; + case '=': + if (key != null) { + output.write(c); + break; + } + size = output.size(); + key = size == 0 ? "" : output.toString(charset); + output.setCount(0); + break; + case '+': + output.write(' '); + break; + case '%': + int code0 = in.read(); + int code1 = in.read(); + output.write(decodeHexChar(code0, code1)); + break; + default: + output.write(c); + break; + } + + totalLength++; + if (maxLength >= 0 && totalLength > maxLength) + throw new IllegalStateException("Form is too large"); + } + + size = output.size(); + if (key != null) { + value = size == 0 ? "" : output.toString(charset); + output.setCount(0); + map.add(key, value); + } else if (size > 0) + map.add(output.toString(charset), ""); + } + + } + + /** + * Decode String with % encoding. + * This method makes the assumption that the majority of calls + * will need no decoding. + * + * @param encoded the encoded string to decode + * @return the decoded string + */ + public static String decodeString(String encoded) { + return decodeString(encoded, 0, encoded.length(), ENCODING); + } + + /** + * Decode String with % encoding. + * This method makes the assumption that the majority of calls + * will need no decoding. + * + * @param encoded the encoded string to decode + * @param offset the offset in the encoded string to decode from + * @param length the length of characters in the encoded string to decode + * @param charset the charset to use for decoding + * @return the decoded string + */ + public static String decodeString(String encoded, int offset, int length, Charset charset) { + if (charset == null || StandardCharsets.UTF_8.equals(charset)) { + Utf8StringBuilder buffer = null; + + for (int i = 0; i < length; i++) { + char c = encoded.charAt(offset + i); + if (c < 0 || c > 0xff) { + if (buffer == null) { + buffer = new Utf8StringBuilder(length); + buffer.getStringBuilder().append(encoded, offset, offset + i + 1); + } else + buffer.getStringBuilder().append(c); + } else if (c == '+') { + if (buffer == null) { + buffer = new Utf8StringBuilder(length); + buffer.getStringBuilder().append(encoded, offset, offset + i); + } + + buffer.getStringBuilder().append(' '); + } else if (c == '%') { + if (buffer == null) { + buffer = new Utf8StringBuilder(length); + buffer.getStringBuilder().append(encoded, offset, offset + i); + } + + if ((i + 2) < length) { + int o = offset + i + 1; + i += 2; + byte b = (byte) TypeUtils.parseInt(encoded, o, 2, 16); + buffer.append(b); + } else { + buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT); + i = length; + } + } else if (buffer != null) + buffer.getStringBuilder().append(c); + } + + if (buffer == null) { + if (offset == 0 && encoded.length() == length) { + return encoded; + } + return encoded.substring(offset, offset + length); + } + + return buffer.toReplacedString(); + } else { + StringBuilder buffer = null; + + for (int i = 0; i < length; i++) { + char c = encoded.charAt(offset + i); + if (c < 0 || c > 0xff) { + if (buffer == null) { + buffer = new StringBuilder(length); + buffer.append(encoded, offset, offset + i + 1); + } else + buffer.append(c); + } else if (c == '+') { + if (buffer == null) { + buffer = new StringBuilder(length); + buffer.append(encoded, offset, offset + i); + } + + buffer.append(' '); + } else if (c == '%') { + if (buffer == null) { + buffer = new StringBuilder(length); + buffer.append(encoded, offset, offset + i); + } + + byte[] ba = new byte[length]; + int n = 0; + while (c >= 0 && c <= 0xff) { + if (c == '%') { + if (i + 2 < length) { + int o = offset + i + 1; + i += 3; + ba[n] = (byte) TypeUtils.parseInt(encoded, o, 2, 16); + n++; + } else { + ba[n++] = (byte) '?'; + i = length; + } + } else if (c == '+') { + ba[n++] = (byte) ' '; + i++; + } else { + ba[n++] = (byte) c; + i++; + } + + if (i >= length) + break; + c = encoded.charAt(offset + i); + } + + i--; + buffer.append(new String(ba, 0, n, charset)); + + } else if (buffer != null) + buffer.append(c); + } + + if (buffer == null) { + if (offset == 0 && encoded.length() == length) + return encoded; + return encoded.substring(offset, offset + length); + } + + return buffer.toString(); + } + } + + private static char decodeHexChar(int hi, int lo) { + try { + return (char) ((convertHexDigit(hi) << 4) + convertHexDigit(lo)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Not valid encoding '%" + (char) hi + (char) lo + "'"); + } + } + + private static byte decodeHexByte(char hi, char lo) { + try { + return (byte) ((convertHexDigit(hi) << 4) + convertHexDigit(lo)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Not valid encoding '%" + hi + lo + "'"); + } + } + + /** + * Perform URL encoding. + * + * @param string the string to encode + * @return encoded string. + */ + public static String encodeString(String string) { + return encodeString(string, ENCODING); + } + + /** + * Perform URL encoding. + * + * @param string the string to encode + * @param charset the charset to use for encoding + * @return encoded string. + */ + public static String encodeString(String string, Charset charset) { + if (charset == null) + charset = ENCODING; + byte[] bytes = null; + bytes = string.getBytes(charset); + + int len = bytes.length; + byte[] encoded = new byte[bytes.length * 3]; + int n = 0; + boolean noEncode = true; + + for (int i = 0; i < len; i++) { + byte b = bytes[i]; + + if (b == ' ') { + noEncode = false; + encoded[n++] = (byte) '+'; + } else if (b >= 'a' && b <= 'z' || + b >= 'A' && b <= 'Z' || + b >= '0' && b <= '9') { + encoded[n++] = b; + } else { + noEncode = false; + encoded[n++] = (byte) '%'; + byte nibble = (byte) ((b & 0xf0) >> 4); + if (nibble >= 10) + encoded[n++] = (byte) ('A' + nibble - 10); + else + encoded[n++] = (byte) ('0' + nibble); + nibble = (byte) (b & 0xf); + if (nibble >= 10) + encoded[n++] = (byte) ('A' + nibble - 10); + else + encoded[n++] = (byte) ('0' + nibble); + } + } + + if (noEncode) + return string; + + return new String(encoded, 0, n, charset); + } + + public void decode(String query) { + decodeTo(query, this, ENCODING); + } + + public void decode(String query, Charset charset) { + decodeTo(query, this, charset); + } + + /** + * Encode MultiMap with % encoding for UTF8 sequences. + * + * @return the MultiMap as a string with % encoding + */ + public String encode() { + return encode(ENCODING, false); + } + + /** + * Encode MultiMap with % encoding for arbitrary Charset sequences. + * + * @param charset the charset to use for encoding + * @return the MultiMap as a string encoded with % encodings + */ + public String encode(Charset charset) { + return encode(charset, false); + } + + /** + * Encode MultiMap with % encoding. + * + * @param charset the charset to encode with + * @param equalsForNullValue if True, then an '=' is always used, even + * for parameters without a value. e.g. "blah?a=&b=&c=". + * @return the MultiMap as a string encoded with % encodings + */ + public String encode(Charset charset, boolean equalsForNullValue) { + return encode(this, charset, equalsForNullValue); + } + + @Override + public Object clone() { + return new UrlEncoded(this); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/content/handler/HttpContentHandler.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/content/handler/HttpContentHandler.java new file mode 100644 index 000000000..fd3856bcc --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/content/handler/HttpContentHandler.java @@ -0,0 +1,9 @@ +package com.fireflysource.net.http.common.content.handler; + +import com.fireflysource.common.io.AsyncCloseable; + +import java.nio.ByteBuffer; +import java.util.function.BiConsumer; + +public interface HttpContentHandler extends BiConsumer, AsyncCloseable { +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/content/provider/HttpContentProvider.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/content/provider/HttpContentProvider.java new file mode 100644 index 000000000..f1200a0c4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/content/provider/HttpContentProvider.java @@ -0,0 +1,6 @@ +package com.fireflysource.net.http.common.content.provider; + +import com.fireflysource.common.io.InputChannel; + +public interface HttpContentProvider extends InputChannel { +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/BadMessageException.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/BadMessageException.java new file mode 100644 index 000000000..6f67d9c8d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/BadMessageException.java @@ -0,0 +1,50 @@ +package com.fireflysource.net.http.common.exception; + +/** + *

+ * Exception thrown to indicate a Bad HTTP Message has either been received or + * attempted to be generated. Typically these are handled with either 400 or 500 + * responses. + *

+ */ +public class BadMessageException extends RuntimeException { + private static final long serialVersionUID = -4907256166019479626L; + private final int code; + private final String reason; + + public BadMessageException() { + this(400, null); + } + + public BadMessageException(int code) { + this(code, null); + } + + public BadMessageException(String reason) { + this(400, reason); + } + + public BadMessageException(int code, String reason) { + super(code + ": " + reason); + this.code = code; + this.reason = reason; + } + + public BadMessageException(String reason, Throwable cause) { + this(400, reason, cause); + } + + public BadMessageException(int code, String reason, Throwable cause) { + super(code + ": " + reason, cause); + this.code = code; + this.reason = reason; + } + + public int getCode() { + return code; + } + + public String getReason() { + return reason; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/NotSupportContentEncoding.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/NotSupportContentEncoding.java new file mode 100644 index 000000000..76866f9f5 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/NotSupportContentEncoding.java @@ -0,0 +1,13 @@ +package com.fireflysource.net.http.common.exception; + +import java.io.IOException; + +/** + * @author Pengtao Qiu + */ +public class NotSupportContentEncoding extends IOException { + + public NotSupportContentEncoding(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/URISyntaxRuntimeException.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/URISyntaxRuntimeException.java new file mode 100644 index 000000000..02816b409 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/exception/URISyntaxRuntimeException.java @@ -0,0 +1,10 @@ +package com.fireflysource.net.http.common.exception; + +import java.net.URISyntaxException; + +public class URISyntaxRuntimeException extends RuntimeException { + + public URISyntaxRuntimeException(String message, URISyntaxException e) { + super(message, e); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/AcceptMIMEMatchType.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/AcceptMIMEMatchType.java new file mode 100644 index 000000000..3dc93399c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/AcceptMIMEMatchType.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.model; + +/** + * @author Pengtao Qiu + */ +public enum AcceptMIMEMatchType { + PARENT, CHILD, ALL, EXACT +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/AcceptMIMEType.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/AcceptMIMEType.java new file mode 100644 index 000000000..c3aedd120 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/AcceptMIMEType.java @@ -0,0 +1,59 @@ +package com.fireflysource.net.http.common.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class AcceptMIMEType { + private String parentType; + private String childType; + private float quality = 1.0f; + private AcceptMIMEMatchType matchType; + + public String getParentType() { + return parentType; + } + + public void setParentType(String parentType) { + this.parentType = parentType; + } + + public String getChildType() { + return childType; + } + + public void setChildType(String childType) { + this.childType = childType; + } + + public float getQuality() { + return quality; + } + + public void setQuality(float quality) { + this.quality = quality; + } + + public AcceptMIMEMatchType getMatchType() { + return matchType; + } + + public void setMatchType(AcceptMIMEMatchType matchType) { + this.matchType = matchType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AcceptMIMEType that = (AcceptMIMEType) o; + return Objects.equals(parentType, that.parentType) && + Objects.equals(childType, that.childType); + } + + @Override + public int hashCode() { + return Objects.hash(parentType, childType); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/ContentEncoding.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/ContentEncoding.java new file mode 100644 index 000000000..fcf065c58 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/ContentEncoding.java @@ -0,0 +1,32 @@ +package com.fireflysource.net.http.common.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public enum ContentEncoding { + + GZIP("gzip"), + DEFLATE("deflate"), + BR("br"); + + private static class Holder { + private static final Map map = new HashMap<>(4); + } + + private final String value; + + ContentEncoding(String value) { + this.value = value; + Holder.map.put(value, this); + } + + public String getValue() { + return value; + } + + public static Optional from(String value) { + return Optional.ofNullable(value).map(Holder.map::get); + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/Cookie.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/Cookie.java new file mode 100644 index 000000000..930660b88 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/Cookie.java @@ -0,0 +1,348 @@ +package com.fireflysource.net.http.common.model; + +import java.util.Locale; + +public class Cookie { + + // + // The value of the cookie itself. + // + + private String name; // NAME= ... "$Name" style is reserved + private String value; // value of NAME + + // + // Attributes encoded in the header's cookie fields. + // + + private String comment; // ;Comment=VALUE ... describes cookie's use + // ;Discard ... implied by maxAge < 0 + private String domain; // ;Domain=VALUE ... domain that sees cookie + private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire + private String path; // ;Path=VALUE ... URLs that see the cookie + private boolean secure; // ;Secure ... e.g. use SSL + private int version = 0; // ;Version=1 ... means RFC 2109++ style + private boolean isHttpOnly = false; + + public Cookie() { + + } + + /** + * Constructs a cookie with the specified name and value. + * + *

+ * The name must conform to RFC 2109. However, vendors may provide a + * configuration option that allows cookie names conforming to the original + * Netscape Cookie Specification to be accepted. + * + *

+ * The name of a cookie cannot be changed once the cookie has been created. + * + *

+ * The value can be anything the server chooses to send. Its value is + * probably of interest only to the server. The cookie's value can be + * changed after creation with the setValue method. + * + *

+ * By default, cookies are created according to the Netscape cookie + * specification. The version can be changed with the + * setVersion method. + * + * @param name the name of the cookie + * @param value the value of the cookie + * @throws IllegalArgumentException if the cookie name is null or empty or contains any illegal + * characters (for example, a comma, space, or semicolon) or + * matches a token reserved for use by the cookie protocol + * @see #setValue + * @see #setVersion + */ + public Cookie(String name, String value) { + if (name == null || name.length() == 0) { + throw new IllegalArgumentException("the cookie name is empty"); + } + + this.name = name; + this.value = value; + } + + /** + * Returns the comment describing the purpose of this cookie, or + * null if the cookie has no comment. + * + * @return the comment of the cookie, or null if unspecified + * @see #setComment + */ + public String getComment() { + return comment; + } + + /** + * Specifies a comment that describes a cookie's purpose. The comment is + * useful if the browser presents the cookie to the user. Comments are not + * supported by Netscape Version 0 cookies. + * + * @param purpose a String specifying the comment to display to the + * user + * @see #getComment + */ + public void setComment(String purpose) { + comment = purpose; + } + + /** + * Gets the domain name of this Cookie. + * + *

+ * Domain names are formatted according to RFC 2109. + * + * @return the domain name of this Cookie + * @see #setDomain + */ + public String getDomain() { + return domain; + } + + /** + * Specifies the domain within which this cookie should be presented. + * + *

+ * The form of the domain name is specified by RFC 2109. A domain name + * begins with a dot (.foo.com) and means that the cookie is + * visible to servers in a specified Domain Name System (DNS) zone (for + * example, www.foo.com, but not a.b.foo.com). By + * default, cookies are only returned to the server that sent them. + * + * @param domain the domain name within which this cookie is visible; form is + * according to RFC 2109 + * @see #getDomain + */ + public void setDomain(String domain) { + this.domain = domain.toLowerCase(Locale.ENGLISH); // IE allegedly needs + // this + } + + /** + * Gets the maximum age in seconds of this Cookie. + * + *

+ * By default, -1 is returned, which indicates that the cookie + * will persist until browser shutdown. + * + * @return an integer specifying the maximum age of the cookie in seconds; + * if negative, means the cookie persists until browser shutdown + * @see #setMaxAge + */ + public int getMaxAge() { + return maxAge; + } + + /** + * Sets the maximum age in seconds for this Cookie. + * + *

+ * A positive value indicates that the cookie will expire after that many + * seconds have passed. Note that the value is the maximum age when + * the cookie will expire, not the cookie's current age. + * + *

+ * A negative value means that the cookie is not stored persistently and + * will be deleted when the Web browser exits. A zero value causes the + * cookie to be deleted. + * + * @param expiry an integer specifying the maximum age of the cookie in + * seconds; if negative, means the cookie is not stored; if zero, + * deletes the cookie + * @see #getMaxAge + */ + public void setMaxAge(int expiry) { + maxAge = expiry; + } + + /** + * Returns the path on the server to which the browser returns this cookie. + * The cookie is visible to all subpaths on the server. + * + * @return a String specifying a path , for example, + * /catalog + * @see #setPath + */ + public String getPath() { + return path; + } + + /** + * Specifies a path for the cookie to which the client should return the + * cookie. + * + *

+ * The cookie is visible to all the pages in the directory you specify, and + * all the pages in that directory's subdirectories. A cookie's path, for + * example, /catalog, which makes the cookie visible to all + * directories on the server under /catalog. + * + *

+ * Consult RFC 2109 (available on the Internet) for more information on + * setting path names for cookies. + * + * @param uri a String specifying a path + * @see #getPath + */ + public void setPath(String uri) { + path = uri; + } + + /** + * Returns true if the browser is sending cookies only over a + * secure protocol, or false if the browser can send cookies + * using any protocol. + * + * @return true if the browser uses a secure protocol, + * false otherwise + * @see #setSecure + */ + public boolean getSecure() { + return secure; + } + + /** + * Indicates to the browser whether the cookie should only be sent using a + * secure protocol, such as HTTPS or SSL. + * + *

+ * The default value is false. + * + * @param flag if true, sends the cookie from the browser to the + * server only when using a secure protocol; if + * false, sent on any protocol + * @see #getSecure + */ + public void setSecure(boolean flag) { + secure = flag; + } + + /** + * Returns the name of the cookie. The name cannot be changed after + * creation. + * + * @return the name of the cookie + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Gets the current value of this Cookie. + * + * @return the current value of this Cookie + * @see #setValue + */ + public String getValue() { + return value; + } + + /** + * Assigns a new value to this Cookie. + * + *

+ * If you use a binary value, you may want to use BASE64 encoding. + * + *

+ * With Version 0 cookies, values should not contain white space, brackets, + * parentheses, equals signs, commas, double quotes, slashes, question + * marks, at signs, colons, and semicolons. Empty values may not behave the + * same way on all browsers. + * + * @param newValue the new value of the cookie + * @see #getValue + */ + public void setValue(String newValue) { + value = newValue; + } + + /** + * Returns the version of the protocol this cookie complies with. Version 1 + * complies with RFC 2109, and version 0 complies with the original cookie + * specification drafted by Netscape. Cookies provided by a browser use and + * identify the browser's cookie version. + * + * @return 0 if the cookie complies with the original Netscape + * specification; 1 if the cookie complies with RFC 2109 + * @see #setVersion + */ + public int getVersion() { + return version; + } + + /** + * Sets the version of the cookie protocol that this Cookie complies with. + * + *

+ * Version 0 complies with the original Netscape cookie specification. + * Version 1 complies with RFC 2109. + * + *

+ * Since RFC 2109 is still somewhat new, consider version 1 as experimental; + * do not use it yet on production sites. + * + * @param v 0 if the cookie should comply with the original Netscape + * specification; 1 if the cookie should comply with RFC 2109 + * @see #getVersion + */ + public void setVersion(int v) { + version = v; + } + + /** + * Overrides the standard java.lang.Object.clone method to + * return a copy of this Cookie. + */ + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Checks whether this Cookie has been marked as HttpOnly. + * + * @return true if this Cookie has been marked as HttpOnly, false + * otherwise + */ + public boolean isHttpOnly() { + return isHttpOnly; + } + + /** + * Marks or unmarks this Cookie as HttpOnly. + * + *

+ * If isHttpOnly is set to true, this cookie is marked as + * HttpOnly, by adding the HttpOnly attribute to it. + * + *

+ * HttpOnly cookies are not supposed to be exposed to client-side + * scripting code, and may therefore help mitigate certain kinds of + * cross-site scripting attacks. + * + * @param isHttpOnly true if this cookie is to be marked as HttpOnly, false + * otherwise + */ + public void setHttpOnly(boolean isHttpOnly) { + this.isHttpOnly = isHttpOnly; + } + + @Override + public String toString() { + return "Cookie [name=" + name + ", value=" + value + ", comment=" + comment + ", domain=" + domain + ", maxAge=" + + maxAge + ", path=" + path + ", secure=" + secure + ", version=" + version + ", isHttpOnly=" + + isHttpOnly + "]"; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HostPort.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HostPort.java new file mode 100644 index 000000000..498694275 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HostPort.java @@ -0,0 +1,114 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.string.StringUtils; + +/** + * Parse an authority string into Host and Port + *

Parse a string in the form "host:port", handling IPv4 an IPv6 hosts

+ * + *

The System property "com.fireflysource.net.http.model.HostPort.STRIP_IPV6" can be set to a boolean + * value to control of the square brackets are stripped off IPv6 addresses (default false).

+ */ +public class HostPort { + private final static boolean STRIP_IPV6 = Boolean.parseBoolean(System.getProperty("com.fireflysource.net.http.model.HostPort.STRIP_IPV6", "false")); + + private final String host; + private final int port; + + public HostPort(String authority) throws IllegalArgumentException { + if (authority == null) + throw new IllegalArgumentException("No Authority"); + try { + if (authority.isEmpty()) { + host = authority; + port = 0; + } else if (authority.charAt(0) == '[') { + // ipv6reference + int close = authority.lastIndexOf(']'); + if (close < 0) + throw new IllegalArgumentException("Bad IPv6 host"); + host = STRIP_IPV6 ? authority.substring(1, close) : authority.substring(0, close + 1); + + if (authority.length() > close + 1) { + if (authority.charAt(close + 1) != ':') + throw new IllegalArgumentException("Bad IPv6 port"); + port = StringUtils.toInt(authority, close + 2); + } else + port = 0; + } else { + // ipv4address or hostname + int c = authority.lastIndexOf(':'); + if (c >= 0) { + host = authority.substring(0, c); + port = StringUtils.toInt(authority, c + 1); + } else { + host = authority; + port = 0; + } + } + } catch (IllegalArgumentException iae) { + throw iae; + } catch (final Exception ex) { + throw new IllegalArgumentException("Bad HostPort") { + { + initCause(ex); + } + }; + } + if (host == null) + throw new IllegalArgumentException("Bad host"); + if (port < 0) + throw new IllegalArgumentException("Bad port"); + } + + /* ------------------------------------------------------------ */ + + /** + * Normalize IPv6 address as per https://www.ietf.org/rfc/rfc2732.txt + * + * @param host A host name + * @return Host name surrounded by '[' and ']' as needed. + */ + public static String normalizeHost(String host) { + // if it is normalized IPv6 or could not be IPv6, return + if (host.isEmpty() || host.charAt(0) == '[' || host.indexOf(':') < 0) + return host; + + // normalize with [ ] + return "[" + host + "]"; + } + + /* ------------------------------------------------------------ */ + + /** + * Get the host. + * + * @return the host + */ + public String getHost() { + return host; + } + + /* ------------------------------------------------------------ */ + + /** + * Get the port. + * + * @return the port + */ + public int getPort() { + return port; + } + + /* ------------------------------------------------------------ */ + + /** + * Get the port. + * + * @param defaultPort, the default port to return if a port is not specified + * @return the port + */ + public int getPort(int defaultPort) { + return port > 0 ? port : defaultPort; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HostPortHttpField.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HostPortHttpField.java new file mode 100644 index 000000000..cd5915576 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HostPortHttpField.java @@ -0,0 +1,49 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.net.http.common.exception.BadMessageException; + +public class HostPortHttpField extends HttpField { + + private final HostPort hostPort; + + public HostPortHttpField(String authority) { + this(HttpHeader.HOST, HttpHeader.HOST.getValue(), authority); + } + + public HostPortHttpField(HttpHeader header, String name, String authority) { + super(header, name, authority); + try { + hostPort = new HostPort(authority); + } catch (Exception e) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad HostPort", e); + } + } + + /** + * Get the host. + * + * @return the host + */ + public String getHost() { + return hostPort.getHost(); + } + + /** + * Get the port. + * + * @return the port + */ + public int getPort() { + return hostPort.getPort(); + } + + /** + * Get the port. + * + * @param defaultPort The default port to return if no port set + * @return the port + */ + public int getPort(int defaultPort) { + return hostPort.getPort(defaultPort); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpCompliance.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpCompliance.java new file mode 100644 index 000000000..aeb7d2a6d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpCompliance.java @@ -0,0 +1,170 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +/** + * HTTP compliance modes for HTTP parsing and handling. + * A Compliance mode consists of a set of {@link HttpComplianceSection}s which are applied + * when the mode is enabled. + *

+ * Currently the set of modes is an enum and cannot be dynamically extended, but future major releases may convert this + * to a class. To modify modes there are four custom modes that can be modified by setting the property + * HttpCompliance.CUSTOMn (where 'n' is '0', '1', '2' or '3'), to a comma separated + * list of sections. The list should start with one of the following strings:

+ *
0
No {@link HttpComplianceSection}s
+ *
*
All {@link HttpComplianceSection}s
+ *
RFC2616
The set of {@link HttpComplianceSection}s application to https://tools.ietf.org/html/rfc2616, + * but not https://tools.ietf.org/html/rfc7230
+ *
RFC7230
The set of {@link HttpComplianceSection}s application to https://tools.ietf.org/html/rfc7230
+ *
+ * The remainder of the list can contain then names of {@link HttpComplianceSection}s to include them in the mode, or prefixed + * with a '-' to exclude thm from the mode. Note that modes may have some historic minor differences from the strict + * RFC compliance, for example the RFC2616_LEGACY HttpCompliance is defined as + * RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE. + *

+ * Note also that the {@link EnumSet} return by {@link HttpCompliance#sections()} is mutable, so that modes may + * be altered in code and will affect all usages of the mode. + */ +public enum HttpCompliance { + + LEGACY(sectionsBySpec("0,METHOD_CASE_SENSITIVE")), + + /** + * The legacy RFC2616 support, which incorrectly excludes + * {@link HttpComplianceSection#METHOD_CASE_SENSITIVE}, + * {@link HttpComplianceSection#FIELD_COLON}, + * {@link HttpComplianceSection#TRANSFER_ENCODING_WITH_CONTENT_LENGTH}, + * {@link HttpComplianceSection#MULTIPLE_CONTENT_LENGTHS}, + */ + RFC2616_LEGACY(sectionsBySpec("RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE,-TRANSFER_ENCODING_WITH_CONTENT_LENGTH,-MULTIPLE_CONTENT_LENGTHS")), + + /** + * The strict RFC2616 support mode + */ + RFC2616(sectionsBySpec("RFC2616")), + + /** + * RFC7230 support, which incorrectly excludes {@link HttpComplianceSection#METHOD_CASE_SENSITIVE} + */ + RFC7230_LEGACY(sectionsBySpec("RFC7230,-METHOD_CASE_SENSITIVE")), + + /** + * The RFC7230 support mode + */ + RFC7230(sectionsBySpec("RFC7230")), + + @Deprecated + CUSTOM0(sectionsByProperty("CUSTOM0")), + + @Deprecated + CUSTOM1(sectionsByProperty("CUSTOM1")), + + @Deprecated + CUSTOM2(sectionsByProperty("CUSTOM2")), + + @Deprecated + CUSTOM3(sectionsByProperty("CUSTOM3")); + + private static final LazyLogger LOG = SystemLogger.create(HttpCompliance.class); + private final static Map REQUIRED = new HashMap<>(); + + static { + for (HttpComplianceSection section : HttpComplianceSection.values()) { + for (HttpCompliance compliance : HttpCompliance.values()) { + if (compliance.sections().contains(section)) { + REQUIRED.put(section, compliance); + break; + } + } + } + } + + private final EnumSet SECTIONS; + + HttpCompliance(EnumSet sections) { + SECTIONS = sections; + } + + private static EnumSet sectionsByProperty(String property) { + String s = System.getProperty(HttpCompliance.class.getName() + property); + return sectionsBySpec(s == null ? "*" : s); + } + + static EnumSet sectionsBySpec(String spec) { + EnumSet sections; + String[] elements = spec.split("\\s*,\\s*"); + int i = 0; + + switch (elements[i]) { + case "0": + sections = EnumSet.noneOf(HttpComplianceSection.class); + i++; + break; + + case "*": + i++; + sections = EnumSet.allOf(HttpComplianceSection.class); + break; + + case "RFC2616": + sections = EnumSet.complementOf(EnumSet.of( + HttpComplianceSection.NO_FIELD_FOLDING, + HttpComplianceSection.NO_HTTP_0_9)); + i++; + break; + + case "RFC7230": + i++; + sections = EnumSet.allOf(HttpComplianceSection.class); + break; + + default: + sections = EnumSet.noneOf(HttpComplianceSection.class); + break; + } + + while (i < elements.length) { + String element = elements[i++]; + boolean exclude = element.startsWith("-"); + if (exclude) + element = element.substring(1); + HttpComplianceSection section = HttpComplianceSection.valueOf(element); + if (section == null) { + LOG.warn("Unknown section '" + element + "' in HttpCompliance spec: " + spec); + continue; + } + if (exclude) + sections.remove(section); + else + sections.add(section); + + } + + return sections; + } + + /** + * @param section The section to query + * @return The minimum compliance required to enable the section. + */ + public static HttpCompliance requiredCompliance(HttpComplianceSection section) { + return REQUIRED.get(section); + } + + /** + * Get the set of {@link HttpComplianceSection}s supported by this compliance mode. This set + * is mutable, so it can be modified. Any modification will affect all usages of the mode + * within the same {@link ClassLoader}. + * + * @return The set of {@link HttpComplianceSection}s supported by this compliance mode. + */ + public EnumSet sections() { + return SECTIONS; + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpComplianceSection.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpComplianceSection.java new file mode 100644 index 000000000..c4841248a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpComplianceSection.java @@ -0,0 +1,30 @@ +package com.fireflysource.net.http.common.model; + +public enum HttpComplianceSection { + CASE_INSENSITIVE_FIELD_VALUE_CACHE("", "Use case insensitive field value cache"), + METHOD_CASE_SENSITIVE("https://tools.ietf.org/html/rfc7230#section-3.1.1", "Method is case-sensitive"), + FIELD_COLON("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"), + FIELD_NAME_CASE_INSENSITIVE("https://tools.ietf.org/html/rfc7230#section-3.2", "Field name is case-insensitive"), + NO_WS_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Whitespace not allowed after field name"), + NO_FIELD_FOLDING("https://tools.ietf.org/html/rfc7230#section-3.2.4", "No line Folding"), + NO_HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2", "No HTTP/0.9"), + TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"), + MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths"); + + final String url; + final String description; + + HttpComplianceSection(String url, String description) { + this.url = url; + this.description = description; + } + + public String getURL() { + return url; + } + + public String getDescription() { + return description; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpField.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpField.java new file mode 100644 index 000000000..782ec2d29 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpField.java @@ -0,0 +1,356 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.string.StringUtils; + +import java.util.Objects; + +public class HttpField { + + private final static String ZERO_QUALITY = "q=0"; + private final HttpHeader header; + private final String name; + private final String value; + // cached hashcode for case insensitive name + private int hash = 0; + + public HttpField(HttpHeader header, String name, String value) { + this.header = header; + this.name = name; + this.value = value; + } + + public HttpField(HttpHeader header, String value) { + this(header, header.getValue(), value); + } + + public HttpField(HttpHeader header, HttpHeaderValue value) { + this(header, header.getValue(), value.getValue()); + } + + public HttpField(String name, String value) { + this(HttpHeader.CACHE.get(name), name, value); + } + + public HttpHeader getHeader() { + return header; + } + + public String getName() { + return name; + } + + public String getLowerCaseName() { + return header != null ? header.getLowerCaseValue() : StringUtils.asciiToLowerCase(name); + } + + public String getValue() { + return value; + } + + public int getIntValue() { + return Integer.parseInt(value); + } + + public long getLongValue() { + return Long.parseLong(value); + } + + public String[] getValues() { + if (value == null) + return null; + + QuotedCSV list = new QuotedCSV(false, value); + return list.getValues().toArray(new String[list.size()]); + } + + /** + * Look for a value in a possible multi valued field + * + * @param search Values to search for (case insensitive) + * @return True iff the value is contained in the field value entirely or + * as an element of a quoted comma separated list. List element parameters (eg qualities) are ignored, + * except if they are q=0, in which case the item itself is ignored. + */ + public boolean contains(String search) { + if (search == null) + return value == null; + if (search.length() == 0) + return false; + if (value == null) + return false; + if (search.equals(value)) + return true; + + search = StringUtils.asciiToLowerCase(search); + + int state = 0; + int match = 0; + int param = 0; + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (state) { + case 0: // initial white space + switch (c) { + case '"': // open quote + match = 0; + state = 2; + break; + + case ',': // ignore leading empty field + break; + + case ';': // ignore leading empty field parameter + param = -1; + match = -1; + state = 5; + break; + + case ' ': // more white space + case '\t': + break; + + default: // character + match = Character.toLowerCase(c) == search.charAt(0) ? 1 : -1; + state = 1; + break; + } + break; + + case 1: // In token + switch (c) { + case ',': // next field + // Have we matched the token? + if (match == search.length()) + return true; + state = 0; + break; + + case ';': + param = match >= 0 ? 0 : -1; + state = 5; // parameter + break; + + default: + if (match > 0) { + if (match < search.length()) + match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1; + else if (c != ' ' && c != '\t') + match = -1; + } + break; + + } + break; + + case 2: // In Quoted token + switch (c) { + case '\\': // quoted character + state = 3; + break; + + case '"': // end quote + state = 4; + break; + + default: + if (match >= 0) { + if (match < search.length()) + match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1; + else + match = -1; + } + } + break; + + case 3: // In Quoted character in quoted token + if (match >= 0) { + if (match < search.length()) + match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1; + else + match = -1; + } + state = 2; + break; + + case 4: // WS after end quote + switch (c) { + case ' ': // white space + case '\t': // white space + break; + + case ';': + state = 5; // parameter + break; + + case ',': // end token + // Have we matched the token? + if (match == search.length()) + return true; + state = 0; + break; + + default: + // This is an illegal token, just ignore + match = -1; + } + break; + + case 5: // parameter + switch (c) { + case ',': // end token + // Have we matched the token and not q=0? + if (param != ZERO_QUALITY.length() && match == search.length()) + return true; + param = 0; + state = 0; + break; + + case ' ': // white space + case '\t': // white space + break; + + default: + if (param >= 0) { + if (param < ZERO_QUALITY.length()) + param = Character.toLowerCase(c) == ZERO_QUALITY.charAt(param) ? (param + 1) : -1; + else if (c != '0' && c != '.') + param = -1; + } + + } + break; + + default: + throw new IllegalStateException(); + } + } + + return param != ZERO_QUALITY.length() && match == search.length(); + } + + + @Override + public String toString() { + String v = getValue(); + return getName() + ": " + (v == null ? "" : v); + } + + public boolean isSameName(HttpField field) { + @SuppressWarnings("ReferenceEquality") + boolean sameObject = (field == this); + + if (field == null) + return false; + if (sameObject) + return true; + if (header != null && header == field.getHeader()) + return true; + if (name.equalsIgnoreCase(field.getName())) + return true; + return false; + } + + private int nameHashCode() { + int h = this.hash; + int len = name.length(); + if (h == 0 && len > 0) { + for (int i = 0; i < len; i++) { + // simple case insensitive hash + char c = name.charAt(i); + // assuming us-ascii (per last paragraph on http://tools.ietf.org/html/rfc7230#section-3.2.4) + if ((c >= 'a' && c <= 'z')) + c -= 0x20; + h = 31 * h + c; + } + this.hash = h; + } + return h; + } + + @Override + public int hashCode() { + int vhc = Objects.hashCode(value); + if (header == null) + return vhc ^ nameHashCode(); + return vhc ^ header.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof HttpField)) + return false; + HttpField field = (HttpField) o; + if (header != field.getHeader()) + return false; + if (!name.equalsIgnoreCase(field.getName())) + return false; + if (value == null && field.getValue() != null) + return false; + return Objects.equals(value, field.getValue()); + } + + public static class IntValueHttpField extends HttpField { + private final int intValue; + + public IntValueHttpField(HttpHeader header, String name, String value, int intValue) { + super(header, name, value); + this.intValue = intValue; + } + + public IntValueHttpField(HttpHeader header, String name, String value) { + this(header, name, value, Integer.parseInt(value)); + } + + public IntValueHttpField(HttpHeader header, String name, int intValue) { + this(header, name, Integer.toString(intValue), intValue); + } + + public IntValueHttpField(HttpHeader header, int value) { + this(header, header.getValue(), value); + } + + @Override + public int getIntValue() { + return intValue; + } + + @Override + public long getLongValue() { + return intValue; + } + } + + public static class LongValueHttpField extends HttpField { + private final long longValue; + + public LongValueHttpField(HttpHeader header, String name, String value, long longValue) { + super(header, name, value); + this.longValue = longValue; + } + + public LongValueHttpField(HttpHeader header, String name, String value) { + this(header, name, value, Long.parseLong(value)); + } + + public LongValueHttpField(HttpHeader header, String name, long value) { + this(header, name, Long.toString(value), value); + } + + public LongValueHttpField(HttpHeader header, long value) { + this(header, header.getValue(), value); + } + + @Override + public int getIntValue() { + return (int) longValue; + } + + @Override + public long getLongValue() { + return longValue; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpFields.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpFields.java new file mode 100644 index 000000000..57286693b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpFields.java @@ -0,0 +1,835 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.QuotedStringTokenizer; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.codec.DateGenerator; +import com.fireflysource.net.http.common.codec.DateParser; + +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * HTTP Fields. A collection of HTTP header and or Trailer fields. + * + * <p> + * This class is not synchronized as it is expected that modifications will only + * be performed by a single thread. + * + * <p> + * The cookie handling provided by this class is guided by the Servlet + * specification and RFC6265. + */ +public class HttpFields implements Iterable { + + private static final LazyLogger LOG = SystemLogger.create(HttpFields.class); + + private HttpField[] fields; + private int size; + + /** + * Initialize an empty HttpFields. + */ + public HttpFields() { + fields = new HttpField[20]; + } + + /** + * Initialize an empty HttpFields. + * + * @param capacity the capacity of the http fields + */ + public HttpFields(int capacity) { + fields = new HttpField[capacity]; + } + + /** + * Initialize HttpFields from copy. + * + * @param fields the fields to copy data from + */ + public HttpFields(HttpFields fields) { + this.fields = Arrays.copyOf(fields.fields, fields.fields.length + 10); + size = fields.size; + } + + /** + * Get field value without parameters. Some field values can have + * parameters. This method separates the value from the parameters and + * optionally populates a map with the parameters. For example: + * + * <PRE> + * <p> + * FieldName : Value ; param1=val1 ; param2=val2 + * + * </PRE> + * + * @param value The Field value, possibly with parameters. + * @return The value. + */ + public static String stripParameters(String value) { + if (value == null) + return null; + + int i = value.indexOf(';'); + if (i < 0) + return value; + return value.substring(0, i).trim(); + } + + /** + * Get field value parameters. Some field values can have parameters. This + * method separates the value from the parameters and optionally populates a + * map with the parameters. For example: + * + * <PRE> + * <p> + * FieldName : Value ; param1=val1 ; param2=val2 + * + * </PRE> + * + * @param value The Field value, possibly with parameters. + * @param parameters A map to populate with the parameters, or null + * @return The value. + */ + public static String valueParameters(String value, Map parameters) { + if (value == null) + return null; + + int i = value.indexOf(';'); + if (i < 0) + return value; + if (parameters == null) + return value.substring(0, i).trim(); + + StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true); + while (tok1.hasMoreTokens()) { + String token = tok1.nextToken(); + StringTokenizer tok2 = new QuotedStringTokenizer(token, "= "); + if (tok2.hasMoreTokens()) { + String paramName = tok2.nextToken(); + String paramVal = null; + if (tok2.hasMoreTokens()) + paramVal = tok2.nextToken(); + parameters.put(paramName, paramVal); + } + } + + return value.substring(0, i).trim(); + } + + public int size() { + return size; + } + + @Override + public Iterator iterator() { + return new Itr(); + } + + public Stream stream() { + return StreamSupport.stream(Arrays.spliterator(fields, 0, size), false); + } + + /** + * Get Collection of header names. + * + * @return the unique set of field names. + */ + public Set getFieldNamesCollection() { + final Set set = new HashSet<>(size); + for (HttpField f : this) { + if (f != null) + set.add(f.getName()); + } + return set; + } + + /** + * Get enumeration of header _names. Returns an enumeration of strings + * representing the header _names for this request. + * + * @return an enumeration of field names + */ + public Enumeration getFieldNames() { + return Collections.enumeration(getFieldNamesCollection()); + } + + /** + * Get a Field by index. + * + * @param index the field index + * @return A Field value or null if the Field value has not been set + */ + public HttpField getField(int index) { + if (index >= size) + throw new NoSuchElementException(); + return fields[index]; + } + + public HttpField getField(HttpHeader header) { + for (int i = 0; i < size; i++) { + HttpField f = fields[i]; + if (f.getHeader() == header) + return f; + } + return null; + } + + public HttpField getField(String name) { + for (int i = 0; i < size; i++) { + HttpField f = fields[i]; + if (f.getName().equalsIgnoreCase(name)) + return f; + } + return null; + } + + public boolean contains(HttpField field) { + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.isSameName(field) && (f.equals(field) || f.contains(field.getValue()))) + return true; + } + return false; + } + + public boolean contains(HttpHeader header, String value) { + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.getHeader() == header && f.contains(value)) + return true; + } + return false; + } + + public boolean contains(String name, String value) { + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.getName().equalsIgnoreCase(name) && f.contains(value)) + return true; + } + return false; + } + + public boolean contains(HttpHeader header) { + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.getHeader() == header) + return true; + } + return false; + } + + public boolean containsKey(String name) { + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.getName().equalsIgnoreCase(name)) + return true; + } + return false; + } + + @Deprecated + public String getStringField(HttpHeader header) { + return get(header); + } + + public String get(HttpHeader header) { + for (int i = 0; i < size; i++) { + HttpField f = fields[i]; + if (f.getHeader() == header) + return f.getValue(); + } + return null; + } + + @Deprecated + public String getStringField(String name) { + return get(name); + } + + public String get(String header) { + for (int i = 0; i < size; i++) { + HttpField f = fields[i]; + if (f.getName().equalsIgnoreCase(header)) + return f.getValue(); + } + return null; + } + + /** + * Get multiple header of the same name + * + * @param header the header + * @return List the values + */ + public List getValuesList(HttpHeader header) { + final List list = new LinkedList<>(); + for (HttpField f : this) + if (f.getHeader() == header) + list.add(f.getValue()); + return list; + } + + /** + * Get multiple header of the same name + * + * @param name the case-insensitive field name + * @return List the header values + */ + public List getValuesList(String name) { + final List list = new LinkedList<>(); + for (HttpField f : this) + if (f.getName().equalsIgnoreCase(name)) + list.add(f.getValue()); + return list; + } + + /** + * Add comma separated values, but only if not already present. + * + * @param header The header to add the value(s) to + * @param values The value(s) to add + * @return True if headers were modified + */ + public boolean addCSV(HttpHeader header, String... values) { + QuotedCSV existing = null; + for (HttpField f : this) { + if (f.getHeader() == header) { + if (existing == null) + existing = new QuotedCSV(false); + existing.addValue(f.getValue()); + } + } + + String value = addCSV(existing, values); + if (value != null) { + add(header, value); + return true; + } + return false; + } + + /** + * Add comma separated values, but only if not already present. + * + * @param name The header to add the value(s) to + * @param values The value(s) to add + * @return True if headers were modified + */ + public boolean addCSV(String name, String... values) { + QuotedCSV existing = null; + for (HttpField f : this) { + if (f.getName().equalsIgnoreCase(name)) { + if (existing == null) + existing = new QuotedCSV(false); + existing.addValue(f.getValue()); + } + } + String value = addCSV(existing, values); + if (value != null) { + add(name, value); + return true; + } + return false; + } + + protected String addCSV(QuotedCSV existing, String... values) { + // remove any existing values from the new values + boolean add = true; + if (existing != null && !existing.isEmpty()) { + add = false; + + for (int i = values.length; i-- > 0; ) { + String unquoted = QuotedCSV.unquote(values[i]); + if (existing.getValues().contains(unquoted)) + values[i] = null; + else + add = true; + } + } + + if (add) { + StringBuilder value = new StringBuilder(); + for (String v : values) { + if (v == null) + continue; + if (value.length() > 0) + value.append(", "); + value.append(v); + } + if (value.length() > 0) + return value.toString(); + } + + return null; + } + + /** + * Get multiple field values of the same name, split as a {@link QuotedCSV} + * + * @param header The header + * @param keepQuotes True if the fields are kept quoted + * @return List the values with OWS stripped + */ + public List getCSV(HttpHeader header, boolean keepQuotes) { + QuotedCSV values = null; + for (HttpField f : this) { + if (f.getHeader() == header) { + if (values == null) + values = new QuotedCSV(keepQuotes); + values.addValue(f.getValue()); + } + } + return values == null ? Collections.emptyList() : values.getValues(); + } + + /** + * Get multiple field values of the same name as a {@link QuotedCSV} + * + * @param name the case-insensitive field name + * @param keepQuotes True if the fields are kept quoted + * @return List the values with OWS stripped + */ + public List getCSV(String name, boolean keepQuotes) { + QuotedCSV values = null; + for (HttpField f : this) { + if (f.getName().equalsIgnoreCase(name)) { + if (values == null) + values = new QuotedCSV(keepQuotes); + values.addValue(f.getValue()); + } + } + return values == null ? Collections.emptyList() : values.getValues(); + } + + /** + * Get multiple field values of the same name, split and sorted as a + * {@link QuotedQualityCSV} + * + * @param header The header + * @return List the values in quality order with the q param and OWS + * stripped + */ + public List getQualityCSV(HttpHeader header) { + QuotedQualityCSV values = null; + for (HttpField f : this) { + if (f.getHeader() == header) { + if (values == null) + values = new QuotedQualityCSV(); + values.addValue(f.getValue()); + } + } + + return values == null ? Collections.emptyList() : values.getValues(); + } + + /** + * Get multiple field values of the same name, split and sorted as a + * {@link QuotedQualityCSV} + * + * @param name the case-insensitive field name + * @return List the values in quality order with the q param and OWS + * stripped + */ + public List getQualityCSV(String name) { + QuotedQualityCSV values = null; + for (HttpField f : this) { + if (f.getName().equalsIgnoreCase(name)) { + if (values == null) + values = new QuotedQualityCSV(); + values.addValue(f.getValue()); + } + } + return values == null ? Collections.emptyList() : values.getValues(); + } + + /** + * Get multi headers + * + * @param name the case-insensitive field name + * @return Enumeration of the values + */ + public Enumeration getValues(final String name) { + for (int i = 0; i < size; i++) { + final HttpField f = fields[i]; + + if (f.getName().equalsIgnoreCase(name) && f.getValue() != null) { + final int first = i; + return new Enumeration() { + HttpField field = f; + int i = first + 1; + + @Override + public boolean hasMoreElements() { + if (field == null) { + while (i < size) { + field = fields[i++]; + if (field.getName().equalsIgnoreCase(name) && field.getValue() != null) + return true; + } + field = null; + return false; + } + return true; + } + + @Override + public String nextElement() throws NoSuchElementException { + if (hasMoreElements()) { + String value = field.getValue(); + field = null; + return value; + } + throw new NoSuchElementException(); + } + }; + } + } + + List empty = Collections.emptyList(); + return Collections.enumeration(empty); + } + + public void put(HttpField field) { + boolean put = false; + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.isSameName(field)) { + if (put) { + System.arraycopy(fields, i + 1, fields, i, --size - i); + } else { + fields[i] = field; + put = true; + } + } + } + if (!put) + add(field); + } + + /** + * Set a field. + * + * @param name the name of the field + * @param value the value of the field. If null the field is cleared. + */ + public void put(String name, String value) { + if (value == null) + remove(name); + else + put(new HttpField(name, value)); + } + + public void put(HttpHeader header, HttpHeaderValue value) { + put(header, value.toString()); + } + + /** + * Set a field. + * + * @param header the header name of the field + * @param value the value of the field. If null the field is cleared. + */ + public void put(HttpHeader header, String value) { + if (value == null) + remove(header); + else + put(new HttpField(header, value)); + } + + /** + * Set a field. + * + * @param name the name of the field + * @param list the List value of the field. If null the field is cleared. + */ + public void put(String name, List list) { + remove(name); + for (String v : list) + if (v != null) + add(name, v); + } + + /** + * Add to or set a field. If the field is allowed to have multiple values, + * add will add multiple headers of the same name. + * + * @param name the name of the field + * @param value the value of the field. + */ + public void add(String name, String value) { + if (value == null) + return; + + HttpField field = new HttpField(name, value); + add(field); + } + + public void add(HttpHeader header, HttpHeaderValue value) { + add(header, value.toString()); + } + + /** + * Add to or set a field. If the field is allowed to have multiple values, + * add will add multiple headers of the same name. + * + * @param header the header + * @param value the value of the field. + */ + public void add(HttpHeader header, String value) { + if (value == null) + throw new IllegalArgumentException("null value"); + + HttpField field = new HttpField(header, value); + add(field); + } + + /** + * Remove a field. + * + * @param name the field to remove + * @return the header that was removed + */ + public HttpField remove(HttpHeader name) { + HttpField removed = null; + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.getHeader() == name) { + removed = f; + System.arraycopy(fields, i + 1, fields, i, --size - i); + } + } + return removed; + } + + /** + * Remove a field. + * + * @param name the field to remove + * @return the header that was removed + */ + public HttpField remove(String name) { + HttpField removed = null; + for (int i = size; i-- > 0; ) { + HttpField f = fields[i]; + if (f.getName().equalsIgnoreCase(name)) { + removed = f; + System.arraycopy(fields, i + 1, fields, i, --size - i); + } + } + return removed; + } + + /** + * Get a header as an long value. Returns the value of an integer field or + * -1 if not found. The case of the field name is ignored. + * + * @param name the case-insensitive field name + * @return the value of the field as a long + * @throws NumberFormatException If bad long found + */ + public long getLongField(String name) throws NumberFormatException { + HttpField field = getField(name); + return field == null ? -1L : field.getLongValue(); + } + + /** + * Get a header as a date value. Returns the value of a date field, or -1 if + * not found. The case of the field name is ignored. + * + * @param name the case-insensitive field name + * @return the value of the field as a number of milliseconds since unix + * epoch + */ + public long getDateField(String name) { + HttpField field = getField(name); + if (field == null) + return -1; + + String val = valueParameters(field.getValue(), null); + if (val == null) + return -1; + + final long date = DateParser.parseDate(val); + if (date == -1) + throw new IllegalArgumentException("Cannot convert date: " + val); + return date; + } + + /** + * Sets the value of an long field. + * + * @param name the field name + * @param value the field long value + */ + public void putLongField(HttpHeader name, long value) { + String v = Long.toString(value); + put(name, v); + } + + /** + * Sets the value of an long field. + * + * @param name the field name + * @param value the field long value + */ + public void putLongField(String name, long value) { + String v = Long.toString(value); + put(name, v); + } + + /** + * Sets the value of a date field. + * + * @param name the field name + * @param date the field date value + */ + public void putDateField(HttpHeader name, long date) { + String d = DateGenerator.formatDate(date); + put(name, d); + } + + /** + * Sets the value of a date field. + * + * @param name the field name + * @param date the field date value + */ + public void putDateField(String name, long date) { + String d = DateGenerator.formatDate(date); + put(name, d); + } + + /** + * Sets the value of a date field. + * + * @param name the field name + * @param date the field date value + */ + public void addDateField(String name, long date) { + String d = DateGenerator.formatDate(date); + add(name, d); + } + + @Override + public int hashCode() { + int hash = 0; + for (HttpField field : fields) + hash += field.hashCode(); + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof HttpFields)) + return false; + + HttpFields that = (HttpFields) o; + + // Order is not important, so we cannot rely on List.equals(). + if (size() != that.size()) + return false; + + loop: + for (HttpField fi : this) { + for (HttpField fa : that) { + if (fi.equals(fa)) + continue loop; + } + return false; + } + return true; + } + + @Override + public String toString() { + try { + StringBuilder buffer = new StringBuilder(); + for (HttpField field : this) { + if (field != null) { + String tmp = field.getName(); + if (tmp != null) + buffer.append(tmp); + buffer.append(": "); + tmp = field.getValue(); + if (tmp != null) + buffer.append(tmp); + buffer.append("\r\n"); + } + } + buffer.append("\r\n"); + return buffer.toString(); + } catch (Exception e) { + LOG.warn("http fields toString exception", e); + return e.toString(); + } + } + + public void clear() { + size = 0; + } + + public void add(HttpField field) { + if (field != null) { + if (size == fields.length) + fields = Arrays.copyOf(fields, size * 2); + fields[size++] = field; + } + } + + public void addAll(HttpFields fields) { + for (int i = 0; i < fields.size; i++) + add(fields.fields[i]); + } + + /** + * Add fields from another HttpFields instance. Single valued fields are + * replaced, while all others are added. + * + * @param fields the fields to add + */ + public void add(HttpFields fields) { + if (fields == null) + return; + + Enumeration e = fields.getFieldNames(); + while (e.hasMoreElements()) { + String name = e.nextElement(); + Enumeration values = fields.getValues(name); + while (values.hasMoreElements()) + add(name, values.nextElement()); + } + } + + private class Itr implements Iterator { + int cursor; // index of next element to return + int last = -1; + + public boolean hasNext() { + return cursor != size; + } + + public HttpField next() { + int i = cursor; + if (i >= size) + throw new NoSuchElementException(); + cursor = i + 1; + return fields[last = i]; + } + + public void remove() { + if (last < 0) + throw new IllegalStateException(); + + System.arraycopy(fields, last + 1, fields, last, --size - last); + cursor = last; + last = -1; + } + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpHeader.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpHeader.java new file mode 100644 index 000000000..5b1065951 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpHeader.java @@ -0,0 +1,178 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.string.StringUtils; + + +public enum HttpHeader { + + /** + * General Fields. + */ + CONNECTION("Connection"), + CACHE_CONTROL("Cache-Control"), + DATE("Date"), + PRAGMA("Pragma"), + PROXY_CONNECTION("Proxy-Connection"), + TRAILER("Trailer"), + TRANSFER_ENCODING("Transfer-Encoding"), + UPGRADE("Upgrade"), + VIA("Via"), + WARNING("Warning"), + NEGOTIATE("Negotiate"), + + /** + * Entity Fields. + */ + ALLOW("Allow"), + CONTENT_ENCODING("Content-Encoding"), + CONTENT_LANGUAGE("Content-Language"), + CONTENT_LENGTH("Content-Length"), + CONTENT_LOCATION("Content-Location"), + CONTENT_MD5("Content-MD5"), + CONTENT_RANGE("Content-Range"), + CONTENT_TYPE("Content-Type"), + EXPIRES("Expires"), + LAST_MODIFIED("Last-Modified"), + + /** + * Request Fields. + */ + ACCEPT("Accept"), + ACCEPT_CHARSET("Accept-Charset"), + ACCEPT_ENCODING("Accept-Encoding"), + ACCEPT_LANGUAGE("Accept-Language"), + AUTHORIZATION("Authorization"), + EXPECT("Expect"), + FORWARDED("Forwarded"), + FROM("From"), + HOST("Host"), + IF_MATCH("If-Match"), + IF_MODIFIED_SINCE("If-Modified-Since"), + IF_NONE_MATCH("If-None-Match"), + IF_RANGE("If-Range"), + IF_UNMODIFIED_SINCE("If-Unmodified-Since"), + KEEP_ALIVE("Keep-Alive"), + MAX_FORWARDS("Max-Forwards"), + PROXY_AUTHORIZATION("Proxy-Authorization"), + RANGE("Range"), + REQUEST_RANGE("Request-Range"), + REFERER("Referer"), + TE("TE"), + USER_AGENT("User-Agent"), + X_FORWARDED_FOR("X-Forwarded-For"), + X_FORWARDED_PROTO("X-Forwarded-Proto"), + X_FORWARDED_SERVER("X-Forwarded-Server"), + X_FORWARDED_HOST("X-Forwarded-Host"), + + /** + * Response Fields. + */ + ACCEPT_RANGES("Accept-Ranges"), + AGE("Age"), + ETAG("ETag"), + LOCATION("Location"), + PROXY_AUTHENTICATE("Proxy-Authenticate"), + RETRY_AFTER("Retry-After"), + SERVER("Server"), + SERVLET_ENGINE("Servlet-Engine"), + VARY("Vary"), + WWW_AUTHENTICATE("WWW-Authenticate"), + + /** + * WebSocket Fields. + */ + ORIGIN("Origin"), + SEC_WEBSOCKET_KEY("Sec-WebSocket-Key"), + SEC_WEBSOCKET_VERSION("Sec-WebSocket-Version"), + SEC_WEBSOCKET_EXTENSIONS("Sec-WebSocket-Extensions"), + SEC_WEBSOCKET_SUBPROTOCOL("Sec-WebSocket-Protocol"), + SEC_WEBSOCKET_ACCEPT("Sec-WebSocket-Accept"), + + /** + * Other Fields. + */ + COOKIE("Cookie"), + SET_COOKIE("Set-Cookie"), + SET_COOKIE2("Set-Cookie2"), + MIME_VERSION("MIME-Version"), + IDENTITY("identity"), + + X_POWERED_BY("X-Powered-By"), + HTTP2_SETTINGS("HTTP2-Settings"), + + STRICT_TRANSPORT_SECURITY("Strict-Transport-Security"), + + /** + * HTTP2 Fields. + */ + C_METHOD(":method"), + C_SCHEME(":scheme"), + C_AUTHORITY(":authority"), + C_PATH(":path"), + C_STATUS(":status"), + + /** + * CORS Fields + */ + ACCESS_CONTROL_ALLOW_ORIGIN("Access-Control-Allow-Origin"), + ACCESS_CONTROL_EXPOSE_HEADERS("Access-Control-Expose-Headers"), + ACCESS_CONTROL_MAX_AGE("Access-Control-Max-Age"), + ACCESS_CONTROL_ALLOW_CREDENTIALS("Access-Control-Allow-Credentials"), + ACCESS_CONTROL_ALLOW_METHODS("Access-Control-Allow-Methods"), + ACCESS_CONTROL_ALLOW_HEADERS("Access-Control-Allow-Headers"), + ACCESS_CONTROL_REQUEST_METHOD("Access-Control-Request-Method"), + ACCESS_CONTROL_REQUEST_HEADERS("Access-Control-Request-Headers"), + + UNKNOWN("::UNKNOWN::"); + + public final static Trie CACHE = new ArrayTrie<>(700); + + static { + for (HttpHeader header : HttpHeader.values()) { + if (header != UNKNOWN) { + if (!CACHE.put(header.toString(), header)) { + throw new IllegalStateException(); + } + } + } + } + + private final String value; + private final String lowerCaseValue; + private final byte[] bytes; + private final byte[] bytesColonSpace; + + HttpHeader(String value) { + this.value = value; + this.lowerCaseValue = StringUtils.asciiToLowerCase(value); + bytes = StringUtils.getUtf8Bytes(value); + bytesColonSpace = StringUtils.getUtf8Bytes(value + ": "); + } + + public byte[] getBytes() { + return bytes; + } + + public byte[] getBytesColonSpace() { + return bytesColonSpace; + } + + public boolean is(String value) { + return this.value.equalsIgnoreCase(value); + } + + public String getValue() { + return value; + } + + public String getLowerCaseValue() { + return lowerCaseValue; + } + + @Override + public String toString() { + return value; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpHeaderValue.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpHeaderValue.java new file mode 100644 index 000000000..790ac70bd --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpHeaderValue.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.string.StringUtils; + +import java.util.EnumSet; + +/** + * + */ +public enum HttpHeaderValue { + + CLOSE("close"), + CHUNKED("chunked"), + GZIP("gzip"), + COMPRESS("compress"), + DEFLATE("deflate"), + BR("br"), + IDENTITY("identity"), + KEEP_ALIVE("keep-alive"), + CONTINUE("100-continue"), + PROCESSING("102-processing"), + TE("TE"), + BYTES("bytes"), + NO_CACHE("no-cache"), + UPGRADE("Upgrade"), + UNKNOWN("::UNKNOWN::"); + + public final static Trie CACHE = new ArrayTrie<>(); + private static final EnumSet __known = EnumSet.of( + HttpHeader.CONNECTION, + HttpHeader.TRANSFER_ENCODING, + HttpHeader.CONTENT_ENCODING); + + static { + for (HttpHeaderValue value : HttpHeaderValue.values()) + if (value != UNKNOWN) + CACHE.put(value.toString(), value); + } + + private final String value; + private final byte[] bytes; + + HttpHeaderValue(String value) { + this.value = value; + bytes = StringUtils.getUtf8Bytes(value); + } + + public static boolean hasKnownValues(HttpHeader header) { + if (header == null) + return false; + return __known.contains(header); + } + + public boolean is(String value) { + return this.value.equalsIgnoreCase(value); + } + + public String getValue() { + return value; + } + + public byte[] getBytes() { + return bytes; + } + + @Override + public String toString() { + return value; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpMethod.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpMethod.java new file mode 100644 index 000000000..dcfc2e38b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpMethod.java @@ -0,0 +1,153 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.trie.ArrayTernaryTrie; +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.string.StringUtils; + +import java.nio.ByteBuffer; + +/** + * @author Pengtao Qiu + */ +public enum HttpMethod { + GET, + POST, + HEAD, + PUT, + PATCH, + OPTIONS, + DELETE, + TRACE, + CONNECT, + MOVE, + PROXY, + PRI; + + public final static Trie CACHE = new ArrayTernaryTrie<>(false); + public final static Trie INSENSITIVE_CACHE = new ArrayTrie<>(); + + static { + for (HttpMethod method : HttpMethod.values()) + CACHE.put(method.toString(), method); + } + + static { + for (HttpMethod method : HttpMethod.values()) + INSENSITIVE_CACHE.put(method.toString(), method); + } + + private final String value; + private final byte[] bytes; + + HttpMethod() { + value = this.name(); + bytes = StringUtils.getUtf8Bytes(value); + } + + public static HttpMethod from(String value) { + return CACHE.get(value); + } + + /** + * Optimized lookup to find a method name and trailing space in a byte array. + * + * @param bytes Array containing ISO-8859-1 characters + * @param position The first valid index + * @param limit The first non valid index + * @return A HttpMethod if a match or null if no easy match. + */ + public static HttpMethod lookAheadGet(byte[] bytes, final int position, int limit) { + int length = limit - position; + if (length < 4) + return null; + switch (bytes[position]) { + case 'G': + if (bytes[position + 1] == 'E' && bytes[position + 2] == 'T' && bytes[position + 3] == ' ') + return GET; + break; + case 'P': + if (bytes[position + 1] == 'O' && bytes[position + 2] == 'S' && bytes[position + 3] == 'T' && length >= 5 && bytes[position + 4] == ' ') + return POST; + if (bytes[position + 1] == 'R' && bytes[position + 2] == 'O' && bytes[position + 3] == 'X' && length >= 6 && bytes[position + 4] == 'Y' && bytes[position + 5] == ' ') + return PROXY; + if (bytes[position + 1] == 'U' && bytes[position + 2] == 'T' && bytes[position + 3] == ' ') + return PUT; + if (bytes[position + 1] == 'R' && bytes[position + 2] == 'I' && bytes[position + 3] == ' ') + return PRI; + break; + case 'H': + if (bytes[position + 1] == 'E' && bytes[position + 2] == 'A' && bytes[position + 3] == 'D' && length >= 5 && bytes[position + 4] == ' ') + return HEAD; + break; + case 'O': + if (bytes[position + 1] == 'P' && bytes[position + 2] == 'T' && bytes[position + 3] == 'I' && length >= 8 && + bytes[position + 4] == 'O' && bytes[position + 5] == 'N' && bytes[position + 6] == 'S' && bytes[position + 7] == ' ') + return OPTIONS; + break; + case 'D': + if (bytes[position + 1] == 'E' && bytes[position + 2] == 'L' && bytes[position + 3] == 'E' && length >= 7 && + bytes[position + 4] == 'T' && bytes[position + 5] == 'E' && bytes[position + 6] == ' ') + return DELETE; + break; + case 'T': + if (bytes[position + 1] == 'R' && bytes[position + 2] == 'A' && bytes[position + 3] == 'C' && length >= 6 && + bytes[position + 4] == 'E' && bytes[position + 5] == ' ') + return TRACE; + break; + case 'C': + if (bytes[position + 1] == 'O' && bytes[position + 2] == 'N' && bytes[position + 3] == 'N' && length >= 8 && + bytes[position + 4] == 'E' && bytes[position + 5] == 'C' && bytes[position + 6] == 'T' && bytes[position + 7] == ' ') + return CONNECT; + break; + case 'M': + if (bytes[position + 1] == 'O' && bytes[position + 2] == 'V' && bytes[position + 3] == 'E' && length >= 5 && bytes[position + 4] == ' ') + return MOVE; + break; + + default: + break; + } + return null; + } + + /** + * Optimized lookup to find a method name and trailing space in a byte array. + * + * @param buffer buffer containing ISO-8859-1 characters, it is not modified. + * @return A HttpMethod if a match or null if no easy match. + */ + public static HttpMethod lookAheadGet(ByteBuffer buffer) { + if (buffer.hasArray()) + return lookAheadGet(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.arrayOffset() + buffer.limit()); + + int l = buffer.remaining(); + if (l >= 4) { + HttpMethod m = CACHE.getBest(buffer, 0, l); + if (m != null) { + int ml = m.getValue().length(); + if (l > ml && buffer.get(buffer.position() + ml) == ' ') + return m; + } + } + return null; + } + + public boolean is(String value) { + return this.value.equalsIgnoreCase(value); + } + + public String getValue() { + return value; + } + + public byte[] getBytes() { + return bytes; + } + + @Override + public String toString() { + return value; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpScheme.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpScheme.java new file mode 100644 index 000000000..a182ddbda --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpScheme.java @@ -0,0 +1,45 @@ +package com.fireflysource.net.http.common.model; + + +import com.fireflysource.common.string.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +public enum HttpScheme { + HTTP("http"), HTTPS("https"), WS("ws"), WSS("wss"); + + private final String value; + private final byte[] bytes; + + HttpScheme(String value) { + this.value = value; + bytes = StringUtils.getUtf8Bytes(value); + Holder.cache.put(value, this); + } + + public static HttpScheme from(String value) { + return Holder.cache.get(value); + } + + public boolean is(String value) { + return this.value.equalsIgnoreCase(value); + } + + public String getValue() { + return value; + } + + public byte[] getBytes() { + return bytes; + } + + @Override + public String toString() { + return value; + } + + private static class Holder { + private static final Map cache = new HashMap<>(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpStatus.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpStatus.java new file mode 100644 index 000000000..3e50bbf5a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpStatus.java @@ -0,0 +1,353 @@ +package com.fireflysource.net.http.common.model; + +/** + *

+ * Http Status Codes + *

+ * + * @see IANA HTTP + * Status Code Registry + */ +public class HttpStatus { + public final static int CONTINUE_100 = 100; + public final static int SWITCHING_PROTOCOLS_101 = 101; + public final static int PROCESSING_102 = 102; + + public final static int OK_200 = 200; + public final static int CREATED_201 = 201; + public final static int ACCEPTED_202 = 202; + public final static int NON_AUTHORITATIVE_INFORMATION_203 = 203; + public final static int NO_CONTENT_204 = 204; + public final static int RESET_CONTENT_205 = 205; + public final static int PARTIAL_CONTENT_206 = 206; + public final static int MULTI_STATUS_207 = 207; + + public final static int MULTIPLE_CHOICES_300 = 300; + public final static int MOVED_PERMANENTLY_301 = 301; + public final static int MOVED_TEMPORARILY_302 = 302; + public final static int FOUND_302 = 302; + public final static int SEE_OTHER_303 = 303; + public final static int NOT_MODIFIED_304 = 304; + public final static int USE_PROXY_305 = 305; + public final static int TEMPORARY_REDIRECT_307 = 307; + public final static int PERMANENT_REDIRECT_308 = 308; + + public final static int BAD_REQUEST_400 = 400; + public final static int UNAUTHORIZED_401 = 401; + public final static int PAYMENT_REQUIRED_402 = 402; + public final static int FORBIDDEN_403 = 403; + public final static int NOT_FOUND_404 = 404; + public final static int METHOD_NOT_ALLOWED_405 = 405; + public final static int NOT_ACCEPTABLE_406 = 406; + public final static int PROXY_AUTHENTICATION_REQUIRED_407 = 407; + public final static int REQUEST_TIMEOUT_408 = 408; + public final static int CONFLICT_409 = 409; + public final static int GONE_410 = 410; + public final static int LENGTH_REQUIRED_411 = 411; + public final static int PRECONDITION_FAILED_412 = 412; + @Deprecated + public final static int REQUEST_ENTITY_TOO_LARGE_413 = 413; + public final static int PAYLOAD_TOO_LARGE_413 = 413; + @Deprecated + public final static int REQUEST_URI_TOO_LONG_414 = 414; + public final static int URI_TOO_LONG_414 = 414; + public final static int UNSUPPORTED_MEDIA_TYPE_415 = 415; + @Deprecated + public final static int REQUESTED_RANGE_NOT_SATISFIABLE_416 = 416; + public final static int RANGE_NOT_SATISFIABLE_416 = 416; + public final static int EXPECTATION_FAILED_417 = 417; + public final static int IM_A_TEAPOT_418 = 418; + public final static int ENHANCE_YOUR_CALM_420 = 420; + public final static int MISDIRECTED_REQUEST_421 = 421; + public final static int UNPROCESSABLE_ENTITY_422 = 422; + public final static int LOCKED_423 = 423; + public final static int FAILED_DEPENDENCY_424 = 424; + public final static int UPGRADE_REQUIRED_426 = 426; + public final static int PRECONDITION_REQUIRED_428 = 428; + public final static int TOO_MANY_REQUESTS_429 = 429; + public final static int REQUEST_HEADER_FIELDS_TOO_LARGE_431 = 431; + public final static int UNAVAILABLE_FOR_LEGAL_REASONS_451 = 451; + + public final static int INTERNAL_SERVER_ERROR_500 = 500; + public final static int NOT_IMPLEMENTED_501 = 501; + public final static int BAD_GATEWAY_502 = 502; + public final static int SERVICE_UNAVAILABLE_503 = 503; + public final static int GATEWAY_TIMEOUT_504 = 504; + public final static int HTTP_VERSION_NOT_SUPPORTED_505 = 505; + public final static int INSUFFICIENT_STORAGE_507 = 507; + public final static int LOOP_DETECTED_508 = 508; + public final static int NOT_EXTENDED_510 = 510; + public final static int NETWORK_AUTHENTICATION_REQUIRED_511 = 511; + + public static final int MAX_CODE = 511; + + private static final Code[] codeMap = new Code[MAX_CODE + 1]; + + static { + for (Code code : Code.values()) { + codeMap[code.code] = code; + } + } + + /** + * Get the HttpStatusCode for a specific code + * + * @param code the code to lookup. + * @return the {@link HttpStatus} if found, or null if not found. + */ + public static Code getCode(int code) { + if (code <= MAX_CODE) { + return codeMap[code]; + } + return null; + } + + /** + * Get the status message for a specific code. + * + * @param code the code to look up + * @return the specific message, or the code number itself if code does not + * match known list. + */ + public static String getMessage(int code) { + Code codeEnum = getCode(code); + if (codeEnum != null) { + return codeEnum.getMessage(); + } else { + return Integer.toString(code); + } + } + + /** + * Simple test against an code to determine if it falls into the + * Informational message category as defined in the + * RFC 1945 - HTTP/1.0, and + * RFC 7231 - HTTP/1.1. + * + * @param code the code to test. + * @return true if within range of codes that belongs to + * Informational messages. + */ + public static boolean isInformational(int code) { + return ((100 <= code) && (code <= 199)); + } + + /** + * Simple test against an code to determine if it falls into the + * Success message category as defined in the + * RFC 1945 - HTTP/1.0, and + * RFC 7231 - HTTP/1.1. + * + * @param code the code to test. + * @return true if within range of codes that belongs to + * Success messages. + */ + public static boolean isSuccess(int code) { + return ((200 <= code) && (code <= 299)); + } + + /** + * Simple test against an code to determine if it falls into the + * Redirection message category as defined in the + * RFC 1945 - HTTP/1.0, and + * RFC 7231 - HTTP/1.1. + * + * @param code the code to test. + * @return true if within range of codes that belongs to + * Redirection messages. + */ + public static boolean isRedirection(int code) { + return ((300 <= code) && (code <= 399)); + } + + /** + * Simple test against an code to determine if it falls into the + * Client Error message category as defined in the + * RFC 1945 - HTTP/1.0, and + * RFC 7231 - HTTP/1.1. + * + * @param code the code to test. + * @return true if within range of codes that belongs to + * Client Error messages. + */ + public static boolean isClientError(int code) { + return ((400 <= code) && (code <= 499)); + } + + /** + * Simple test against an code to determine if it falls into the + * Server Error message category as defined in the + * RFC 1945 - HTTP/1.0, and + * RFC 7231 - HTTP/1.1. + * + * @param code the code to test. + * @return true if within range of codes that belongs to + * Server Error messages. + */ + public static boolean isServerError(int code) { + return ((500 <= code) && (code <= 599)); + } + + public enum Code { + CONTINUE(CONTINUE_100, "Continue"), + SWITCHING_PROTOCOLS(SWITCHING_PROTOCOLS_101, "Switching Protocols"), + PROCESSING(PROCESSING_102, "Processing"), + + + OK(OK_200, "OK"), + CREATED(CREATED_201, "Created"), + ACCEPTED(ACCEPTED_202, "Accepted"), + NON_AUTHORITATIVE_INFORMATION(NON_AUTHORITATIVE_INFORMATION_203, "Non Authoritative Information"), + NO_CONTENT(NO_CONTENT_204, "No Content"), + RESET_CONTENT(RESET_CONTENT_205, "Reset Content"), + PARTIAL_CONTENT(PARTIAL_CONTENT_206, "Partial Content"), + MULTI_STATUS(MULTI_STATUS_207, "Multi-Status"), + + MULTIPLE_CHOICES(MULTIPLE_CHOICES_300, "Multiple Choices"), + MOVED_PERMANENTLY(MOVED_PERMANENTLY_301, "Moved Permanently"), + MOVED_TEMPORARILY(MOVED_TEMPORARILY_302, "Moved Temporarily"), + FOUND(FOUND_302, "Found"), + SEE_OTHER(SEE_OTHER_303, "See Other"), + NOT_MODIFIED(NOT_MODIFIED_304, "Not Modified"), + USE_PROXY(USE_PROXY_305, "Use Proxy"), + TEMPORARY_REDIRECT(TEMPORARY_REDIRECT_307, "Temporary Redirect"), + PERMANET_REDIRECT(PERMANENT_REDIRECT_308, "Permanent Redirect"), + + BAD_REQUEST(BAD_REQUEST_400, "Bad Request"), + UNAUTHORIZED(UNAUTHORIZED_401, "Unauthorized"), + PAYMENT_REQUIRED(PAYMENT_REQUIRED_402, "Payment Required"), + FORBIDDEN(FORBIDDEN_403, "Forbidden"), + NOT_FOUND(NOT_FOUND_404, "Not Found"), + METHOD_NOT_ALLOWED(METHOD_NOT_ALLOWED_405, "Method Not Allowed"), + NOT_ACCEPTABLE(NOT_ACCEPTABLE_406, "Not Acceptable"), + PROXY_AUTHENTICATION_REQUIRED(PROXY_AUTHENTICATION_REQUIRED_407, "Proxy Authentication Required"), + REQUEST_TIMEOUT(REQUEST_TIMEOUT_408, "Request Timeout"), + CONFLICT(CONFLICT_409, "Conflict"), + GONE(GONE_410, "Gone"), + LENGTH_REQUIRED(LENGTH_REQUIRED_411, "Length Required"), + PRECONDITION_FAILED(PRECONDITION_FAILED_412, "Precondition Failed"), + PAYLOAD_TOO_LARGE(PAYLOAD_TOO_LARGE_413, "Payload Too Large"), + URI_TOO_LONG(URI_TOO_LONG_414, "URI Too Long"), + UNSUPPORTED_MEDIA_TYPE(UNSUPPORTED_MEDIA_TYPE_415, "Unsupported Media Type"), + RANGE_NOT_SATISFIABLE(RANGE_NOT_SATISFIABLE_416, "Range Not Satisfiable"), + EXPECTATION_FAILED(EXPECTATION_FAILED_417, "Expectation Failed"), + IM_A_TEAPOT(IM_A_TEAPOT_418, "I'm a Teapot"), + ENHANCE_YOUR_CALM(ENHANCE_YOUR_CALM_420, "Enhance your Calm"), + MISDIRECTED_REQUEST(MISDIRECTED_REQUEST_421, "Misdirected Request"), + UNPROCESSABLE_ENTITY(UNPROCESSABLE_ENTITY_422, "Unprocessable Entity"), + LOCKED(LOCKED_423, "Locked"), + FAILED_DEPENDENCY(FAILED_DEPENDENCY_424, "Failed Dependency"), + UPGRADE_REQUIRED(UPGRADE_REQUIRED_426, "Upgrade Required"), + PRECONDITION_REQUIRED(PRECONDITION_REQUIRED_428, "Precondition Required"), + TOO_MANY_REQUESTS(TOO_MANY_REQUESTS_429, "Too Many Requests"), + REQUEST_HEADER_FIELDS_TOO_LARGE(REQUEST_HEADER_FIELDS_TOO_LARGE_431, "Request Header Fields Too Large"), + UNAVAILABLE_FOR_LEGAL_REASONS(UNAVAILABLE_FOR_LEGAL_REASONS_451, "Unavailable for Legal Reason"), + + INTERNAL_SERVER_ERROR(INTERNAL_SERVER_ERROR_500, "Server Error"), + NOT_IMPLEMENTED(NOT_IMPLEMENTED_501, "Not Implemented"), + BAD_GATEWAY(BAD_GATEWAY_502, "Bad Gateway"), + SERVICE_UNAVAILABLE(SERVICE_UNAVAILABLE_503, "Service Unavailable"), + GATEWAY_TIMEOUT(GATEWAY_TIMEOUT_504, "Gateway Timeout"), + HTTP_VERSION_NOT_SUPPORTED(HTTP_VERSION_NOT_SUPPORTED_505, "HTTP Version Not Supported"), + INSUFFICIENT_STORAGE(INSUFFICIENT_STORAGE_507, "Insufficient Storage"), + LOOP_DETECTED(LOOP_DETECTED_508, "Loop Detected"), + NOT_EXTENDED(NOT_EXTENDED_510, "Not Extended"), + NETWORK_AUTHENTICATION_REQUIRED(NETWORK_AUTHENTICATION_REQUIRED_511, "Network Authentication Required"), + ; + + private final int code; + private final String message; + + Code(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public boolean equals(int code) { + return (this.code == code); + } + + @Override + public String toString() { + return String.format("[%03d %s]", this.code, this.getMessage()); + } + + /** + * Simple test against an code to determine if it falls into the + * Informational message category as defined in the + * RFC 1945 - HTTP/1.0, + * and RFC 7231 - + * HTTP/1.1. + * + * @return true if within range of codes that belongs to + * Informational messages. + */ + public boolean isInformational() { + return HttpStatus.isInformational(this.code); + } + + /** + * Simple test against an code to determine if it falls into the + * Success message category as defined in the + * RFC 1945 - HTTP/1.0, + * and RFC 7231 - + * HTTP/1.1. + * + * @return true if within range of codes that belongs to + * Success messages. + */ + public boolean isSuccess() { + return HttpStatus.isSuccess(this.code); + } + + /** + * Simple test against an code to determine if it falls into the + * Redirection message category as defined in the + * RFC 1945 - HTTP/1.0, + * and RFC 7231 - + * HTTP/1.1. + * + * @return true if within range of codes that belongs to + * Redirection messages. + */ + public boolean isRedirection() { + return HttpStatus.isRedirection(this.code); + } + + /** + * Simple test against an code to determine if it falls into the + * Client Error message category as defined in the + * RFC 1945 - HTTP/1.0, + * and RFC 7231 - + * HTTP/1.1. + * + * @return true if within range of codes that belongs to + * Client Error messages. + */ + public boolean isClientError() { + return HttpStatus.isClientError(this.code); + } + + /** + * Simple test against an code to determine if it falls into the + * Server Error message category as defined in the + * RFC 1945 - HTTP/1.0, + * and RFC 7231 - + * HTTP/1.1. + * + * @return true if within range of codes that belongs to + * Server Error messages. + */ + public boolean isServerError() { + return HttpStatus.isServerError(this.code); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpTokens.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpTokens.java new file mode 100644 index 000000000..82a0bfeb7 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpTokens.java @@ -0,0 +1,158 @@ +package com.fireflysource.net.http.common.model; + + +import com.fireflysource.common.object.TypeUtils; + +/** + * HTTP constants + */ +public class HttpTokens { + public static final byte COLON = (byte) ':'; + public static final byte TAB = 0x09; + public static final byte LINE_FEED = 0x0A; + public static final byte CARRIAGE_RETURN = 0x0D; + public static final byte SPACE = 0x20; + public static final byte[] CRLF = {CARRIAGE_RETURN, LINE_FEED}; + public final static Token[] TOKENS = new Token[256]; + + static { + for (int b = 0; b < 256; b++) { + // token = 1*tchar + // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + // / DIGIT / ALPHA + // ; any VCHAR, except delimiters + // quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + // qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text + // obs-text = %x80-FF + // comment = "(" *( ctext / quoted-pair / comment ) ")" + // ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text + // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + + switch (b) { + case LINE_FEED: + TOKENS[b] = new Token((byte) b, Type.LF); + break; + case CARRIAGE_RETURN: + TOKENS[b] = new Token((byte) b, Type.CR); + break; + case SPACE: + TOKENS[b] = new Token((byte) b, Type.SPACE); + break; + case TAB: + TOKENS[b] = new Token((byte) b, Type.HTAB); + break; + case COLON: + TOKENS[b] = new Token((byte) b, Type.COLON); + break; + + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '-': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + TOKENS[b] = new Token((byte) b, Type.TCHAR); + break; + + default: + if (b >= 0x30 && b <= 0x39) // DIGIT + TOKENS[b] = new Token((byte) b, Type.DIGIT); + else if (b >= 0x41 && b <= 0x5A) // ALPHA (uppercase) + TOKENS[b] = new Token((byte) b, Type.ALPHA); + else if (b >= 0x61 && b <= 0x7A) // ALPHA (lowercase) + TOKENS[b] = new Token((byte) b, Type.ALPHA); + else if (b >= 0x21 && b <= 0x7E) // Visible + TOKENS[b] = new Token((byte) b, Type.VCHAR); + else if (b >= 0x80) // OBS + TOKENS[b] = new Token((byte) b, Type.OTEXT); + else + TOKENS[b] = new Token((byte) b, Type.CNTL); + } + } + } + + public enum EndOfContent {UNKNOWN_CONTENT, NO_CONTENT, EOF_CONTENT, CONTENT_LENGTH, CHUNKED_CONTENT} + + public enum Type { + CNTL, // Control characters excluding LF, CR + HTAB, // Horizontal tab + LF, // Line feed + CR, // Carriage return + SPACE, // Space + COLON, // Colon character + DIGIT, // Digit + ALPHA, // Alpha + TCHAR, // token characters excluding COLON,DIGIT,ALPHA, which is equivalent to VCHAR excluding delimiters + VCHAR, // Visible characters excluding COLON,DIGIT,ALPHA + OTEXT // Obsolete text + } + + public static class Token { + private final Type type; + private final byte b; + private final char c; + private final int hex; + + private Token(byte b, Type type) { + this.type = type; + this.b = b; + c = (char) (0xff & b); + char lc = (c >= 'A' & c <= 'Z') ? ((char) (c - 'A' + 'a')) : c; + hex = (this.type == Type.DIGIT || this.type == Type.ALPHA && lc >= 'a' && lc <= 'f') ? TypeUtils.convertHexDigit(b) : -1; + } + + public Type getType() { + return type; + } + + public byte getByte() { + return b; + } + + public char getChar() { + return c; + } + + public boolean isHexDigit() { + return hex >= 0; + } + + public int getHexDigit() { + return hex; + } + + @Override + public String toString() { + switch (type) { + case SPACE: + case COLON: + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + return type + "='" + c + "'"; + + case CR: + return "CR=\\r"; + + case LF: + return "LF=\\n"; + + default: + return String.format("%s=0x%x", type, b); + } + } + + } +} + diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpURI.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpURI.java new file mode 100644 index 000000000..eebe7e6c5 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpURI.java @@ -0,0 +1,663 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.map.MultiMap; +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.net.http.common.codec.URIUtils; +import com.fireflysource.net.http.common.codec.UrlEncoded; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Http URI. Parse a HTTP URI from a string or byte array. Given a URI + * http://user@host:port/path/info;param?query#fragment this class + * will split it into the following undecoded optional elements: + *
    + *
  • {@link #getScheme()} - http:
  • + *
  • {@link #getAuthority()} - //name@host:port
  • + *
  • {@link #getHost()} - host
  • + *
  • {@link #getPort()} - port
  • + *
  • {@link #getPath()} - /path/info
  • + *
  • {@link #getParam()} - param
  • + *
  • {@link #getQuery()} - query
  • + *
  • {@link #getFragment()} - fragment
  • + *
+ * + *

+ * Any parameters will be returned from {@link #getPath()}, but are excluded + * from the return value of {@link #getDecodedPath()}. If there are multiple + * parameters, the {@link #getParam()} method returns only the last one. + */ +public class HttpURI { + String uri; + String decodedPath; + private String scheme; + private String user; + private String host; + private int port; + private String path; + private String param; + private String query; + private String fragment; + + public HttpURI() { + } + + public HttpURI(String scheme, String host, int port, String path, String param, String query, String fragment) { + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.param = param; + this.query = query; + this.fragment = fragment; + } + + public HttpURI(HttpURI uri) { + this(uri.scheme, uri.host, uri.port, uri.path, uri.param, uri.query, uri.fragment); + this.uri = uri.uri; + } + + public HttpURI(String uri) { + port = -1; + parse(State.START, uri, 0, uri.length()); + } + + public HttpURI(URI uri) { + this.uri = null; + + scheme = uri.getScheme(); + host = uri.getHost(); + if (host == null && uri.getRawSchemeSpecificPart().startsWith("//")) + host = ""; + port = uri.getPort(); + user = uri.getUserInfo(); + path = uri.getRawPath(); + + decodedPath = uri.getPath(); + if (decodedPath != null) { + int p = decodedPath.lastIndexOf(';'); + if (p >= 0) + param = decodedPath.substring(p + 1); + } + query = uri.getRawQuery(); + fragment = uri.getFragment(); + + decodedPath = null; + } + + public HttpURI(String scheme, String host, int port, String pathQuery) { + uri = null; + + this.scheme = scheme; + this.host = host; + this.port = port; + + if (pathQuery != null) + parse(State.PATH, pathQuery, 0, pathQuery.length()); + + } + + /** + * Construct a normalized URI. + * Port is not set if it is the default port. + * + * @param scheme the URI scheme + * @param host the URI hose + * @param port the URI port + * @param path the URI path + * @param param the URI param + * @param query the URI query + * @param fragment the URI fragment + * @return the normalized URI + */ + public static HttpURI createHttpURI(String scheme, String host, int port, String path, String param, String query, String fragment) { + if (port == 80 && HttpScheme.HTTP.is(scheme)) + port = 0; + if (port == 443 && HttpScheme.HTTPS.is(scheme)) + port = 0; + return new HttpURI(scheme, host, port, path, param, query, fragment); + } + + public void parse(String uri) { + clear(); + this.uri = uri; + parse(State.START, uri, 0, uri.length()); + } + + /** + * Parse according to https://tools.ietf.org/html/rfc7230#section-5.3 + * + * @param method the request method + * @param uri the request uri + */ + public void parseRequestTarget(String method, String uri) { + clear(); + this.uri = uri; + + if (HttpMethod.CONNECT.is(method)) + path = uri; + else + parse(uri.startsWith("/") ? State.PATH : State.START, uri, 0, uri.length()); + } + + @Deprecated + public void parseConnect(String uri) { + clear(); + this.uri = uri; + path = uri; + } + + public void parse(String uri, int offset, int length) { + clear(); + int end = offset + length; + this.uri = uri.substring(offset, end); + parse(State.START, uri, offset, end); + } + + private void parse(State state, final String uri, final int offset, final int end) { + boolean encoded = false; + int mark = offset; + int path_mark = 0; + + for (int i = offset; i < end; i++) { + char c = uri.charAt(i); + + switch (state) { + case START: { + switch (c) { + case '/': + mark = i; + state = State.HOST_OR_PATH; + break; + case ';': + mark = i + 1; + state = State.PARAM; + break; + case '?': + // assume empty path (if seen at start) + path = ""; + mark = i + 1; + state = State.QUERY; + break; + case '#': + mark = i + 1; + state = State.FRAGMENT; + break; + case '*': + path = "*"; + state = State.ASTERISK; + break; + + default: + mark = i; + if (scheme == null) + state = State.SCHEME_OR_PATH; + else { + path_mark = i; + state = State.PATH; + } + } + + continue; + } + + case SCHEME_OR_PATH: { + switch (c) { + case ':': + // must have been a scheme + scheme = uri.substring(mark, i); + // Start again with scheme set + state = State.START; + break; + + case '/': + // must have been in a path and still are + state = State.PATH; + break; + + case ';': + // must have been in a path + mark = i + 1; + state = State.PARAM; + break; + + case '?': + // must have been in a path + path = uri.substring(mark, i); + mark = i + 1; + state = State.QUERY; + break; + + case '%': + // must have be in an encoded path + encoded = true; + state = State.PATH; + break; + + case '#': + // must have been in a path + path = uri.substring(mark, i); + state = State.FRAGMENT; + break; + } + continue; + } + + case HOST_OR_PATH: { + switch (c) { + case '/': + host = ""; + mark = i + 1; + state = State.HOST; + break; + + case '@': + case ';': + case '?': + case '#': + // was a path, look again + i--; + path_mark = mark; + state = State.PATH; + break; + default: + // it is a path + path_mark = mark; + state = State.PATH; + } + continue; + } + + case HOST: { + switch (c) { + case '/': + host = uri.substring(mark, i); + path_mark = mark = i; + state = State.PATH; + break; + case ':': + if (i > mark) + host = uri.substring(mark, i); + mark = i + 1; + state = State.PORT; + break; + case '@': + if (user != null) + throw new IllegalArgumentException("Bad authority"); + user = uri.substring(mark, i); + mark = i + 1; + break; + + case '[': + state = State.IPV6; + break; + } + continue; + } + + case IPV6: { + switch (c) { + case '/': + throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri); + case ']': + c = uri.charAt(++i); + host = uri.substring(mark, i); + if (c == ':') { + mark = i + 1; + state = State.PORT; + } else { + path_mark = mark = i; + state = State.PATH; + } + break; + } + + continue; + } + + case PORT: { + if (c == '@') { + if (user != null) + throw new IllegalArgumentException("Bad authority"); + // It wasn't a port, but a password! + user = host + ":" + uri.substring(mark, i); + mark = i + 1; + state = State.HOST; + } else if (c == '/') { + port = TypeUtils.parseInt(uri, mark, i - mark, 10); + path_mark = mark = i; + state = State.PATH; + } + continue; + } + + case PATH: { + switch (c) { + case ';': + mark = i + 1; + state = State.PARAM; + break; + case '?': + path = uri.substring(path_mark, i); + mark = i + 1; + state = State.QUERY; + break; + case '#': + path = uri.substring(path_mark, i); + mark = i + 1; + state = State.FRAGMENT; + break; + case '%': + encoded = true; + break; + } + continue; + } + + case PARAM: { + switch (c) { + case '?': + path = uri.substring(path_mark, i); + param = uri.substring(mark, i); + mark = i + 1; + state = State.QUERY; + break; + case '#': + path = uri.substring(path_mark, i); + param = uri.substring(mark, i); + mark = i + 1; + state = State.FRAGMENT; + break; + case '/': + encoded = true; + // ignore internal params + state = State.PATH; + break; + case ';': + // multiple parameters + mark = i + 1; + break; + } + continue; + } + + case QUERY: { + if (c == '#') { + query = uri.substring(mark, i); + mark = i + 1; + state = State.FRAGMENT; + } + continue; + } + + case ASTERISK: { + throw new IllegalArgumentException("Bad character '*'"); + } + + case FRAGMENT: { + fragment = uri.substring(mark, end); + i = end; + } + } + } + + + switch (state) { + case START: + break; + case SCHEME_OR_PATH: + path = uri.substring(mark, end); + break; + + case HOST_OR_PATH: + path = uri.substring(mark, end); + break; + + case HOST: + if (end > mark) + host = uri.substring(mark, end); + break; + + case IPV6: + throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri); + + case PORT: + port = TypeUtils.parseInt(uri, mark, end - mark, 10); + break; + + case ASTERISK: + break; + + case FRAGMENT: + fragment = uri.substring(mark, end); + break; + + case PARAM: + path = uri.substring(path_mark, end); + param = uri.substring(mark, end); + break; + + case PATH: + path = uri.substring(path_mark, end); + break; + + case QUERY: + query = uri.substring(mark, end); + break; + } + + if (!encoded) { + if (param == null) + decodedPath = path; + else + decodedPath = path.substring(0, path.length() - param.length() - 1); + } + } + + public String getScheme() { + return scheme; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + uri = null; + } + + public String getHost() { + // Return null for empty host to retain compatibility with java.net.URI + if (host != null && host.length() == 0) + return null; + return host; + } + + public int getPort() { + return port; + } + + /** + * The parsed Path. + * + * @return the path as parsed on valid URI. null for invalid URI. + */ + public String getPath() { + return path; + } + + /** + * @param path the path + */ + public void setPath(String path) { + uri = null; + this.path = path; + decodedPath = null; + } + + public String getDecodedPath() { + if (decodedPath == null && path != null) + decodedPath = URIUtils.decodePath(path); + return decodedPath; + } + + public String getParam() { + return param; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + uri = null; + } + + public boolean hasQuery() { + return query != null && query.length() > 0; + } + + public String getFragment() { + return fragment; + } + + public void decodeQueryTo(MultiMap parameters) { + if (query == null) + return; + UrlEncoded.decodeUtf8To(query, parameters); + } + + public void decodeQueryTo(MultiMap parameters, String encoding) throws UnsupportedEncodingException { + decodeQueryTo(parameters, Charset.forName(encoding)); + } + + public void decodeQueryTo(MultiMap parameters, Charset encoding) throws UnsupportedEncodingException { + if (query == null) + return; + + if (encoding == null || StandardCharsets.UTF_8.equals(encoding)) + UrlEncoded.decodeUtf8To(query, parameters); + else + UrlEncoded.decodeTo(query, parameters, encoding); + } + + public void clear() { + uri = null; + + scheme = null; + host = null; + port = -1; + path = null; + param = null; + query = null; + fragment = null; + + decodedPath = null; + } + + public boolean isAbsolute() { + return scheme != null && scheme.length() > 0; + } + + @Override + public String toString() { + if (uri == null) { + StringBuilder out = new StringBuilder(); + + if (scheme != null) + out.append(scheme).append(':'); + + if (host != null) { + out.append("//"); + if (user != null) + out.append(user).append('@'); + out.append(host); + } + + if (port > 0) + out.append(':').append(port); + + if (path != null) + out.append(path); + + if (query != null) + out.append('?').append(query); + + if (fragment != null) + out.append('#').append(fragment); + + if (out.length() > 0) + uri = out.toString(); + else + uri = ""; + } + return uri; + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof HttpURI)) + return false; + return toString().equals(o.toString()); + } + + /** + * @param host the host + * @param port the port + */ + public void setAuthority(String host, int port) { + this.host = host; + this.port = port; + uri = null; + } + + public URI toURI() throws URISyntaxException { + return new URI(scheme, null, host, port, path, query == null ? null : UrlEncoded.decodeString(query), fragment); + } + + public String getPathQuery() { + if (query == null) + return path; + return path + "?" + query; + } + + public void setPathQuery(String path) { + uri = null; + this.path = null; + decodedPath = null; + param = null; + fragment = null; + if (path != null) + parse(State.PATH, path, 0, path.length()); + } + + public String getAuthority() { + if (port > 0) + return host + ":" + port; + return host; + } + + public String getUser() { + return user; + } + + + private enum State { + START, + HOST_OR_PATH, + SCHEME_OR_PATH, + HOST, + IPV6, + PORT, + PATH, + PARAM, + QUERY, + FRAGMENT, + ASTERISK + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpVersion.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpVersion.java new file mode 100644 index 000000000..e21e23b14 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/HttpVersion.java @@ -0,0 +1,120 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.string.StringUtils; + +import java.nio.ByteBuffer; + +public enum HttpVersion { + HTTP_0_9("HTTP/0.9", 9), + HTTP_1_0("HTTP/1.0", 10), + HTTP_1_1("HTTP/1.1", 11), + HTTP_2("HTTP/2.0", 20); + + public final static Trie CACHE = new ArrayTrie<>(); + + static { + for (HttpVersion version : HttpVersion.values()) + CACHE.put(version.toString(), version); + } + + private final String value; + private final int version; + private final byte[] bytes; + + HttpVersion(String value, int version) { + this.value = value; + this.version = version; + bytes = StringUtils.getUtf8Bytes(value); + } + + public static HttpVersion from(String value) { + return CACHE.get(value); + } + + public static HttpVersion fromVersion(int version) { + switch (version) { + case 9: + return HttpVersion.HTTP_0_9; + case 10: + return HttpVersion.HTTP_1_0; + case 11: + return HttpVersion.HTTP_1_1; + case 20: + return HttpVersion.HTTP_2; + default: + throw new IllegalArgumentException(); + } + } + + /** + * Optimised lookup to find a Http Version and whitespace in a byte array. + * + * @param bytes Array containing ISO-8859-1 characters + * @param position The first valid index + * @param limit The first non valid index + * @return A HttpMethod if a match or null if no easy match. + */ + public static HttpVersion lookAheadGet(byte[] bytes, int position, int limit) { + int length = limit - position; + if (length < 9) + return null; + + if (bytes[position + 4] == '/' && bytes[position + 6] == '.' && Character.isWhitespace((char) bytes[position + 8]) && + ((bytes[position] == 'H' && bytes[position + 1] == 'T' && bytes[position + 2] == 'T' && bytes[position + 3] == 'P') || + (bytes[position] == 'h' && bytes[position + 1] == 't' && bytes[position + 2] == 't' && bytes[position + 3] == 'p'))) { + switch (bytes[position + 5]) { + case '1': + switch (bytes[position + 7]) { + case '0': + return HTTP_1_0; + case '1': + return HTTP_1_1; + } + break; + case '2': + switch (bytes[position + 7]) { + case '0': + return HTTP_2; + } + break; + } + } + + return null; + } + + /** + * Optimised lookup to find a HTTP Version and trailing white space in a byte array. + * + * @param buffer buffer containing ISO-8859-1 characters + * @return A HttpVersion if a match or null if no easy match. + */ + public static HttpVersion lookAheadGet(ByteBuffer buffer) { + if (buffer.hasArray()) + return lookAheadGet(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.arrayOffset() + buffer.limit()); + return null; + } + + public boolean is(String value) { + return this.value.equalsIgnoreCase(value); + } + + public String getValue() { + return value; + } + + public byte[] getBytes() { + return bytes; + } + + public int getVersion() { + return version; + } + + @Override + public String toString() { + return value; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/MetaData.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/MetaData.java new file mode 100644 index 000000000..36f1599ab --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/MetaData.java @@ -0,0 +1,279 @@ +package com.fireflysource.net.http.common.model; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Optional; +import java.util.function.Supplier; + +public class MetaData implements Iterable { + private final HttpFields fields; + private HttpVersion httpVersion; + private long contentLength; + private Supplier trailers; + private boolean onlyTrailer; + + public MetaData(HttpVersion version, HttpFields fields) { + this(version, fields, Long.MIN_VALUE); + } + + public MetaData(HttpVersion version, HttpFields fields, long contentLength) { + httpVersion = version; + this.fields = fields; + this.contentLength = contentLength < 0 ? Long.MIN_VALUE : contentLength; + } + + protected void recycle() { + httpVersion = null; + if (fields != null) { + fields.clear(); + } + contentLength = Long.MIN_VALUE; + trailers = null; + } + + public boolean isRequest() { + return false; + } + + public boolean isResponse() { + return false; + } + + /** + * @return the HTTP version of this MetaData object + */ + public HttpVersion getHttpVersion() { + return httpVersion; + } + + /** + * @param httpVersion the HTTP version to set + */ + public void setHttpVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + } + + /** + * @return the HTTP fields of this MetaData object + */ + public HttpFields getFields() { + return fields; + } + + public Supplier getTrailerSupplier() { + return trailers; + } + + public boolean isOnlyTrailer() { + return onlyTrailer; + } + + public void setOnlyTrailer(boolean onlyTrailer) { + this.onlyTrailer = onlyTrailer; + } + + public void setTrailerSupplier(Supplier trailers) { + this.trailers = trailers; + } + + /** + * @return the content length if available, otherwise {@link Long#MIN_VALUE} + */ + public long getContentLength() { + if (contentLength == Long.MIN_VALUE) { + return Optional.ofNullable(fields.getField(HttpHeader.CONTENT_LENGTH)) + .map(HttpField::getValue) + .map(Long::parseLong) + .orElse(Long.MIN_VALUE); + } else { + return contentLength; + } + } + + /** + * @return an iterator over the HTTP fields + * @see #getFields() + */ + public Iterator iterator() { + HttpFields fields = getFields(); + return fields == null ? Collections.emptyIterator() : fields.iterator(); + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder(); + for (HttpField field : this) + out.append(field).append(System.lineSeparator()); + return out.toString(); + } + + public static class Request extends MetaData { + private String method; + private HttpURI uri; + + public Request(HttpFields fields) { + this(null, null, null, fields); + } + + public Request(String method, HttpURI uri, HttpVersion version, HttpFields fields) { + this(method, uri, version, fields, Long.MIN_VALUE); + } + + public Request(String method, HttpURI uri, HttpVersion version, HttpFields fields, long contentLength) { + super(version, fields, contentLength); + this.method = method; + this.uri = uri; + } + + public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields) { + this(method, new HttpURI(scheme == null ? null : scheme.getValue(), hostPort.getHost(), hostPort.getPort(), uri), version, fields); + } + + public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength) { + this(method, new HttpURI(scheme == null ? null : scheme.getValue(), hostPort.getHost(), hostPort.getPort(), uri), version, fields, contentLength); + } + + public Request(String method, String scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength) { + this(method, new HttpURI(scheme, hostPort.getHost(), hostPort.getPort(), uri), version, fields, contentLength); + } + + public Request(Request request) { + super(request.getHttpVersion(), new HttpFields(request.getFields()), request.getContentLength()); + this.method = request.getMethod(); + this.uri = new HttpURI(request.getURI()); + } + + public void recycle() { + super.recycle(); + method = null; + if (uri != null) + uri.clear(); + } + + @Override + public boolean isRequest() { + return true; + } + + /** + * @return the HTTP method + */ + public String getMethod() { + return method; + } + + /** + * @param method the HTTP method to set + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * @return the HTTP URI + */ + public HttpURI getURI() { + return uri; + } + + /** + * @param uri the HTTP URI to set + */ + public void setURI(HttpURI uri) { + this.uri = uri; + } + + /** + * @return the HTTP URI in string form + */ + public String getURIString() { + return uri == null ? null : uri.toString(); + } + + @Override + public String toString() { + HttpFields fields = getFields(); + return String.format("%s{u=%s,%s,h=%d,cl=%d}", + getMethod(), getURI(), getHttpVersion(), fields == null ? -1 : fields.size(), getContentLength()); + } + } + + public static class Response extends MetaData { + private int status; + private String reason; + + public Response() { + this(null, 0, null); + } + + public Response(HttpFields httpFields) { + this(null, 0, httpFields); + } + + public Response(HttpVersion version, int status, HttpFields fields) { + this(version, status, fields, Long.MIN_VALUE); + } + + public Response(HttpVersion version, int status, HttpFields fields, long contentLength) { + super(version, fields, contentLength); + this.status = status; + } + + public Response(HttpVersion version, int status, String reason, HttpFields fields, long contentLength) { + super(version, fields, contentLength); + this.reason = reason; + this.status = status; + } + + public Response(Response response) { + super(response.getHttpVersion(), new HttpFields(response.getFields()), response.getContentLength()); + this.reason = response.reason; + this.status = response.status; + } + + @Override + public boolean isResponse() { + return true; + } + + /** + * @return the HTTP status + */ + public int getStatus() { + return status; + } + + /** + * @param status the HTTP status to set + */ + public void setStatus(int status) { + this.status = status; + } + + /** + * @return the HTTP reason + */ + public String getReason() { + return reason; + } + + /** + * @param reason the HTTP reason to set + */ + public void setReason(String reason) { + this.reason = reason; + } + + public void recycle() { + super.recycle(); + reason = null; + status = 0; + } + + @Override + public String toString() { + HttpFields fields = getFields(); + return String.format("%s{s=%d,h=%d,cl=%d}", getHttpVersion(), getStatus(), fields == null ? -1 : fields.size(), getContentLength()); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/MimeTypes.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/MimeTypes.java new file mode 100644 index 000000000..ab32b47a6 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/MimeTypes.java @@ -0,0 +1,629 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.codec.PreEncodedHttpField; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class MimeTypes { + + private static final LazyLogger LOG = SystemLogger.create(MimeTypes.class); + + private static final Map DFT_MIME_MAP = new HashMap<>(); + private static final Map INFERRED_ENCODINGS = new HashMap<>(); + private static final Map ASSUMED_ENCODINGS = new HashMap<>(); + private static final Trie CACHE = new ArrayTrie<>(512); + + static { + for (Type type : Type.values()) { + CACHE.put(type.toString(), type); + + int charset = type.toString().indexOf(";charset="); + if (charset > 0) { + String alt = type.toString().replace(";charset=", "; charset="); + CACHE.put(alt, type); + } + + if (type.isAssumedCharset()) + ASSUMED_ENCODINGS.put(type.getValue(), type.getCharsetString()); + } + + String path = MimeTypes.class.getPackage().getName().replace('.', '/'); + String resourceName = path + "/mime.properties"; + try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName)) { + if (stream == null) { + LOG.warn("Missing mime-type resource: {}", resourceName); + } else { + try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + Properties props = new Properties(); + props.load(reader); + props.stringPropertyNames().stream() + .filter(Objects::nonNull) + .forEach(x -> + DFT_MIME_MAP.put(StringUtils.asciiToLowerCase(x), normalizeMimeType(props.getProperty(x)))); + + if (DFT_MIME_MAP.size() == 0) { + LOG.warn("Empty mime types at {}", resourceName); + } else if (DFT_MIME_MAP.size() < props.keySet().size()) { + LOG.warn("Duplicate or null mime-type extension in resource: {}", resourceName); + } + } catch (IOException e) { + LOG.warn(e.toString()); + } + + } + } catch (IOException e) { + LOG.warn(e.toString()); + } + + resourceName = path + "/encoding.properties"; + try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName)) { + if (stream == null) + LOG.warn("Missing encoding resource: {}", resourceName); + else { + try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + Properties props = new Properties(); + props.load(reader); + props.stringPropertyNames().stream() + .filter(Objects::nonNull) + .forEach(t -> + { + String charset = props.getProperty(t); + if (charset.startsWith("-")) + ASSUMED_ENCODINGS.put(t, charset.substring(1)); + else + INFERRED_ENCODINGS.put(t, props.getProperty(t)); + }); + + if (INFERRED_ENCODINGS.size() == 0) { + LOG.warn("Empty encodings at {}", resourceName); + } else if ((INFERRED_ENCODINGS.size() + ASSUMED_ENCODINGS.size()) < props.keySet().size()) { + LOG.warn("Null or duplicate encodings in resource: {}", resourceName); + } + } catch (IOException e) { + LOG.warn(e.toString()); + } + } + } catch (IOException e) { + LOG.warn(e.toString()); + } + } + + private final Map _mimeMap = new HashMap<>(); + + /** + * Constructor. + */ + public MimeTypes() { + } + + /** + * Get the MIME type by filename extension. + * Lookup only the static default mime map. + * + * @param filename A file name + * @return MIME type matching the longest dot extension of the + * file name. + */ + public static String getDefaultMimeByExtension(String filename) { + String type = null; + + if (filename != null) { + int i = -1; + while (type == null) { + i = filename.indexOf(".", i + 1); + + if (i < 0 || i >= filename.length()) + break; + + String ext = StringUtils.asciiToLowerCase(filename.substring(i + 1)); + type = DFT_MIME_MAP.get(ext); + } + } + + if (type == null) { + type = DFT_MIME_MAP.get("*"); + } + + return type; + } + + public static Set getKnownMimeTypes() { + return new HashSet<>(DFT_MIME_MAP.values()); + } + + private static String normalizeMimeType(String type) { + Type t = CACHE.get(type); + if (t != null) + return t.getValue(); + + return StringUtils.asciiToLowerCase(type); + } + + public static String getCharsetFromContentType(String value) { + if (value == null) + return null; + int end = value.length(); + int state = 0; + int start = 0; + boolean quote = false; + int i = 0; + for (; i < end; i++) { + char b = value.charAt(i); + + if (quote && state != 10) { + if ('"' == b) + quote = false; + continue; + } + + if (';' == b && state <= 8) { + state = 1; + continue; + } + + switch (state) { + case 0: + if ('"' == b) { + quote = true; + break; + } + break; + + case 1: + if ('c' == b) state = 2; + else if (' ' != b) state = 0; + break; + case 2: + if ('h' == b) state = 3; + else state = 0; + break; + case 3: + if ('a' == b) state = 4; + else state = 0; + break; + case 4: + if ('r' == b) state = 5; + else state = 0; + break; + case 5: + if ('s' == b) state = 6; + else state = 0; + break; + case 6: + if ('e' == b) state = 7; + else state = 0; + break; + case 7: + if ('t' == b) state = 8; + else state = 0; + break; + + case 8: + if ('=' == b) state = 9; + else if (' ' != b) state = 0; + break; + + case 9: + if (' ' == b) + break; + if ('"' == b) { + quote = true; + start = i + 1; + state = 10; + break; + } + start = i; + state = 10; + break; + + case 10: + if (!quote && (';' == b || ' ' == b) || + (quote && '"' == b)) + return StringUtils.normalizeCharset(value, start, i - start); + } + } + + if (state == 10) + return StringUtils.normalizeCharset(value, start, i - start); + + return null; + } + + /** + * Access a mutable map of mime type to the charset inferred from that content type. + * An inferred encoding is used by when encoding/decoding a stream and is + * explicitly set in any metadata (eg Content-Type). + * + * @return Map of mime type to charset + */ + public static Map getInferredEncodings() { + return INFERRED_ENCODINGS; + } + + /** + * Access a mutable map of mime type to the charset assumed for that content type. + * An assumed encoding is used by when encoding/decoding a stream, but is not + * explicitly set in any metadata (eg Content-Type). + * + * @return Map of mime type to charset + */ + public static Map getAssumedEncodings() { + return INFERRED_ENCODINGS; + } + + @Deprecated + public static String inferCharsetFromContentType(String contentType) { + return getCharsetAssumedFromContentType(contentType); + } + + public static String getCharsetInferredFromContentType(String contentType) { + return INFERRED_ENCODINGS.get(contentType); + } + + public static String getCharsetAssumedFromContentType(String contentType) { + return ASSUMED_ENCODINGS.get(contentType); + } + + public static String getContentTypeWithoutCharset(String value) { + int end = value.length(); + int state = 0; + int start = 0; + boolean quote = false; + int i = 0; + StringBuilder builder = null; + for (; i < end; i++) { + char b = value.charAt(i); + + if ('"' == b) { + quote = !quote; + + switch (state) { + case 11: + builder.append(b); + break; + case 10: + break; + case 9: + builder = new StringBuilder(); + builder.append(value, 0, start + 1); + state = 10; + break; + default: + start = i; + state = 0; + } + continue; + } + + if (quote) { + if (builder != null && state != 10) + builder.append(b); + continue; + } + + switch (state) { + case 0: + if (';' == b) + state = 1; + else if (' ' != b) + start = i; + break; + + case 1: + if ('c' == b) state = 2; + else if (' ' != b) state = 0; + break; + case 2: + if ('h' == b) state = 3; + else state = 0; + break; + case 3: + if ('a' == b) state = 4; + else state = 0; + break; + case 4: + if ('r' == b) state = 5; + else state = 0; + break; + case 5: + if ('s' == b) state = 6; + else state = 0; + break; + case 6: + if ('e' == b) state = 7; + else state = 0; + break; + case 7: + if ('t' == b) state = 8; + else state = 0; + break; + case 8: + if ('=' == b) state = 9; + else if (' ' != b) state = 0; + break; + + case 9: + if (' ' == b) + break; + builder = new StringBuilder(); + builder.append(value, 0, start + 1); + state = 10; + break; + + case 10: + if (';' == b) { + builder.append(b); + state = 11; + } + break; + case 11: + if (' ' != b) + builder.append(b); + } + } + if (builder == null) + return value; + return builder.toString(); + + } + + public static String getContentTypeMIMEType(String contentType) { + if (StringUtils.hasText(contentType)) { + // parsing content-type + String[] strings = StringUtils.split(contentType, ';'); + return strings[0]; + } else { + return null; + } + } + + public static List getAcceptMIMETypes(String accept) { + if (StringUtils.hasText(accept)) { + List list = new ArrayList<>(); + // parsing accept + String[] strings = StringUtils.split(accept, ','); + for (String string : strings) { + String[] s = StringUtils.split(string, ';'); + list.add(s[0].trim()); + } + return list; + } else { + return Collections.emptyList(); + } + } + + public static List parseAcceptMIMETypes(String accept) { + return Optional.ofNullable(accept) + .filter(StringUtils::hasText) + .map(a -> StringUtils.split(a, ',')) + .map(Arrays::stream) + .map(MimeTypes::apply) + .orElse(Collections.emptyList()); + } + + private static List apply(Stream stream) { + return stream.map(String::trim) + .filter(StringUtils::hasText) + .map(type -> { + String[] mimeTypeAndFields = StringUtils.split(type, ';'); + AcceptMIMEType acceptMIMEType = new AcceptMIMEType(); + + // parse the MIME type + String[] mimeType = StringUtils.split(mimeTypeAndFields[0].trim(), '/'); + String parentType = mimeType[0].trim(); + String childType = mimeType[1].trim(); + acceptMIMEType.setParentType(parentType); + acceptMIMEType.setChildType(childType); + if (parentType.equals("*")) { + if (childType.equals("*")) { + acceptMIMEType.setMatchType(AcceptMIMEMatchType.ALL); + } else { + acceptMIMEType.setMatchType(AcceptMIMEMatchType.CHILD); + } + } else { + if (childType.equals("*")) { + acceptMIMEType.setMatchType(AcceptMIMEMatchType.PARENT); + } else { + acceptMIMEType.setMatchType(AcceptMIMEMatchType.EXACT); + } + } + + // parse the quality + if (mimeTypeAndFields.length > 1) { + Arrays.stream(mimeTypeAndFields) + .filter(v -> v.contains("=")) + .map(v -> StringUtils.split(v, '=')) + .filter(v -> v.length > 1) + .filter(v -> v[0].contains("q")) + .map(v -> v[1].trim()) + .map(Float::parseFloat) + .findAny() + .ifPresent(acceptMIMEType::setQuality); + } + + return acceptMIMEType; + }) + .sorted((a1, a2) -> Float.compare(a2.getQuality(), a1.getQuality())) + .collect(Collectors.toList()); + } + + public Map getMimeMap() { + return _mimeMap; + } + + /** + * @param mimeMap A Map of file extension to mime-type. + */ + public void setMimeMap(Map mimeMap) { + _mimeMap.clear(); + if (mimeMap != null) { + for (Entry ext : mimeMap.entrySet()) { + _mimeMap.put(StringUtils.asciiToLowerCase(ext.getKey()), normalizeMimeType(ext.getValue())); + } + } + } + + /** + * Get the MIME type by filename extension. + * Lookup the content and static default mime maps. + * + * @param filename A file name + * @return MIME type matching the longest dot extension of the + * file name. + */ + public String getMimeByExtension(String filename) { + String type = null; + + if (filename != null) { + int i = -1; + while (type == null) { + i = filename.indexOf(".", i + 1); + + if (i < 0 || i >= filename.length()) + break; + + String ext = StringUtils.asciiToLowerCase(filename.substring(i + 1)); + type = _mimeMap.get(ext); + if (type == null) + type = DFT_MIME_MAP.get(ext); + } + } + + if (type == null) { + type = _mimeMap.get("*"); + if (type == null) { + type = DFT_MIME_MAP.get("*"); + } + } + + return type; + } + + /** + * Set a mime mapping + * + * @param extension the extension + * @param type the mime type + */ + public void addMimeMapping(String extension, String type) { + _mimeMap.put(StringUtils.asciiToLowerCase(extension), normalizeMimeType(type)); + } + + public enum Type { + FORM_ENCODED("application/x-www-form-urlencoded"), + MESSAGE_HTTP("message/http"), + MULTIPART_BYTERANGES("multipart/byteranges"), + MULTIPART_FORM_DATA("multipart/form-data"), + + TEXT_HTML("text/html"), + TEXT_PLAIN("text/plain"), + TEXT_XML("text/xml"), + TEXT_JSON("text/json", StandardCharsets.UTF_8), + APPLICATION_JSON("application/json", StandardCharsets.UTF_8), + + TEXT_HTML_8859_1("text/html;charset=iso-8859-1", TEXT_HTML), + TEXT_HTML_UTF_8("text/html;charset=utf-8", TEXT_HTML), + + TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1", TEXT_PLAIN), + TEXT_PLAIN_UTF_8("text/plain;charset=utf-8", TEXT_PLAIN), + + TEXT_XML_8859_1("text/xml;charset=iso-8859-1", TEXT_XML), + TEXT_XML_UTF_8("text/xml;charset=utf-8", TEXT_XML), + + TEXT_JSON_8859_1("text/json;charset=iso-8859-1", TEXT_JSON), + TEXT_JSON_UTF_8("text/json;charset=utf-8", TEXT_JSON), + + APPLICATION_JSON_8859_1("application/json;charset=iso-8859-1", APPLICATION_JSON), + APPLICATION_JSON_UTF_8("application/json;charset=utf-8", APPLICATION_JSON); + + + private final String value; + private final byte[] bytes; + private final Type baseType; + private final Charset charset; + private final String charsetString; + private final boolean assumedCharset; + private final HttpField field; + + Type(String value) { + this.value = value; + bytes = StringUtils.getUtf8Bytes(value); + baseType = this; + charset = null; + charsetString = null; + assumedCharset = false; + field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, this.value); + } + + Type(String value, Type baseType) { + this.value = value; + bytes = StringUtils.getUtf8Bytes(value); + this.baseType = baseType; + int i = value.indexOf(";charset="); + charset = Charset.forName(value.substring(i + 9)); + charsetString = charset.toString().toLowerCase(Locale.ENGLISH); + assumedCharset = false; + field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, this.value); + } + + Type(String value, Charset charset) { + this.value = value; + bytes = StringUtils.getUtf8Bytes(value); + baseType = this; + this.charset = charset; + charsetString = this.charset == null ? null : this.charset.toString().toLowerCase(Locale.ENGLISH); + assumedCharset = true; + field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, this.value); + } + + public Charset getCharset() { + return charset; + } + + public String getCharsetString() { + return charsetString; + } + + public boolean is(String s) { + return value.equalsIgnoreCase(s); + } + + public String getValue() { + return value; + } + + public byte[] getBytes() { + return bytes; + } + + @Override + public String toString() { + return value; + } + + public boolean isAssumedCharset() { + return assumedCharset; + } + + public HttpField getContentTypeField() { + return field; + } + + public Type getBaseType() { + return baseType; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/QuotedCSV.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/QuotedCSV.java new file mode 100644 index 000000000..51c7b89e4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/QuotedCSV.java @@ -0,0 +1,283 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.string.StringUtils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Implements a quoted comma separated list of values + * in accordance with RFC7230. + * OWS is removed and quoted characters ignored for parsing. + * + * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" + * @see "https://tools.ietf.org/html/rfc7230#section-7" + */ +public class QuotedCSV implements Iterable { + + protected final List values = new ArrayList<>(); + protected final boolean keepQuotes; + + public QuotedCSV(String... values) { + this(true, values); + } + + public QuotedCSV(boolean keepQuotes, String... values) { + this.keepQuotes = keepQuotes; + for (String v : values) + addValue(v); + } + + public static String unquote(String s) { + if (!StringUtils.hasText(s)) { + return s; + } + // handle trivial cases + int l = s.length(); + // Look for any quotes + int i = 0; + for (; i < l; i++) { + char c = s.charAt(i); + if (c == '"') + break; + } + if (i == l) + return s; + + boolean quoted = true; + boolean sloshed = false; + StringBuilder buffer = new StringBuilder(); + buffer.append(s, 0, i); + i++; + for (; i < l; i++) { + char c = s.charAt(i); + if (quoted) { + if (sloshed) { + buffer.append(c); + sloshed = false; + } else if (c == '"') + quoted = false; + else if (c == '\\') + sloshed = true; + else + buffer.append(c); + } else if (c == '"') + quoted = true; + else + buffer.append(c); + } + return buffer.toString(); + } + + /** + * Add and parse a value string(s) + * + * @param value A value that may contain one or more Quoted CSV items. + */ + public void addValue(String value) { + if (value == null) + return; + + StringBuilder buffer = new StringBuilder(); + + int l = value.length(); + State state = State.VALUE; + boolean quoted = false; + boolean sloshed = false; + int nws_length = 0; + int last_length = 0; + int value_length = -1; + int param_name = -1; + int param_value = -1; + + for (int i = 0; i <= l; i++) { + char c = i == l ? 0 : value.charAt(i); + + // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6 + if (quoted && c != 0) { + if (sloshed) + sloshed = false; + else { + switch (c) { + case '\\': + sloshed = true; + if (!keepQuotes) + continue; + break; + case '"': + quoted = false; + if (!keepQuotes) + continue; + break; + } + } + + buffer.append(c); + nws_length = buffer.length(); + continue; + } + + // Handle common cases + switch (c) { + case ' ': + case '\t': + if (buffer.length() > last_length) // not leading OWS + buffer.append(c); + continue; + + case '"': + quoted = true; + if (keepQuotes) { + if (state == State.PARAM_VALUE && param_value < 0) + param_value = nws_length; + buffer.append(c); + } else if (state == State.PARAM_VALUE && param_value < 0) + param_value = nws_length; + nws_length = buffer.length(); + continue; + + case ';': + buffer.setLength(nws_length); // trim following OWS + if (state == State.VALUE) { + parsedValue(buffer); + value_length = buffer.length(); + } else + parsedParam(buffer, value_length, param_name, param_value); + nws_length = buffer.length(); + param_name = param_value = -1; + buffer.append(c); + last_length = ++nws_length; + state = State.PARAM_NAME; + continue; + + case ',': + case 0: + if (nws_length > 0) { + buffer.setLength(nws_length); // trim following OWS + switch (state) { + case VALUE: + parsedValue(buffer); +// value_length = buffer.length(); + break; + case PARAM_NAME: + case PARAM_VALUE: + parsedParam(buffer, value_length, param_name, param_value); + break; + } + values.add(buffer.toString()); + } + buffer.setLength(0); + last_length = 0; + nws_length = 0; + value_length = param_name = param_value = -1; + state = State.VALUE; + continue; + + case '=': + switch (state) { + case VALUE: + // It wasn't really a value, it was a param name + value_length = param_name = 0; + buffer.setLength(nws_length); // trim following OWS + String param = buffer.toString(); + buffer.setLength(0); + parsedValue(buffer); + value_length = buffer.length(); + buffer.append(param); + buffer.append(c); + last_length = ++nws_length; + state = State.PARAM_VALUE; + continue; + + case PARAM_NAME: + buffer.setLength(nws_length); // trim following OWS + buffer.append(c); + last_length = ++nws_length; + state = State.PARAM_VALUE; + continue; + + case PARAM_VALUE: + if (param_value < 0) + param_value = nws_length; + buffer.append(c); + nws_length = buffer.length(); + continue; + } + continue; + + default: { + switch (state) { + case VALUE: { + buffer.append(c); + nws_length = buffer.length(); + continue; + } + + case PARAM_NAME: { + if (param_name < 0) + param_name = nws_length; + buffer.append(c); + nws_length = buffer.length(); + continue; + } + + case PARAM_VALUE: { + if (param_value < 0) + param_value = nws_length; + buffer.append(c); + nws_length = buffer.length(); + } + } + } + } + } + } + + /** + * Called when a value has been parsed + * + * @param buffer Containing the trimmed value, which may be mutated + */ + protected void parsedValue(StringBuilder buffer) { + } + + /** + * Called when a parameter has been parsed + * + * @param buffer Containing the trimmed value and all parameters, which may be mutated + * @param valueLength The length of the value + * @param paramName The index of the start of the parameter just parsed + * @param paramValue The index of the start of the parameter value just parsed, or -1 + */ + protected void parsedParam(StringBuilder buffer, int valueLength, int paramName, int paramValue) { + } + + public int size() { + return values.size(); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public List getValues() { + return values; + } + + @Override + public Iterator iterator() { + return values.iterator(); + } + + @Override + public String toString() { + List list = new ArrayList<>(); + for (String s : this) { + list.add(s); + } + return list.toString(); + } + + private enum State {VALUE, PARAM_NAME, PARAM_VALUE} +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/model/QuotedQualityCSV.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/QuotedQualityCSV.java new file mode 100644 index 000000000..aaf95a8ce --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/model/QuotedQualityCSV.java @@ -0,0 +1,148 @@ +package com.fireflysource.net.http.common.model; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +import static java.lang.Integer.MIN_VALUE; + +/** + * Implements a quoted comma separated list of quality values in accordance with + * RFC7230 and RFC7231. Values are returned sorted in quality order, with OWS + * and the quality parameters removed. + * + * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" + * @see "https://tools.ietf.org/html/rfc7230#section-7" + * @see "https://tools.ietf.org/html/rfc7231#section-5.3.1" + */ +public class QuotedQualityCSV extends QuotedCSV implements Iterable { + private final static Double ZERO = 0.0; + private final static Double ONE = 1.0; + + /** + * Function to apply a most specific MIME encoding secondary ordering + */ + public static Function MOST_SPECIFIC = s -> { + String[] elements = s.split("/"); + return 1000000 * elements.length + 1000 * elements[0].length() + elements[elements.length - 1].length(); + }; + + private final List quality = new ArrayList<>(); + private final Function secondaryOrdering; + private boolean sorted = false; + + + /** + * Sorts values with equal quality according to the length of the value String. + */ + public QuotedQualityCSV() { + this((s) -> 0); + } + + /** + * Sorts values with equal quality according to given order. + * + * @param preferredOrder Array indicating the preferred order of known values + */ + public QuotedQualityCSV(String[] preferredOrder) { + this((s) -> { + for (int i = 0; i < preferredOrder.length; ++i) + if (preferredOrder[i].equals(s)) + return preferredOrder.length - i; + + if ("*".equals(s)) + return preferredOrder.length; + + return MIN_VALUE; + }); + } + + /** + * Orders values with equal quality with the given function. + * + * @param secondaryOrdering Function to apply an ordering other than specified by quality + */ + public QuotedQualityCSV(Function secondaryOrdering) { + this.secondaryOrdering = secondaryOrdering; + } + + @Override + protected void parsedValue(StringBuilder buffer) { + super.parsedValue(buffer); + quality.add(ONE); + } + + @Override + protected void parsedParam(StringBuilder buffer, int valueLength, int paramName, int paramValue) { + if (paramName < 0) { + if (buffer.charAt(buffer.length() - 1) == ';') { + buffer.setLength(buffer.length() - 1); + } + } else if (paramValue >= 0 && + buffer.charAt(paramName) == 'q' && paramValue > paramName && + buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=') { + Double q; + try { + q = (keepQuotes && buffer.charAt(paramValue) == '"') + ? Double.parseDouble(buffer.substring(paramValue + 1, buffer.length() - 1)) + : Double.parseDouble(buffer.substring(paramValue)); + } catch (Exception e) { + q = ZERO; + } + buffer.setLength(Math.max(0, paramName - 1)); + + if (!ONE.equals(q)) { + quality.set(quality.size() - 1, q); + } + } + } + + public List getValues() { + if (!sorted) { + sort(); + } + return values; + } + + @Override + public Iterator iterator() { + if (!sorted) { + sort(); + } + return values.iterator(); + } + + protected void sort() { + sorted = true; + + Double last = ZERO; + int lastSecondaryOrder = Integer.MIN_VALUE; + + for (int i = values.size(); i-- > 0; ) { + String v = values.get(i); + Double q = quality.get(i); + + int compare = last.compareTo(q); + if (compare > 0 || (compare == 0 && secondaryOrdering.apply(v) < lastSecondaryOrder)) { + values.set(i, values.get(i + 1)); + values.set(i + 1, v); + quality.set(i, quality.get(i + 1)); + quality.set(i + 1, q); + last = ZERO; + lastSecondaryOrder = 0; + i = values.size(); + continue; + } + + last = q; + lastSecondaryOrder = secondaryOrdering.apply(v); + } + + int last_element = quality.size(); + while (last_element > 0 && quality.get(--last_element).equals(ZERO)) { + quality.remove(last_element); + values.remove(last_element); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v1/decoder/HttpParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v1/decoder/HttpParser.java new file mode 100644 index 000000000..1715a51e3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v1/decoder/HttpParser.java @@ -0,0 +1,1787 @@ +package com.fireflysource.net.http.common.v1.decoder; + +import com.fireflysource.common.collection.trie.ArrayTernaryTrie; +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.Utf8StringBuilder; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.codec.PreEncodedHttpField; +import com.fireflysource.net.http.common.exception.BadMessageException; +import com.fireflysource.net.http.common.model.*; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; + +import static com.fireflysource.net.http.common.model.HttpComplianceSection.MULTIPLE_CONTENT_LENGTHS; +import static com.fireflysource.net.http.common.model.HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; +import static com.fireflysource.net.http.common.model.HttpTokens.EndOfContent; + + +/** + * A Parser for 1.0 and 1.1 as defined by RFC7230 + *

+ * This parser parses HTTP client and server messages from buffers + * passed in the {@link #parseNext(ByteBuffer)} method. The parsed + * elements of the HTTP message are passed as event calls to the + * {@link HttpHandler} instance the parser is constructed with. + * If the passed handler is a {@link RequestHandler} then server side + * parsing is performed and if it is a {@link ResponseHandler}, then + * client side parsing is done. + *

+ *

+ * The contract of the {@link HttpHandler} API is that if a call returns + * true then the call to {@link #parseNext(ByteBuffer)} will return as + * soon as possible also with a true response. Typically this indicates + * that the parsing has reached a stage where the caller should process + * the events accumulated by the handler. It is the preferred calling + * style that handling such as calling a servlet to process a request, + * should be done after a true return from {@link #parseNext(ByteBuffer)} + * rather than from within the scope of a call like + * {@link RequestHandler#messageComplete()} + *

+ *

+ * For performance, the parse is heavily dependent on the + * {@link Trie#getBest(ByteBuffer, int, int)} method to look ahead in a + * single pass for both the structure ( : and CRLF ) and semantic (which + * header and value) of a header. Specifically the static {@link HttpHeader#CACHE} + * is used to lookup common combinations of headers and values + * (eg. "Connection: close"), or just header names (eg. "Connection:" ). + * For headers who's value is not known statically (eg. Host, COOKIE) then a + * per parser dynamic Trie of {@link HttpFields} from previous parsed messages + * is used to help the parsing of subsequent messages. + *

+ *

+ * The parser can work in varying compliance modes: + *

+ *
RFC7230
(default) Compliance with RFC7230
+ *
RFC2616
Wrapped headers and HTTP/0.9 supported
+ *
LEGACY
(aka STRICT) Adherence to Servlet Specification requirement for + * exact case of header names, bypassing the header caches, which are case insensitive, + * otherwise equivalent to RFC2616
+ *
+ * + * @see RFC 7230 + */ +public class HttpParser { + public static final LazyLogger LOG = SystemLogger.create(HttpParser.class); + @Deprecated + public final static String STRICT = "com.fireflysource.net.http.common.v1.decoder.HttpParser.STRICT"; + public final static int INITIAL_URI_LENGTH = 256; + /** + * Cache of common {@link HttpField}s including:
    + *
  • Common static combinations such as:
      + *
    • Connection: close + *
    • Accept-Encoding: gzip + *
    • Content-Length: 0 + *
    + *
  • Combinations of Content-Type header for common mime types by common charsets + *
  • Most common headers with null values so that a lookup will at least + * determine the header name even if the name:value combination is not cached + *
+ */ + public final static Trie CACHE = new ArrayTrie<>(2048); + private final static int MAX_CHUNK_LENGTH = Integer.MAX_VALUE / 16 - 16; + private final static EnumSet IDLE_STATES = EnumSet.of(State.START, State.END, State.CLOSE, State.CLOSED); + private final static EnumSet COMPLETE_STATES = EnumSet.of(State.END, State.CLOSE, State.CLOSED); + private static final boolean DEBUG = LOG.isDebugEnabled(); // Cache debug to help branch prediction + + static { + CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)); + CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE)); + CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.UPGRADE)); + CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate, br")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip,deflate,sdch")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-US,en;q=0.5")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-GB,en-US;q=0.8,en;q=0.6")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-AU,en;q=0.9,it-IT;q=0.8,it;q=0.7,en-GB;q=0.6,en-US;q=0.5")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_CHARSET, "ISO-8859-1,utf-8;q=0.7,*;q=0.3")); + CACHE.put(new HttpField(HttpHeader.ACCEPT, "*/*")); + CACHE.put(new HttpField(HttpHeader.ACCEPT, "image/png,image/*;q=0.8,*/*;q=0.5")); + CACHE.put(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")); + CACHE.put(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")); + CACHE.put(new HttpField(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES)); + CACHE.put(new HttpField(HttpHeader.PRAGMA, "no-cache")); + CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "private, no-cache, no-cache=Set-Cookie, proxy-revalidate")); + CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "no-cache")); + CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "max-age=0")); + CACHE.put(new HttpField(HttpHeader.CONTENT_LENGTH, "0")); + CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING, "gzip")); + CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING, "deflate")); + CACHE.put(new HttpField(HttpHeader.TRANSFER_ENCODING, "chunked")); + CACHE.put(new HttpField(HttpHeader.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT")); + + // Add common Content types as fields + for (String type : new String[]{"text/plain", "text/html", "text/xml", "text/json", "application/json", "application/x-www-form-urlencoded"}) { + HttpField field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type); + CACHE.put(field); + + for (String charset : new String[]{"utf-8", "iso-8859-1"}) { + CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset)); + CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset)); + CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset.toUpperCase(Locale.ENGLISH))); + CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset.toUpperCase(Locale.ENGLISH))); + } + } + + // Add headers with null values so HttpParser can avoid looking up name again for unknown values + for (HttpHeader h : HttpHeader.values()) + if (!CACHE.put(new HttpField(h, (String) null))) + throw new IllegalStateException("CACHE FULL"); + } + + private final HttpHandler handler; + private final RequestHandler requestHandler; + private final ResponseHandler responseHandler; + private final ComplianceHandler complianceHandler; + private final int maxHeaderBytes; + private final HttpCompliance compliance; + private final EnumSet complianceSections; + private final StringBuilder string = new StringBuilder(); + private HttpField field; + private HttpHeader header; + private String headerString; + private String valueString; + private int responseStatus; + private int headerBytes; + private boolean host; + private boolean headerComplete; + + private State state = State.START; + private FieldState fieldState = FieldState.FIELD; + private boolean eof; + private HttpMethod method; + private String methodString; + private HttpVersion version; + private final Utf8StringBuilder uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune? + private EndOfContent endOfContent; + private boolean hasContentLength; + private long contentLength = -1; + private long contentPosition; + private int chunkLength; + private int chunkPosition; + private boolean headResponse; + private boolean cr; + private ByteBuffer contentChunk; + private Trie fieldCache; + + private int length; + + public HttpParser(RequestHandler handler) { + this(handler, -1, compliance()); + } + + public HttpParser(ResponseHandler handler) { + this(handler, -1, compliance()); + } + + public HttpParser(RequestHandler handler, int maxHeaderBytes) { + this(handler, maxHeaderBytes, compliance()); + } + + + public HttpParser(ResponseHandler handler, int maxHeaderBytes) { + this(handler, maxHeaderBytes, compliance()); + } + + + @Deprecated + public HttpParser(RequestHandler handler, int maxHeaderBytes, boolean strict) { + this(handler, maxHeaderBytes, strict ? HttpCompliance.LEGACY : compliance()); + } + + + @Deprecated + public HttpParser(ResponseHandler handler, int maxHeaderBytes, boolean strict) { + this(handler, maxHeaderBytes, strict ? HttpCompliance.LEGACY : compliance()); + } + + + public HttpParser(RequestHandler handler, HttpCompliance compliance) { + this(handler, -1, compliance); + } + + + public HttpParser(RequestHandler handler, int maxHeaderBytes, HttpCompliance compliance) { + this(handler, null, maxHeaderBytes, compliance == null ? compliance() : compliance); + } + + + public HttpParser(ResponseHandler handler, int maxHeaderBytes, HttpCompliance compliance) { + this(null, handler, maxHeaderBytes, compliance == null ? compliance() : compliance); + } + + + private HttpParser(RequestHandler requestHandler, ResponseHandler responseHandler, int maxHeaderBytes, HttpCompliance compliance) { + handler = requestHandler != null ? requestHandler : responseHandler; + this.requestHandler = requestHandler; + this.responseHandler = responseHandler; + this.maxHeaderBytes = maxHeaderBytes; + this.compliance = compliance; + complianceSections = compliance.sections(); + complianceHandler = (ComplianceHandler) (handler instanceof ComplianceHandler ? handler : null); + } + + private static HttpCompliance compliance() { + boolean strict = Boolean.getBoolean(STRICT); + if (strict) { + LOG.warn("Deprecated property used: " + STRICT); + return HttpCompliance.LEGACY; + } + return HttpCompliance.RFC7230; + } + + public HttpHandler getHandler() { + return handler; + } + + /** + * Check RFC compliance violation + * + * @param violation The compliance section violation + * @return True if the current compliance level is set so as to Not allow this violation + */ + protected boolean complianceViolation(HttpComplianceSection violation) { + return complianceViolation(violation, null); + } + + /** + * Check RFC compliance violation + * + * @param violation The compliance section violation + * @param reason The reason for the violation + * @return True if the current compliance level is set so as to Not allow this violation + */ + protected boolean complianceViolation(HttpComplianceSection violation, String reason) { + if (complianceSections.contains(violation)) + return true; + if (reason == null) + reason = violation.getDescription(); + if (complianceHandler != null) + complianceHandler.onComplianceViolation(compliance, violation, reason); + + return false; + } + + protected void handleViolation(HttpComplianceSection section, String reason) { + if (complianceHandler != null) + complianceHandler.onComplianceViolation(compliance, section, reason); + } + + protected String caseInsensitiveHeader(String orig, String normative) { + if (complianceSections.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE)) + return normative; + if (!orig.equals(normative)) + handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE, orig); + return orig; + } + + public long getContentLength() { + return contentLength; + } + + public long getContentRead() { + return contentPosition; + } + + /** + * Set if a HEAD response is expected + * + * @param head true if head response is expected + */ + public void setHeadResponse(boolean head) { + headResponse = head; + } + + protected void setResponseStatus(int status) { + responseStatus = status; + } + + public State getState() { + return state; + } + + protected void setState(State state) { + if (DEBUG) + LOG.debug("{} --> {}", this.state, state); + this.state = state; + } + + protected void setState(FieldState state) { + if (DEBUG) + LOG.debug("{}:{} --> {}", this.state, field != null ? field : headerString != null ? headerString : string, state); + fieldState = state; + } + + public boolean inContentState() { + return state.ordinal() >= State.CONTENT.ordinal() && state.ordinal() < State.END.ordinal(); + } + + + public boolean inHeaderState() { + return state.ordinal() < State.CONTENT.ordinal(); + } + + + public boolean isChunking() { + return endOfContent == EndOfContent.CHUNKED_CONTENT; + } + + + public boolean isStart() { + return isState(State.START); + } + + + public boolean isClose() { + return isState(State.CLOSE); + } + + + public boolean isClosed() { + return isState(State.CLOSED); + } + + + public boolean isIdle() { + return IDLE_STATES.contains(state); + } + + + public boolean isComplete() { + return COMPLETE_STATES.contains(state); + } + + + public boolean isState(State state) { + return this.state == state; + } + + + private HttpTokens.Token next(ByteBuffer buffer) { + byte ch = buffer.get(); + + HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch]; + + switch (t.getType()) { + case CNTL: + throw new IllegalCharacterException(state, t, buffer); + + case LF: + cr = false; + break; + + case CR: + if (cr) + throw new BadMessageException("Bad EOL"); + + cr = true; + if (buffer.hasRemaining()) { + // Don't count the CRs and LFs of the chunked encoding. + if (maxHeaderBytes > 0 && (state == State.HEADER || state == State.TRAILER)) + headerBytes++; + return next(buffer); + } + + return null; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case HTAB: + case SPACE: + case OTEXT: + case COLON: + if (cr) + throw new BadMessageException("Bad EOL"); + break; + + default: + break; + } + + return t; + } + + + /* Quick lookahead for the start state looking for a request method or a HTTP version, + * otherwise skip white space until something else to parse. + */ + private boolean quickStart(ByteBuffer buffer) { + if (requestHandler != null) { + method = HttpMethod.lookAheadGet(buffer); + if (method != null) { + methodString = method.getValue(); + buffer.position(buffer.position() + methodString.length() + 1); + + setState(State.SPACE1); + return false; + } + } else if (responseHandler != null) { + version = HttpVersion.lookAheadGet(buffer); + if (version != null) { + buffer.position(buffer.position() + version.getValue().length() + 1); + setState(State.SPACE1); + return false; + } + } + + // Quick start look + while (state == State.START && buffer.hasRemaining()) { + HttpTokens.Token t = next(buffer); + if (t == null) + break; + + switch (t.getType()) { + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: { + string.setLength(0); + string.append(t.getChar()); + setState(requestHandler != null ? State.METHOD : State.RESPONSE_VERSION); + return false; + } + case OTEXT: + case SPACE: + case HTAB: + throw new IllegalCharacterException(state, t, buffer); + + default: + break; + } + + // count this white space as a header byte to avoid DOS + if (maxHeaderBytes > 0 && ++headerBytes > maxHeaderBytes) { + LOG.warn("padding is too large >" + maxHeaderBytes); + throw new BadMessageException(HttpStatus.BAD_REQUEST_400); + } + } + return false; + } + + + private void setString(String s) { + string.setLength(0); + string.append(s); + length = s.length(); + } + + + private String takeString() { + string.setLength(length); + String s = string.toString(); + string.setLength(0); + length = -1; + return s; + } + + + private boolean handleHeaderContentMessage() { + boolean handle_header = handler.headerComplete(); + headerComplete = true; + boolean handle_content = handler.contentComplete(); + boolean handle_message = handler.messageComplete(); + return handle_header || handle_content || handle_message; + } + + + private boolean handleContentMessage() { + boolean handle_content = handler.contentComplete(); + boolean handle_message = handler.messageComplete(); + return handle_content || handle_message; + } + + + /* Parse a request or response line + */ + private boolean parseLine(ByteBuffer buffer) { + boolean handle = false; + + // Process headers + while (state.ordinal() < State.HEADER.ordinal() && buffer.hasRemaining() && !handle) { + // process each character + HttpTokens.Token t = next(buffer); + if (t == null) + break; + + if (maxHeaderBytes > 0 && ++headerBytes > maxHeaderBytes) { + if (state == State.URI) { + LOG.warn("URI is too large >" + maxHeaderBytes); + throw new BadMessageException(HttpStatus.URI_TOO_LONG_414); + } else { + if (requestHandler != null) + LOG.warn("request is too large >" + maxHeaderBytes); + else + LOG.warn("response is too large >" + maxHeaderBytes); + throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431); + } + } + + switch (state) { + case METHOD: + switch (t.getType()) { + case SPACE: + length = string.length(); + methodString = takeString(); + + if (complianceSections.contains(HttpComplianceSection.METHOD_CASE_SENSITIVE)) { + HttpMethod method = HttpMethod.CACHE.get(methodString); + if (method != null) + methodString = method.getValue(); + } else { + HttpMethod method = HttpMethod.INSENSITIVE_CACHE.get(methodString); + + if (method != null) { + if (!method.getValue().equals(methodString)) + handleViolation(HttpComplianceSection.METHOD_CASE_SENSITIVE, methodString); + methodString = method.getValue(); + } + } + + setState(State.SPACE1); + break; + + case LF: + throw new BadMessageException("No URI"); + + case ALPHA: + case DIGIT: + case TCHAR: + string.append(t.getChar()); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case RESPONSE_VERSION: + switch (t.getType()) { + case SPACE: + length = string.length(); + String version = takeString(); + this.version = HttpVersion.CACHE.get(version); + checkVersion(); + setState(State.SPACE1); + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + string.append(t.getChar()); + break; + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case SPACE1: + switch (t.getType()) { + case SPACE: + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + if (responseHandler != null) { + if (t.getType() != HttpTokens.Type.DIGIT) { + throw new IllegalCharacterException(state, t, buffer); + } + setState(State.STATUS); + setResponseStatus(t.getByte() - '0'); + } else { + uri.reset(); + setState(State.URI); + // quick scan for space or EoBuffer + if (buffer.hasArray()) { + byte[] array = buffer.array(); + int p = buffer.arrayOffset() + buffer.position(); + int l = buffer.arrayOffset() + buffer.limit(); + int i = p; + while (i < l && array[i] > HttpTokens.SPACE) { + i++; + } + int len = i - p; + headerBytes += len; + + if (maxHeaderBytes > 0 && ++headerBytes > maxHeaderBytes) { + LOG.warn("URI is too large >" + maxHeaderBytes); + throw new BadMessageException(HttpStatus.URI_TOO_LONG_414); + } + uri.append(array, p - 1, len + 1); + buffer.position(i - buffer.arrayOffset()); + } else { + uri.append(t.getByte()); + } + } + break; + + default: + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, requestHandler != null ? "No URI" : "No Status"); + } + break; + + case STATUS: + switch (t.getType()) { + case SPACE: + setState(State.SPACE2); + break; + + case DIGIT: + responseStatus = responseStatus * 10 + (t.getByte() - '0'); + if (responseStatus >= 1000) { + throw new BadMessageException("Bad status"); + } + break; + + case LF: + setState(State.HEADER); + handle |= responseHandler.startResponse(version, responseStatus, null); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case URI: + switch (t.getType()) { + case SPACE: + setState(State.SPACE2); + break; + + case LF: + // HTTP/0.9 + if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9, "No request version")) { + throw new BadMessageException("HTTP/0.9 not supported"); + } + handle = requestHandler.startRequest(methodString, uri.toString(), HttpVersion.HTTP_0_9); + setState(State.END); + BufferUtils.clear(buffer); + handle |= handleHeaderContentMessage(); + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + case OTEXT: + uri.append(t.getByte()); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case SPACE2: + switch (t.getType()) { + case SPACE: + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + string.setLength(0); + string.append(t.getChar()); + if (responseHandler != null) { + length = 1; + setState(State.REASON); + } else { + setState(State.REQUEST_VERSION); + + // try quick look ahead for HTTP Version + HttpVersion version; + if (buffer.position() > 0 && buffer.hasArray()) + version = HttpVersion.lookAheadGet(buffer.array(), buffer.arrayOffset() + buffer.position() - 1, buffer.arrayOffset() + buffer.limit()); + else + version = HttpVersion.CACHE.getBest(buffer, 0, buffer.remaining()); + + if (version != null) { + int pos = buffer.position() + version.getValue().length() - 1; + if (pos < buffer.limit()) { + byte n = buffer.get(pos); + if (n == HttpTokens.CARRIAGE_RETURN) { + cr = true; + this.version = version; + checkVersion(); + string.setLength(0); + buffer.position(pos + 1); + } else if (n == HttpTokens.LINE_FEED) { + this.version = version; + checkVersion(); + string.setLength(0); + buffer.position(pos); + } + } + } + } + break; + + case LF: + if (responseHandler != null) { + setState(State.HEADER); + handle |= responseHandler.startResponse(version, responseStatus, null); + } else { + // HTTP/0.9 + if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9, "No request version")) { + throw new BadMessageException("HTTP/0.9 not supported"); + } + handle = requestHandler.startRequest(methodString, uri.toString(), HttpVersion.HTTP_0_9); + setState(State.END); + BufferUtils.clear(buffer); + handle |= handleHeaderContentMessage(); + } + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case REQUEST_VERSION: + switch (t.getType()) { + case LF: + if (version == null) { + length = string.length(); + version = HttpVersion.CACHE.get(takeString()); + } + checkVersion(); + + // Should we try to cache header fields? + if (fieldCache == null && version.getVersion() >= HttpVersion.HTTP_1_1.getVersion() && handler.getHeaderCacheSize() > 0) { + int header_cache = handler.getHeaderCacheSize(); + fieldCache = new ArrayTernaryTrie<>(header_cache); + } + + setState(State.HEADER); + + handle |= requestHandler.startRequest(methodString, uri.toString(), version); + continue; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + string.append(t.getChar()); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case REASON: + switch (t.getType()) { + case LF: + String reason = takeString(); + setState(State.HEADER); + handle |= responseHandler.startResponse(version, responseStatus, reason); + continue; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + case OTEXT: // TODO should this be UTF8 + string.append(t.getChar()); + length = string.length(); + break; + + case SPACE: + case HTAB: + string.append(t.getChar()); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + default: + throw new IllegalStateException(state.toString()); + } + } + + return handle; + } + + private void checkVersion() { + if (version == null) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Unknown Version"); + } + if (version.getVersion() < 10 || version.getVersion() > 20) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Version"); + } + } + + private void parsedHeader() { + // handler last header if any. Delayed to here just in case there was a continuation line (above) + if (headerString != null || valueString != null) { + // Handle known headers + if (header != null) { + boolean add_to_connection_trie = false; + switch (header) { + case CONTENT_LENGTH: + if (hasContentLength) { + if (complianceViolation(MULTIPLE_CONTENT_LENGTHS)) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.getDescription()); + } + if (convertContentLength(valueString) != contentLength) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, MULTIPLE_CONTENT_LENGTHS.getDescription()); + } + } + hasContentLength = true; + + if (endOfContent == EndOfContent.CHUNKED_CONTENT && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Content-Length"); + } + if (endOfContent != EndOfContent.CHUNKED_CONTENT) { + contentLength = convertContentLength(valueString); + if (contentLength <= 0) { + endOfContent = EndOfContent.NO_CONTENT; + } else { + endOfContent = EndOfContent.CONTENT_LENGTH; + } + } + break; + + case TRANSFER_ENCODING: + if (hasContentLength && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Transfer-Encoding and Content-Length"); + } + if (HttpHeaderValue.CHUNKED.is(valueString)) { + endOfContent = EndOfContent.CHUNKED_CONTENT; + contentLength = -1; + } else { + List values = new QuotedCSV(valueString).getValues(); + if (values.size() > 0 && HttpHeaderValue.CHUNKED.is(values.get(values.size() - 1))) { + endOfContent = EndOfContent.CHUNKED_CONTENT; + contentLength = -1; + } else if (values.stream().anyMatch(HttpHeaderValue.CHUNKED::is)) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad chunking"); + } + } + break; + + case HOST: + host = true; + if (!(field instanceof HostPortHttpField) && valueString != null && !valueString.isEmpty()) { + field = new HostPortHttpField(header, + complianceSections.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE) ? header.getValue() : headerString, + valueString); + add_to_connection_trie = fieldCache != null; + } + break; + + case CONNECTION: + // Don't cache headers if not persistent + if (HttpHeaderValue.CLOSE.is(valueString) || new QuotedCSV(valueString).getValues().stream().anyMatch(HttpHeaderValue.CLOSE::is)) { + fieldCache = null; + } + break; + + case AUTHORIZATION: + case ACCEPT: + case ACCEPT_CHARSET: + case ACCEPT_ENCODING: + case ACCEPT_LANGUAGE: + case COOKIE: + case CACHE_CONTROL: + case USER_AGENT: + add_to_connection_trie = fieldCache != null && field == null; + break; + + default: + break; + + } + + if (add_to_connection_trie && !fieldCache.isFull() && header != null && valueString != null) { + if (field == null) { + field = new HttpField(header, caseInsensitiveHeader(headerString, header.getValue()), valueString); + } + fieldCache.put(field); + } + } + handler.parsedHeader(field != null ? field : new HttpField(header, headerString, valueString)); + } + + headerString = valueString = null; + header = null; + field = null; + } + + private void parsedTrailer() { + // handler last header if any. Delayed to here just in case there was a continuation line (above) + if (headerString != null || valueString != null) { + handler.parsedTrailer(field != null ? field : new HttpField(header, headerString, valueString)); + } + headerString = valueString = null; + header = null; + field = null; + } + + private long convertContentLength(String valueString) { + try { + return Long.parseLong(valueString); + } catch (NumberFormatException e) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Invalid Content-Length Value", e); + } + } + + + /* + * Parse the message headers and return true if the handler has signalled for a return + */ + protected boolean parseFields(ByteBuffer buffer) { + // Process headers + while ((state == State.HEADER || state == State.TRAILER) && buffer.hasRemaining()) { + // process each character + HttpTokens.Token t = next(buffer); + if (t == null) { + break; + } + if (maxHeaderBytes > 0 && ++headerBytes > maxHeaderBytes) { + boolean header = state == State.HEADER; + LOG.warn("{} is too large {}>{}", header ? "Header" : "Trailer", headerBytes, maxHeaderBytes); + throw new BadMessageException(header ? + HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431 : + HttpStatus.PAYLOAD_TOO_LARGE_413); + } + + switch (fieldState) { + case FIELD: + switch (t.getType()) { + case COLON: + case SPACE: + case HTAB: { + if (complianceViolation(HttpComplianceSection.NO_FIELD_FOLDING, headerString)) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Header Folding"); + } + // header value without name - continuation? + if (valueString == null || valueString.isEmpty()) { + string.setLength(0); + length = 0; + } else { + setString(valueString); + string.append(' '); + length++; + valueString = null; + } + setState(FieldState.VALUE); + break; + } + + case LF: { + // process previous header + if (state == State.HEADER) { + parsedHeader(); + } else { + parsedTrailer(); + } + contentPosition = 0; + + // End of headers or trailers? + if (state == State.TRAILER) { + setState(State.END); + return handler.messageComplete(); + } + + // Was there a required host header? + if (!host && version == HttpVersion.HTTP_1_1 && requestHandler != null) { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "No Host"); + } + + // is it a response that cannot have a body? + if (responseHandler != null && // response + (responseStatus == 304 || // not-modified response + responseStatus == 204 || // no-content response + responseStatus < 200)) { // 1xx response + endOfContent = EndOfContent.NO_CONTENT; // ignore any other headers set + // else if we don't know framing + } else if (endOfContent == EndOfContent.UNKNOWN_CONTENT) { + if (responseStatus == 0 // request + || responseStatus == 304 // not-modified response + || responseStatus == 204 // no-content response + || responseStatus < 200) { // 1xx response + endOfContent = EndOfContent.NO_CONTENT; + } else { + endOfContent = EndOfContent.EOF_CONTENT; + } + } + + // How is the message ended? + switch (endOfContent) { + case EOF_CONTENT: { + setState(State.EOF_CONTENT); + boolean handle = handler.headerComplete(); + headerComplete = true; + return handle; + } + case CHUNKED_CONTENT: { + setState(State.CHUNKED_CONTENT); + boolean handle = handler.headerComplete(); + headerComplete = true; + return handle; + } + case NO_CONTENT: { + setState(State.END); + return handleHeaderContentMessage(); + } + default: { + setState(State.CONTENT); + boolean handle = handler.headerComplete(); + headerComplete = true; + return handle; + } + } + } + + case ALPHA: + case DIGIT: + case TCHAR: { + // process previous header + if (state == State.HEADER) { + parsedHeader(); + } else { + parsedTrailer(); + } + // handle new header + if (buffer.hasRemaining()) { + // Try a look ahead for the known header name and value. + HttpField cached_field = fieldCache == null ? null : fieldCache.getBest(buffer, -1, buffer.remaining()); + if (cached_field == null) { + cached_field = CACHE.getBest(buffer, -1, buffer.remaining()); + } + if (cached_field != null) { + String n = cached_field.getName(); + String v = cached_field.getValue(); + + if (!complianceSections.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE)) { + // Have to get the fields exactly from the buffer to match case + String en = BufferUtils.toString(buffer, buffer.position() - 1, n.length(), StandardCharsets.US_ASCII); + if (!n.equals(en)) { + handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE, en); + n = en; + cached_field = new HttpField(cached_field.getHeader(), n, v); + } + } + + if (v != null && !complianceSections.contains(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE)) { + String ev = BufferUtils.toString(buffer, buffer.position() + n.length() + 1, v.length(), StandardCharsets.ISO_8859_1); + if (!v.equals(ev)) { + handleViolation(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE, ev + "!=" + v); + v = ev; + cached_field = new HttpField(cached_field.getHeader(), n, v); + } + } + + header = cached_field.getHeader(); + headerString = n; + + if (v == null) { + // Header only + setState(FieldState.VALUE); + string.setLength(0); + length = 0; + buffer.position(buffer.position() + n.length() + 1); + break; + } + + // Header and value + int pos = buffer.position() + n.length() + v.length() + 1; + byte peek = buffer.get(pos); + if (peek == HttpTokens.CARRIAGE_RETURN || peek == HttpTokens.LINE_FEED) { + field = cached_field; + valueString = v; + setState(FieldState.IN_VALUE); + + if (peek == HttpTokens.CARRIAGE_RETURN) { + cr = true; + buffer.position(pos + 1); + } else { + buffer.position(pos); + } + break; + } + setState(FieldState.IN_VALUE); + setString(v); + buffer.position(pos); + break; + } + } + + // New header + setState(FieldState.IN_NAME); + string.setLength(0); + string.append(t.getChar()); + length = 1; + } + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case IN_NAME: + switch (t.getType()) { + case SPACE: + case HTAB: + //Ignore trailing whitespaces ? + if (!complianceViolation(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME, null)) { + headerString = takeString(); + header = HttpHeader.CACHE.get(headerString); + length = -1; + setState(FieldState.WS_AFTER_NAME); + break; + } + throw new IllegalCharacterException(state, t, buffer); + + case COLON: + headerString = takeString(); + header = HttpHeader.CACHE.get(headerString); + length = -1; + setState(FieldState.VALUE); + break; + + case LF: + headerString = takeString(); + header = HttpHeader.CACHE.get(headerString); + string.setLength(0); + valueString = ""; + length = -1; + + if (!complianceViolation(HttpComplianceSection.FIELD_COLON, headerString)) { + setState(FieldState.FIELD); + break; + } + throw new IllegalCharacterException(state, t, buffer); + + case ALPHA: + case DIGIT: + case TCHAR: + string.append(t.getChar()); + length = string.length(); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case WS_AFTER_NAME: + + switch (t.getType()) { + case SPACE: + case HTAB: + break; + + case COLON: + setState(FieldState.VALUE); + break; + + case LF: + if (!complianceViolation(HttpComplianceSection.FIELD_COLON, headerString)) { + setState(FieldState.FIELD); + break; + } + throw new IllegalCharacterException(state, t, buffer); + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case VALUE: + switch (t.getType()) { + case LF: + string.setLength(0); + valueString = ""; + length = -1; + + setState(FieldState.FIELD); + break; + + case SPACE: + case HTAB: + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + case OTEXT: // TODO review? should this be a utf8 string? + string.append(t.getChar()); + length = string.length(); + setState(FieldState.IN_VALUE); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + case IN_VALUE: + switch (t.getType()) { + case LF: + if (length > 0) { + valueString = takeString(); + length = -1; + } + setState(FieldState.FIELD); + break; + + case SPACE: + case HTAB: + string.append(t.getChar()); + break; + + case ALPHA: + case DIGIT: + case TCHAR: + case VCHAR: + case COLON: + case OTEXT: // TODO review? should this be a utf8 string? + string.append(t.getChar()); + length = string.length(); + break; + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + + default: + throw new IllegalStateException(state.toString()); + + } + } + + return false; + } + + + /** + * Parse until next Event. + * + * @param buffer the buffer to parse + * @return True if an {@link RequestHandler} method was called and it returned true; + */ + public boolean parseNext(ByteBuffer buffer) { + if (DEBUG) + LOG.debug("parseNext s={} {}", state, BufferUtils.toDetailString(buffer)); + try { + // Start a request/response + if (state == State.START) { + version = null; + method = null; + methodString = null; + endOfContent = EndOfContent.UNKNOWN_CONTENT; + header = null; + if (quickStart(buffer)) { + return true; + } + } + + // Request/response line + if (state.ordinal() >= State.START.ordinal() && state.ordinal() < State.HEADER.ordinal()) { + if (parseLine(buffer)) { + return true; + } + } + + // parse headers + if (state == State.HEADER) { + if (parseFields(buffer)) { + return true; + } + } + + // parse content + if (state.ordinal() >= State.CONTENT.ordinal() && state.ordinal() < State.TRAILER.ordinal()) { + // Handle HEAD response + if (responseStatus > 0 && headResponse) { + setState(State.END); + return handleContentMessage(); + } else { + if (parseContent(buffer)) { + return true; + } + } + } + + // parse headers + if (state == State.TRAILER) { + if (parseFields(buffer)) { + return true; + } + } + + // handle end states + if (state == State.END) { + // eat white space + while (buffer.remaining() > 0 && buffer.get(buffer.position()) <= HttpTokens.SPACE) { + buffer.get(); + } + } else if (isClose() || isClosed()) { + BufferUtils.clear(buffer); + } + + // Handle EOF + if (eof && !buffer.hasRemaining()) { + switch (state) { + case CLOSED: + break; + + case START: + setState(State.CLOSED); + handler.earlyEOF(); + break; + + case END: + case CLOSE: + setState(State.CLOSED); + break; + + case EOF_CONTENT: + case TRAILER: + if (fieldState == FieldState.FIELD) { + // Be forgiving of missing last CRLF + setState(State.CLOSED); + return handleContentMessage(); + } + setState(State.CLOSED); + handler.earlyEOF(); + break; + + case CONTENT: + case CHUNKED_CONTENT: + case CHUNK_SIZE: + case CHUNK_PARAMS: + case CHUNK: + setState(State.CLOSED); + handler.earlyEOF(); + break; + + default: + if (DEBUG) + LOG.debug("{} EOF in {}", this, state); + setState(State.CLOSED); + handler.badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400)); + break; + } + } + } catch (BadMessageException x) { + BufferUtils.clear(buffer); + badMessage(x); + } catch (Throwable x) { + BufferUtils.clear(buffer); + badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400, requestHandler != null ? "Bad Request" : "Bad Response", x)); + } + return false; + } + + protected void badMessage(BadMessageException x) { + if (DEBUG) + LOG.debug("Parse exception: " + this + " for " + handler, x); + setState(State.CLOSE); + if (headerComplete) + handler.earlyEOF(); + else + handler.badMessage(x); + } + + protected boolean parseContent(ByteBuffer buffer) { + int remaining = buffer.remaining(); + if (remaining == 0 && state == State.CONTENT) { + long content = contentLength - contentPosition; + if (content == 0) { + setState(State.END); + return handleContentMessage(); + } + } + + // Handle _content + while (state.ordinal() < State.TRAILER.ordinal() && remaining > 0) { + switch (state) { + case EOF_CONTENT: + contentChunk = buffer.duplicate(); + contentPosition += remaining; + buffer.position(buffer.position() + remaining); + if (handler.content(contentChunk)) { + return true; + } + break; + + case CONTENT: { + long content = contentLength - contentPosition; + if (content == 0) { + setState(State.END); + return handleContentMessage(); + } else { + contentChunk = buffer.duplicate(); + + // limit content by expected size + if (remaining > content) { + // We can cast remaining to an int as we know that it is smaller than + // or equal to length which is already an int. + contentChunk.limit(contentChunk.position() + (int) content); + } + + contentPosition += contentChunk.remaining(); + buffer.position(buffer.position() + contentChunk.remaining()); + + if (handler.content(contentChunk)) { + return true; + } + if (contentPosition == contentLength) { + setState(State.END); + return handleContentMessage(); + } + } + break; + } + + case CHUNKED_CONTENT: { + HttpTokens.Token t = next(buffer); + if (t == null) + break; + switch (t.getType()) { + case LF: + break; + + case DIGIT: + chunkLength = t.getHexDigit(); + chunkPosition = 0; + setState(State.CHUNK_SIZE); + break; + + case ALPHA: + if (t.isHexDigit()) { + chunkLength = t.getHexDigit(); + chunkPosition = 0; + setState(State.CHUNK_SIZE); + break; + } + throw new IllegalCharacterException(state, t, buffer); + + default: + throw new IllegalCharacterException(state, t, buffer); + } + break; + } + + case CHUNK_SIZE: { + HttpTokens.Token t = next(buffer); + if (t == null) { + break; + } + switch (t.getType()) { + case LF: + if (chunkLength == 0) { + setState(State.TRAILER); + if (handler.contentComplete()) + return true; + } else { + setState(State.CHUNK); + } + break; + + case SPACE: + setState(State.CHUNK_PARAMS); + break; + + default: + if (t.isHexDigit()) { + if (chunkLength > MAX_CHUNK_LENGTH) { + throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413); + } + chunkLength = chunkLength * 16 + t.getHexDigit(); + } else { + setState(State.CHUNK_PARAMS); + } + } + break; + } + + case CHUNK_PARAMS: { + HttpTokens.Token t = next(buffer); + if (t == null) { + break; + } + if (t.getType() == HttpTokens.Type.LF) { + if (chunkLength == 0) { + setState(State.TRAILER); + if (handler.contentComplete()) { + return true; + } + } else { + setState(State.CHUNK); + } + } + break; + } + + case CHUNK: { + int chunk = chunkLength - chunkPosition; + if (chunk == 0) { + setState(State.CHUNKED_CONTENT); + } else { + contentChunk = buffer.duplicate(); + + if (remaining > chunk) { + contentChunk.limit(contentChunk.position() + chunk); + } + chunk = contentChunk.remaining(); + + contentPosition += chunk; + chunkPosition += chunk; + buffer.position(buffer.position() + chunk); + if (handler.content(contentChunk)) { + return true; + } + } + break; + } + + case CLOSED: { + BufferUtils.clear(buffer); + return false; + } + + default: + break; + + } + + remaining = buffer.remaining(); + } + return false; + } + + + public boolean isAtEOF() { + return eof; + } + + + /** + * Signal that the associated data source is at EOF + */ + public void atEOF() { + if (DEBUG) + LOG.debug("atEOF {}", this); + eof = true; + } + + + /** + * Request that the associated data source be closed + */ + public void close() { + if (DEBUG) + LOG.debug("close {}", this); + setState(State.CLOSE); + } + + + public void reset() { + if (DEBUG) { + LOG.debug("reset {}", this); + } + // reset state + if (state == State.CLOSE || state == State.CLOSED) { + return; + } + setState(State.START); + endOfContent = EndOfContent.UNKNOWN_CONTENT; + contentLength = -1; + hasContentLength = false; + contentPosition = 0; + responseStatus = 0; + contentChunk = null; + headerBytes = 0; + host = false; + headerComplete = false; + } + + public Trie getFieldCache() { + return fieldCache; + } + + @Override + public String toString() { + return String.format("%s{s=%s,%d of %d}", + getClass().getSimpleName(), + state, + contentPosition, + contentLength); + } + + + // States + public enum FieldState { + FIELD, + IN_NAME, + VALUE, + IN_VALUE, + WS_AFTER_NAME, + } + + + // States + public enum State { + START, + METHOD, + RESPONSE_VERSION, + SPACE1, + STATUS, + URI, + SPACE2, + REQUEST_VERSION, + REASON, + PROXY, + HEADER, + CONTENT, + EOF_CONTENT, + CHUNKED_CONTENT, + CHUNK_SIZE, + CHUNK_PARAMS, + CHUNK, + TRAILER, + END, + CLOSE, // The associated stream/endpoint should be closed + CLOSED // The associated stream/endpoint is at EOF + } + + + /* Event Handler interface + * These methods return true if the caller should process the events + * so far received (eg return from parseNext and call HttpChannel.handle). + * If multiple callbacks are called in sequence (eg + * headerComplete then messageComplete) from the same point in the parsing + * then it is sufficient for the caller to process the events only once. + */ + public interface HttpHandler { + boolean content(ByteBuffer item); + + boolean headerComplete(); + + boolean contentComplete(); + + boolean messageComplete(); + + /** + * This is the method called by parser when a HTTP Header name and value is found + * + * @param field The field parsed + */ + void parsedHeader(HttpField field); + + /** + * This is the method called by parser when a HTTP Trailer name and value is found + * + * @param field The field parsed + */ + default void parsedTrailer(HttpField field) { + } + + + /** + * Called to signal that an EOF was received unexpectedly + * during the parsing of an HTTP message + */ + void earlyEOF(); + + + /** + * Called to signal that a bad HTTP message has been received. + * + * @param failure the failure with the bad message information + */ + default void badMessage(BadMessageException failure) { + badMessage(failure.getCode(), failure.getReason()); + } + + /** + * @deprecated use {@link #badMessage(BadMessageException)} instead + */ + @Deprecated + default void badMessage(int status, String reason) { + } + + + /** + * @return the size in bytes of the per parser header cache + */ + int getHeaderCacheSize(); + } + + + public interface RequestHandler extends HttpHandler { + /** + * This is the method called by parser when the HTTP request line is parsed + * + * @param method The method + * @param uri The raw bytes of the URI. These are copied into a ByteBuffer that will not be changed until this parser is reset and reused. + * @param version the http version in use + * @return true if handling parsing should return. + */ + boolean startRequest(String method, String uri, HttpVersion version); + + } + + + public interface ResponseHandler extends HttpHandler { + /** + * This is the method called by parser when the HTTP request line is parsed + * + * @param version the http version in use + * @param status the response status + * @param reason the response reason phrase + * @return true if handling parsing should return + */ + boolean startResponse(HttpVersion version, int status, String reason); + } + + + public interface ComplianceHandler extends HttpHandler { + @Deprecated + default void onComplianceViolation(HttpCompliance compliance, HttpCompliance required, String reason) { + } + + default void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String details) { + onComplianceViolation(compliance, HttpCompliance.requiredCompliance(violation), details); + } + } + + + private static class IllegalCharacterException extends BadMessageException { + private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer) { + super(400, String.format("Illegal character %s", token)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Illegal character %s in state=%s for buffer %s", token, state, BufferUtils.toDetailString(buffer))); + } + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v1/encoder/HttpGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v1/encoder/HttpGenerator.java new file mode 100644 index 000000000..b4b7c9e66 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v1/encoder/HttpGenerator.java @@ -0,0 +1,801 @@ +package com.fireflysource.net.http.common.v1.encoder; + +import com.fireflysource.common.collection.trie.ArrayTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.ProjectVersion; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.codec.PreEncodedHttpField; +import com.fireflysource.net.http.common.exception.BadMessageException; +import com.fireflysource.net.http.common.model.*; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.function.Supplier; + +import static com.fireflysource.net.http.common.model.HttpStatus.INTERNAL_SERVER_ERROR_500; +import static com.fireflysource.net.http.common.model.HttpTokens.EndOfContent; + +/** + * HttpGenerator. Builds HTTP Messages. + *

+ * If the system property "com.fireflysource.net.http.common.v1.encoder.HttpGenerator.STRICT" is set to true, + * then the generator will strictly pass on the exact strings received from methods and header + * fields. Otherwise a fast case insensitive string lookup is used that may alter the + * case and white space of some methods/headers + */ +public class HttpGenerator { + private final static LazyLogger LOG = SystemLogger.create(HttpGenerator.class); + + public static final MetaData.Response CONTINUE_100_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 100, null, null, -1); + public static final MetaData.Response PROGRESS_102_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 102, null, null, -1); + public final static MetaData.Response RESPONSE_500_INFO = + new MetaData.Response(HttpVersion.HTTP_1_1, INTERNAL_SERVER_ERROR_500, null, new HttpFields() {{ + put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); + }}, 0); + + // other statics + public static final int CHUNK_SIZE = 12; + private final static byte[] COLON_SPACE = new byte[]{':', ' '}; + private final static int SEND_SERVER = 0x01; + private final static int SEND_XPOWERED_BY = 0x02; + private final static Trie ASSUMED_CONTENT_METHODS = new ArrayTrie<>(8); + + // common _content + private static final byte[] ZERO_CHUNK = {(byte) '0', (byte) '\015', (byte) '\012'}; + private static final byte[] LAST_CHUNK = {(byte) '0', (byte) '\015', (byte) '\012', (byte) '\015', (byte) '\012'}; + private static final byte[] CONTENT_LENGTH_0 = StringUtils.getBytes("Content-Length: 0\r\n"); + private static final byte[] CONNECTION_CLOSE = StringUtils.getBytes("Connection: close\r\n"); + private static final byte[] HTTP_1_1_SPACE = StringUtils.getBytes(HttpVersion.HTTP_1_1 + " "); + private static final byte[] TRANSFER_ENCODING_CHUNKED = StringUtils.getBytes("Transfer-Encoding: chunked\r\n"); + private static final byte[][] SEND = new byte[][]{ + new byte[0], + StringUtils.getBytes("Server: Firefly(" + ProjectVersion.getValue() + ")\r\n"), + StringUtils.getBytes("X-Powered-By: Firefly(" + ProjectVersion.getValue() + ")\r\n"), + StringUtils.getBytes( + "Server: Firefly(" + ProjectVersion.getValue() + ")\r\n" + + "X-Powered-By: Firefly(" + ProjectVersion.getValue() + ")\r\n") + }; + private static final PreparedResponse[] PREPARED_RESPONSE = new PreparedResponse[HttpStatus.MAX_CODE + 1]; + + static { + ASSUMED_CONTENT_METHODS.put(HttpMethod.POST.getValue(), Boolean.TRUE); + ASSUMED_CONTENT_METHODS.put(HttpMethod.PUT.getValue(), Boolean.TRUE); + } + + static { + int versionLength = HttpVersion.HTTP_1_1.toString().length(); + + for (int i = 0; i < PREPARED_RESPONSE.length; i++) { + HttpStatus.Code code = HttpStatus.getCode(i); + if (code == null) { + continue; + } + String reason = code.getMessage(); + byte[] line = new byte[versionLength + 5 + reason.length() + 2]; + ByteBuffer.wrap(HttpVersion.HTTP_1_1.getBytes()).get(line, 0, versionLength); + line[versionLength] = ' '; + line[versionLength + 1] = (byte) ('0' + i / 100); + line[versionLength + 2] = (byte) ('0' + (i % 100) / 10); + line[versionLength + 3] = (byte) ('0' + (i % 10)); + line[versionLength + 4] = ' '; + for (int j = 0; j < reason.length(); j++) { + line[versionLength + 5 + j] = (byte) reason.charAt(j); + } + line[versionLength + 5 + reason.length()] = HttpTokens.CARRIAGE_RETURN; + line[versionLength + 6 + reason.length()] = HttpTokens.LINE_FEED; + + PREPARED_RESPONSE[i] = new PreparedResponse(); + PREPARED_RESPONSE[i].schemeCode = Arrays.copyOfRange(line, 0, versionLength + 5); + PREPARED_RESPONSE[i].reason = Arrays.copyOfRange(line, versionLength + 5, line.length - 2); + PREPARED_RESPONSE[i].responseLine = line; + } + } + + private final int send; + private State state = State.START; + private EndOfContent endOfContent = EndOfContent.UNKNOWN_CONTENT; + private long contentPrepared = 0; + private boolean noContentResponse = false; + private Boolean persistent = null; + private Supplier trailers = null; + // data + private boolean needCRLF = false; + + + public HttpGenerator() { + this(false, false); + } + + + public HttpGenerator(boolean sendServerVersion, boolean sendXPoweredBy) { + send = (sendServerVersion ? SEND_SERVER : 0) | (sendXPoweredBy ? SEND_XPOWERED_BY : 0); + } + + public static void setServerVersion(String serverVersion) { + SEND[SEND_SERVER] = StringUtils.getBytes("Server: " + serverVersion + "\r\n"); + SEND[SEND_XPOWERED_BY] = StringUtils.getBytes("X-Powered-By: " + serverVersion + "\r\n"); + SEND[SEND_SERVER | SEND_XPOWERED_BY] = StringUtils.getBytes( + "Server: " + serverVersion + "\r\n" + + "X-Powered-By: " + serverVersion + "\r\n"); + } + + private static void putContentLength(ByteBuffer header, long contentLength) { + if (contentLength == 0) { + header.put(CONTENT_LENGTH_0); + } else { + header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); + BufferUtils.putDecLong(header, contentLength); + header.put(HttpTokens.CRLF); + } + } + + public static byte[] getReasonBuffer(int code) { + PreparedResponse status = code < PREPARED_RESPONSE.length ? PREPARED_RESPONSE[code] : null; + if (status != null) { + return status.reason; + } else { + return null; + } + } + + private static void putSanitisedName(String s, ByteBuffer buffer) { + int l = s.length(); + for (int i = 0; i < l; i++) { + char c = s.charAt(i); + + if (c < 0 || c > 0xff || c == '\r' || c == '\n' || c == ':') { + buffer.put((byte) '?'); + } else { + buffer.put((byte) (0xff & c)); + } + } + } + + private static void putSanitisedValue(String s, ByteBuffer buffer) { + int l = s.length(); + for (int i = 0; i < l; i++) { + char c = s.charAt(i); + + if (c < 0 || c > 0xff || c == '\r' || c == '\n') { + buffer.put((byte) ' '); + } else { + buffer.put((byte) (0xff & c)); + } + } + } + + public static void putTo(HttpField field, ByteBuffer bufferInFillMode) { + if (field instanceof PreEncodedHttpField) { + ((PreEncodedHttpField) field).putTo(bufferInFillMode, HttpVersion.HTTP_1_0); + } else { + HttpHeader header = field.getHeader(); + if (header != null) { + bufferInFillMode.put(header.getBytesColonSpace()); + } else { + putSanitisedName(field.getName(), bufferInFillMode); + bufferInFillMode.put(COLON_SPACE); + } + putSanitisedValue(field.getValue(), bufferInFillMode); + BufferUtils.putCRLF(bufferInFillMode); + } + } + + public static void putTo(HttpFields fields, ByteBuffer bufferInFillMode) { + for (HttpField field : fields) { + if (field != null) { + putTo(field, bufferInFillMode); + } + } + BufferUtils.putCRLF(bufferInFillMode); + } + + public void reset() { + state = State.START; + endOfContent = EndOfContent.UNKNOWN_CONTENT; + noContentResponse = false; + persistent = null; + contentPrepared = 0; + needCRLF = false; + trailers = null; + } + + @Deprecated + public boolean getSendServerVersion() { + return (send & SEND_SERVER) != 0; + } + + @Deprecated + public void setSendServerVersion(boolean sendServerVersion) { + throw new UnsupportedOperationException(); + } + + public State getState() { + return state; + } + + public boolean isState(State state) { + return this.state == state; + } + + public boolean isIdle() { + return state == State.START; + } + + public boolean isEnd() { + return state == State.END; + } + + public boolean isCommitted() { + return state.ordinal() >= State.COMMITTED.ordinal(); + } + + public boolean isChunking() { + return endOfContent == EndOfContent.CHUNKED_CONTENT; + } + + public boolean isNoContent() { + return noContentResponse; + } + + /** + * @return true, if known to be persistent + */ + public boolean isPersistent() { + return Boolean.TRUE.equals(persistent); + } + + public void setPersistent(boolean persistent) { + this.persistent = persistent; + } + + public boolean isWritten() { + return contentPrepared > 0; + } + + public long getContentPrepared() { + return contentPrepared; + } + + public void abort() { + persistent = false; + state = State.END; + endOfContent = null; + } + + public Result generateRequest(MetaData.Request info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) { + switch (state) { + case START: { + if (info == null) { + return Result.NEED_INFO; + } + if (header == null) { + return Result.NEED_HEADER; + } + // prepare the header + int pos = BufferUtils.flipToFill(header); + try { + // generate ResponseLine + generateRequestLine(info, header); + + if (info.getHttpVersion() == HttpVersion.HTTP_0_9) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "HTTP/0.9 not supported"); + } + generateHeaders(info, header, content, last); + + boolean expect100 = info.getFields().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.getValue()); + + if (expect100) { + state = State.COMMITTED; + } else { + // handle the content. + int len = BufferUtils.length(content); + if (len > 0) { + contentPrepared += len; + if (isChunking()) { + prepareChunk(header, len); + } + } + state = last ? State.COMPLETING : State.COMMITTED; + } + + return Result.FLUSH; + } catch (BadMessageException e) { + throw e; + } catch (BufferOverflowException e) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Request header too large", e); + } catch (Exception e) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, e.getMessage(), e); + } finally { + BufferUtils.flipToFlush(header, pos); + } + } + + case COMMITTED: { + return committed(chunk, content, last); + } + + case COMPLETING: { + return completing(chunk, content); + } + + case END: + if (BufferUtils.hasContent(content)) { + if (LOG.isDebugEnabled()) { + LOG.debug("discarding content in COMPLETING"); + } + BufferUtils.clear(content); + } + return Result.DONE; + + default: + throw new IllegalStateException(); + } + } + + private Result committed(ByteBuffer chunk, ByteBuffer content, boolean last) { + int len = BufferUtils.length(content); + + // handle the content. + if (len > 0) { + if (isChunking()) { + if (chunk == null) { + return Result.NEED_CHUNK; + } + BufferUtils.clearToFill(chunk); + prepareChunk(chunk, len); + BufferUtils.flipToFlush(chunk, 0); + } + contentPrepared += len; + } + + if (last) { + state = State.COMPLETING; + return len > 0 ? Result.FLUSH : Result.CONTINUE; + } + return len > 0 ? Result.FLUSH : Result.DONE; + } + + private Result completing(ByteBuffer chunk, ByteBuffer content) { + if (BufferUtils.hasContent(content)) { + if (LOG.isDebugEnabled()) { + LOG.debug("discarding content in COMPLETING"); + } + BufferUtils.clear(content); + } + + if (isChunking()) { + if (trailers != null) { + // Do we need a chunk buffer? + if (chunk == null || chunk.capacity() <= CHUNK_SIZE) { + return Result.NEED_CHUNK_TRAILER; + } + HttpFields trailers = this.trailers.get(); + + if (trailers != null) { + // Write the last chunk + BufferUtils.clearToFill(chunk); + generateTrailers(chunk, trailers); + BufferUtils.flipToFlush(chunk, 0); + endOfContent = EndOfContent.UNKNOWN_CONTENT; + return Result.FLUSH; + } + } + + // Do we need a chunk buffer? + if (chunk == null) { + return Result.NEED_CHUNK; + } + // Write the last chunk + BufferUtils.clearToFill(chunk); + prepareChunk(chunk, 0); + BufferUtils.flipToFlush(chunk, 0); + endOfContent = EndOfContent.UNKNOWN_CONTENT; + return Result.FLUSH; + } + + state = State.END; + return Boolean.TRUE.equals(persistent) ? Result.DONE : Result.SHUTDOWN_OUT; + + } + + public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) { + switch (state) { + case START: { + if (info == null) { + return Result.NEED_INFO; + } + HttpVersion version = info.getHttpVersion(); + if (version == null) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "No version"); + } + if (version == HttpVersion.HTTP_0_9) { + persistent = false; + endOfContent = EndOfContent.EOF_CONTENT; + if (BufferUtils.hasContent(content)) { + contentPrepared += content.remaining(); + } + state = last ? State.COMPLETING : State.COMMITTED; + return Result.FLUSH; + } + + // Do we need a response header + if (header == null) { + return Result.NEED_HEADER; + } + // prepare the header + int pos = BufferUtils.flipToFill(header); + try { + // generate ResponseLine + generateResponseLine(info, header); + + // Handle 1xx and no content responses + int status = info.getStatus(); + if (status >= 100 && status < 200) { + noContentResponse = true; + + if (status != HttpStatus.SWITCHING_PROTOCOLS_101) { + header.put(HttpTokens.CRLF); + state = State.COMPLETING_1XX; + return Result.FLUSH; + } + } else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIED_304) { + noContentResponse = true; + } + + generateHeaders(info, header, content, last); + + // handle the content. + int len = BufferUtils.length(content); + if (len > 0) { + contentPrepared += len; + if (isChunking() && !head) { + prepareChunk(header, len); + } + } + state = last ? State.COMPLETING : State.COMMITTED; + } catch (BadMessageException e) { + throw e; + } catch (BufferOverflowException e) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Response header too large", e); + } catch (Exception e) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, e.getMessage(), e); + } finally { + BufferUtils.flipToFlush(header, pos); + } + + return Result.FLUSH; + } + + case COMMITTED: { + return committed(chunk, content, last); + } + + case COMPLETING_1XX: { + reset(); + return Result.DONE; + } + + case COMPLETING: { + return completing(chunk, content); + } + + case END: + if (BufferUtils.hasContent(content)) { + if (LOG.isDebugEnabled()) { + LOG.debug("discarding content in COMPLETING"); + } + BufferUtils.clear(content); + } + return Result.DONE; + + default: + throw new IllegalStateException(); + } + } + + private void prepareChunk(ByteBuffer chunk, int remaining) { + // if we need CRLF add this to header + if (needCRLF) { + BufferUtils.putCRLF(chunk); + } + // Add the chunk size to the header + if (remaining > 0) { + BufferUtils.putHexInt(chunk, remaining); + BufferUtils.putCRLF(chunk); + needCRLF = true; + } else { + chunk.put(LAST_CHUNK); + needCRLF = false; + } + } + + private void generateTrailers(ByteBuffer buffer, HttpFields trailer) { + // if we need CRLF add this to header + if (needCRLF) { + BufferUtils.putCRLF(buffer); + } + // Add the chunk size to the header + buffer.put(ZERO_CHUNK); + + int n = trailer.size(); + for (int f = 0; f < n; f++) { + HttpField field = trailer.getField(f); + putTo(field, buffer); + } + + BufferUtils.putCRLF(buffer); + } + + private void generateRequestLine(MetaData.Request request, ByteBuffer header) { + header.put(StringUtils.getBytes(request.getMethod())); + header.put((byte) ' '); + header.put(StringUtils.getBytes(request.getURIString())); + header.put((byte) ' '); + header.put(request.getHttpVersion().getBytes()); + header.put(HttpTokens.CRLF); + } + + private void generateResponseLine(MetaData.Response response, ByteBuffer header) { + // Look for prepared response line + int status = response.getStatus(); + PreparedResponse preprepared = status < PREPARED_RESPONSE.length ? PREPARED_RESPONSE[status] : null; + String reason = response.getReason(); + if (preprepared != null) { + if (reason == null) { + header.put(preprepared.responseLine); + } else { + header.put(preprepared.schemeCode); + header.put(getReasonBytes(reason)); + header.put(HttpTokens.CRLF); + } + } else { // generate response line + header.put(HTTP_1_1_SPACE); + header.put((byte) ('0' + status / 100)); + header.put((byte) ('0' + (status % 100) / 10)); + header.put((byte) ('0' + (status % 10))); + header.put((byte) ' '); + if (reason == null) { + header.put((byte) ('0' + status / 100)); + header.put((byte) ('0' + (status % 100) / 10)); + header.put((byte) ('0' + (status % 10))); + } else { + header.put(getReasonBytes(reason)); + } + header.put(HttpTokens.CRLF); + } + } + + private byte[] getReasonBytes(String reason) { + if (reason.length() > 1024) { + reason = reason.substring(0, 1024); + } + byte[] _bytes = StringUtils.getBytes(reason); + + for (int i = _bytes.length; i-- > 0; ) { + if (_bytes[i] == '\r' || _bytes[i] == '\n') { + _bytes[i] = '?'; + } + } + return _bytes; + } + + private void generateHeaders(MetaData info, ByteBuffer header, ByteBuffer content, boolean last) { + final MetaData.Request request = (info instanceof MetaData.Request) ? (MetaData.Request) info : null; + final MetaData.Response response = (info instanceof MetaData.Response) ? (MetaData.Response) info : null; + + if (LOG.isDebugEnabled()) { + LOG.debug("generateHeaders {} last={} content={}", info, last, BufferUtils.toDetailString(content)); + LOG.debug(info.getFields().toString()); + } + + // default field values + int send = this.send; + HttpField transfer_encoding = null; + boolean http11 = info.getHttpVersion() == HttpVersion.HTTP_1_1; + boolean close = false; + trailers = http11 ? info.getTrailerSupplier() : null; + boolean chunked_hint = trailers != null; + boolean content_type = false; + long content_length = info.getContentLength(); + boolean content_length_field = false; + + // Generate fields + HttpFields fields = info.getFields(); + if (fields != null) { + int n = fields.size(); + for (int f = 0; f < n; f++) { + HttpField field = fields.getField(f); + HttpHeader h = field.getHeader(); + if (h == null) { + putTo(field, header); + } else { + switch (h) { + case CONTENT_LENGTH: + if (content_length < 0) { + content_length = field.getLongValue(); + } else if (content_length != field.getLongValue()) { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, String.format("Incorrect Content-Length %d!=%d", content_length, field.getLongValue())); + } + content_length_field = true; + break; + + case CONTENT_TYPE: { + // write the field to the header + content_type = true; + putTo(field, header); + break; + } + + case TRANSFER_ENCODING: { + if (http11) { + // Don't add yet, treat this only as a hint that there is content + // with a preference to chunk if we can + transfer_encoding = field; + chunked_hint = field.contains(HttpHeaderValue.CHUNKED.getValue()); + } + break; + } + + case CONNECTION: { + putTo(field, header); + if (field.contains(HttpHeaderValue.CLOSE.getValue())) { + close = true; + persistent = false; + } + + if (info.getHttpVersion() == HttpVersion.HTTP_1_0 && persistent == null && field.contains(HttpHeaderValue.KEEP_ALIVE.getValue())) { + persistent = true; + } + break; + } + + case SERVER: { + send = send & ~SEND_SERVER; + putTo(field, header); + break; + } + + default: + putTo(field, header); + } + } + } + } + + // Can we work out the content length? + if (last && content_length < 0 && trailers == null) { + content_length = contentPrepared + BufferUtils.length(content); + } + // Calculate how to end _content and connection, _content length and transfer encoding + // settings from http://tools.ietf.org/html/rfc7230#section-3.3.3 + + boolean assumed_content_request = request != null && Boolean.TRUE.equals(ASSUMED_CONTENT_METHODS.get(request.getMethod())); + boolean assumed_content = assumed_content_request || content_type || chunked_hint; + boolean nocontent_request = request != null && content_length <= 0 && !assumed_content; + + if (persistent == null) { + persistent = http11 || (request != null && HttpMethod.CONNECT.is(request.getMethod())); + } + // If the message is known not to have content + if (noContentResponse || nocontent_request) { + // We don't need to indicate a body length + endOfContent = EndOfContent.NO_CONTENT; + + // But it is an error if there actually is content + if (contentPrepared > 0 || content_length > 0) { + if (contentPrepared == 0 && last) { + // TODO discard content for backward compatibility with 9.3 releases + // TODO review if it is still needed in 9.4 or can we just throw. + content.clear(); + } else { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response"); + } + } + } + // Else if we are HTTP/1.1, and the content length is unknown, and we are either persistent + // or it is a request with content (which cannot EOF), or the app has requested chunk + else if (http11 && (chunked_hint || content_length < 0 && (persistent || assumed_content_request))) { + // we use chunk + endOfContent = EndOfContent.CHUNKED_CONTENT; + + // try to use user supplied encoding as it may have other values. + if (transfer_encoding == null) + header.put(TRANSFER_ENCODING_CHUNKED); + else if (transfer_encoding.toString().endsWith(HttpHeaderValue.CHUNKED.toString())) { + putTo(transfer_encoding, header); + transfer_encoding = null; + } else if (!chunked_hint) { + putTo(new HttpField(HttpHeader.TRANSFER_ENCODING, transfer_encoding.getValue() + ",chunked"), header); + transfer_encoding = null; + } else { + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Bad Transfer-Encoding"); + } + } + // Else if we have known the content length and are a request or a persistent response, + else if (content_length >= 0 && (request != null || persistent)) { + // Use the content length + endOfContent = EndOfContent.CONTENT_LENGTH; + putContentLength(header, content_length); + } + // Else if we are a response + else if (response != null) { + // We must use EOF - even if we were trying to be persistent + endOfContent = EndOfContent.EOF_CONTENT; + persistent = false; + if (content_length >= 0 && (content_length > 0 || assumed_content || content_length_field)) { + putContentLength(header, content_length); + } + if (http11 && !close) { + header.put(CONNECTION_CLOSE); + } + } + // Else we must be a request + else { + // with no way to indicate body length + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Unknown content length for request"); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(endOfContent.toString()); + } + // Add transfer encoding if it is not chunking + if (transfer_encoding != null) { + if (chunked_hint) { + String v = transfer_encoding.getValue(); + int c = v.lastIndexOf(','); + if (c > 0 && v.lastIndexOf(HttpHeaderValue.CHUNKED.toString(), c) > c) { + putTo(new HttpField(HttpHeader.TRANSFER_ENCODING, v.substring(0, c).trim()), header); + } + } else { + putTo(transfer_encoding, header); + } + } + + // Send server? + int status = response != null ? response.getStatus() : -1; + if (status > 199) { + header.put(SEND[send]); + } + // end the header. + header.put(HttpTokens.CRLF); + } + + @Override + public String toString() { + return String.format("%s@%x{s=%s}", + getClass().getSimpleName(), + hashCode(), + state); + } + + // states + public enum State { + START, + COMMITTED, + COMPLETING, + COMPLETING_1XX, + END + } + + public enum Result { + NEED_CHUNK, // Need a small chunk buffer of CHUNK_SIZE + NEED_INFO, // Need the request/response metadata info + NEED_HEADER, // Need buffer to build HTTP headers into + NEED_CHUNK_TRAILER, // Need a large chunk buffer for last chunk and trailers + FLUSH, // The buffers previously generated should be flushed + CONTINUE, // Continue generating the message + SHUTDOWN_OUT, // Need EOF to be signaled + DONE // The current phase of generation is complete + } + + // Build cache of response lines for status + private static class PreparedResponse { + byte[] reason; + byte[] schemeCode; + byte[] responseLine; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/BodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/BodyParser.java new file mode 100644 index 000000000..eed0a8817 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/BodyParser.java @@ -0,0 +1,160 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.v2.frame.*; + +import java.nio.ByteBuffer; + +/** + *

The base parser for the frame body of HTTP/2 frames.

+ *

Subclasses implement {@link #parse(ByteBuffer)} to parse + * the frame specific body.

+ * + * @see Parser + */ +public abstract class BodyParser { + public static final LazyLogger LOG = SystemLogger.create(BodyParser.class); + + private final HeaderParser headerParser; + private final Parser.Listener listener; + + protected BodyParser(HeaderParser headerParser, Parser.Listener listener) { + this.headerParser = headerParser; + this.listener = listener; + } + + /** + *

Parses the body bytes in the given {@code buffer}; only the body + * bytes are consumed, therefore when this method returns, the buffer + * may contain unconsumed bytes.

+ * + * @param buffer the buffer to parse + * @return true if the whole body bytes were parsed, false if not enough + * body bytes were present in the buffer + */ + public abstract boolean parse(ByteBuffer buffer); + + protected void emptyBody(ByteBuffer buffer) { + connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_frame"); + } + + protected boolean hasFlag(int bit) { + return headerParser.hasFlag(bit); + } + + protected boolean isPadding() { + return headerParser.hasFlag(Flags.PADDING); + } + + protected boolean isEndStream() { + return headerParser.hasFlag(Flags.END_STREAM); + } + + protected int getStreamId() { + return headerParser.getStreamId(); + } + + protected int getBodyLength() { + return headerParser.getLength(); + } + + protected void notifyData(DataFrame frame) { + try { + listener.onData(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyHeaders(HeadersFrame frame) { + try { + listener.onHeaders(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyPriority(PriorityFrame frame) { + try { + listener.onPriority(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyReset(ResetFrame frame) { + try { + listener.onReset(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifySettings(SettingsFrame frame) { + try { + listener.onSettings(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyPushPromise(PushPromiseFrame frame) { + try { + listener.onPushPromise(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyPing(PingFrame frame) { + try { + listener.onPing(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyGoAway(GoAwayFrame frame) { + try { + listener.onGoAway(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void notifyWindowUpdate(WindowUpdateFrame frame) { + try { + listener.onWindowUpdate(frame); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected boolean connectionFailure(ByteBuffer buffer, int error, String reason) { + BufferUtils.clear(buffer); + notifyConnectionFailure(error, reason); + return false; + } + + private void notifyConnectionFailure(int error, String reason) { + try { + listener.onConnectionFailure(error, reason); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + protected void streamFailure(int streamId, int error, String reason) { + notifyStreamFailure(streamId, error, reason); + } + + private void notifyStreamFailure(int streamId, int error, String reason) { + try { + listener.onStreamFailure(streamId, error, reason); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ContinuationBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ContinuationBodyParser.java new file mode 100644 index 000000000..ea0d294ee --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ContinuationBodyParser.java @@ -0,0 +1,89 @@ +package com.fireflysource.net.http.common.v2.decoder; + + +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.HeadersFrame; + +import java.nio.ByteBuffer; + +public class ContinuationBodyParser extends BodyParser { + private final HeaderBlockParser headerBlockParser; + private final HeaderBlockFragments headerBlockFragments; + private State state = State.PREPARE; + private int length; + + public ContinuationBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments) { + super(headerParser, listener); + this.headerBlockParser = headerBlockParser; + this.headerBlockFragments = headerBlockFragments; + } + + @Override + protected void emptyBody(ByteBuffer buffer) { + if (hasFlag(Flags.END_HEADERS)) + onHeaders(); + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_continuation_frame"); + + if (getStreamId() != headerBlockFragments.getStreamId()) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_continuation_stream"); + + length = getBodyLength(); + state = State.FRAGMENT; + break; + } + case FRAGMENT: { + int remaining = buffer.remaining(); + if (remaining < length) { + headerBlockFragments.storeFragment(buffer, remaining, false); + length -= remaining; + break; + } else { + boolean last = hasFlag(Flags.END_HEADERS); + headerBlockFragments.storeFragment(buffer, length, last); + reset(); + if (last) + return onHeaders(); + return true; + } + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private boolean onHeaders() { + ByteBuffer headerBlock = headerBlockFragments.complete(); + MetaData metaData = headerBlockParser.parse(headerBlock, headerBlock.remaining()); + if (metaData == HeaderBlockParser.SESSION_FAILURE) + return false; + if (metaData == null || metaData == HeaderBlockParser.STREAM_FAILURE) + return true; + HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, headerBlockFragments.getPriorityFrame(), headerBlockFragments.isEndStream()); + frame.setEndHeaders(hasFlag(Flags.END_HEADERS)); + notifyHeaders(frame); + return true; + } + + private void reset() { + state = State.PREPARE; + length = 0; + } + + private enum State { + PREPARE, FRAGMENT + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/DataBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/DataBodyParser.java new file mode 100644 index 000000000..dc39c0db4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/DataBodyParser.java @@ -0,0 +1,109 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.DataFrame; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; + +import java.nio.ByteBuffer; + +public class DataBodyParser extends BodyParser { + private State state = State.PREPARE; + private int padding; + private int paddingLength; + private int length; + + public DataBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + private void reset() { + state = State.PREPARE; + padding = 0; + paddingLength = 0; + length = 0; + } + + @Override + protected void emptyBody(ByteBuffer buffer) { + if (isPadding()) + connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_data_frame"); + else + onData(BufferUtils.EMPTY_BUFFER, false, 0); + } + + @Override + public boolean parse(ByteBuffer buffer) { + boolean loop = false; + while (buffer.hasRemaining() || loop) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_data_frame"); + + length = getBodyLength(); + state = isPadding() ? State.PADDING_LENGTH : State.DATA; + break; + } + case PADDING_LENGTH: { + padding = 1; // We have seen this byte. + paddingLength = buffer.get() & 0xFF; + --length; + length -= paddingLength; + state = State.DATA; + loop = length == 0; + if (length < 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_data_frame_padding"); + break; + } + case DATA: { + int size = Math.min(buffer.remaining(), length); + int position = buffer.position(); + int limit = buffer.limit(); + buffer.limit(position + size); + ByteBuffer slice = buffer.slice(); + buffer.limit(limit); + buffer.position(position + size); + + length -= size; + if (length == 0) { + state = State.PADDING; + loop = paddingLength == 0; + // Padding bytes include the bytes that define the + // padding length plus the actual padding bytes. + onData(slice, false, padding + paddingLength); + } else { + // We got partial data, simulate a smaller frame, and stay in DATA state. + // No padding for these synthetic frames (even if we have read + // the padding length already), it will be accounted at the end. + onData(slice, true, 0); + } + break; + } + case PADDING: { + int size = Math.min(buffer.remaining(), paddingLength); + buffer.position(buffer.position() + size); + paddingLength -= size; + if (paddingLength == 0) { + reset(); + return true; + } + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private void onData(ByteBuffer buffer, boolean fragment, int padding) { + DataFrame frame = new DataFrame(getStreamId(), buffer, !fragment && isEndStream(), padding); + notifyData(frame); + } + + private enum State { + PREPARE, PADDING_LENGTH, DATA, PADDING + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/GoAwayBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/GoAwayBodyParser.java new file mode 100644 index 000000000..875537c75 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/GoAwayBodyParser.java @@ -0,0 +1,132 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.GoAwayFrame; + +import java.nio.ByteBuffer; + +public class GoAwayBodyParser extends BodyParser { + private State state = State.PREPARE; + private int cursor; + private int length; + private int lastStreamId; + private int error; + private byte[] payload; + + public GoAwayBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + length = 0; + lastStreamId = 0; + error = 0; + payload = null; + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + state = State.LAST_STREAM_ID; + length = getBodyLength(); + break; + } + case LAST_STREAM_ID: { + if (buffer.remaining() >= 4) { + lastStreamId = buffer.getInt(); + lastStreamId &= 0x7F_FF_FF_FF; + state = State.ERROR; + length -= 4; + if (length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_go_away_frame"); + } else { + state = State.LAST_STREAM_ID_BYTES; + cursor = 4; + } + break; + } + case LAST_STREAM_ID_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + lastStreamId += currByte << (8 * cursor); + --length; + if (cursor > 0 && length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_go_away_frame"); + if (cursor == 0) { + lastStreamId &= 0x7F_FF_FF_FF; + state = State.ERROR; + if (length == 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_go_away_frame"); + } + break; + } + case ERROR: { + if (buffer.remaining() >= 4) { + error = buffer.getInt(); + state = State.PAYLOAD; + length -= 4; + if (length < 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_go_away_frame"); + if (length == 0) + return onGoAway(lastStreamId, error, null); + } else { + state = State.ERROR_BYTES; + cursor = 4; + } + break; + } + case ERROR_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + error += currByte << (8 * cursor); + --length; + if (cursor > 0 && length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_go_away_frame"); + if (cursor == 0) { + state = State.PAYLOAD; + if (length == 0) + return onGoAway(lastStreamId, error, null); + } + break; + } + case PAYLOAD: { + payload = new byte[length]; + if (buffer.remaining() >= length) { + buffer.get(payload); + return onGoAway(lastStreamId, error, payload); + } else { + state = State.PAYLOAD_BYTES; + cursor = length; + } + break; + } + case PAYLOAD_BYTES: { + payload[payload.length - cursor] = buffer.get(); + --cursor; + if (cursor == 0) + return onGoAway(lastStreamId, error, payload); + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private boolean onGoAway(int lastStreamId, int error, byte[] payload) { + GoAwayFrame frame = new GoAwayFrame(lastStreamId, error, payload); + reset(); + notifyGoAway(frame); + return true; + } + + private enum State { + PREPARE, LAST_STREAM_ID, LAST_STREAM_ID_BYTES, ERROR, ERROR_BYTES, PAYLOAD, PAYLOAD_BYTES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderBlockFragments.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderBlockFragments.java new file mode 100644 index 000000000..673d3c419 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderBlockFragments.java @@ -0,0 +1,66 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.PriorityFrame; + +import java.nio.ByteBuffer; + +public class HeaderBlockFragments { + private PriorityFrame priorityFrame; + private boolean endStream; + private int streamId; + private ByteBuffer storage; + + public void storeFragment(ByteBuffer fragment, int length, boolean last) { + if (storage == null) { + int space = last ? length : length * 2; + storage = ByteBuffer.allocate(space); + } + + // Grow the storage if necessary. + if (storage.remaining() < length) { + int space = last ? length : length * 2; + int capacity = storage.position() + space; + ByteBuffer newStorage = ByteBuffer.allocate(capacity); + storage.flip(); + newStorage.put(storage); + storage = newStorage; + } + + // Copy the fragment into the storage. + int limit = fragment.limit(); + fragment.limit(fragment.position() + length); + storage.put(fragment); + fragment.limit(limit); + } + + public PriorityFrame getPriorityFrame() { + return priorityFrame; + } + + public void setPriorityFrame(PriorityFrame priorityFrame) { + this.priorityFrame = priorityFrame; + } + + public boolean isEndStream() { + return endStream; + } + + public void setEndStream(boolean endStream) { + this.endStream = endStream; + } + + public ByteBuffer complete() { + ByteBuffer result = storage; + storage = null; + result.flip(); + return result; + } + + public int getStreamId() { + return streamId; + } + + public void setStreamId(int streamId) { + this.streamId = streamId; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderBlockParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderBlockParser.java new file mode 100644 index 000000000..d042f8266 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderBlockParser.java @@ -0,0 +1,93 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.model.HttpVersion; +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.hpack.HpackDecoder; +import com.fireflysource.net.http.common.v2.hpack.HpackException; + +import java.nio.ByteBuffer; + +public class HeaderBlockParser { + public static final MetaData STREAM_FAILURE = new MetaData(HttpVersion.HTTP_2, null); + public static final MetaData SESSION_FAILURE = new MetaData(HttpVersion.HTTP_2, null); + public static final LazyLogger LOG = SystemLogger.create(HeaderBlockParser.class); + + private final HeaderParser headerParser; + private final HpackDecoder hpackDecoder; + private final BodyParser notifier; + private ByteBuffer blockBuffer; + + public HeaderBlockParser(HeaderParser headerParser, HpackDecoder hpackDecoder, BodyParser notifier) { + this.headerParser = headerParser; + this.hpackDecoder = hpackDecoder; + this.notifier = notifier; + } + + /** + * Parses @{code blockLength} HPACK bytes from the given {@code buffer}. + * + * @param buffer the buffer to parse + * @param blockLength the length of the HPACK block + * @return null, if the buffer contains less than {@code blockLength} bytes; + * {@link #STREAM_FAILURE} if parsing the HPACK block produced a stream failure; + * {@link #SESSION_FAILURE} if parsing the HPACK block produced a session failure; + * a valid MetaData object if the parsing was successful. + */ + public MetaData parse(ByteBuffer buffer, int blockLength) { + // We must wait for the all the bytes of the header block to arrive. + // If they are not all available, accumulate them. + // When all are available, decode them. + + int accumulated = blockBuffer == null ? 0 : blockBuffer.position(); + int remaining = blockLength - accumulated; + + if (buffer.remaining() < remaining) { + if (blockBuffer == null) { + blockBuffer = BufferUtils.allocate(blockLength); + BufferUtils.clearToFill(blockBuffer); + } + blockBuffer.put(buffer); + return null; + } else { + int limit = buffer.limit(); + buffer.limit(buffer.position() + remaining); + ByteBuffer toDecode; + if (blockBuffer != null) { + blockBuffer.put(buffer); + BufferUtils.flipToFlush(blockBuffer, 0); + toDecode = blockBuffer; + } else { + toDecode = buffer; + } + + try { + return hpackDecoder.decode(toDecode); + } catch (HpackException.StreamException x) { + if (LOG.isDebugEnabled()) + LOG.debug("hpack stream exception", x); + notifier.streamFailure(headerParser.getStreamId(), ErrorCode.PROTOCOL_ERROR.code, "invalid_hpack_block"); + return STREAM_FAILURE; + } catch (HpackException.CompressionException x) { + if (LOG.isDebugEnabled()) + LOG.debug("hpack compression exception", x); + notifier.connectionFailure(buffer, ErrorCode.COMPRESSION_ERROR.code, "invalid_hpack_block"); + return SESSION_FAILURE; + } catch (HpackException.SessionException x) { + if (LOG.isDebugEnabled()) + LOG.debug("hpack session exception", x); + notifier.connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_hpack_block"); + return SESSION_FAILURE; + } finally { + buffer.limit(limit); + + if (blockBuffer != null) { + blockBuffer = null; + } + } + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderParser.java new file mode 100644 index 000000000..09358e776 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeaderParser.java @@ -0,0 +1,118 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; + +import java.nio.ByteBuffer; + +/** + *

The parser for the frame header of HTTP/2 frames.

+ * + * @see Parser + */ +public class HeaderParser { + private State state = State.LENGTH; + private int cursor; + + private int length; + private int type; + private int flags; + private int streamId; + + protected void reset() { + state = State.LENGTH; + cursor = 0; + + length = 0; + type = 0; + flags = 0; + streamId = 0; + } + + /** + *

Parses the header bytes in the given {@code buffer}; only the header + * bytes are consumed, therefore when this method returns, the buffer may + * contain unconsumed bytes.

+ * + * @param buffer the buffer to parse + * @return true if the whole header bytes were parsed, false if not enough + * header bytes were present in the buffer + */ + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case LENGTH: { + int octet = buffer.get() & 0xFF; + length = (length << 8) + octet; + if (++cursor == 3) { + length &= Frame.MAX_MAX_LENGTH; + state = State.TYPE; + } + break; + } + case TYPE: { + type = buffer.get() & 0xFF; + state = State.FLAGS; + break; + } + case FLAGS: { + flags = buffer.get() & 0xFF; + state = State.STREAM_ID; + break; + } + case STREAM_ID: { + if (buffer.remaining() >= 4) { + streamId = buffer.getInt(); + // Most significant bit MUST be ignored as per specification. + streamId &= 0x7F_FF_FF_FF; + return true; + } else { + state = State.STREAM_ID_BYTES; + cursor = 4; + } + break; + } + case STREAM_ID_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + streamId += currByte << (8 * cursor); + if (cursor == 0) { + // Most significant bit MUST be ignored as per specification. + streamId &= 0x7F_FF_FF_FF; + return true; + } + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + public int getLength() { + return length; + } + + public int getFrameType() { + return type; + } + + public boolean hasFlag(int bit) { + return (flags & bit) == bit; + } + + public int getStreamId() { + return streamId; + } + + @Override + public String toString() { + return String.format("[%s|%d|%d|%d]", FrameType.from(getFrameType()), getLength(), flags, getStreamId()); + } + + private enum State { + LENGTH, TYPE, FLAGS, STREAM_ID, STREAM_ID_BYTES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeadersBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeadersBodyParser.java new file mode 100644 index 000000000..46475e12e --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/HeadersBodyParser.java @@ -0,0 +1,187 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.frame.*; + +import java.nio.ByteBuffer; + +public class HeadersBodyParser extends BodyParser { + private final HeaderBlockParser headerBlockParser; + private final HeaderBlockFragments headerBlockFragments; + private State state = State.PREPARE; + private int cursor; + private int length; + private int paddingLength; + private boolean exclusive; + private int parentStreamId; + private int weight; + + public HeadersBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser, HeaderBlockFragments headerBlockFragments) { + super(headerParser, listener); + this.headerBlockParser = headerBlockParser; + this.headerBlockFragments = headerBlockFragments; + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + length = 0; + paddingLength = 0; + exclusive = false; + parentStreamId = 0; + weight = 0; + } + + @Override + protected void emptyBody(ByteBuffer buffer) { + if (hasFlag(Flags.END_HEADERS)) { + MetaData metaData = headerBlockParser.parse(BufferUtils.EMPTY_BUFFER, 0); + onHeaders(0, 0, false, metaData); + } else { + headerBlockFragments.setStreamId(getStreamId()); + headerBlockFragments.setEndStream(isEndStream()); + if (hasFlag(Flags.PRIORITY)) + connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_priority_frame"); + } + } + + @Override + public boolean parse(ByteBuffer buffer) { + boolean loop = false; + while (buffer.hasRemaining() || loop) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) { + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_frame"); + } + + length = getBodyLength(); + + if (isPadding()) + state = State.PADDING_LENGTH; + else if (hasFlag(Flags.PRIORITY)) + state = State.EXCLUSIVE; + else + state = State.HEADERS; + break; + } + case PADDING_LENGTH: { + paddingLength = buffer.get() & 0xFF; + --length; + length -= paddingLength; + state = hasFlag(Flags.PRIORITY) ? State.EXCLUSIVE : State.HEADERS; + loop = length == 0; + if (length < 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_headers_frame_padding"); + break; + } + case EXCLUSIVE: { + // We must only peek the first byte and not advance the buffer + // because the 31 least significant bits represent the stream id. + int currByte = buffer.get(buffer.position()); + exclusive = (currByte & 0x80) == 0x80; + state = State.PARENT_STREAM_ID; + break; + } + case PARENT_STREAM_ID: { + if (buffer.remaining() >= 4) { + parentStreamId = buffer.getInt(); + parentStreamId &= 0x7F_FF_FF_FF; + length -= 4; + state = State.WEIGHT; + if (length < 1) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_headers_frame"); + } else { + state = State.PARENT_STREAM_ID_BYTES; + cursor = 4; + } + break; + } + case PARENT_STREAM_ID_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + parentStreamId += currByte << (8 * cursor); + --length; + if (cursor > 0 && length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_headers_frame"); + if (cursor == 0) { + parentStreamId &= 0x7F_FF_FF_FF; + state = State.WEIGHT; + if (length < 1) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_headers_frame"); + } + break; + } + case WEIGHT: { + // SPEC: stream cannot depend on itself. + if (getStreamId() == parentStreamId) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame"); + weight = (buffer.get() & 0xFF) + 1; + --length; + state = State.HEADERS; + loop = length == 0; + break; + } + case HEADERS: { + if (hasFlag(Flags.END_HEADERS)) { + MetaData metaData = headerBlockParser.parse(buffer, length); + if (metaData == HeaderBlockParser.SESSION_FAILURE) + return false; + if (metaData != null) { + if (LOG.isDebugEnabled()) + LOG.debug("Parsed {} frame hpack from {}", FrameType.HEADERS, buffer); + state = State.PADDING; + loop = paddingLength == 0; + if (metaData != HeaderBlockParser.STREAM_FAILURE) + onHeaders(parentStreamId, weight, exclusive, metaData); + } + } else { + int remaining = buffer.remaining(); + if (remaining < length) { + headerBlockFragments.storeFragment(buffer, remaining, false); + length -= remaining; + } else { + headerBlockFragments.setStreamId(getStreamId()); + headerBlockFragments.setEndStream(isEndStream()); + if (hasFlag(Flags.PRIORITY)) + headerBlockFragments.setPriorityFrame(new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive)); + headerBlockFragments.storeFragment(buffer, length, false); + state = State.PADDING; + loop = paddingLength == 0; + } + } + break; + } + case PADDING: { + int size = Math.min(buffer.remaining(), paddingLength); + buffer.position(buffer.position() + size); + paddingLength -= size; + if (paddingLength == 0) { + reset(); + return true; + } + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private void onHeaders(int parentStreamId, int weight, boolean exclusive, MetaData metaData) { + PriorityFrame priorityFrame = null; + if (hasFlag(Flags.PRIORITY)) + priorityFrame = new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive); + HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, priorityFrame, isEndStream()); + frame.setEndHeaders(hasFlag(Flags.END_HEADERS)); + notifyHeaders(frame); + } + + private enum State { + PREPARE, PADDING_LENGTH, EXCLUSIVE, PARENT_STREAM_ID, PARENT_STREAM_ID_BYTES, WEIGHT, HEADERS, PADDING + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/Parser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/Parser.java new file mode 100644 index 000000000..aec03da5c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/Parser.java @@ -0,0 +1,325 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.v2.frame.*; +import com.fireflysource.net.http.common.v2.hpack.HpackDecoder; + +import java.nio.ByteBuffer; +import java.util.function.UnaryOperator; + +/** + *

The HTTP/2 protocol parser.

+ *

This parser makes use of the {@link HeaderParser} and of + * {@link BodyParser}s to parse HTTP/2 frames.

+ */ +public class Parser { + public static final LazyLogger LOG = SystemLogger.create(Parser.class); + + private final Listener listener; + private final HeaderParser headerParser; + private final HpackDecoder hpackDecoder; + private final BodyParser[] bodyParsers; + private UnknownBodyParser unknownBodyParser; + private int maxFrameLength; + private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS; + private boolean continuation; + private State state = State.HEADER; + + public Parser(Listener listener, int maxDynamicTableSize, int maxHeaderSize) { + this.listener = listener; + this.headerParser = new HeaderParser(); + this.hpackDecoder = new HpackDecoder(maxDynamicTableSize, maxHeaderSize); + this.maxFrameLength = Frame.DEFAULT_MAX_LENGTH; + this.bodyParsers = new BodyParser[FrameType.values().length]; + } + + public void init(UnaryOperator wrapper) { + Listener listener = wrapper.apply(this.listener); + unknownBodyParser = new UnknownBodyParser(headerParser, listener); + HeaderBlockParser headerBlockParser = new HeaderBlockParser(headerParser, hpackDecoder, unknownBodyParser); + HeaderBlockFragments headerBlockFragments = new HeaderBlockFragments(); + bodyParsers[FrameType.DATA.getType()] = new DataBodyParser(headerParser, listener); + bodyParsers[FrameType.HEADERS.getType()] = new HeadersBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments); + bodyParsers[FrameType.PRIORITY.getType()] = new PriorityBodyParser(headerParser, listener); + bodyParsers[FrameType.RST_STREAM.getType()] = new ResetBodyParser(headerParser, listener); + bodyParsers[FrameType.SETTINGS.getType()] = new SettingsBodyParser(headerParser, listener, getMaxSettingsKeys()); + bodyParsers[FrameType.PUSH_PROMISE.getType()] = new PushPromiseBodyParser(headerParser, listener, headerBlockParser); + bodyParsers[FrameType.PING.getType()] = new PingBodyParser(headerParser, listener); + bodyParsers[FrameType.GO_AWAY.getType()] = new GoAwayBodyParser(headerParser, listener); + bodyParsers[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateBodyParser(headerParser, listener); + bodyParsers[FrameType.CONTINUATION.getType()] = new ContinuationBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments); + } + + private void reset() { + headerParser.reset(); + state = State.HEADER; + } + + /** + *

Parses the given {@code buffer} bytes and emit events to a {@link Listener}.

+ *

When this method returns, the buffer may not be fully consumed, so invocations + * to this method should be wrapped in a loop:

+ *
+     * while (buffer.hasRemaining())
+     *     parser.parse(buffer);
+     * 
+ * + * @param buffer the buffer to parse + */ + public void parse(ByteBuffer buffer) { + try { + while (true) { + switch (state) { + case HEADER: { + if (!parseHeader(buffer)) + return; + break; + } + case BODY: { + if (!parseBody(buffer)) + return; + break; + } + default: { + throw new IllegalStateException(); + } + } + } + } catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("http2 frame parsing exception", x); + connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "parser_error"); + } + } + + protected boolean parseHeader(ByteBuffer buffer) { + if (!headerParser.parse(buffer)) + return false; + + if (LOG.isDebugEnabled()) + LOG.debug("Parsed {} frame header from {}", headerParser, buffer); + + if (headerParser.getLength() > getMaxFrameLength()) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR, "invalid_frame_length"); + + FrameType frameType = FrameType.from(getFrameType()); + if (continuation) { + // SPEC: CONTINUATION frames must be consecutive. + if (frameType != FrameType.CONTINUATION) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "expected_continuation_frame"); + if (headerParser.hasFlag(Flags.END_HEADERS)) + continuation = false; + } else { + if (frameType == FrameType.HEADERS) + continuation = !headerParser.hasFlag(Flags.END_HEADERS); + else if (frameType == FrameType.CONTINUATION) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "unexpected_continuation_frame"); + } + state = State.BODY; + return true; + } + + protected boolean parseBody(ByteBuffer buffer) { + int type = getFrameType(); + if (type < 0 || type >= bodyParsers.length) { + // Unknown frame types must be ignored. + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring unknown frame type {}", Integer.toHexString(type)); + if (!unknownBodyParser.parse(buffer)) + return false; + reset(); + return true; + } + + BodyParser bodyParser = bodyParsers[type]; + if (headerParser.getLength() == 0) { + bodyParser.emptyBody(buffer); + } else { + if (!bodyParser.parse(buffer)) + return false; + } + if (LOG.isDebugEnabled()) + LOG.debug("Parsed {} frame body from {}", FrameType.from(type), buffer); + reset(); + return true; + } + + private boolean connectionFailure(ByteBuffer buffer, ErrorCode error, String reason) { + return unknownBodyParser.connectionFailure(buffer, error.code, reason); + } + + protected int getFrameType() { + return headerParser.getFrameType(); + } + + protected boolean hasFlag(int bit) { + return headerParser.hasFlag(bit); + } + + public int getMaxFrameLength() { + return maxFrameLength; + } + + public void setMaxFrameLength(int maxFrameLength) { + this.maxFrameLength = maxFrameLength; + } + + public int getMaxSettingsKeys() { + return maxSettingsKeys; + } + + public void setMaxSettingsKeys(int maxSettingsKeys) { + this.maxSettingsKeys = maxSettingsKeys; + } + + protected void notifyConnectionFailure(int error, String reason) { + try { + listener.onConnectionFailure(error, reason); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + public interface Listener { + void onData(DataFrame frame); + + void onHeaders(HeadersFrame frame); + + void onPriority(PriorityFrame frame); + + void onReset(ResetFrame frame); + + void onSettings(SettingsFrame frame); + + void onPushPromise(PushPromiseFrame frame); + + void onPing(PingFrame frame); + + void onGoAway(GoAwayFrame frame); + + void onWindowUpdate(WindowUpdateFrame frame); + + void onStreamFailure(int streamId, int error, String reason); + + void onConnectionFailure(int error, String reason); + + class Adapter implements Listener { + @Override + public void onData(DataFrame frame) { + } + + @Override + public void onHeaders(HeadersFrame frame) { + } + + @Override + public void onPriority(PriorityFrame frame) { + } + + @Override + public void onReset(ResetFrame frame) { + } + + @Override + public void onSettings(SettingsFrame frame) { + } + + @Override + public void onPushPromise(PushPromiseFrame frame) { + } + + @Override + public void onPing(PingFrame frame) { + } + + @Override + public void onGoAway(GoAwayFrame frame) { + } + + @Override + public void onWindowUpdate(WindowUpdateFrame frame) { + } + + @Override + public void onStreamFailure(int streamId, int error, String reason) { + } + + @Override + public void onConnectionFailure(int error, String reason) { + LOG.warn("Connection failure: {}/{}", error, reason); + } + } + + class Wrapper implements Listener { + private final Parser.Listener listener; + + public Wrapper(Parser.Listener listener) { + this.listener = listener; + } + + public Listener getParserListener() { + return listener; + } + + @Override + public void onData(DataFrame frame) { + listener.onData(frame); + } + + @Override + public void onHeaders(HeadersFrame frame) { + listener.onHeaders(frame); + } + + @Override + public void onPriority(PriorityFrame frame) { + listener.onPriority(frame); + } + + @Override + public void onReset(ResetFrame frame) { + listener.onReset(frame); + } + + @Override + public void onSettings(SettingsFrame frame) { + listener.onSettings(frame); + } + + @Override + public void onPushPromise(PushPromiseFrame frame) { + listener.onPushPromise(frame); + } + + @Override + public void onPing(PingFrame frame) { + listener.onPing(frame); + } + + @Override + public void onGoAway(GoAwayFrame frame) { + listener.onGoAway(frame); + } + + @Override + public void onWindowUpdate(WindowUpdateFrame frame) { + listener.onWindowUpdate(frame); + } + + @Override + public void onStreamFailure(int streamId, int error, String reason) { + listener.onStreamFailure(streamId, error, reason); + } + + @Override + public void onConnectionFailure(int error, String reason) { + listener.onConnectionFailure(error, reason); + } + } + } + + private enum State { + HEADER, BODY + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PingBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PingBodyParser.java new file mode 100644 index 000000000..17bf50226 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PingBodyParser.java @@ -0,0 +1,74 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.PingFrame; + +import java.nio.ByteBuffer; + +public class PingBodyParser extends BodyParser { + private State state = State.PREPARE; + private int cursor; + private byte[] payload; + + public PingBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + payload = null; + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() != 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_ping_frame"); + // SPEC: wrong body length is treated as connection error. + if (getBodyLength() != 8) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_ping_frame"); + state = State.PAYLOAD; + break; + } + case PAYLOAD: { + payload = new byte[8]; + if (buffer.remaining() >= 8) { + buffer.get(payload); + return onPing(payload); + } else { + state = State.PAYLOAD_BYTES; + cursor = 8; + } + break; + } + case PAYLOAD_BYTES: { + payload[8 - cursor] = buffer.get(); + --cursor; + if (cursor == 0) + return onPing(payload); + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private boolean onPing(byte[] payload) { + PingFrame frame = new PingFrame(payload, hasFlag(Flags.ACK)); + reset(); + notifyPing(frame); + return true; + } + + private enum State { + PREPARE, PAYLOAD, PAYLOAD_BYTES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PrefaceParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PrefaceParser.java new file mode 100644 index 000000000..3bf2c26aa --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PrefaceParser.java @@ -0,0 +1,59 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.PrefaceFrame; + +import java.nio.ByteBuffer; + +public class PrefaceParser { + public static final LazyLogger LOG = SystemLogger.create(PrefaceParser.class); + + private final Parser.Listener listener; + private int cursor; + + public PrefaceParser(Parser.Listener listener) { + this.listener = listener; + } + + /** + *

Advances this parser after the {@link PrefaceFrame#PREFACE_PREAMBLE_BYTES}.

+ *

This allows the HTTP/1.1 parser to parse the preamble of the preface, + * which is a legal HTTP/1.1 request, and this parser will parse the remaining + * bytes, that are not parseable by a HTTP/1.1 parser.

+ */ + protected void directUpgrade() { + if (cursor != 0) + throw new IllegalStateException(); + cursor = PrefaceFrame.PREFACE_PREAMBLE_BYTES.length; + } + + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + int currByte = buffer.get(); + if (currByte != PrefaceFrame.PREFACE_BYTES[cursor]) { + BufferUtils.clear(buffer); + notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_preface"); + return false; + } + ++cursor; + if (cursor == PrefaceFrame.PREFACE_BYTES.length) { + cursor = 0; + if (LOG.isDebugEnabled()) + LOG.debug("Parsed preface bytes from {}", buffer); + return true; + } + } + return false; + } + + protected void notifyConnectionFailure(int error, String reason) { + try { + listener.onConnectionFailure(error, reason); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PriorityBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PriorityBodyParser.java new file mode 100644 index 000000000..aba814567 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PriorityBodyParser.java @@ -0,0 +1,93 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.PriorityFrame; + +import java.nio.ByteBuffer; + +public class PriorityBodyParser extends BodyParser { + private State state = State.PREPARE; + private int cursor; + private boolean exclusive; + private int parentStreamId; + + public PriorityBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + exclusive = false; + parentStreamId = 0; + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame"); + int length = getBodyLength(); + if (length != 5) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_priority_frame"); + state = State.EXCLUSIVE; + break; + } + case EXCLUSIVE: { + // We must only peek the first byte and not advance the buffer + // because the 31 least significant bits represent the stream id. + int currByte = buffer.get(buffer.position()); + exclusive = (currByte & 0x80) == 0x80; + state = State.PARENT_STREAM_ID; + break; + } + case PARENT_STREAM_ID: { + if (buffer.remaining() >= 4) { + parentStreamId = buffer.getInt(); + parentStreamId &= 0x7F_FF_FF_FF; + state = State.WEIGHT; + } else { + state = State.PARENT_STREAM_ID_BYTES; + cursor = 4; + } + break; + } + case PARENT_STREAM_ID_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + parentStreamId += currByte << (8 * cursor); + if (cursor == 0) { + parentStreamId &= 0x7F_FF_FF_FF; + state = State.WEIGHT; + } + break; + } + case WEIGHT: { + // SPEC: stream cannot depend on itself. + if (getStreamId() == parentStreamId) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame"); + int weight = (buffer.get() & 0xFF) + 1; + return onPriority(parentStreamId, weight, exclusive); + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private boolean onPriority(int parentStreamId, int weight, boolean exclusive) { + PriorityFrame frame = new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive); + reset(); + notifyPriority(frame); + return true; + } + + private enum State { + PREPARE, EXCLUSIVE, PARENT_STREAM_ID, PARENT_STREAM_ID_BYTES, WEIGHT + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PushPromiseBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PushPromiseBodyParser.java new file mode 100644 index 000000000..682af0a8b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/PushPromiseBodyParser.java @@ -0,0 +1,129 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.PushPromiseFrame; + +import java.nio.ByteBuffer; + +public class PushPromiseBodyParser extends BodyParser { + private final HeaderBlockParser headerBlockParser; + private State state = State.PREPARE; + private int cursor; + private int length; + private int paddingLength; + private int streamId; + + public PushPromiseBodyParser(HeaderParser headerParser, Parser.Listener listener, HeaderBlockParser headerBlockParser) { + super(headerParser, listener); + this.headerBlockParser = headerBlockParser; + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + length = 0; + paddingLength = 0; + streamId = 0; + } + + @Override + public boolean parse(ByteBuffer buffer) { + boolean loop = false; + while (buffer.hasRemaining() || loop) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_push_promise_frame"); + + // For now we don't support PUSH_PROMISE frames that don't have END_HEADERS. + if (!hasFlag(Flags.END_HEADERS)) + return connectionFailure(buffer, ErrorCode.INTERNAL_ERROR.code, "unsupported_push_promise_frame"); + + length = getBodyLength(); + + if (isPadding()) { + state = State.PADDING_LENGTH; + } else { + state = State.STREAM_ID; + } + break; + } + case PADDING_LENGTH: { + paddingLength = buffer.get() & 0xFF; + --length; + length -= paddingLength; + state = State.STREAM_ID; + if (length < 4) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_push_promise_frame"); + break; + } + case STREAM_ID: { + if (buffer.remaining() >= 4) { + streamId = buffer.getInt(); + streamId &= 0x7F_FF_FF_FF; + length -= 4; + state = State.HEADERS; + loop = length == 0; + } else { + state = State.STREAM_ID_BYTES; + cursor = 4; + } + break; + } + case STREAM_ID_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + streamId += currByte << (8 * cursor); + --length; + if (cursor > 0 && length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_push_promise_frame"); + if (cursor == 0) { + streamId &= 0x7F_FF_FF_FF; + state = State.HEADERS; + loop = length == 0; + } + break; + } + case HEADERS: { + MetaData metaData = headerBlockParser.parse(buffer, length); + if (metaData == HeaderBlockParser.SESSION_FAILURE) + return false; + if (metaData != null) { + state = State.PADDING; + loop = paddingLength == 0; + if (metaData != HeaderBlockParser.STREAM_FAILURE) + onPushPromise(streamId, metaData); + } + break; + } + case PADDING: { + int size = Math.min(buffer.remaining(), paddingLength); + buffer.position(buffer.position() + size); + paddingLength -= size; + if (paddingLength == 0) { + reset(); + return true; + } + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private void onPushPromise(int streamId, MetaData metaData) { + PushPromiseFrame frame = new PushPromiseFrame(getStreamId(), streamId, metaData); + frame.setEndHeaders(hasFlag(Flags.END_HEADERS)); + notifyPushPromise(frame); + } + + private enum State { + PREPARE, PADDING_LENGTH, STREAM_ID, STREAM_ID_BYTES, HEADERS, PADDING + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ResetBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ResetBodyParser.java new file mode 100644 index 000000000..5fab9ba79 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ResetBodyParser.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.ResetFrame; + +import java.nio.ByteBuffer; + +public class ResetBodyParser extends BodyParser { + private State state = State.PREPARE; + private int cursor; + private int error; + + public ResetBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + error = 0; + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_rst_stream_frame"); + int length = getBodyLength(); + if (length != 4) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_rst_stream_frame"); + state = State.ERROR; + break; + } + case ERROR: { + if (buffer.remaining() >= 4) { + return onReset(buffer.getInt()); + } else { + state = State.ERROR_BYTES; + cursor = 4; + } + break; + } + case ERROR_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + error += currByte << (8 * cursor); + if (cursor == 0) + return onReset(error); + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private boolean onReset(int error) { + ResetFrame frame = new ResetFrame(getStreamId(), error); + reset(); + notifyReset(frame); + return true; + } + + private enum State { + PREPARE, ERROR, ERROR_BYTES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ServerParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ServerParser.java new file mode 100644 index 000000000..eb815a57b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/ServerParser.java @@ -0,0 +1,140 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.FrameType; + +import java.nio.ByteBuffer; + +public class ServerParser extends Parser { + private static final LazyLogger LOG = SystemLogger.create(ServerParser.class); + + private final Listener listener; + private final PrefaceParser prefaceParser; + private State state = State.PREFACE; + private boolean notifyPreface = true; + + public ServerParser(Listener listener, int maxDynamicTableSize, int maxHeaderSize) { + super(listener, maxDynamicTableSize, maxHeaderSize); + this.listener = listener; + this.prefaceParser = new PrefaceParser(listener); + } + + /** + *

A direct upgrade is an unofficial upgrade from HTTP/1.1 to HTTP/2.0.

+ *

A direct upgrade has been initiated when the HTTP connection + * sees a request with these bytes:

+ *
+     * PRI * HTTP/2.0\r\n
+     * \r\n
+     * 
+ *

This request is part of the HTTP/2.0 preface, indicating that a + * HTTP/2.0 client is attempting a h2c direct connection.

+ *

This is not a standard HTTP/1.1 Upgrade path.

+ */ + public void directUpgrade() { + if (state != State.PREFACE) + throw new IllegalStateException(); + prefaceParser.directUpgrade(); + } + + /** + *

The standard HTTP/1.1 upgrade path.

+ */ + public void standardUpgrade() { + if (state != State.PREFACE) + throw new IllegalStateException(); + notifyPreface = false; + } + + @Override + public void parse(ByteBuffer buffer) { + try { + if (LOG.isDebugEnabled()) + LOG.debug("Parsing {}", buffer); + + while (true) { + switch (state) { + case PREFACE: { + if (!prefaceParser.parse(buffer)) + return; + if (notifyPreface) + onPreface(); + state = State.SETTINGS; + break; + } + case SETTINGS: { + if (!parseHeader(buffer)) + return; + if (getFrameType() != FrameType.SETTINGS.getType() || hasFlag(Flags.ACK)) { + BufferUtils.clear(buffer); + notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_preface"); + return; + } + if (!parseBody(buffer)) + return; + state = State.FRAMES; + break; + } + case FRAMES: { + // Stay forever in the FRAMES state. + super.parse(buffer); + return; + } + default: { + throw new IllegalStateException(); + } + } + } + } catch (Throwable x) { + LOG.debug("http2 server parser exception", x); + BufferUtils.clear(buffer); + notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "parser_error"); + } + } + + protected void onPreface() { + notifyPreface(); + } + + private void notifyPreface() { + try { + listener.onPreface(); + } catch (Throwable x) { + LOG.info("Failure while notifying listener " + listener, x); + } + } + + public interface Listener extends Parser.Listener { + void onPreface(); + + class Adapter extends Parser.Listener.Adapter implements Listener { + @Override + public void onPreface() { + } + } + + class Wrapper extends Parser.Listener.Wrapper implements Listener { + public Wrapper(ServerParser.Listener listener) { + super(listener); + } + + @Override + public ServerParser.Listener getParserListener() { + return (Listener) super.getParserListener(); + } + + @Override + public void onPreface() { + getParserListener().onPreface(); + } + } + } + + private enum State { + PREFACE, SETTINGS, FRAMES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/SettingsBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/SettingsBodyParser.java new file mode 100644 index 000000000..c027cb4b9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/SettingsBodyParser.java @@ -0,0 +1,202 @@ +package com.fireflysource.net.http.common.v2.decoder; + + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.SettingsFrame; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +public class SettingsBodyParser extends BodyParser { + private static final LazyLogger LOG = SystemLogger.create(SettingsBodyParser.class); + + private final int maxKeys; + private State state = State.PREPARE; + private int cursor; + private int length; + private int settingId; + private int settingValue; + private int keys; + private Map settings; + + public SettingsBodyParser(HeaderParser headerParser, Parser.Listener listener) { + this(headerParser, listener, SettingsFrame.DEFAULT_MAX_KEYS); + } + + public SettingsBodyParser(HeaderParser headerParser, Parser.Listener listener, int maxKeys) { + super(headerParser, listener); + this.maxKeys = maxKeys; + } + + protected void reset() { + state = State.PREPARE; + cursor = 0; + length = 0; + settingId = 0; + settingValue = 0; + settings = null; + } + + public int getMaxKeys() { + return maxKeys; + } + + @Override + protected void emptyBody(ByteBuffer buffer) { + onSettings(buffer, new HashMap<>()); + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + // SPEC: wrong streamId is treated as connection error. + if (getStreamId() != 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_frame"); + length = getBodyLength(); + settings = new HashMap<>(); + state = State.SETTING_ID; + break; + } + case SETTING_ID: { + if (buffer.remaining() >= 2) { + settingId = buffer.getShort() & 0xFF_FF; + state = State.SETTING_VALUE; + length -= 2; + if (length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_settings_frame"); + } else { + cursor = 2; + settingId = 0; + state = State.SETTING_ID_BYTES; + } + break; + } + case SETTING_ID_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + settingId += currByte << (8 * cursor); + --length; + if (length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_settings_frame"); + if (cursor == 0) { + state = State.SETTING_VALUE; + } + break; + } + case SETTING_VALUE: { + if (buffer.remaining() >= 4) { + settingValue = buffer.getInt(); + if (LOG.isDebugEnabled()) + LOG.debug(String.format("setting %d=%d", settingId, settingValue)); + if (!onSetting(buffer, settings, settingId, settingValue)) + return false; + state = State.SETTING_ID; + length -= 4; + if (length == 0) + return onSettings(buffer, settings); + } else { + cursor = 4; + settingValue = 0; + state = State.SETTING_VALUE_BYTES; + } + break; + } + case SETTING_VALUE_BYTES: { + int currByte = buffer.get() & 0xFF; + --cursor; + settingValue += currByte << (8 * cursor); + --length; + if (cursor > 0 && length <= 0) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_settings_frame"); + if (cursor == 0) { + if (LOG.isDebugEnabled()) + LOG.debug(String.format("setting %d=%d", settingId, settingValue)); + if (!onSetting(buffer, settings, settingId, settingValue)) + return false; + state = State.SETTING_ID; + if (length == 0) + return onSettings(buffer, settings); + } + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + protected boolean onSetting(ByteBuffer buffer, Map settings, int key, int value) { + ++keys; + if (keys > getMaxKeys()) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_settings_frame"); + settings.put(key, value); + return true; + } + + protected boolean onSettings(ByteBuffer buffer, Map settings) { + Integer enablePush = settings.get(SettingsFrame.ENABLE_PUSH); + if (enablePush != null && enablePush != 0 && enablePush != 1) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_enable_push"); + + Integer initialWindowSize = settings.get(SettingsFrame.INITIAL_WINDOW_SIZE); + // Values greater than Integer.MAX_VALUE will overflow to negative. + if (initialWindowSize != null && initialWindowSize < 0) + return connectionFailure(buffer, ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_settings_initial_window_size"); + + Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE); + if (maxFrameLength != null && (maxFrameLength < Frame.DEFAULT_MAX_LENGTH || maxFrameLength > Frame.MAX_MAX_LENGTH)) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size"); + + SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK)); + reset(); + notifySettings(frame); + return true; + } + + public static SettingsFrame parseBody(final ByteBuffer buffer) { + final int bodyLength = buffer.remaining(); + final AtomicReference frameRef = new AtomicReference<>(); + SettingsBodyParser parser = new SettingsBodyParser(null, null) { + @Override + protected int getStreamId() { + return 0; + } + + @Override + protected int getBodyLength() { + return bodyLength; + } + + @Override + protected boolean onSettings(ByteBuffer buffer, Map settings) { + frameRef.set(new SettingsFrame(settings, false)); + return true; + } + + @Override + protected boolean connectionFailure(ByteBuffer buffer, int error, String reason) { + frameRef.set(null); + return false; + } + }; + if (bodyLength == 0) + parser.emptyBody(buffer); + else + parser.parse(buffer); + return frameRef.get(); + } + + private enum State { + PREPARE, SETTING_ID, SETTING_ID_BYTES, SETTING_VALUE, SETTING_VALUE_BYTES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/UnknownBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/UnknownBodyParser.java new file mode 100644 index 000000000..e047490fe --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/UnknownBodyParser.java @@ -0,0 +1,29 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import java.nio.ByteBuffer; + +public class UnknownBodyParser extends BodyParser { + private int cursor; + + public UnknownBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + @Override + public boolean parse(ByteBuffer buffer) { + int length = cursor == 0 ? getBodyLength() : cursor; + cursor = consume(buffer, length); + return cursor == 0; + } + + private int consume(ByteBuffer buffer, int length) { + int remaining = buffer.remaining(); + if (remaining >= length) { + buffer.position(buffer.position() + length); + return 0; + } else { + buffer.position(buffer.limit()); + return length - remaining; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/WindowUpdateBodyParser.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/WindowUpdateBodyParser.java new file mode 100644 index 000000000..6d90315bb --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/decoder/WindowUpdateBodyParser.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.http.common.v2.decoder; + +import com.fireflysource.net.http.common.v2.frame.ErrorCode; +import com.fireflysource.net.http.common.v2.frame.WindowUpdateFrame; + +import java.nio.ByteBuffer; + +public class WindowUpdateBodyParser extends BodyParser { + private State state = State.PREPARE; + private int cursor; + private int windowDelta; + + public WindowUpdateBodyParser(HeaderParser headerParser, Parser.Listener listener) { + super(headerParser, listener); + } + + private void reset() { + state = State.PREPARE; + cursor = 0; + windowDelta = 0; + } + + @Override + public boolean parse(ByteBuffer buffer) { + while (buffer.hasRemaining()) { + switch (state) { + case PREPARE: { + int length = getBodyLength(); + if (length != 4) + return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_window_update_frame"); + state = State.WINDOW_DELTA; + break; + } + case WINDOW_DELTA: { + if (buffer.remaining() >= 4) { + windowDelta = buffer.getInt() & 0x7F_FF_FF_FF; + return onWindowUpdate(windowDelta); + } else { + state = State.WINDOW_DELTA_BYTES; + cursor = 4; + } + break; + } + case WINDOW_DELTA_BYTES: { + byte currByte = buffer.get(); + --cursor; + windowDelta += (currByte & 0xFF) << 8 * cursor; + if (cursor == 0) { + windowDelta &= 0x7F_FF_FF_FF; + return onWindowUpdate(windowDelta); + } + break; + } + default: { + throw new IllegalStateException(); + } + } + } + return false; + } + + private boolean onWindowUpdate(int windowDelta) { + WindowUpdateFrame frame = new WindowUpdateFrame(getStreamId(), windowDelta); + reset(); + notifyWindowUpdate(frame); + return true; + } + + private enum State { + PREPARE, WINDOW_DELTA, WINDOW_DELTA_BYTES + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/DataGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/DataGenerator.java new file mode 100644 index 000000000..b660e0cbe --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/DataGenerator.java @@ -0,0 +1,62 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.DataFrame; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class DataGenerator { + private final HeaderGenerator headerGenerator; + + public DataGenerator(HeaderGenerator headerGenerator) { + this.headerGenerator = headerGenerator; + } + + public FrameBytes generate(DataFrame frame, int maxLength) { + return generateData(frame.getStreamId(), frame.getData(), frame.isEndStream(), maxLength); + } + + public FrameBytes generateData(int streamId, ByteBuffer data, boolean last, int maxLength) { + if (streamId < 0) + throw new IllegalArgumentException("Invalid stream id: " + streamId); + + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + + int dataLength = data.remaining(); + int maxFrameSize = headerGenerator.getMaxFrameSize(); + int length = Math.min(dataLength, Math.min(maxFrameSize, maxLength)); + if (length == dataLength) { + generateFrame(streamId, data, last, frameBytes); + } else { + int limit = data.limit(); + int newLimit = data.position() + length; + data.limit(newLimit); + ByteBuffer slice = data.slice(); + data.position(newLimit); + data.limit(limit); + generateFrame(streamId, slice, false, frameBytes); + } + frameBytes.setLength(Frame.HEADER_LENGTH + length); + return frameBytes; + } + + private void generateFrame(int streamId, ByteBuffer data, boolean last, FrameBytes frameBytes) { + int length = data.remaining(); + + int flags = Flags.NONE; + if (last) + flags |= Flags.END_STREAM; + + ByteBuffer header = headerGenerator.generate(FrameType.DATA, Frame.HEADER_LENGTH + length, length, flags, streamId); + BufferUtils.flipToFlush(header, 0); + frameBytes.getByteBuffers().add(header); + // Skip empty data buffers. + if (data.remaining() > 0) + frameBytes.getByteBuffers().add(data); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/DisconnectGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/DisconnectGenerator.java new file mode 100644 index 000000000..319213ca3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/DisconnectGenerator.java @@ -0,0 +1,14 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.net.http.common.v2.frame.Frame; + +public class DisconnectGenerator extends FrameGenerator { + public DisconnectGenerator() { + super(null); + } + + @Override + public FrameBytes generate(Frame frame) { + return FrameBytes.EMPTY; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/FrameBytes.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/FrameBytes.java new file mode 100644 index 000000000..23e66fb1e --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/FrameBytes.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.net.http.common.v2.frame.Frame; + +import java.nio.ByteBuffer; +import java.util.List; + +public class FrameBytes { + + public static final FrameBytes EMPTY = new FrameBytes(); + + private int length; + private List byteBuffers; + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + public List getByteBuffers() { + return byteBuffers; + } + + public void setByteBuffers(List byteBuffers) { + this.byteBuffers = byteBuffers; + } + + public int getDataLength() { + if (length > 0) + return length - Frame.HEADER_LENGTH; + else + return 0; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/FrameGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/FrameGenerator.java new file mode 100644 index 000000000..d09afbefb --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/FrameGenerator.java @@ -0,0 +1,24 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; + +import java.nio.ByteBuffer; + +public abstract class FrameGenerator { + private final HeaderGenerator headerGenerator; + + protected FrameGenerator(HeaderGenerator headerGenerator) { + this.headerGenerator = headerGenerator; + } + + public abstract FrameBytes generate(Frame frame); + + protected ByteBuffer generateHeader(FrameType frameType, int length, int flags, int streamId) { + return headerGenerator.generate(frameType, Frame.HEADER_LENGTH + length, length, flags, streamId); + } + + public int getMaxFrameSize() { + return headerGenerator.getMaxFrameSize(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/Generator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/Generator.java new file mode 100644 index 000000000..7e652e0d5 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/Generator.java @@ -0,0 +1,58 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.net.http.common.v2.frame.DataFrame; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.hpack.HpackEncoder; + +public class Generator { + private final HeaderGenerator headerGenerator; + private final HpackEncoder hpackEncoder; + private final FrameGenerator[] generators; + private final DataGenerator dataGenerator; + + public Generator() { + this(4096, 0); + } + + public Generator(int maxDynamicTableSize, int maxHeaderBlockFragment) { + + headerGenerator = new HeaderGenerator(); + hpackEncoder = new HpackEncoder(maxDynamicTableSize); + + this.generators = new FrameGenerator[FrameType.values().length]; + this.generators[FrameType.HEADERS.getType()] = new HeadersGenerator(headerGenerator, hpackEncoder, maxHeaderBlockFragment); + this.generators[FrameType.PRIORITY.getType()] = new PriorityGenerator(headerGenerator); + this.generators[FrameType.RST_STREAM.getType()] = new ResetGenerator(headerGenerator); + this.generators[FrameType.SETTINGS.getType()] = new SettingsGenerator(headerGenerator); + this.generators[FrameType.PUSH_PROMISE.getType()] = new PushPromiseGenerator(headerGenerator, hpackEncoder); + this.generators[FrameType.PING.getType()] = new PingGenerator(headerGenerator); + this.generators[FrameType.GO_AWAY.getType()] = new GoAwayGenerator(headerGenerator); + this.generators[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateGenerator(headerGenerator); + this.generators[FrameType.CONTINUATION.getType()] = null; // Never generated explicitly. + this.generators[FrameType.PREFACE.getType()] = new PrefaceGenerator(); + this.generators[FrameType.DISCONNECT.getType()] = new DisconnectGenerator(); + + this.dataGenerator = new DataGenerator(headerGenerator); + } + + public void setHeaderTableSize(int headerTableSize) { + hpackEncoder.setRemoteMaxDynamicTableSize(headerTableSize); + } + + public void setMaxFrameSize(int maxFrameSize) { + headerGenerator.setMaxFrameSize(maxFrameSize); + } + + public FrameBytes control(Frame frame) { + return generators[frame.getType().getType()].generate(frame); + } + + public FrameBytes data(DataFrame frame, int maxLength) { + return dataGenerator.generate(frame, maxLength); + } + + public void setMaxHeaderListSize(int value) { + hpackEncoder.setMaxHeaderListSize(value); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/GoAwayGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/GoAwayGenerator.java new file mode 100644 index 000000000..0cf5f8181 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/GoAwayGenerator.java @@ -0,0 +1,55 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.GoAwayFrame; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.LinkedList; + +public class GoAwayGenerator extends FrameGenerator { + + public GoAwayGenerator(HeaderGenerator headerGenerator) { + super(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + GoAwayFrame goAwayFrame = (GoAwayFrame) frame; + return generateGoAway(goAwayFrame.getLastStreamId(), goAwayFrame.getError(), goAwayFrame.getPayload()); + } + + public FrameBytes generateGoAway(int lastStreamId, int error, byte[] payload) { + if (lastStreamId < 0) { + lastStreamId = 0; + } + + // The last streamId + the error code. + int fixedLength = 4 + 4; + + // Make sure we don't exceed the default frame max length. + int maxPayloadLength = Frame.DEFAULT_MAX_LENGTH - fixedLength; + if (payload != null && payload.length > maxPayloadLength) + payload = Arrays.copyOfRange(payload, 0, maxPayloadLength); + + int length = fixedLength + (payload != null ? payload.length : 0); + ByteBuffer header = generateHeader(FrameType.GO_AWAY, length, Flags.NONE, 0); + + header.putInt(lastStreamId); + header.putInt(error); + + if (payload != null) { + header.put(payload); + } + + BufferUtils.flipToFlush(header, 0); + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setLength(Frame.HEADER_LENGTH + length); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + return frameBytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/HeaderGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/HeaderGenerator.java new file mode 100644 index 000000000..fd9bbb0f4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/HeaderGenerator.java @@ -0,0 +1,31 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; + +import java.nio.ByteBuffer; + +public class HeaderGenerator { + private int maxFrameSize = Frame.DEFAULT_MAX_LENGTH; + + public ByteBuffer generate(FrameType frameType, int capacity, int length, int flags, int streamId) { + ByteBuffer header = BufferUtils.allocate(capacity); + BufferUtils.flipToFill(header); + header.put((byte) ((length & 0x00_FF_00_00) >>> 16)); + header.put((byte) ((length & 0x00_00_FF_00) >>> 8)); + header.put((byte) ((length & 0x00_00_00_FF))); + header.put((byte) frameType.getType()); + header.put((byte) flags); + header.putInt(streamId); + return header; + } + + public int getMaxFrameSize() { + return maxFrameSize; + } + + public void setMaxFrameSize(int maxFrameSize) { + this.maxFrameSize = maxFrameSize; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/HeadersGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/HeadersGenerator.java new file mode 100644 index 000000000..e8e2ec5ee --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/HeadersGenerator.java @@ -0,0 +1,117 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.frame.*; +import com.fireflysource.net.http.common.v2.hpack.HpackEncoder; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class HeadersGenerator extends FrameGenerator { + + private final HpackEncoder encoder; + private final int maxHeaderBlockFragment; + private final PriorityGenerator priorityGenerator; + + public HeadersGenerator(HeaderGenerator headerGenerator, HpackEncoder encoder) { + this(headerGenerator, encoder, 0); + } + + public HeadersGenerator(HeaderGenerator headerGenerator, HpackEncoder encoder, int maxHeaderBlockFragment) { + super(headerGenerator); + this.encoder = encoder; + this.maxHeaderBlockFragment = maxHeaderBlockFragment; + this.priorityGenerator = new PriorityGenerator(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + HeadersFrame headersFrame = (HeadersFrame) frame; + return generateHeaders(headersFrame.getStreamId(), headersFrame.getMetaData(), headersFrame.getPriority(), headersFrame.isEndStream()); + } + + public FrameBytes generateHeaders(int streamId, MetaData metaData, PriorityFrame priority, boolean endStream) { + if (streamId < 0) + throw new IllegalArgumentException("Invalid stream id: " + streamId); + + int flags = Flags.NONE; + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + + if (priority != null) + flags = Flags.PRIORITY; + + int maxFrameSize = getMaxFrameSize(); + + ByteBuffer hpacked = BufferUtils.allocate(maxFrameSize); + BufferUtils.clearToFill(hpacked); + encoder.encode(hpacked, metaData); + int hpackedLength = hpacked.position(); + BufferUtils.flipToFlush(hpacked, 0); + + // Split into CONTINUATION frames if necessary. + if (maxHeaderBlockFragment > 0 && hpackedLength > maxHeaderBlockFragment) { + if (endStream) + flags |= Flags.END_STREAM; + + int length = maxHeaderBlockFragment; + if (priority != null) + length += PriorityFrame.PRIORITY_LENGTH; + + ByteBuffer header = generateHeader(FrameType.HEADERS, length, flags, streamId); + generatePriority(header, priority); + BufferUtils.flipToFlush(header, 0); + frameBytes.getByteBuffers().add(header); + hpacked.limit(maxHeaderBlockFragment); + frameBytes.getByteBuffers().add(hpacked.slice()); + + int totalLength = Frame.HEADER_LENGTH + length; + + int position = maxHeaderBlockFragment; + int limit = position + maxHeaderBlockFragment; + while (limit < hpackedLength) { + hpacked.position(position).limit(limit); + header = generateHeader(FrameType.CONTINUATION, maxHeaderBlockFragment, Flags.NONE, streamId); + BufferUtils.flipToFlush(header, 0); + frameBytes.getByteBuffers().add(header); + frameBytes.getByteBuffers().add(hpacked.slice()); + position += maxHeaderBlockFragment; + limit += maxHeaderBlockFragment; + totalLength += Frame.HEADER_LENGTH + maxHeaderBlockFragment; + } + + hpacked.position(position).limit(hpackedLength); + header = generateHeader(FrameType.CONTINUATION, hpacked.remaining(), Flags.END_HEADERS, streamId); + BufferUtils.flipToFlush(header, 0); + frameBytes.getByteBuffers().add(header); + frameBytes.getByteBuffers().add(hpacked); + totalLength += Frame.HEADER_LENGTH + hpacked.remaining(); + frameBytes.setLength(totalLength); + return frameBytes; + } else { + flags |= Flags.END_HEADERS; + if (endStream) + flags |= Flags.END_STREAM; + + int length = hpackedLength; + if (priority != null) + length += PriorityFrame.PRIORITY_LENGTH; + + ByteBuffer header = generateHeader(FrameType.HEADERS, length, flags, streamId); + generatePriority(header, priority); + BufferUtils.flipToFlush(header, 0); + frameBytes.getByteBuffers().add(header); + frameBytes.getByteBuffers().add(hpacked); + frameBytes.setLength(Frame.HEADER_LENGTH + length); + return frameBytes; + } + } + + private void generatePriority(ByteBuffer header, PriorityFrame priority) { + if (priority != null) { + priorityGenerator.generatePriorityBody(header, priority.getStreamId(), + priority.getParentStreamId(), priority.getWeight(), priority.isExclusive()); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PingGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PingGenerator.java new file mode 100644 index 000000000..0a46ad38d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PingGenerator.java @@ -0,0 +1,41 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.PingFrame; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class PingGenerator extends FrameGenerator { + + public PingGenerator(HeaderGenerator headerGenerator) { + super(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + PingFrame pingFrame = (PingFrame) frame; + return generatePing(pingFrame.getPayload(), pingFrame.isReply()); + } + + public FrameBytes generatePing(byte[] payload, boolean reply) { + if (payload.length != PingFrame.PING_LENGTH) { + throw new IllegalArgumentException("Invalid payload length: " + payload.length); + } + + ByteBuffer header = generateHeader(FrameType.PING, PingFrame.PING_LENGTH, reply ? Flags.ACK : Flags.NONE, 0); + + header.put(payload); + + BufferUtils.flipToFlush(header, 0); + + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + frameBytes.setLength(Frame.HEADER_LENGTH + PingFrame.PING_LENGTH); + return frameBytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PrefaceGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PrefaceGenerator.java new file mode 100644 index 000000000..1542497d2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PrefaceGenerator.java @@ -0,0 +1,22 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.PrefaceFrame; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class PrefaceGenerator extends FrameGenerator { + public PrefaceGenerator() { + super(null); + } + + @Override + public FrameBytes generate(Frame frame) { + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(ByteBuffer.wrap(PrefaceFrame.PREFACE_BYTES)); + frameBytes.setLength(PrefaceFrame.PREFACE_BYTES.length); + return frameBytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PriorityGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PriorityGenerator.java new file mode 100644 index 000000000..ebd221821 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PriorityGenerator.java @@ -0,0 +1,50 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.PriorityFrame; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class PriorityGenerator extends FrameGenerator { + + public PriorityGenerator(HeaderGenerator headerGenerator) { + super(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + PriorityFrame priorityFrame = (PriorityFrame) frame; + return generatePriority(priorityFrame.getStreamId(), priorityFrame.getParentStreamId(), priorityFrame.getWeight(), priorityFrame.isExclusive()); + } + + public FrameBytes generatePriority(int streamId, int parentStreamId, int weight, boolean exclusive) { + ByteBuffer header = generateHeader(FrameType.PRIORITY, PriorityFrame.PRIORITY_LENGTH, Flags.NONE, streamId); + generatePriorityBody(header, streamId, parentStreamId, weight, exclusive); + BufferUtils.flipToFlush(header, 0); + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + frameBytes.setLength(Frame.HEADER_LENGTH + PriorityFrame.PRIORITY_LENGTH); + return frameBytes; + } + + public void generatePriorityBody(ByteBuffer header, int streamId, int parentStreamId, int weight, boolean exclusive) { + if (streamId < 0) + throw new IllegalArgumentException("Invalid stream id: " + streamId); + if (parentStreamId < 0) + throw new IllegalArgumentException("Invalid parent stream id: " + parentStreamId); + if (parentStreamId == streamId) + throw new IllegalArgumentException("Stream " + streamId + " cannot depend on stream " + parentStreamId); + if (weight < 1 || weight > 256) + throw new IllegalArgumentException("Invalid weight: " + weight); + + if (exclusive) + parentStreamId |= 0x80_00_00_00; + header.putInt(parentStreamId); + header.put((byte) (weight - 1)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PushPromiseGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PushPromiseGenerator.java new file mode 100644 index 000000000..dedcbe454 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/PushPromiseGenerator.java @@ -0,0 +1,60 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.PushPromiseFrame; +import com.fireflysource.net.http.common.v2.hpack.HpackEncoder; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class PushPromiseGenerator extends FrameGenerator { + + private final HpackEncoder encoder; + + public PushPromiseGenerator(HeaderGenerator headerGenerator, HpackEncoder encoder) { + super(headerGenerator); + this.encoder = encoder; + } + + @Override + public FrameBytes generate(Frame frame) { + PushPromiseFrame pushPromiseFrame = (PushPromiseFrame) frame; + return generatePushPromise(pushPromiseFrame.getStreamId(), pushPromiseFrame.getPromisedStreamId(), pushPromiseFrame.getMetaData()); + } + + public FrameBytes generatePushPromise(int streamId, int promisedStreamId, MetaData metaData) { + if (streamId < 0) + throw new IllegalArgumentException("Invalid stream id: " + streamId); + if (promisedStreamId < 0) + throw new IllegalArgumentException("Invalid promised stream id: " + promisedStreamId); + + int maxFrameSize = getMaxFrameSize(); + // The promised streamId space. + int extraSpace = 4; + maxFrameSize -= extraSpace; + + ByteBuffer hpacked = BufferUtils.allocate(maxFrameSize); + BufferUtils.clearToFill(hpacked); + encoder.encode(hpacked, metaData); + int hpackedLength = hpacked.position(); + BufferUtils.flipToFlush(hpacked, 0); + + int length = hpackedLength + extraSpace; + int flags = Flags.END_HEADERS; + + ByteBuffer header = generateHeader(FrameType.PUSH_PROMISE, length, flags, streamId); + header.putInt(promisedStreamId); + BufferUtils.flipToFlush(header, 0); + + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + frameBytes.getByteBuffers().add(hpacked); + frameBytes.setLength(Frame.HEADER_LENGTH + length); + return frameBytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/ResetGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/ResetGenerator.java new file mode 100644 index 000000000..1c87ec873 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/ResetGenerator.java @@ -0,0 +1,36 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.ResetFrame; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class ResetGenerator extends FrameGenerator { + public ResetGenerator(HeaderGenerator headerGenerator) { + super(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + ResetFrame resetFrame = (ResetFrame) frame; + return generateReset(resetFrame.getStreamId(), resetFrame.getError()); + } + + public FrameBytes generateReset(int streamId, int error) { + if (streamId < 0) + throw new IllegalArgumentException("Invalid stream id: " + streamId); + + ByteBuffer header = generateHeader(FrameType.RST_STREAM, ResetFrame.RESET_LENGTH, Flags.NONE, streamId); + header.putInt(error); + BufferUtils.flipToFlush(header, 0); + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + frameBytes.setLength(Frame.HEADER_LENGTH + ResetFrame.RESET_LENGTH); + return frameBytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/SettingsGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/SettingsGenerator.java new file mode 100644 index 000000000..9391c4ed5 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/SettingsGenerator.java @@ -0,0 +1,58 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.SettingsFrame; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Map; + +public class SettingsGenerator extends FrameGenerator { + public SettingsGenerator(HeaderGenerator headerGenerator) { + super(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + SettingsFrame settingsFrame = (SettingsFrame) frame; + return generateSettings(settingsFrame.getSettings(), settingsFrame.isReply()); + } + + public FrameBytes generateSettings(Map settings, boolean reply) { + // Two bytes for the identifier, four bytes for the value. + int entryLength = 2 + 4; + int length = entryLength * settings.size(); + if (length > getMaxFrameSize()) + throw new IllegalArgumentException("Invalid settings, too big"); + + ByteBuffer header = generateHeader(FrameType.SETTINGS, length, reply ? Flags.ACK : Flags.NONE, 0); + + for (Map.Entry entry : settings.entrySet()) { + header.putShort(entry.getKey().shortValue()); + header.putInt(entry.getValue()); + } + + BufferUtils.flipToFlush(header, 0); + + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + frameBytes.setLength(Frame.HEADER_LENGTH + length); + return frameBytes; + } + + public static ByteBuffer generateSettingsBody(Map settings) { + int size = settings.size() * (2 + 4); + ByteBuffer buffer = BufferUtils.allocate(size); + final int pos = BufferUtils.flipToFill(buffer); + for (Map.Entry entry : settings.entrySet()) { + buffer.putShort(entry.getKey().shortValue()); + buffer.putInt(entry.getValue()); + } + BufferUtils.flipToFlush(buffer, pos); + return buffer; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/WindowUpdateGenerator.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/WindowUpdateGenerator.java new file mode 100644 index 000000000..877a568eb --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/encoder/WindowUpdateGenerator.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.http.common.v2.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.frame.Flags; +import com.fireflysource.net.http.common.v2.frame.Frame; +import com.fireflysource.net.http.common.v2.frame.FrameType; +import com.fireflysource.net.http.common.v2.frame.WindowUpdateFrame; + +import java.nio.ByteBuffer; +import java.util.LinkedList; + +public class WindowUpdateGenerator extends FrameGenerator { + public WindowUpdateGenerator(HeaderGenerator headerGenerator) { + super(headerGenerator); + } + + @Override + public FrameBytes generate(Frame frame) { + WindowUpdateFrame windowUpdateFrame = (WindowUpdateFrame) frame; + return generateWindowUpdate(windowUpdateFrame.getStreamId(), windowUpdateFrame.getWindowDelta()); + } + + public FrameBytes generateWindowUpdate(int streamId, int windowUpdate) { + if (windowUpdate < 0) + throw new IllegalArgumentException("Invalid window update: " + windowUpdate); + + ByteBuffer header = generateHeader(FrameType.WINDOW_UPDATE, WindowUpdateFrame.WINDOW_UPDATE_LENGTH, Flags.NONE, streamId); + header.putInt(windowUpdate); + BufferUtils.flipToFlush(header, 0); + + FrameBytes frameBytes = new FrameBytes(); + frameBytes.setByteBuffers(new LinkedList<>()); + frameBytes.getByteBuffers().add(header); + frameBytes.setLength(Frame.HEADER_LENGTH + WindowUpdateFrame.WINDOW_UPDATE_LENGTH); + return frameBytes; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/CloseState.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/CloseState.java new file mode 100644 index 000000000..4758565fe --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/CloseState.java @@ -0,0 +1,53 @@ +package com.fireflysource.net.http.common.v2.frame; + +/** + * The set of close states for a stream or a session. + *
+ *                rcv hc
+ * NOT_CLOSED ---------------> REMOTELY_CLOSED
+ *      |                             |
+ *   gen|                             |gen
+ *    hc|                             |hc
+ *      |                             |
+ *      v              rcv hc         v
+ * LOCALLY_CLOSING --------------> CLOSING
+ *      |                             |
+ *   snd|                             |gen
+ *    hc|                             |hc
+ *      |                             |
+ *      v              rcv hc         v
+ * LOCALLY_CLOSED ----------------> CLOSED
+ * 
+ */ +public enum CloseState { + /** + * Fully open. + */ + NOT_CLOSED, + /** + * A half-close frame has been generated. + */ + LOCALLY_CLOSING, + /** + * A half-close frame has been generated and sent. + */ + LOCALLY_CLOSED, + /** + * A half-close frame has been received. + */ + REMOTELY_CLOSED, + /** + * A half-close frame has been received, and a half-close frame has been generated, but not yet sent. + */ + CLOSING, + /** + * Fully closed. + */ + CLOSED; + + public enum Event { + RECEIVED, + BEFORE_SEND, + AFTER_SEND + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/DataFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/DataFrame.java new file mode 100644 index 000000000..273c0767c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/DataFrame.java @@ -0,0 +1,53 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.nio.ByteBuffer; + +public class DataFrame extends Frame { + private final int streamId; + private final ByteBuffer data; + private final boolean endStream; + private final int padding; + + public DataFrame(int streamId, ByteBuffer data, boolean endStream) { + this(streamId, data, endStream, 0); + } + + public DataFrame(int streamId, ByteBuffer data, boolean endStream, int padding) { + super(FrameType.DATA); + this.streamId = streamId; + this.data = data; + this.endStream = endStream; + this.padding = padding; + } + + public int getStreamId() { + return streamId; + } + + public ByteBuffer getData() { + return data; + } + + public boolean isEndStream() { + return endStream; + } + + /** + * @return the number of data bytes remaining. + */ + public int remaining() { + return data.remaining(); + } + + /** + * @return the number of bytes used for padding that count towards flow control. + */ + public int padding() { + return padding; + } + + @Override + public String toString() { + return String.format("%s#%d{length:%d,end=%b}", super.toString(), streamId, data.remaining(), endStream); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/DisconnectFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/DisconnectFrame.java new file mode 100644 index 000000000..4c7ecdb6a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/DisconnectFrame.java @@ -0,0 +1,7 @@ +package com.fireflysource.net.http.common.v2.frame; + +public class DisconnectFrame extends Frame { + public DisconnectFrame() { + super(FrameType.DISCONNECT); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/ErrorCode.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/ErrorCode.java new file mode 100644 index 000000000..bb92ccfc3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/ErrorCode.java @@ -0,0 +1,95 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Standard HTTP/2 error codes. + */ +public enum ErrorCode { + /** + * Indicates no errors. + */ + NO_ERROR(0), + /** + * Indicates a generic HTTP/2 protocol violation. + */ + PROTOCOL_ERROR(1), + /** + * Indicates an internal error. + */ + INTERNAL_ERROR(2), + /** + * Indicates an HTTP/2 flow control violation. + */ + FLOW_CONTROL_ERROR(3), + /** + * Indicates that a SETTINGS frame did not receive a reply in a timely manner. + */ + SETTINGS_TIMEOUT_ERROR(4), + /** + * Indicates that a stream frame has been received after the stream closed. + */ + STREAM_CLOSED_ERROR(5), + /** + * Indicates that a frame has an invalid length. + */ + FRAME_SIZE_ERROR(6), + /** + * Indicates that a stream rejected before application processing. + */ + REFUSED_STREAM_ERROR(7), + /** + * Indicates that a stream is no longer needed. + */ + CANCEL_STREAM_ERROR(8), + /** + * Indicates inability to maintain the HPACK compression context. + */ + COMPRESSION_ERROR(9), + /** + * Indicates that the connection established by an HTTP CONNECT was abnormally closed. + */ + HTTP_CONNECT_ERROR(10), + /** + * Indicates that the other peer might be generating excessive load. + */ + ENHANCE_YOUR_CALM_ERROR(11), + /** + * Indicates that the transport properties do not meet minimum security requirements. + */ + INADEQUATE_SECURITY_ERROR(12), + /** + * Indicates that HTTP/1.1 must be used rather than HTTP/2. + */ + HTTP_1_1_REQUIRED_ERROR(13); + + public final int code; + + ErrorCode(int code) { + this.code = code; + Codes.codes.put(code, this); + } + + public static ErrorCode from(int error) { + return Codes.codes.get(error); + } + + public static String toString(int error, String defaultError) { + ErrorCode errorCode = from(error); + String result; + if (errorCode != null) { + result = errorCode.name().toLowerCase(Locale.ENGLISH); + } else if (defaultError == null) { + result = String.valueOf(error); + } else { + result = defaultError; + } + return result; + } + + private static class Codes { + private static final Map codes = new HashMap<>(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/FailureFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/FailureFrame.java new file mode 100644 index 000000000..8fb0337ba --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/FailureFrame.java @@ -0,0 +1,20 @@ +package com.fireflysource.net.http.common.v2.frame; + +public class FailureFrame extends Frame { + private final int error; + private final String reason; + + public FailureFrame(int error, String reason) { + super(FrameType.FAILURE); + this.error = error; + this.reason = reason; + } + + public int getError() { + return error; + } + + public String getReason() { + return reason; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/Flags.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/Flags.java new file mode 100644 index 000000000..29a5e5188 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/Flags.java @@ -0,0 +1,10 @@ +package com.fireflysource.net.http.common.v2.frame; + +public interface Flags { + int NONE = 0x00; + int END_STREAM = 0x01; + int ACK = 0x01; + int END_HEADERS = 0x04; + int PADDING = 0x08; + int PRIORITY = 0x20; +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/Frame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/Frame.java new file mode 100644 index 000000000..7f2d7e68b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/Frame.java @@ -0,0 +1,23 @@ +package com.fireflysource.net.http.common.v2.frame; + +public abstract class Frame { + public static final int HEADER_LENGTH = 9; + public static final int DEFAULT_MAX_LENGTH = 0x40_00; + public static final int MAX_MAX_LENGTH = 0xFF_FF_FF; + public static final Frame[] EMPTY_ARRAY = new Frame[0]; + + private final FrameType type; + + protected Frame(FrameType type) { + this.type = type; + } + + public FrameType getType() { + return type; + } + + @Override + public String toString() { + return String.format("%s@%x", getClass().getSimpleName(), hashCode()); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/FrameType.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/FrameType.java new file mode 100644 index 000000000..9f9454043 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/FrameType.java @@ -0,0 +1,40 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.util.HashMap; +import java.util.Map; + +public enum FrameType { + DATA(0), + HEADERS(1), + PRIORITY(2), + RST_STREAM(3), + SETTINGS(4), + PUSH_PROMISE(5), + PING(6), + GO_AWAY(7), + WINDOW_UPDATE(8), + CONTINUATION(9), + // Synthetic frames only needed by the implementation. + PREFACE(10), + DISCONNECT(11), + FAILURE(12); + + private final int type; + + FrameType(int type) { + this.type = type; + Types.types.put(type, this); + } + + public static FrameType from(int type) { + return Types.types.get(type); + } + + public int getType() { + return type; + } + + private static class Types { + private static final Map types = new HashMap<>(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/GoAwayFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/GoAwayFrame.java new file mode 100644 index 000000000..b995b26d4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/GoAwayFrame.java @@ -0,0 +1,54 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.nio.charset.StandardCharsets; + +public class GoAwayFrame extends Frame { + private final CloseState closeState; + private final int lastStreamId; + private final int error; + private final byte[] payload; + + public GoAwayFrame(int lastStreamId, int error, byte[] payload) { + this(CloseState.REMOTELY_CLOSED, lastStreamId, error, payload); + } + + public GoAwayFrame(CloseState closeState, int lastStreamId, int error, byte[] payload) { + super(FrameType.GO_AWAY); + this.closeState = closeState; + this.lastStreamId = lastStreamId; + this.error = error; + this.payload = payload; + } + + public int getLastStreamId() { + return lastStreamId; + } + + public int getError() { + return error; + } + + public byte[] getPayload() { + return payload; + } + + public String tryConvertPayload() { + if (payload == null || payload.length == 0) + return ""; + try { + return new String(payload, StandardCharsets.UTF_8); + } catch (Throwable x) { + return ""; + } + } + + @Override + public String toString() { + return String.format("%s,%d/%s/%s/%s", + super.toString(), + lastStreamId, + ErrorCode.toString(error, null), + tryConvertPayload(), + closeState); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/HeadersFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/HeadersFrame.java new file mode 100644 index 000000000..605eac60c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/HeadersFrame.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.model.MetaData; + +public class HeadersFrame extends Frame { + private final int streamId; + private final MetaData metaData; + private final PriorityFrame priority; + private final boolean endStream; + private boolean endHeaders; + + /** + *

Creates a new {@code HEADERS} frame with an unspecified stream {@code id}.

+ *

The stream {@code id} will be generated by the implementation while sending + * this frame to the other peer.

+ * + * @param metaData the metadata containing HTTP request information + * @param priority the PRIORITY frame associated with this HEADERS frame + * @param endStream whether this frame ends the stream + */ + public HeadersFrame(MetaData metaData, PriorityFrame priority, boolean endStream) { + this(0, metaData, priority, endStream); + } + + /** + *

Creates a new {@code HEADERS} frame with the specified stream {@code id}.

+ *

{@code HEADERS} frames with a specific stream {@code id} are typically used + * in responses to request {@code HEADERS} frames.

+ * + * @param streamId the stream id + * @param metaData the metadata containing HTTP request/response information + * @param priority the PRIORITY frame associated with this HEADERS frame + * @param endStream whether this frame ends the stream + */ + public HeadersFrame(int streamId, MetaData metaData, PriorityFrame priority, boolean endStream) { + super(FrameType.HEADERS); + this.streamId = streamId; + this.metaData = metaData; + this.priority = priority; + this.endStream = endStream; + } + + public int getStreamId() { + return streamId; + } + + public MetaData getMetaData() { + return metaData; + } + + public PriorityFrame getPriority() { + return priority; + } + + public boolean isEndStream() { + return endStream; + } + + public boolean isEndHeaders() { + return endHeaders; + } + + public void setEndHeaders(boolean endHeaders) { + this.endHeaders = endHeaders; + } + + @Override + public String toString() { + return String.format("%s#%d{end=%b}%s", super.toString(), streamId, endStream, + priority == null ? "" : String.format("+%s", priority)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PingFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PingFrame.java new file mode 100644 index 000000000..009568f15 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PingFrame.java @@ -0,0 +1,74 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.util.Objects; + +public class PingFrame extends Frame { + public static final int PING_LENGTH = 8; + private static final byte[] EMPTY_PAYLOAD = new byte[8]; + + private final byte[] payload; + private final boolean reply; + + /** + * Creates a PING frame with an empty payload. + * + * @param reply whether this PING frame is a reply + */ + public PingFrame(boolean reply) { + this(EMPTY_PAYLOAD, reply); + } + + /** + * Creates a PING frame with the given {@code long} {@code value} as payload. + * + * @param value the value to use as a payload for this PING frame + * @param reply whether this PING frame is a reply + */ + public PingFrame(long value, boolean reply) { + this(toBytes(value), reply); + } + + /** + * Creates a PING frame with the given {@code payload}. + * + * @param payload the payload for this PING frame + * @param reply whether this PING frame is a reply + */ + public PingFrame(byte[] payload, boolean reply) { + super(FrameType.PING); + this.payload = Objects.requireNonNull(payload); + if (payload.length != PING_LENGTH) + throw new IllegalArgumentException("PING payload must be 8 bytes"); + this.reply = reply; + } + + private static byte[] toBytes(long value) { + byte[] result = new byte[8]; + for (int i = result.length - 1; i >= 0; --i) { + result[i] = (byte) (value & 0xFF); + value >>= 8; + } + return result; + } + + private static long toLong(byte[] payload) { + long result = 0; + for (int i = 0; i < 8; ++i) { + result <<= 8; + result |= (payload[i] & 0xFF); + } + return result; + } + + public byte[] getPayload() { + return payload; + } + + public long getPayloadAsLong() { + return toLong(payload); + } + + public boolean isReply() { + return reply; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PrefaceFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PrefaceFrame.java new file mode 100644 index 000000000..ecf9b7b04 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PrefaceFrame.java @@ -0,0 +1,28 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.nio.charset.StandardCharsets; + +public class PrefaceFrame extends Frame { + /** + * The bytes of the HTTP/2 preface that form a legal HTTP/1.1 + * request, used in the direct upgrade. + */ + public static final byte[] PREFACE_PREAMBLE_BYTES = ( + "PRI * HTTP/2.0\r\n" + + "\r\n" + ).getBytes(StandardCharsets.US_ASCII); + + /** + * The HTTP/2 preface bytes. + */ + public static final byte[] PREFACE_BYTES = ( + "PRI * HTTP/2.0\r\n" + + "\r\n" + + "SM\r\n" + + "\r\n" + ).getBytes(StandardCharsets.US_ASCII); + + public PrefaceFrame() { + super(FrameType.PREFACE); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PriorityFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PriorityFrame.java new file mode 100644 index 000000000..f36c16c3c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PriorityFrame.java @@ -0,0 +1,52 @@ +package com.fireflysource.net.http.common.v2.frame; + +public class PriorityFrame extends Frame { + public static final int PRIORITY_LENGTH = 5; + + private final int streamId; + private final int parentStreamId; + private final int weight; + private final boolean exclusive; + + public PriorityFrame(int parentStreamId, int weight, boolean exclusive) { + this(0, parentStreamId, weight, exclusive); + } + + public PriorityFrame(int streamId, int parentStreamId, int weight, boolean exclusive) { + super(FrameType.PRIORITY); + this.streamId = streamId; + this.parentStreamId = parentStreamId; + this.weight = weight; + this.exclusive = exclusive; + } + + public int getStreamId() { + return streamId; + } + + /** + * @return int of the Parent Stream + * @deprecated use {@link #getParentStreamId()} instead. + */ + @Deprecated + public int getDependentStreamId() { + return getParentStreamId(); + } + + public int getParentStreamId() { + return parentStreamId; + } + + public int getWeight() { + return weight; + } + + public boolean isExclusive() { + return exclusive; + } + + @Override + public String toString() { + return String.format("%s#%d/#%d{weight=%d,exclusive=%b}", super.toString(), streamId, parentStreamId, weight, exclusive); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PushPromiseFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PushPromiseFrame.java new file mode 100644 index 000000000..080e96b53 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/PushPromiseFrame.java @@ -0,0 +1,42 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.model.MetaData; + +public class PushPromiseFrame extends Frame { + private final int streamId; + private final int promisedStreamId; + private final MetaData metaData; + private boolean endHeaders; + + public PushPromiseFrame(int streamId, int promisedStreamId, MetaData metaData) { + super(FrameType.PUSH_PROMISE); + this.streamId = streamId; + this.promisedStreamId = promisedStreamId; + this.metaData = metaData; + } + + public int getStreamId() { + return streamId; + } + + public int getPromisedStreamId() { + return promisedStreamId; + } + + public MetaData getMetaData() { + return metaData; + } + + public boolean isEndHeaders() { + return endHeaders; + } + + public void setEndHeaders(boolean endHeaders) { + this.endHeaders = endHeaders; + } + + @Override + public String toString() { + return String.format("%s#%d/#%d", super.toString(), streamId, promisedStreamId); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/ResetFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/ResetFrame.java new file mode 100644 index 000000000..764a9292a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/ResetFrame.java @@ -0,0 +1,27 @@ +package com.fireflysource.net.http.common.v2.frame; + +public class ResetFrame extends Frame { + public static final int RESET_LENGTH = 4; + + private final int streamId; + private final int error; + + public ResetFrame(int streamId, int error) { + super(FrameType.RST_STREAM); + this.streamId = streamId; + this.error = error; + } + + public int getStreamId() { + return streamId; + } + + public int getError() { + return error; + } + + @Override + public String toString() { + return String.format("%s#%d{%s}", super.toString(), streamId, ErrorCode.toString(error, null)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/SettingsFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/SettingsFrame.java new file mode 100644 index 000000000..3de5960ba --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/SettingsFrame.java @@ -0,0 +1,49 @@ +package com.fireflysource.net.http.common.v2.frame; + +import java.util.HashMap; +import java.util.Map; + +public class SettingsFrame extends Frame { + + public static final int DEFAULT_MAX_KEYS = 64; + + public static final int HEADER_TABLE_SIZE = 1; + public static final int ENABLE_PUSH = 2; + public static final int MAX_CONCURRENT_STREAMS = 3; + public static final int INITIAL_WINDOW_SIZE = 4; + public static final int MAX_FRAME_SIZE = 5; + public static final int MAX_HEADER_LIST_SIZE = 6; + + public static final SettingsFrame DEFAULT_SETTINGS_FRAME; + + static { + Map settings = new HashMap<>(); + settings.put(HEADER_TABLE_SIZE, 4096); + settings.put(ENABLE_PUSH, 1); + settings.put(INITIAL_WINDOW_SIZE, 65535); + settings.put(MAX_FRAME_SIZE, 16384); + DEFAULT_SETTINGS_FRAME = new SettingsFrame(settings, false); + } + + private final Map settings; + private final boolean reply; + + public SettingsFrame(Map settings, boolean reply) { + super(FrameType.SETTINGS); + this.settings = settings; + this.reply = reply; + } + + public Map getSettings() { + return settings; + } + + public boolean isReply() { + return reply; + } + + @Override + public String toString() { + return String.format("%s,reply=%b:%s", super.toString(), reply, settings); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/WindowUpdateFrame.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/WindowUpdateFrame.java new file mode 100644 index 000000000..15615eefa --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/frame/WindowUpdateFrame.java @@ -0,0 +1,35 @@ +package com.fireflysource.net.http.common.v2.frame; + +public class WindowUpdateFrame extends Frame { + public static final int WINDOW_UPDATE_LENGTH = 4; + + private final int streamId; + private final int windowDelta; + + public WindowUpdateFrame(int streamId, int windowDelta) { + super(FrameType.WINDOW_UPDATE); + this.streamId = streamId; + this.windowDelta = windowDelta; + } + + public int getStreamId() { + return streamId; + } + + public int getWindowDelta() { + return windowDelta; + } + + public boolean isStreamWindowUpdate() { + return streamId != 0; + } + + public boolean isSessionWindowUpdate() { + return streamId == 0; + } + + @Override + public String toString() { + return String.format("%s#%d,delta=%d", super.toString(), streamId, windowDelta); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/AuthorityHttpField.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/AuthorityHttpField.java new file mode 100644 index 000000000..2d8c9fc82 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/AuthorityHttpField.java @@ -0,0 +1,18 @@ +package com.fireflysource.net.http.common.v2.hpack; + + +import com.fireflysource.net.http.common.model.HostPortHttpField; +import com.fireflysource.net.http.common.model.HttpHeader; + +public class AuthorityHttpField extends HostPortHttpField { + public final static String AUTHORITY = HpackContext.STATIC_TABLE[1][0]; + + public AuthorityHttpField(String authority) { + super(HttpHeader.C_AUTHORITY, AUTHORITY, authority); + } + + @Override + public String toString() { + return String.format("%s(preparsed h=%s p=%d)", super.toString(), getHost(), getPort()); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackContext.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackContext.java new file mode 100644 index 000000000..58be93402 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackContext.java @@ -0,0 +1,431 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.common.collection.trie.ArrayTernaryTrie; +import com.fireflysource.common.collection.trie.Trie; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpMethod; +import com.fireflysource.net.http.common.model.HttpScheme; + +import java.nio.ByteBuffer; +import java.util.*; + +/** + * HPACK - Header Compression for HTTP/2 + *

+ * This class maintains the compression context for a single HTTP/2 connection. + * Specifically it holds the static and dynamic Header Field Tables and the + * associated sizes and limits. + *

+ *

+ * It is compliant with draft 11 of the specification + *

+ */ +public class HpackContext { + + private static final LazyLogger LOG = SystemLogger.create(HpackContext.class); + + private static final String EMPTY = ""; + public static final String[][] STATIC_TABLE = { + {null, null}, + /* 1 */ {":authority", EMPTY}, + /* 2 */ {":method", "GET"}, + /* 3 */ {":method", "POST"}, + /* 4 */ {":path", "/"}, + /* 5 */ {":path", "/index.html"}, + /* 6 */ {":scheme", "http"}, + /* 7 */ {":scheme", "https"}, + /* 8 */ {":status", "200"}, + /* 9 */ {":status", "204"}, + /* 10 */ {":status", "206"}, + /* 11 */ {":status", "304"}, + /* 12 */ {":status", "400"}, + /* 13 */ {":status", "404"}, + /* 14 */ {":status", "500"}, + /* 15 */ {"accept-charset", EMPTY}, + /* 16 */ {"accept-encoding", "gzip, deflate"}, + /* 17 */ {"accept-language", EMPTY}, + /* 18 */ {"accept-ranges", EMPTY}, + /* 19 */ {"accept", EMPTY}, + /* 20 */ {"access-control-allow-origin", EMPTY}, + /* 21 */ {"age", EMPTY}, + /* 22 */ {"allow", EMPTY}, + /* 23 */ {"authorization", EMPTY}, + /* 24 */ {"cache-control", EMPTY}, + /* 25 */ {"content-disposition", EMPTY}, + /* 26 */ {"content-encoding", EMPTY}, + /* 27 */ {"content-language", EMPTY}, + /* 28 */ {"content-length", EMPTY}, + /* 29 */ {"content-location", EMPTY}, + /* 30 */ {"content-range", EMPTY}, + /* 31 */ {"content-type", EMPTY}, + /* 32 */ {"cookie", EMPTY}, + /* 33 */ {"date", EMPTY}, + /* 34 */ {"etag", EMPTY}, + /* 35 */ {"expect", EMPTY}, + /* 36 */ {"expires", EMPTY}, + /* 37 */ {"from", EMPTY}, + /* 38 */ {"host", EMPTY}, + /* 39 */ {"if-match", EMPTY}, + /* 40 */ {"if-modified-since", EMPTY}, + /* 41 */ {"if-none-match", EMPTY}, + /* 42 */ {"if-range", EMPTY}, + /* 43 */ {"if-unmodified-since", EMPTY}, + /* 44 */ {"last-modified", EMPTY}, + /* 45 */ {"link", EMPTY}, + /* 46 */ {"location", EMPTY}, + /* 47 */ {"max-forwards", EMPTY}, + /* 48 */ {"proxy-authenticate", EMPTY}, + /* 49 */ {"proxy-authorization", EMPTY}, + /* 50 */ {"range", EMPTY}, + /* 51 */ {"referer", EMPTY}, + /* 52 */ {"refresh", EMPTY}, + /* 53 */ {"retry-after", EMPTY}, + /* 54 */ {"server", EMPTY}, + /* 55 */ {"set-cookie", EMPTY}, + /* 56 */ {"strict-transport-security", EMPTY}, + /* 57 */ {"transfer-encoding", EMPTY}, + /* 58 */ {"user-agent", EMPTY}, + /* 59 */ {"vary", EMPTY}, + /* 60 */ {"via", EMPTY}, + /* 61 */ {"www-authenticate", EMPTY} + }; + + private static final Map STATIC_FIELD_MAP = new HashMap<>(); + private static final Trie STATIC_NAME_MAP = new ArrayTernaryTrie<>(true, 512); + private static final StaticEntry[] STATIC_TABLE_BY_HEADER = new StaticEntry[HttpHeader.UNKNOWN.ordinal()]; + private static final StaticEntry[] STATIC_TABLE_ENTRIES = new StaticEntry[STATIC_TABLE.length]; + public static final int STATIC_SIZE = STATIC_TABLE.length - 1; + + static { + Set added = new HashSet<>(); + for (int i = 1; i < STATIC_TABLE.length; i++) { + StaticEntry entry = null; + + String name = STATIC_TABLE[i][0]; + String value = STATIC_TABLE[i][1]; + HttpHeader header = HttpHeader.CACHE.get(name); + if (header != null && value != null) { + switch (header) { + case C_METHOD: { + + HttpMethod method = HttpMethod.CACHE.get(value); + if (method != null) + entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, method)); + break; + } + + case C_SCHEME: { + + HttpScheme scheme = HttpScheme.from(value); + if (scheme != null) + entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, scheme)); + break; + } + + case C_STATUS: { + entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, Integer.valueOf(value))); + break; + } + + default: + break; + } + } + + if (entry == null) + entry = new StaticEntry(i, header == null ? new HttpField(STATIC_TABLE[i][0], value) : new HttpField(header, name, value)); + + STATIC_TABLE_ENTRIES[i] = entry; + + if (entry.field.getValue() != null) + STATIC_FIELD_MAP.put(entry.field, entry); + + if (!added.contains(entry.field.getName())) { + added.add(entry.field.getName()); + STATIC_NAME_MAP.put(entry.field.getName(), entry); + if (STATIC_NAME_MAP.get(entry.field.getName()) == null) + throw new IllegalStateException("name trie too small"); + } + } + + for (HttpHeader h : HttpHeader.values()) { + StaticEntry entry = STATIC_NAME_MAP.get(h.getValue()); + if (entry != null) + STATIC_TABLE_BY_HEADER[h.ordinal()] = entry; + } + } + + private int maxDynamicTableSizeInBytes; + private int dynamicTableSizeInBytes; + private final DynamicTable dynamicTable; + private final Map fieldMap = new HashMap<>(); + private final Map nameMap = new HashMap<>(); + + public HpackContext(int maxDynamicTableSize) { + maxDynamicTableSizeInBytes = maxDynamicTableSize; + int guesstimateEntries = 10 + maxDynamicTableSize / (32 + 10 + 10); + dynamicTable = new DynamicTable(guesstimateEntries); + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] created max=%d", hashCode(), maxDynamicTableSize)); + } + + public void resize(int newMaxDynamicTableSize) { + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), maxDynamicTableSizeInBytes, newMaxDynamicTableSize)); + maxDynamicTableSizeInBytes = newMaxDynamicTableSize; + dynamicTable.evict(); + } + + public Entry get(HttpField field) { + Entry entry = fieldMap.get(field); + if (entry == null) + entry = STATIC_FIELD_MAP.get(field); + return entry; + } + + public Entry get(String name) { + Entry entry = STATIC_NAME_MAP.get(name); + if (entry != null) + return entry; + return nameMap.get(StringUtils.asciiToLowerCase(name)); + } + + public Entry get(int index) { + if (index <= STATIC_SIZE) + return STATIC_TABLE_ENTRIES[index]; + + return dynamicTable.get(index); + } + + public Entry get(HttpHeader header) { + Entry e = STATIC_TABLE_BY_HEADER[header.ordinal()]; + if (e == null) + return get(header.getValue()); + return e; + } + + public static Entry getStatic(HttpHeader header) { + return STATIC_TABLE_BY_HEADER[header.ordinal()]; + } + + public Entry add(HttpField field) { + Entry entry = new Entry(field); + int size = entry.getSize(); + if (size > maxDynamicTableSizeInBytes) { + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] !added size %d>%d", hashCode(), size, maxDynamicTableSizeInBytes)); + dynamicTable.evictAll(); + return null; + } + dynamicTableSizeInBytes += size; + dynamicTable.add(entry); + fieldMap.put(field, entry); + nameMap.put(StringUtils.asciiToLowerCase(field.getName()), entry); + + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] added %s", hashCode(), entry)); + dynamicTable.evict(); + return entry; + } + + /** + * @return Current dynamic table size in entries + */ + public int size() { + return dynamicTable.size(); + } + + /** + * @return Current Dynamic table size in Octets + */ + public int getDynamicTableSize() { + return dynamicTableSizeInBytes; + } + + /** + * @return Max Dynamic table size in Octets + */ + public int getMaxDynamicTableSize() { + return maxDynamicTableSizeInBytes; + } + + public int index(Entry entry) { + if (entry.slot < 0) + return 0; + if (entry.isStatic()) + return entry.slot; + + return dynamicTable.index(entry); + } + + public static int staticIndex(HttpHeader header) { + if (header == null) + return 0; + Entry entry = STATIC_NAME_MAP.get(header.getValue()); + if (entry == null) + return 0; + return entry.slot; + } + + @Override + public String toString() { + return String.format("HpackContext@%x{entries=%d,size=%d,max=%d}", hashCode(), dynamicTable.size(), dynamicTableSizeInBytes, maxDynamicTableSizeInBytes); + } + + private class DynamicTable { + Entry[] entries; + int size; + int offset; + int growby; + + private DynamicTable(int initCapacity) { + entries = new Entry[initCapacity]; + growby = initCapacity; + } + + public void add(Entry entry) { + if (size == entries.length) { + Entry[] entries = new Entry[this.entries.length + growby]; + for (int i = 0; i < size; i++) { + int slot = (offset + i) % this.entries.length; + entries[i] = this.entries[slot]; + entries[i].slot = i; + } + this.entries = entries; + offset = 0; + } + int slot = (size++ + offset) % entries.length; + entries[slot] = entry; + entry.slot = slot; + } + + public int index(Entry entry) { + return STATIC_SIZE + size - (entry.slot - offset + entries.length) % entries.length; + } + + public Entry get(int index) { + int d = index - STATIC_SIZE - 1; + if (d < 0 || d >= size) + return null; + int slot = (offset + size - d - 1) % entries.length; + return entries[slot]; + } + + public int size() { + return size; + } + + private void evict() { + while (dynamicTableSizeInBytes > maxDynamicTableSizeInBytes) { + Entry entry = entries[offset]; + entries[offset] = null; + offset = (offset + 1) % entries.length; + size--; + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] evict %s", HpackContext.this.hashCode(), entry)); + dynamicTableSizeInBytes -= entry.getSize(); + entry.slot = -1; + fieldMap.remove(entry.getHttpField()); + String lc = StringUtils.asciiToLowerCase(entry.getHttpField().getName()); + if (entry == nameMap.get(lc)) + nameMap.remove(lc); + } + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d", HpackContext.this.hashCode(), dynamicTable.size(), dynamicTableSizeInBytes, maxDynamicTableSizeInBytes)); + } + + private void evictAll() { + if (LOG.isDebugEnabled()) + LOG.debug(String.format("HdrTbl[%x] evictAll", HpackContext.this.hashCode())); + fieldMap.clear(); + nameMap.clear(); + offset = 0; + size = 0; + dynamicTableSizeInBytes = 0; + Arrays.fill(entries, null); + } + } + + public static class Entry { + final HttpField field; + int slot; // The index within it's an array + + Entry() { + slot = -1; + field = null; + } + + Entry(HttpField field) { + this.field = field; + } + + public int getSize() { + String value = field.getValue(); + return 32 + field.getName().length() + (value == null ? 0 : value.length()); + } + + public HttpField getHttpField() { + return field; + } + + public boolean isStatic() { + return false; + } + + public byte[] getStaticHuffmanValue() { + return null; + } + + @Override + public String toString() { + return String.format("{%s,%d,%s,%x}", isStatic() ? "S" : "D", slot, field, hashCode()); + } + } + + public static class StaticEntry extends Entry { + private final byte[] huffmanValue; + private final byte encodedField; + + StaticEntry(int index, HttpField field) { + super(field); + slot = index; + String value = field.getValue(); + if (value != null && value.length() > 0) { + int huffmanLen = Huffman.octetsNeeded(value); + if (huffmanLen < 0) + throw new IllegalStateException("bad value"); + int lenLen = NBitInteger.octectsNeeded(7, huffmanLen); + huffmanValue = new byte[1 + lenLen + huffmanLen]; + ByteBuffer buffer = ByteBuffer.wrap(huffmanValue); + + // Indicate Huffman + buffer.put((byte) 0x80); + // Add huffman length + NBitInteger.encode(buffer, 7, huffmanLen); + // Encode value + Huffman.encode(buffer, value); + } else + huffmanValue = null; + + encodedField = (byte) (0x80 | index); + } + + @Override + public boolean isStatic() { + return true; + } + + @Override + public byte[] getStaticHuffmanValue() { + return huffmanValue; + } + + public byte getEncodedField() { + return encodedField; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackDecoder.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackDecoder.java new file mode 100644 index 000000000..387486fd8 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackDecoder.java @@ -0,0 +1,256 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpTokens; +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v2.hpack.HpackContext.Entry; +import org.slf4j.Logger; + +import java.nio.ByteBuffer; + +/** + * Hpack Decoder + *

+ * This is not thread safe and may only be called by 1 thread at a time. + *

+ */ +public class HpackDecoder { + public static final Logger LOG = SystemLogger.create(HpackDecoder.class); + + public static final HttpField.LongValueHttpField CONTENT_LENGTH_0 = + new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, 0L); + + private final HpackContext context; + private final MetaDataBuilder builder; + private int localMaxDynamicTableSize; + + /** + * @param localMaxDynamicTableSize The maximum allowed size of the local dynamic header field table. + * @param maxHeaderSize The maximum allowed size of a headers block, expressed as total of all name and value characters, plus 32 per field + */ + public HpackDecoder(int localMaxDynamicTableSize, int maxHeaderSize) { + context = new HpackContext(localMaxDynamicTableSize); + this.localMaxDynamicTableSize = localMaxDynamicTableSize; + builder = new MetaDataBuilder(maxHeaderSize); + } + + public HpackContext getHpackContext() { + return context; + } + + public void setLocalMaxDynamicTableSize(int localMaxdynamciTableSize) { + localMaxDynamicTableSize = localMaxdynamciTableSize; + } + + public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException, HpackException.StreamException { + if (LOG.isDebugEnabled()) + LOG.debug(String.format("CtxTbl[%x] decoding %d octets", context.hashCode(), buffer.remaining())); + + // If the buffer is big, don't even think about decoding it + if (buffer.remaining() > builder.getMaxSize()) + throw new HpackException.SessionException("431 Request Header Fields too large"); + + boolean emitted = false; + + while (buffer.hasRemaining()) { + if (LOG.isDebugEnabled() && buffer.hasArray()) { + int l = Math.min(buffer.remaining(), 32); + LOG.debug("decode {}{}", + TypeUtils.toHexString(buffer.array(), buffer.arrayOffset() + buffer.position(), l), + l < buffer.remaining() ? "..." : ""); + } + + byte b = buffer.get(); + if (b < 0) { + // 7.1 indexed if the high bit is set + int index = NBitInteger.decode(buffer, 7); + Entry entry = context.get(index); + if (entry == null) + throw new HpackException.SessionException("Unknown index %d", index); + + if (entry.isStatic()) { + if (LOG.isDebugEnabled()) + LOG.debug("decode IdxStatic {}", entry); + // emit field + emitted = true; + builder.emit(entry.getHttpField()); + + // TODO copy and add to reference set if there is room + // _context.add(entry.getHttpField()); + } else { + if (LOG.isDebugEnabled()) + LOG.debug("decode Idx {}", entry); + // emit + emitted = true; + builder.emit(entry.getHttpField()); + } + } else { + // look at the first nibble in detail + byte f = (byte) ((b & 0xF0) >> 4); + String name; + HttpHeader header; + String value; + + boolean indexed; + int nameIndex; + + switch (f) { + case 2: // 7.3 + case 3: // 7.3 + // change table size + int size = NBitInteger.decode(buffer, 5); + if (LOG.isDebugEnabled()) + LOG.debug("decode resize=" + size); + if (size > localMaxDynamicTableSize) + throw new IllegalArgumentException(); + if (emitted) + throw new HpackException.CompressionException("Dynamic table resize after fields"); + context.resize(size); + continue; + + case 0: // 7.2.2 + case 1: // 7.2.3 + indexed = false; + nameIndex = NBitInteger.decode(buffer, 4); + break; + + case 4: // 7.2.1 + case 5: // 7.2.1 + case 6: // 7.2.1 + case 7: // 7.2.1 + indexed = true; + nameIndex = NBitInteger.decode(buffer, 6); + break; + + default: + throw new IllegalStateException(); + } + + boolean huffmanName = false; + + // decode the name + if (nameIndex > 0) { + Entry nameEntry = context.get(nameIndex); + name = nameEntry.getHttpField().getName(); + header = nameEntry.getHttpField().getHeader(); + } else { + huffmanName = (buffer.get() & 0x80) == 0x80; + int length = NBitInteger.decode(buffer, 7); + builder.checkSize(length, huffmanName); + if (huffmanName) + name = Huffman.decode(buffer, length); + else + name = toASCIIString(buffer, length); + check: + for (int i = name.length(); i-- > 0; ) { + char c = name.charAt(i); + if (c > 0xff) { + builder.streamException("Illegal header name %s", name); + break; + } + HttpTokens.Token token = HttpTokens.TOKENS[0xFF & c]; + switch (token.getType()) { + case ALPHA: + if (c >= 'A' && c <= 'Z') { + builder.streamException("Uppercase header name %s", name); + break check; + } + break; + + case COLON: + case TCHAR: + case DIGIT: + break; + + default: + builder.streamException("Illegal header name %s", name); + break check; + } + } + header = HttpHeader.CACHE.get(name); + } + + // decode the value + boolean huffmanValue = (buffer.get() & 0x80) == 0x80; + int length = NBitInteger.decode(buffer, 7); + builder.checkSize(length, huffmanValue); + if (huffmanValue) + value = Huffman.decode(buffer, length); + else + value = toASCIIString(buffer, length); + + // Make the new field + HttpField field; + if (header == null) { + // just make a normal field and bypass header name lookup + field = new HttpField(null, name, value); + } else { + // might be worthwhile to create a value HttpField if it is indexed + // and/or of a type that may be looked up multiple times. + switch (header) { + case C_STATUS: + if (indexed) + field = new HttpField.IntValueHttpField(header, name, value); + else + field = new HttpField(header, name, value); + break; + + case C_AUTHORITY: + field = new AuthorityHttpField(value); + break; + + case CONTENT_LENGTH: + if ("0".equals(value)) + field = CONTENT_LENGTH_0; + else + field = new HttpField.LongValueHttpField(header, name, value); + break; + + default: + field = new HttpField(header, name, value); + break; + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("decoded '{}' by {}/{}/{}", + field, + nameIndex > 0 ? "IdxName" : (huffmanName ? "HuffName" : "LitName"), + huffmanValue ? "HuffVal" : "LitVal", + indexed ? "Idx" : ""); + } + + // emit the field + emitted = true; + builder.emit(field); + + // if indexed add to dynamic table + if (indexed) + context.add(field); + } + } + + return builder.build(); + } + + public static String toASCIIString(ByteBuffer buffer, int length) { + StringBuilder builder = new StringBuilder(length); + int position = buffer.position(); + int start = buffer.arrayOffset() + position; + int end = start + length; + buffer.position(position + length); + byte[] array = buffer.array(); + for (int i = start; i < end; i++) { + builder.append((char) (0x7f & array[i])); + } + return builder.toString(); + } + + @Override + public String toString() { + return String.format("HpackDecoder@%x{%s}", hashCode(), context); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackEncoder.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackEncoder.java new file mode 100644 index 000000000..66a91558f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackEncoder.java @@ -0,0 +1,389 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.http.common.codec.PreEncodedHttpField; +import com.fireflysource.net.http.common.model.*; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import static com.fireflysource.net.http.common.v2.hpack.HpackContext.Entry; +import static com.fireflysource.net.http.common.v2.hpack.HpackContext.StaticEntry; + +public class HpackEncoder { + private static final LazyLogger LOG = SystemLogger.create(HpackEncoder.class); + + private static final HttpField[] STATUSES = new HttpField[599]; + static final EnumSet DO_NOT_HUFFMAN = EnumSet.of( + HttpHeader.AUTHORIZATION, + HttpHeader.CONTENT_MD5, + HttpHeader.PROXY_AUTHENTICATE, + HttpHeader.PROXY_AUTHORIZATION); + static final EnumSet DO_NOT_INDEX = EnumSet.of( + // HttpHeader.C_PATH, // TODO more data needed + // HttpHeader.DATE, // TODO more data needed + HttpHeader.AUTHORIZATION, + HttpHeader.CONTENT_MD5, + HttpHeader.CONTENT_RANGE, + HttpHeader.ETAG, + HttpHeader.IF_MODIFIED_SINCE, + HttpHeader.IF_UNMODIFIED_SINCE, + HttpHeader.IF_NONE_MATCH, + HttpHeader.IF_RANGE, + HttpHeader.IF_MATCH, + HttpHeader.LOCATION, + HttpHeader.RANGE, + HttpHeader.RETRY_AFTER, + // HttpHeader.EXPIRES, + HttpHeader.LAST_MODIFIED, + HttpHeader.SET_COOKIE, + HttpHeader.SET_COOKIE2); + static final EnumSet NEVER_INDEX = EnumSet.of( + HttpHeader.AUTHORIZATION, + HttpHeader.SET_COOKIE, + HttpHeader.SET_COOKIE2); + private static final EnumSet IGNORED_HEADERS = EnumSet.of(HttpHeader.CONNECTION, HttpHeader.KEEP_ALIVE, + HttpHeader.PROXY_CONNECTION, HttpHeader.TRANSFER_ENCODING, HttpHeader.UPGRADE); + private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers"); + private static final PreEncodedHttpField C_SCHEME_HTTP = new PreEncodedHttpField(HttpHeader.C_SCHEME, "http"); + private static final PreEncodedHttpField C_SCHEME_HTTPS = new PreEncodedHttpField(HttpHeader.C_SCHEME, "https"); + private static final EnumMap C_METHODS = new EnumMap<>(HttpMethod.class); + + static { + for (HttpStatus.Code code : HttpStatus.Code.values()) { + STATUSES[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode())); + } + for (HttpMethod method : HttpMethod.values()) { + C_METHODS.put(method, new PreEncodedHttpField(HttpHeader.C_METHOD, method.getValue())); + } + } + + private final HpackContext context; + private final boolean debug; + private int remoteMaxDynamicTableSize; + private int localMaxDynamicTableSize; + private int maxHeaderListSize; + private int headerListSize; + private boolean validateEncoding = true; + + public HpackEncoder() { + this(4096, 4096, -1); + } + + public HpackEncoder(int localMaxDynamicTableSize) { + this(localMaxDynamicTableSize, 4096, -1); + } + + public HpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize) { + this(localMaxDynamicTableSize, remoteMaxDynamicTableSize, -1); + } + + public HpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize, int maxHeaderListSize) { + context = new HpackContext(remoteMaxDynamicTableSize); + this.remoteMaxDynamicTableSize = remoteMaxDynamicTableSize; + this.localMaxDynamicTableSize = localMaxDynamicTableSize; + this.maxHeaderListSize = maxHeaderListSize; + debug = LOG.isDebugEnabled(); + } + + public int getMaxHeaderListSize() { + return maxHeaderListSize; + } + + public void setMaxHeaderListSize(int maxHeaderListSize) { + this.maxHeaderListSize = maxHeaderListSize; + } + + public HpackContext getHpackContext() { + return context; + } + + public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize) { + this.remoteMaxDynamicTableSize = remoteMaxDynamicTableSize; + } + + public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize) { + this.localMaxDynamicTableSize = localMaxDynamicTableSize; + } + + public boolean isValidateEncoding() { + return validateEncoding; + } + + public void setValidateEncoding(boolean validateEncoding) { + this.validateEncoding = validateEncoding; + } + + public void encode(ByteBuffer buffer, MetaData metadata) throws HpackException { + try { + if (LOG.isDebugEnabled()) + LOG.debug(String.format("CtxTbl[%x] encoding", context.hashCode())); + + HttpFields fields = metadata.getFields(); + // Verify that we can encode without errors. + if (isValidateEncoding() && fields != null) { + for (HttpField field : fields) { + String name = field.getName(); + char firstChar = name.charAt(0); + if (firstChar <= ' ' || firstChar == ':') + throw new HpackException.StreamException("Invalid header name: '%s'", name); + } + } + + headerListSize = 0; + int pos = buffer.position(); + + // Check the dynamic table sizes! + int maxDynamicTableSize = Math.min(remoteMaxDynamicTableSize, localMaxDynamicTableSize); + if (maxDynamicTableSize != context.getMaxDynamicTableSize()) + encodeMaxDynamicTableSize(buffer, maxDynamicTableSize); + + // Add Request/response meta fields + if (!metadata.isOnlyTrailer()) { + if (metadata.isRequest()) { + MetaData.Request request = (MetaData.Request) metadata; + + String scheme = request.getURI().getScheme(); + encode(buffer, HttpScheme.HTTPS.is(scheme) ? C_SCHEME_HTTPS : C_SCHEME_HTTP); + String method = request.getMethod(); + HttpMethod httpMethod = method == null ? null : HttpMethod.from(method); + HttpField methodField = C_METHODS.get(httpMethod); + encode(buffer, methodField == null ? new HttpField(HttpHeader.C_METHOD, method) : methodField); + encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority())); + encode(buffer, new HttpField(HttpHeader.C_PATH, request.getURI().getPathQuery())); + } else if (metadata.isResponse()) { + MetaData.Response response = (MetaData.Response) metadata; + int code = response.getStatus(); + HttpField status = code < STATUSES.length ? STATUSES[code] : null; + if (status == null) + status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code); + encode(buffer, status); + } + } + + // Remove fields as specified in RFC 7540, 8.1.2.2. + if (fields != null) { + // For example: Connection: Close, TE, Upgrade, Custom. + Set hopHeaders = null; + for (String value : fields.getCSV(HttpHeader.CONNECTION, false)) { + if (hopHeaders == null) + hopHeaders = new HashSet<>(); + hopHeaders.add(StringUtils.asciiToLowerCase(value)); + } + for (HttpField field : fields) { + HttpHeader header = field.getHeader(); + if (header != null && IGNORED_HEADERS.contains(header)) + continue; + if (header == HttpHeader.TE) { + if (field.contains("trailers")) + encode(buffer, TE_TRAILERS); + continue; + } + String name = field.getLowerCaseName(); + if (hopHeaders != null && hopHeaders.contains(name)) + continue; + encode(buffer, field); + } + } + + // Check size + if (maxHeaderListSize > 0 && headerListSize > maxHeaderListSize) { + LOG.warn("Header list size too large {} > {}", headerListSize, maxHeaderListSize); + if (LOG.isDebugEnabled()) + LOG.debug("metadata={}", metadata); + } + + if (LOG.isDebugEnabled()) + LOG.debug(String.format("CtxTbl[%x] encoded %d octets", context.hashCode(), buffer.position() - pos)); + } catch (HpackException x) { + throw x; + } catch (Throwable x) { + HpackException.SessionException failure = new HpackException.SessionException("Could not hpack encode %s", metadata); + failure.initCause(x); + throw failure; + } + } + + public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize) { + if (maxDynamicTableSize > remoteMaxDynamicTableSize) + throw new IllegalArgumentException(); + buffer.put((byte) 0x20); + NBitInteger.encode(buffer, 5, maxDynamicTableSize); + context.resize(maxDynamicTableSize); + } + + public void encode(ByteBuffer buffer, HttpField field) { + if (field.getValue() == null) + field = new HttpField(field.getHeader(), field.getName(), ""); + + int fieldSize = field.getName().length() + field.getValue().length(); + headerListSize += fieldSize + 32; + + final int p = debug ? buffer.position() : -1; + + String encoding = null; + + // Is there an entry for the field? + Entry entry = context.get(field); + if (entry != null) { + // Known field entry, so encode it as indexed + if (entry.isStatic()) { + buffer.put(((StaticEntry) entry).getEncodedField()); + if (debug) + encoding = "IdxFieldS1"; + } else { + int index = context.index(entry); + buffer.put((byte) 0x80); + NBitInteger.encode(buffer, 7, index); + if (debug) + encoding = "IdxField" + (entry.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(7, index)); + } + } else { + // Unknown field entry, so we will have to send literally. + final boolean indexed; + + // But do we know it's name? + HttpHeader header = field.getHeader(); + + // Select encoding strategy + if (header == null) { + // Select encoding strategy for unknown header names + Entry name = context.get(field.getName()); + + if (field instanceof PreEncodedHttpField) { + int i = buffer.position(); + ((PreEncodedHttpField) field).putTo(buffer, HttpVersion.HTTP_2); + byte b = buffer.get(i); + indexed = b < 0 || b >= 0x40; + if (debug) + encoding = indexed ? "PreEncodedIdx" : "PreEncoded"; + } + // has the custom header name been seen before? + else if (name == null) { + // unknown name and value, so let's index this just in case it is + // the first time we have seen a custom name or a custom field. + // unless the name is changing, this is worthwhile + indexed = true; + encodeName(buffer, (byte) 0x40, 6, field.getName(), null); + encodeValue(buffer, true, field.getValue()); + if (debug) + encoding = "LitHuffNHuffVIdx"; + } else { + // known custom name, but unknown value. + // This is probably a custom field with changing value, so don't index. + indexed = false; + encodeName(buffer, (byte) 0x00, 4, field.getName(), null); + encodeValue(buffer, true, field.getValue()); + if (debug) + encoding = "LitHuffNHuffV!Idx"; + } + } else { + // Select encoding strategy for known header names + Entry name = context.get(header); + + if (field instanceof PreEncodedHttpField) { + // Preencoded field + int i = buffer.position(); + ((PreEncodedHttpField) field).putTo(buffer, HttpVersion.HTTP_2); + byte b = buffer.get(i); + indexed = b < 0 || b >= 0x40; + if (debug) + encoding = indexed ? "PreEncodedIdx" : "PreEncoded"; + } else if (DO_NOT_INDEX.contains(header)) { + // Non indexed field + indexed = false; + boolean neverIndex = NEVER_INDEX.contains(header); + boolean huffman = !DO_NOT_HUFFMAN.contains(header); + encodeName(buffer, neverIndex ? (byte) 0x10 : (byte) 0x00, 4, header.getValue(), name); + encodeValue(buffer, huffman, field.getValue()); + + if (debug) + encoding = "Lit" + + ((name == null) ? "HuffN" : ("IdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(4, context.index(name))))) + + (huffman ? "HuffV" : "LitV") + + (neverIndex ? "!!Idx" : "!Idx"); + } else if (fieldSize >= context.getMaxDynamicTableSize() || header == HttpHeader.CONTENT_LENGTH && field.getValue().length() > 2) { + // Non indexed if field too large or a content length for 3 digits or more + indexed = false; + encodeName(buffer, (byte) 0x00, 4, header.getValue(), name); + encodeValue(buffer, true, field.getValue()); + if (debug) + encoding = "LitIdxNS" + (1 + NBitInteger.octectsNeeded(4, context.index(name))) + "HuffV!Idx"; + } else { + // indexed + indexed = true; + boolean huffman = !DO_NOT_HUFFMAN.contains(header); + encodeName(buffer, (byte) 0x40, 6, header.getValue(), name); + encodeValue(buffer, huffman, field.getValue()); + if (debug) + encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(6, context.index(name))))) + + (huffman ? "HuffVIdx" : "LitVIdx"); + } + } + + // If we want the field referenced, then we add it to our table and reference set. + if (indexed) + context.add(field); + } + + if (debug) { + int e = buffer.position(); + if (LOG.isDebugEnabled()) + LOG.debug("encode {}:'{}' to '{}'", encoding, field, TypeUtils.toHexString(buffer.array(), buffer.arrayOffset() + p, e - p)); + } + } + + private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry) { + buffer.put(mask); + if (entry == null) { + // leave name index bits as 0 + // Encode the name always with lowercase huffman + buffer.put((byte) 0x80); + NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name)); + Huffman.encodeLC(buffer, name); + } else { + NBitInteger.encode(buffer, bits, context.index(entry)); + } + } + + static void encodeValue(ByteBuffer buffer, boolean huffman, String value) { + if (huffman) { + // huffman literal value + buffer.put((byte) 0x80); + + int needed = Huffman.octetsNeeded(value); + if (needed >= 0) { + NBitInteger.encode(buffer, 7, needed); + Huffman.encode(buffer, value); + } else { + // Not iso_8859_1 + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(bytes)); + Huffman.encode(buffer, bytes); + } + } else { + // add literal assuming iso_8859_1 + buffer.put((byte) 0x00).mark(); + NBitInteger.encode(buffer, 7, value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c < ' ' || c > 127) { + // Not iso_8859_1, so re-encode as UTF-8 + buffer.reset(); + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + NBitInteger.encode(buffer, 7, bytes.length); + buffer.put(bytes, 0, bytes.length); + return; + } + buffer.put((byte) c); + } + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackException.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackException.java new file mode 100644 index 000000000..1504fc8bf --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackException.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.http.common.v2.hpack; + +public abstract class HpackException extends RuntimeException { + HpackException(String messageFormat, Object... args) { + super(String.format(messageFormat, args)); + } + + /** + * A Stream HPACK exception. + *

Stream exceptions are not fatal to the connection, and the + * hpack state is complete and able to continue handling other + * decoding/encoding for the session. + *

+ */ + public static class StreamException extends HpackException { + StreamException(String messageFormat, Object... args) { + super(messageFormat, args); + } + } + + /** + * A Session HPACK Exception. + *

Session exceptions are fatal for the stream, and the HPACK + * state is unable to decode/encode further.

+ */ + public static class SessionException extends HpackException { + SessionException(String messageFormat, Object... args) { + super(messageFormat, args); + } + } + + public static class CompressionException extends SessionException { + public CompressionException(String messageFormat, Object... args) { + super(messageFormat, args); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackFieldPreEncoder.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackFieldPreEncoder.java new file mode 100644 index 000000000..4ac83305a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/HpackFieldPreEncoder.java @@ -0,0 +1,57 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.codec.HttpFieldPreEncoder; +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpVersion; + +import java.nio.ByteBuffer; + +public class HpackFieldPreEncoder implements HttpFieldPreEncoder { + + @Override + public HttpVersion getHttpVersion() { + return HttpVersion.HTTP_2; + } + + @Override + public byte[] getEncodedField(HttpHeader header, String name, String value) { + boolean not_indexed = HpackEncoder.DO_NOT_INDEX.contains(header); + + ByteBuffer buffer = ByteBuffer.allocate(name.length() + value.length() + 10); + boolean huffman; + int bits; + + if (not_indexed) { + // Non indexed field + boolean never_index = HpackEncoder.NEVER_INDEX.contains(header); + huffman = !HpackEncoder.DO_NOT_HUFFMAN.contains(header); + buffer.put(never_index ? (byte) 0x10 : (byte) 0x00); + bits = 4; + } else if (header == HttpHeader.CONTENT_LENGTH && value.length() > 1) { + // Non indexed content length for 2 digits or more + buffer.put((byte) 0x00); + huffman = true; + bits = 4; + } else { + // indexed + buffer.put((byte) 0x40); + huffman = !HpackEncoder.DO_NOT_HUFFMAN.contains(header); + bits = 6; + } + + int name_idx = HpackContext.staticIndex(header); + if (name_idx > 0) + NBitInteger.encode(buffer, bits, name_idx); + else { + buffer.put((byte) 0x80); + NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name)); + Huffman.encodeLC(buffer, name); + } + + HpackEncoder.encodeValue(buffer, huffman, value); + + buffer.flip(); + return BufferUtils.toArray(buffer); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/Huffman.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/Huffman.java new file mode 100644 index 000000000..473e6fdb6 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/Huffman.java @@ -0,0 +1,497 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.common.string.Utf8StringBuilder; + +import java.nio.ByteBuffer; + +public class Huffman { + + // Appendix C: Huffman Codes + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C + static final int[][] CODES = { + /* ( 0) |11111111|11000 */ {0x1ff8, 13}, + /* ( 1) |11111111|11111111|1011000 */ {0x7fffd8, 23}, + /* ( 2) |11111111|11111111|11111110|0010 */ {0xfffffe2, 28}, + /* ( 3) |11111111|11111111|11111110|0011 */ {0xfffffe3, 28}, + /* ( 4) |11111111|11111111|11111110|0100 */ {0xfffffe4, 28}, + /* ( 5) |11111111|11111111|11111110|0101 */ {0xfffffe5, 28}, + /* ( 6) |11111111|11111111|11111110|0110 */ {0xfffffe6, 28}, + /* ( 7) |11111111|11111111|11111110|0111 */ {0xfffffe7, 28}, + /* ( 8) |11111111|11111111|11111110|1000 */ {0xfffffe8, 28}, + /* ( 9) |11111111|11111111|11101010 */ {0xffffea, 24}, + /* ( 10) |11111111|11111111|11111111|111100 */ {0x3ffffffc, 30}, + /* ( 11) |11111111|11111111|11111110|1001 */ {0xfffffe9, 28}, + /* ( 12) |11111111|11111111|11111110|1010 */ {0xfffffea, 28}, + /* ( 13) |11111111|11111111|11111111|111101 */ {0x3ffffffd, 30}, + /* ( 14) |11111111|11111111|11111110|1011 */ {0xfffffeb, 28}, + /* ( 15) |11111111|11111111|11111110|1100 */ {0xfffffec, 28}, + /* ( 16) |11111111|11111111|11111110|1101 */ {0xfffffed, 28}, + /* ( 17) |11111111|11111111|11111110|1110 */ {0xfffffee, 28}, + /* ( 18) |11111111|11111111|11111110|1111 */ {0xfffffef, 28}, + /* ( 19) |11111111|11111111|11111111|0000 */ {0xffffff0, 28}, + /* ( 20) |11111111|11111111|11111111|0001 */ {0xffffff1, 28}, + /* ( 21) |11111111|11111111|11111111|0010 */ {0xffffff2, 28}, + /* ( 22) |11111111|11111111|11111111|111110 */ {0x3ffffffe, 30}, + /* ( 23) |11111111|11111111|11111111|0011 */ {0xffffff3, 28}, + /* ( 24) |11111111|11111111|11111111|0100 */ {0xffffff4, 28}, + /* ( 25) |11111111|11111111|11111111|0101 */ {0xffffff5, 28}, + /* ( 26) |11111111|11111111|11111111|0110 */ {0xffffff6, 28}, + /* ( 27) |11111111|11111111|11111111|0111 */ {0xffffff7, 28}, + /* ( 28) |11111111|11111111|11111111|1000 */ {0xffffff8, 28}, + /* ( 29) |11111111|11111111|11111111|1001 */ {0xffffff9, 28}, + /* ( 30) |11111111|11111111|11111111|1010 */ {0xffffffa, 28}, + /* ( 31) |11111111|11111111|11111111|1011 */ {0xffffffb, 28}, + /*' ' ( 32) |010100 */ {0x14, 6}, + /*'!' ( 33) |11111110|00 */ {0x3f8, 10}, + /*'"' ( 34) |11111110|01 */ {0x3f9, 10}, + /*'#' ( 35) |11111111|1010 */ {0xffa, 12}, + /*'$' ( 36) |11111111|11001 */ {0x1ff9, 13}, + /*'%' ( 37) |010101 */ {0x15, 6}, + /*'&' ( 38) |11111000 */ {0xf8, 8}, + /*''' ( 39) |11111111|010 */ {0x7fa, 11}, + /*'(' ( 40) |11111110|10 */ {0x3fa, 10}, + /*')' ( 41) |11111110|11 */ {0x3fb, 10}, + /*'*' ( 42) |11111001 */ {0xf9, 8}, + /*'+' ( 43) |11111111|011 */ {0x7fb, 11}, + /*',' ( 44) |11111010 */ {0xfa, 8}, + /*'-' ( 45) |010110 */ {0x16, 6}, + /*'.' ( 46) |010111 */ {0x17, 6}, + /*'/' ( 47) |011000 */ {0x18, 6}, + /*'0' ( 48) |00000 */ {0x0, 5}, + /*'1' ( 49) |00001 */ {0x1, 5}, + /*'2' ( 50) |00010 */ {0x2, 5}, + /*'3' ( 51) |011001 */ {0x19, 6}, + /*'4' ( 52) |011010 */ {0x1a, 6}, + /*'5' ( 53) |011011 */ {0x1b, 6}, + /*'6' ( 54) |011100 */ {0x1c, 6}, + /*'7' ( 55) |011101 */ {0x1d, 6}, + /*'8' ( 56) |011110 */ {0x1e, 6}, + /*'9' ( 57) |011111 */ {0x1f, 6}, + /*':' ( 58) |1011100 */ {0x5c, 7}, + /*';' ( 59) |11111011 */ {0xfb, 8}, + /*'<' ( 60) |11111111|1111100 */ {0x7ffc, 15}, + /*'=' ( 61) |100000 */ {0x20, 6}, + /*'>' ( 62) |11111111|1011 */ {0xffb, 12}, + /*'?' ( 63) |11111111|00 */ {0x3fc, 10}, + /*'@' ( 64) |11111111|11010 */ {0x1ffa, 13}, + /*'A' ( 65) |100001 */ {0x21, 6}, + /*'B' ( 66) |1011101 */ {0x5d, 7}, + /*'C' ( 67) |1011110 */ {0x5e, 7}, + /*'D' ( 68) |1011111 */ {0x5f, 7}, + /*'E' ( 69) |1100000 */ {0x60, 7}, + /*'F' ( 70) |1100001 */ {0x61, 7}, + /*'G' ( 71) |1100010 */ {0x62, 7}, + /*'H' ( 72) |1100011 */ {0x63, 7}, + /*'I' ( 73) |1100100 */ {0x64, 7}, + /*'J' ( 74) |1100101 */ {0x65, 7}, + /*'K' ( 75) |1100110 */ {0x66, 7}, + /*'L' ( 76) |1100111 */ {0x67, 7}, + /*'M' ( 77) |1101000 */ {0x68, 7}, + /*'N' ( 78) |1101001 */ {0x69, 7}, + /*'O' ( 79) |1101010 */ {0x6a, 7}, + /*'P' ( 80) |1101011 */ {0x6b, 7}, + /*'Q' ( 81) |1101100 */ {0x6c, 7}, + /*'R' ( 82) |1101101 */ {0x6d, 7}, + /*'S' ( 83) |1101110 */ {0x6e, 7}, + /*'T' ( 84) |1101111 */ {0x6f, 7}, + /*'U' ( 85) |1110000 */ {0x70, 7}, + /*'V' ( 86) |1110001 */ {0x71, 7}, + /*'W' ( 87) |1110010 */ {0x72, 7}, + /*'X' ( 88) |11111100 */ {0xfc, 8}, + /*'Y' ( 89) |1110011 */ {0x73, 7}, + /*'Z' ( 90) |11111101 */ {0xfd, 8}, + /*'[' ( 91) |11111111|11011 */ {0x1ffb, 13}, + /*'\' ( 92) |11111111|11111110|000 */ {0x7fff0, 19}, + /*']' ( 93) |11111111|11100 */ {0x1ffc, 13}, + /*'^' ( 94) |11111111|111100 */ {0x3ffc, 14}, + /*'_' ( 95) |100010 */ {0x22, 6}, + /*'`' ( 96) |11111111|1111101 */ {0x7ffd, 15}, + /*'a' ( 97) |00011 */ {0x3, 5}, + /*'b' ( 98) |100011 */ {0x23, 6}, + /*'c' ( 99) |00100 */ {0x4, 5}, + /*'d' (100) |100100 */ {0x24, 6}, + /*'e' (101) |00101 */ {0x5, 5}, + /*'f' (102) |100101 */ {0x25, 6}, + /*'g' (103) |100110 */ {0x26, 6}, + /*'h' (104) |100111 */ {0x27, 6}, + /*'i' (105) |00110 */ {0x6, 5}, + /*'j' (106) |1110100 */ {0x74, 7}, + /*'k' (107) |1110101 */ {0x75, 7}, + /*'l' (108) |101000 */ {0x28, 6}, + /*'m' (109) |101001 */ {0x29, 6}, + /*'n' (110) |101010 */ {0x2a, 6}, + /*'o' (111) |00111 */ {0x7, 5}, + /*'p' (112) |101011 */ {0x2b, 6}, + /*'q' (113) |1110110 */ {0x76, 7}, + /*'r' (114) |101100 */ {0x2c, 6}, + /*'s' (115) |01000 */ {0x8, 5}, + /*'t' (116) |01001 */ {0x9, 5}, + /*'u' (117) |101101 */ {0x2d, 6}, + /*'v' (118) |1110111 */ {0x77, 7}, + /*'w' (119) |1111000 */ {0x78, 7}, + /*'x' (120) |1111001 */ {0x79, 7}, + /*'y' (121) |1111010 */ {0x7a, 7}, + /*'z' (122) |1111011 */ {0x7b, 7}, + /*'{' (123) |11111111|1111110 */ {0x7ffe, 15}, + /*'|' (124) |11111111|100 */ {0x7fc, 11}, + /*'}' (125) |11111111|111101 */ {0x3ffd, 14}, + /*'~' (126) |11111111|11101 */ {0x1ffd, 13}, + /* (127) |11111111|11111111|11111111|1100 */ {0xffffffc, 28}, + /* (128) |11111111|11111110|0110 */ {0xfffe6, 20}, + /* (129) |11111111|11111111|010010 */ {0x3fffd2, 22}, + /* (130) |11111111|11111110|0111 */ {0xfffe7, 20}, + /* (131) |11111111|11111110|1000 */ {0xfffe8, 20}, + /* (132) |11111111|11111111|010011 */ {0x3fffd3, 22}, + /* (133) |11111111|11111111|010100 */ {0x3fffd4, 22}, + /* (134) |11111111|11111111|010101 */ {0x3fffd5, 22}, + /* (135) |11111111|11111111|1011001 */ {0x7fffd9, 23}, + /* (136) |11111111|11111111|010110 */ {0x3fffd6, 22}, + /* (137) |11111111|11111111|1011010 */ {0x7fffda, 23}, + /* (138) |11111111|11111111|1011011 */ {0x7fffdb, 23}, + /* (139) |11111111|11111111|1011100 */ {0x7fffdc, 23}, + /* (140) |11111111|11111111|1011101 */ {0x7fffdd, 23}, + /* (141) |11111111|11111111|1011110 */ {0x7fffde, 23}, + /* (142) |11111111|11111111|11101011 */ {0xffffeb, 24}, + /* (143) |11111111|11111111|1011111 */ {0x7fffdf, 23}, + /* (144) |11111111|11111111|11101100 */ {0xffffec, 24}, + /* (145) |11111111|11111111|11101101 */ {0xffffed, 24}, + /* (146) |11111111|11111111|010111 */ {0x3fffd7, 22}, + /* (147) |11111111|11111111|1100000 */ {0x7fffe0, 23}, + /* (148) |11111111|11111111|11101110 */ {0xffffee, 24}, + /* (149) |11111111|11111111|1100001 */ {0x7fffe1, 23}, + /* (150) |11111111|11111111|1100010 */ {0x7fffe2, 23}, + /* (151) |11111111|11111111|1100011 */ {0x7fffe3, 23}, + /* (152) |11111111|11111111|1100100 */ {0x7fffe4, 23}, + /* (153) |11111111|11111110|11100 */ {0x1fffdc, 21}, + /* (154) |11111111|11111111|011000 */ {0x3fffd8, 22}, + /* (155) |11111111|11111111|1100101 */ {0x7fffe5, 23}, + /* (156) |11111111|11111111|011001 */ {0x3fffd9, 22}, + /* (157) |11111111|11111111|1100110 */ {0x7fffe6, 23}, + /* (158) |11111111|11111111|1100111 */ {0x7fffe7, 23}, + /* (159) |11111111|11111111|11101111 */ {0xffffef, 24}, + /* (160) |11111111|11111111|011010 */ {0x3fffda, 22}, + /* (161) |11111111|11111110|11101 */ {0x1fffdd, 21}, + /* (162) |11111111|11111110|1001 */ {0xfffe9, 20}, + /* (163) |11111111|11111111|011011 */ {0x3fffdb, 22}, + /* (164) |11111111|11111111|011100 */ {0x3fffdc, 22}, + /* (165) |11111111|11111111|1101000 */ {0x7fffe8, 23}, + /* (166) |11111111|11111111|1101001 */ {0x7fffe9, 23}, + /* (167) |11111111|11111110|11110 */ {0x1fffde, 21}, + /* (168) |11111111|11111111|1101010 */ {0x7fffea, 23}, + /* (169) |11111111|11111111|011101 */ {0x3fffdd, 22}, + /* (170) |11111111|11111111|011110 */ {0x3fffde, 22}, + /* (171) |11111111|11111111|11110000 */ {0xfffff0, 24}, + /* (172) |11111111|11111110|11111 */ {0x1fffdf, 21}, + /* (173) |11111111|11111111|011111 */ {0x3fffdf, 22}, + /* (174) |11111111|11111111|1101011 */ {0x7fffeb, 23}, + /* (175) |11111111|11111111|1101100 */ {0x7fffec, 23}, + /* (176) |11111111|11111111|00000 */ {0x1fffe0, 21}, + /* (177) |11111111|11111111|00001 */ {0x1fffe1, 21}, + /* (178) |11111111|11111111|100000 */ {0x3fffe0, 22}, + /* (179) |11111111|11111111|00010 */ {0x1fffe2, 21}, + /* (180) |11111111|11111111|1101101 */ {0x7fffed, 23}, + /* (181) |11111111|11111111|100001 */ {0x3fffe1, 22}, + /* (182) |11111111|11111111|1101110 */ {0x7fffee, 23}, + /* (183) |11111111|11111111|1101111 */ {0x7fffef, 23}, + /* (184) |11111111|11111110|1010 */ {0xfffea, 20}, + /* (185) |11111111|11111111|100010 */ {0x3fffe2, 22}, + /* (186) |11111111|11111111|100011 */ {0x3fffe3, 22}, + /* (187) |11111111|11111111|100100 */ {0x3fffe4, 22}, + /* (188) |11111111|11111111|1110000 */ {0x7ffff0, 23}, + /* (189) |11111111|11111111|100101 */ {0x3fffe5, 22}, + /* (190) |11111111|11111111|100110 */ {0x3fffe6, 22}, + /* (191) |11111111|11111111|1110001 */ {0x7ffff1, 23}, + /* (192) |11111111|11111111|11111000|00 */ {0x3ffffe0, 26}, + /* (193) |11111111|11111111|11111000|01 */ {0x3ffffe1, 26}, + /* (194) |11111111|11111110|1011 */ {0xfffeb, 20}, + /* (195) |11111111|11111110|001 */ {0x7fff1, 19}, + /* (196) |11111111|11111111|100111 */ {0x3fffe7, 22}, + /* (197) |11111111|11111111|1110010 */ {0x7ffff2, 23}, + /* (198) |11111111|11111111|101000 */ {0x3fffe8, 22}, + /* (199) |11111111|11111111|11110110|0 */ {0x1ffffec, 25}, + /* (200) |11111111|11111111|11111000|10 */ {0x3ffffe2, 26}, + /* (201) |11111111|11111111|11111000|11 */ {0x3ffffe3, 26}, + /* (202) |11111111|11111111|11111001|00 */ {0x3ffffe4, 26}, + /* (203) |11111111|11111111|11111011|110 */ {0x7ffffde, 27}, + /* (204) |11111111|11111111|11111011|111 */ {0x7ffffdf, 27}, + /* (205) |11111111|11111111|11111001|01 */ {0x3ffffe5, 26}, + /* (206) |11111111|11111111|11110001 */ {0xfffff1, 24}, + /* (207) |11111111|11111111|11110110|1 */ {0x1ffffed, 25}, + /* (208) |11111111|11111110|010 */ {0x7fff2, 19}, + /* (209) |11111111|11111111|00011 */ {0x1fffe3, 21}, + /* (210) |11111111|11111111|11111001|10 */ {0x3ffffe6, 26}, + /* (211) |11111111|11111111|11111100|000 */ {0x7ffffe0, 27}, + /* (212) |11111111|11111111|11111100|001 */ {0x7ffffe1, 27}, + /* (213) |11111111|11111111|11111001|11 */ {0x3ffffe7, 26}, + /* (214) |11111111|11111111|11111100|010 */ {0x7ffffe2, 27}, + /* (215) |11111111|11111111|11110010 */ {0xfffff2, 24}, + /* (216) |11111111|11111111|00100 */ {0x1fffe4, 21}, + /* (217) |11111111|11111111|00101 */ {0x1fffe5, 21}, + /* (218) |11111111|11111111|11111010|00 */ {0x3ffffe8, 26}, + /* (219) |11111111|11111111|11111010|01 */ {0x3ffffe9, 26}, + /* (220) |11111111|11111111|11111111|1101 */ {0xffffffd, 28}, + /* (221) |11111111|11111111|11111100|011 */ {0x7ffffe3, 27}, + /* (222) |11111111|11111111|11111100|100 */ {0x7ffffe4, 27}, + /* (223) |11111111|11111111|11111100|101 */ {0x7ffffe5, 27}, + /* (224) |11111111|11111110|1100 */ {0xfffec, 20}, + /* (225) |11111111|11111111|11110011 */ {0xfffff3, 24}, + /* (226) |11111111|11111110|1101 */ {0xfffed, 20}, + /* (227) |11111111|11111111|00110 */ {0x1fffe6, 21}, + /* (228) |11111111|11111111|101001 */ {0x3fffe9, 22}, + /* (229) |11111111|11111111|00111 */ {0x1fffe7, 21}, + /* (230) |11111111|11111111|01000 */ {0x1fffe8, 21}, + /* (231) |11111111|11111111|1110011 */ {0x7ffff3, 23}, + /* (232) |11111111|11111111|101010 */ {0x3fffea, 22}, + /* (233) |11111111|11111111|101011 */ {0x3fffeb, 22}, + /* (234) |11111111|11111111|11110111|0 */ {0x1ffffee, 25}, + /* (235) |11111111|11111111|11110111|1 */ {0x1ffffef, 25}, + /* (236) |11111111|11111111|11110100 */ {0xfffff4, 24}, + /* (237) |11111111|11111111|11110101 */ {0xfffff5, 24}, + /* (238) |11111111|11111111|11111010|10 */ {0x3ffffea, 26}, + /* (239) |11111111|11111111|1110100 */ {0x7ffff4, 23}, + /* (240) |11111111|11111111|11111010|11 */ {0x3ffffeb, 26}, + /* (241) |11111111|11111111|11111100|110 */ {0x7ffffe6, 27}, + /* (242) |11111111|11111111|11111011|00 */ {0x3ffffec, 26}, + /* (243) |11111111|11111111|11111011|01 */ {0x3ffffed, 26}, + /* (244) |11111111|11111111|11111100|111 */ {0x7ffffe7, 27}, + /* (245) |11111111|11111111|11111101|000 */ {0x7ffffe8, 27}, + /* (246) |11111111|11111111|11111101|001 */ {0x7ffffe9, 27}, + /* (247) |11111111|11111111|11111101|010 */ {0x7ffffea, 27}, + /* (248) |11111111|11111111|11111101|011 */ {0x7ffffeb, 27}, + /* (249) |11111111|11111111|11111111|1110 */ {0xffffffe, 28}, + /* (250) |11111111|11111111|11111101|100 */ {0x7ffffec, 27}, + /* (251) |11111111|11111111|11111101|101 */ {0x7ffffed, 27}, + /* (252) |11111111|11111111|11111101|110 */ {0x7ffffee, 27}, + /* (253) |11111111|11111111|11111101|111 */ {0x7ffffef, 27}, + /* (254) |11111111|11111111|11111110|000 */ {0x7fffff0, 27}, + /* (255) |11111111|11111111|11111011|10 */ {0x3ffffee, 26}, + /*EOS (256) |11111111|11111111|11111111|111111 */ {0x3fffffff, 30} + }; + + static final int[][] LCCODES = new int[CODES.length][]; + static final char EOS = 256; + + // Huffman decode tree stored in a flattened char array for good + // locality of reference. + static final char[] tree; + static final char[] rowsym; + static final byte[] rowbits; + + // Build the Huffman lookup tree and LC TABLE + static { + System.arraycopy(CODES, 0, LCCODES, 0, CODES.length); + for (int i = 'A'; i <= 'Z'; i++) { + LCCODES[i] = LCCODES['a' + i - 'A']; + } + + int r = 0; + for (int i = 0; i < CODES.length; i++) { + r += (CODES[i][1] + 7) / 8; + } + tree = new char[r * 256]; + rowsym = new char[r]; + rowbits = new byte[r]; + + r = 0; + for (int sym = 0; sym < CODES.length; sym++) { + int code = CODES[sym][0]; + int len = CODES[sym][1]; + + int current = 0; + + while (len > 8) { + len -= 8; + int i = ((code >>> len) & 0xFF); + + int t = current * 256 + i; + current = tree[t]; + if (current == 0) { + tree[t] = (char) ++r; + current = r; + } + } + + int terminal = ++r; + rowsym[r] = (char) sym; + int b = len & 0x07; + int terminalBits = b == 0 ? 8 : b; + + rowbits[r] = (byte) terminalBits; + int shift = 8 - len; + int start = current * 256 + ((code << shift) & 0xFF); + int end = start + (1 << shift); + for (int i = start; i < end; i++) { + tree[i] = (char) terminal; + } + } + } + + public static String decode(ByteBuffer buffer) throws HpackException.CompressionException { + return decode(buffer, buffer.remaining()); + } + + public static String decode(ByteBuffer buffer, int length) throws HpackException.CompressionException { + Utf8StringBuilder utf8 = new Utf8StringBuilder(length * 2); + int node = 0; + int current = 0; + int bits = 0; + + for (int i = 0; i < length; i++) { + int b = buffer.get() & 0xFF; + current = (current << 8) | b; + bits += 8; + while (bits >= 8) { + int c = (current >>> (bits - 8)) & 0xFF; + node = tree[node * 256 + c]; + if (rowbits[node] != 0) { + if (rowsym[node] == EOS) + throw new HpackException.CompressionException("EOS in content"); + + // terminal node + utf8.append((byte) (0xFF & rowsym[node])); + bits -= rowbits[node]; + node = 0; + } else { + // non-terminal node + bits -= 8; + } + } + } + + while (bits > 0) { + int c = (current << (8 - bits)) & 0xFF; + int lastNode = node; + node = tree[node * 256 + c]; + + if (rowbits[node] == 0 || rowbits[node] > bits) { + int requiredPadding = 0; + for (int i = 0; i < bits; i++) { + requiredPadding = (requiredPadding << 1) | 1; + } + + if ((c >> (8 - bits)) != requiredPadding) + throw new HpackException.CompressionException("Incorrect padding"); + + node = lastNode; + break; + } + + utf8.append((byte) (0xFF & rowsym[node])); + bits -= rowbits[node]; + node = 0; + } + + if (node != 0) + throw new HpackException.CompressionException("Bad termination"); + + return utf8.toString(); + } + + public static int octetsNeeded(String s) { + return octetsNeeded(CODES, s); + } + + public static int octetsNeeded(byte[] b) { + return octetsNeeded(CODES, b); + } + + public static void encode(ByteBuffer buffer, String s) { + encode(CODES, buffer, s); + } + + public static void encode(ByteBuffer buffer, byte[] b) { + encode(CODES, buffer, b); + } + + public static int octetsNeededLC(String s) { + return octetsNeeded(LCCODES, s); + } + + public static void encodeLC(ByteBuffer buffer, String s) { + encode(LCCODES, buffer, s); + } + + private static int octetsNeeded(final int[][] table, String s) { + int needed = 0; + int len = s.length(); + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (c >= 128 || c < ' ') + return -1; + needed += table[c][1]; + } + + return (needed + 7) / 8; + } + + private static int octetsNeeded(final int[][] table, byte[] b) { + int needed = 0; + int len = b.length; + for (int i = 0; i < len; i++) { + int c = 0xFF & b[i]; + needed += table[c][1]; + } + return (needed + 7) / 8; + } + + /** + * @param table The table to encode by + * @param buffer The buffer to encode to + * @param s The string to encode + */ + private static void encode(final int[][] table, ByteBuffer buffer, String s) { + long current = 0; + int n = 0; + int len = s.length(); + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (c >= 128 || c < ' ') + throw new IllegalArgumentException(); + int code = table[c][0]; + int bits = table[c][1]; + + current <<= bits; + current |= code; + n += bits; + + while (n >= 8) { + n -= 8; + buffer.put((byte) (current >> n)); + } + } + + if (n > 0) { + current <<= (8 - n); + current |= (0xFF >>> n); + buffer.put((byte) (current)); + } + } + + private static void encode(final int[][] table, ByteBuffer buffer, byte[] b) { + long current = 0; + int n = 0; + + int len = b.length; + for (int i = 0; i < len; i++) { + int c = 0xFF & b[i]; + int code = table[c][0]; + int bits = table[c][1]; + + current <<= bits; + current |= code; + n += bits; + + while (n >= 8) { + n -= 8; + buffer.put((byte) (current >> n)); + } + } + + if (n > 0) { + current <<= (8 - n); + current |= (0xFF >>> n); + buffer.put((byte) (current)); + } + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/MetaDataBuilder.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/MetaDataBuilder.java new file mode 100644 index 000000000..563c2d590 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/MetaDataBuilder.java @@ -0,0 +1,238 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.net.http.common.model.*; + +public class MetaDataBuilder { + private final int maxSize; + private int size; + private Integer status; + private String method; + private HttpScheme scheme; + private HostPortHttpField authority; + private String path; + private long contentLength = Long.MIN_VALUE; + private HttpFields fields = new HttpFields(); + private HpackException.StreamException streamException; + private boolean request; + private boolean response; + + /** + * @param maxHeadersSize The maximum size of the headers, expressed as total name and value characters. + */ + protected MetaDataBuilder(int maxHeadersSize) { + maxSize = maxHeadersSize; + } + + /** + * Get the maxSize. + * + * @return the maxSize + */ + public int getMaxSize() { + return maxSize; + } + + /** + * Get the size. + * + * @return the current size in bytes + */ + public int getSize() { + return size; + } + + public void emit(HttpField field) throws HpackException.SessionException { + HttpHeader header = field.getHeader(); + String name = field.getName(); + if (name == null || name.length() == 0) + throw new HpackException.SessionException("Header size 0"); + String value = field.getValue(); + int fieldSize = name.length() + (value == null ? 0 : value.length()); + size += fieldSize + 32; + if (size > maxSize) + throw new HpackException.SessionException("Header size %d > %d", size, maxSize); + + if (field instanceof StaticTableHttpField) { + StaticTableHttpField staticField = (StaticTableHttpField) field; + switch (header) { + case C_STATUS: + if (checkPseudoHeader(header, status)) + status = (Integer) staticField.getStaticValue(); + response = true; + break; + + case C_METHOD: + if (checkPseudoHeader(header, method)) + method = value; + request = true; + break; + + case C_SCHEME: + if (checkPseudoHeader(header, scheme)) + scheme = (HttpScheme) staticField.getStaticValue(); + request = true; + break; + + default: + throw new IllegalArgumentException(name); + } + } else if (header != null) { + switch (header) { + case C_STATUS: + if (checkPseudoHeader(header, status)) + status = field.getIntValue(); + response = true; + break; + + case C_METHOD: + if (checkPseudoHeader(header, method)) + method = value; + request = true; + break; + + case C_SCHEME: + if (checkPseudoHeader(header, scheme) && value != null) + scheme = HttpScheme.from(value); + request = true; + break; + + case C_AUTHORITY: + if (checkPseudoHeader(header, authority)) { + if (field instanceof HostPortHttpField) + authority = (HostPortHttpField) field; + else if (value != null) + authority = new AuthorityHttpField(value); + } + request = true; + break; + + case HOST: + // :authority fields must come first. If we have one, ignore the host header as far as authority goes. + if (authority == null) { + if (field instanceof HostPortHttpField) + authority = (HostPortHttpField) field; + else if (value != null) + authority = new AuthorityHttpField(value); + } + fields.add(field); + break; + + case C_PATH: + if (checkPseudoHeader(header, path)) { + if (value != null && value.length() > 0) + path = value; + else + streamException("No Path"); + } + request = true; + break; + + case CONTENT_LENGTH: + contentLength = field.getLongValue(); + fields.add(field); + break; + + case TE: + if ("trailers".equalsIgnoreCase(value)) + fields.add(field); + else + streamException("Unsupported TE value '%s'", value); + break; + + case CONNECTION: + if ("TE".equalsIgnoreCase(value)) + fields.add(field); + else + streamException("Connection specific field '%s'", header); + break; + + default: + if (name.charAt(0) == ':') + streamException("Unknown pseudo header '%s'", name); + else + fields.add(field); + break; + } + } else { + if (name.charAt(0) == ':') + streamException("Unknown pseudo header '%s'", name); + else + fields.add(field); + } + } + + protected void streamException(String messageFormat, Object... args) { + HpackException.StreamException stream = new HpackException.StreamException(messageFormat, args); + if (streamException == null) + streamException = stream; + else + streamException.addSuppressed(stream); + } + + protected boolean checkPseudoHeader(HttpHeader header, Object value) { + if (fields.size() > 0) { + streamException("Pseudo header %s after fields", header.getValue()); + return false; + } + if (value == null) + return true; + streamException("Duplicate pseudo header %s", header.getValue()); + return false; + } + + public MetaData build() throws HpackException.StreamException { + if (streamException != null) { + streamException.addSuppressed(new Throwable()); + throw streamException; + } + + if (request && response) + throw new HpackException.StreamException("Request and Response headers"); + + HttpFields fields = this.fields; + try { + if (request) { + if (method == null) + throw new HpackException.StreamException("No Method"); + if (scheme == null) + throw new HpackException.StreamException("No Scheme"); + if (path == null) + throw new HpackException.StreamException("No Path"); + return new MetaData.Request(method, scheme, authority, path, HttpVersion.HTTP_2, fields, contentLength); + } + if (response) { + if (status == null) + throw new HpackException.StreamException("No Status"); + return new MetaData.Response(HttpVersion.HTTP_2, status, fields, contentLength); + } + + return new MetaData(HttpVersion.HTTP_2, fields, contentLength); + } finally { + this.fields = new HttpFields(Math.max(16, fields.size() + 5)); + request = false; + response = false; + status = null; + method = null; + scheme = null; + authority = null; + path = null; + size = 0; + contentLength = Long.MIN_VALUE; + } + } + + /** + * Check that the max size will not be exceeded. + * + * @param length the length + * @param huffman the huffman name + * @throws HpackException.SessionException in case of size errors + */ + public void checkSize(int length, boolean huffman) throws HpackException.SessionException { + // Apply a huffman fudge factor + if (huffman) + length = (length * 4) / 3; + if ((size + length) > maxSize) + throw new HpackException.SessionException("Header too large %d > %d", size + length, maxSize); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/NBitInteger.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/NBitInteger.java new file mode 100644 index 000000000..085be099d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/NBitInteger.java @@ -0,0 +1,104 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import java.nio.ByteBuffer; + +public class NBitInteger { + public static int octectsNeeded(int n, int i) { + if (n == 8) { + int nbits = 0xFF; + i = i - nbits; + if (i < 0) + return 1; + if (i == 0) + return 2; + int lz = Integer.numberOfLeadingZeros(i); + int log = 32 - lz; + return 1 + (log + 6) / 7; + } + + int nbits = 0xFF >>> (8 - n); + i = i - nbits; + if (i < 0) + return 0; + if (i == 0) + return 1; + int lz = Integer.numberOfLeadingZeros(i); + int log = 32 - lz; + return (log + 6) / 7; + } + + public static void encode(ByteBuffer buf, int n, int i) { + if (n == 8) { + if (i < 0xFF) { + buf.put((byte) i); + } else { + buf.put((byte) 0xFF); + + int length = i - 0xFF; + while (true) { + if ((length & ~0x7F) == 0) { + buf.put((byte) length); + return; + } else { + buf.put((byte) ((length & 0x7F) | 0x80)); + length >>>= 7; + } + } + } + } else { + int p = buf.position() - 1; + int bits = 0xFF >>> (8 - n); + + if (i < bits) { + buf.put(p, (byte) ((buf.get(p) & ~bits) | i)); + } else { + buf.put(p, (byte) (buf.get(p) | bits)); + + int length = i - bits; + while (true) { + if ((length & ~0x7F) == 0) { + buf.put((byte) length); + return; + } else { + buf.put((byte) ((length & 0x7F) | 0x80)); + length >>>= 7; + } + } + } + } + } + + public static int decode(ByteBuffer buffer, int n) { + if (n == 8) { + int nbits = 0xFF; + + int i = buffer.get() & 0xff; + + if (i == nbits) { + int m = 1; + int b; + do { + b = 0xff & buffer.get(); + i = i + (b & 127) * m; + m = m * 128; + } while ((b & 128) == 128); + } + return i; + } + + int nbits = 0xFF >>> (8 - n); + + int i = buffer.get(buffer.position() - 1) & nbits; + + if (i == nbits) { + int m = 1; + int b; + do { + b = 0xff & buffer.get(); + i = i + (b & 127) * m; + m = m * 128; + } while ((b & 128) == 128); + } + return i; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/StaticTableHttpField.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/StaticTableHttpField.java new file mode 100644 index 000000000..c50b8bb47 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/hpack/StaticTableHttpField.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpHeader; + +public class StaticTableHttpField extends HttpField { + private final Object value; + + public StaticTableHttpField(HttpHeader header, String name, + String valueString, Object value) { + super(header, name, valueString); + if (value == null) + throw new IllegalArgumentException(); + this.value = value; + } + + public StaticTableHttpField(HttpHeader header, String valueString, + Object value) { + this(header, header.getValue(), valueString, value); + } + + public StaticTableHttpField(String name, String valueString, Object value) { + super(name, valueString); + if (value == null) + throw new IllegalArgumentException(); + this.value = value; + } + + public Object getStaticValue() { + return value; + } + + @Override + public String toString() { + return super.toString() + "(evaluated)"; + } +} \ No newline at end of file diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/FlowControl.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/FlowControl.java new file mode 100644 index 000000000..e3971d7ba --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/FlowControl.java @@ -0,0 +1,25 @@ +package com.fireflysource.net.http.common.v2.stream; + +import com.fireflysource.net.http.common.v2.frame.WindowUpdateFrame; + +public interface FlowControl { + + void onStreamCreated(Stream stream); + + void onStreamDestroyed(Stream stream); + + void updateInitialStreamWindow(Http2Connection http2Connection, int initialStreamWindow, boolean local); + + void onWindowUpdate(Http2Connection http2Connection, Stream stream, WindowUpdateFrame frame); + + void onDataReceived(Http2Connection http2Connection, Stream stream, int length); + + void onDataConsumed(Http2Connection http2Connection, Stream stream, int length); + + void windowUpdate(Http2Connection http2Connection, Stream stream, WindowUpdateFrame frame); + + void onDataSending(Stream stream, int length); + + void onDataSent(Stream stream, int length); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/Http2Connection.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/Http2Connection.java new file mode 100644 index 000000000..e1ec618d2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/Http2Connection.java @@ -0,0 +1,244 @@ +package com.fireflysource.net.http.common.v2.stream; + +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.http.common.HttpConnection; +import com.fireflysource.net.http.common.v2.frame.*; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + *

A {@link Http2Connection} represents the client-side endpoint of an HTTP/2 connection to a single origin server.

+ *

Once a {@link Http2Connection} has been obtained, it can be used to open HTTP/2 streams:

+ *

A {@link Http2Connection} is the active part of the endpoint, and by calling its API applications can generate + * events on the connection; conversely {@link Http2Connection.Listener} is the passive part of the endpoint, and + * has results that are invoked when events happen on the connection.

+ * + * @see Http2Connection.Listener + */ +public interface Http2Connection extends HttpConnection { + + /** + *

Sends the given HEADERS {@code frame} to create a new {@link Stream}.

+ * + * @param frame The HEADERS frame containing the HTTP headers + * @param promise The promise that gets notified of the stream creation + * @param listener The listener that gets notified of stream events + */ + void newStream(HeadersFrame frame, Consumer> promise, Stream.Listener listener); + + /** + *

Sends the given HEADERS {@code frame} to create a new {@link Stream}.

+ * + * @param frame The HEADERS frame containing the HTTP headers. + * @param listener The listener that gets notified of stream events. + * @return The future which gets notified of the stream creation. + */ + default CompletableFuture newStream(HeadersFrame frame, Stream.Listener listener) { + CompletableFuture future = new CompletableFuture<>(); + newStream(frame, Result.futureToConsumer(future), listener); + return future; + } + + /** + *

Sends the given PRIORITY {@code frame}.

+ *

If the {@code frame} references a {@code streamId} that does not exist + * (for example {@code 0}), then a new {@code streamId} will be allocated, to + * support unused anchor streams that act as parent for other streams.

+ * + * @param frame The PRIORITY frame to send + * @param result The result that gets notified when the frame has been sent + * @return The new stream id generated by the PRIORITY frame, or the stream id + * that it is already referencing + */ + int priority(PriorityFrame frame, Consumer> result); + + /** + *

Sends the given SETTINGS {@code frame} to configure the http2Connection.

+ * + * @param frame The SETTINGS frame to send + * @param result The result that gets notified when the frame has been sent + */ + void settings(SettingsFrame frame, Consumer> result); + + /** + *

Sends the given PING {@code frame}.

+ *

PING frames may use to test the connection integrity and to measure + * round-trip time.

+ * + * @param frame The PING frame to send + * @param result The result that gets notified when the frame has been sent + */ + void ping(PingFrame frame, Consumer> result); + + /** + *

Closes the http2Connection by sending a GOAWAY frame with the given error code + * and payload.

+ *

The GOAWAY frame is sent only once; subsequent or concurrent attempts to + * close the http2Connection will have no effect.

+ * + * @param error The error code + * @param payload An optional payload (maybe null) + * @param result The result that gets notified when the frame has been sent + * @return True if the frame sent, false if the http2Connection was already closed + */ + boolean close(int error, String payload, Consumer> result); + + /** + * @return Whether the http2Connection is not open + */ + boolean isClosed(); + + /** + * @return A snapshot of all the streams currently belonging to this http2Connection + */ + Collection getStreams(); + + /** + *

Retrieves the stream with the given {@code streamId}.

+ * + * @param streamId The stream id of the stream looked for + * @return The stream with the given id, or null if no such stream exist + */ + Stream getStream(int streamId); + + /** + *

A {@link Listener} is the passive counterpart of a {@link Http2Connection} and + * receives events happening on an HTTP/2 connection.

+ * + * @see Http2Connection + */ + interface Listener { + /** + *

Consumer> method invoked:

+ *
    + *
  • for clients, just before the preface sent, to gather the + * SETTINGS configuration options the client wants to send to the server;
  • + *
  • for servers, just after having received the preface, to gather + * the SETTINGS configuration options the server wants to send to the + * client.
  • + *
+ * + * @param http2Connection The http2Connection + * @return A (possibly empty or null) map containing SETTINGS configuration + * options to send. + */ + Map onPreface(Http2Connection http2Connection); + + /** + *

Consumer> method invoked when a new stream created upon + * receiving a HEADERS frame representing an HTTP request.

+ *

Applications should implement this method to process HTTP requests, + * typically providing an HTTP response via + * {@link Stream#headers(HeadersFrame, Consumer>)}.

+ *

Applications can detect whether request DATA frames will be arriving + * by testing {@link HeadersFrame#isEndStream()}. If the application is + * interested in processing the DATA frames, it must return a + * {@link Stream.Listener} implementation that overrides + * {@link Stream.Listener#onData(Stream, DataFrame, Consumer>)}.

+ * + * @param stream The newly created stream + * @param frame The HEADERS frame received + * @return A {@link Stream.Listener} that will be notified of stream events + */ + Stream.Listener onNewStream(Stream stream, HeadersFrame frame); + + /** + *

Consumer> method invoked when a SETTINGS frame has been received.

+ * + * @param http2Connection The http2Connection + * @param frame The SETTINGS frame received + */ + void onSettings(Http2Connection http2Connection, SettingsFrame frame); + + /** + *

Consumer> method invoked when a PING frame has been received.

+ * + * @param http2Connection The http2Connection + * @param frame The PING frame received + */ + void onPing(Http2Connection http2Connection, PingFrame frame); + + /** + *

Consumer> method invoked when an RST_STREAM frame has been received for an unknown stream.

+ * + * @param http2Connection The http2Connection + * @param frame The RST_STREAM frame received + * @see Stream.Listener#onReset(Stream, ResetFrame) + */ + void onReset(Http2Connection http2Connection, ResetFrame frame); + + /** + *

Consumer> method invoked when a GOAWAY frame has been received.

+ * + * @param http2Connection The http2Connection + * @param frame The GOAWAY frame received + * @param result The result to notify of the GOAWAY processing + */ + default void onClose(Http2Connection http2Connection, GoAwayFrame frame, Consumer> result) { + try { + onClose(http2Connection, frame); + result.accept(Result.SUCCESS); + } catch (Throwable x) { + result.accept(Result.createFailedResult(x)); + } + } + + void onClose(Http2Connection http2Connection, GoAwayFrame frame); + + /** + *

Consumer> method invoked when a failure has been detected for this http2Connection.

+ * + * @param http2Connection The http2Connection + * @param failure The failure + * @param result The result to notify of failure processing + */ + default void onFailure(Http2Connection http2Connection, Throwable failure, Consumer> result) { + try { + onFailure(http2Connection, failure); + result.accept(Result.SUCCESS); + } catch (Throwable x) { + result.accept(Result.createFailedResult(x)); + } + } + + void onFailure(Http2Connection http2Connection, Throwable failure); + + /** + *

Empty implementation of {@link Stream.Listener}.

+ */ + class Adapter implements Http2Connection.Listener { + @Override + public Map onPreface(Http2Connection http2Connection) { + return null; + } + + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { + return null; + } + + @Override + public void onSettings(Http2Connection http2Connection, SettingsFrame frame) { + } + + @Override + public void onPing(Http2Connection http2Connection, PingFrame frame) { + } + + @Override + public void onReset(Http2Connection http2Connection, ResetFrame frame) { + } + + @Override + public void onClose(Http2Connection http2Connection, GoAwayFrame frame) { + } + + @Override + public void onFailure(Http2Connection http2Connection, Throwable failure) { + } + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/Stream.java b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/Stream.java new file mode 100644 index 000000000..6759eeb6a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/common/v2/stream/Stream.java @@ -0,0 +1,282 @@ +package com.fireflysource.net.http.common.v2.stream; + +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.http.common.v2.frame.DataFrame; +import com.fireflysource.net.http.common.v2.frame.HeadersFrame; +import com.fireflysource.net.http.common.v2.frame.PushPromiseFrame; +import com.fireflysource.net.http.common.v2.frame.ResetFrame; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + *

A {@link Stream} represents a bidirectional exchange of data on top of a {@link Http2Connection}.

+ *

Differently from socket streams, where the input and output streams permanently associated + * with the socket (and hence with the connection that the socket represents), there can be multiple + * HTTP/2 streams present concurrent for an HTTP/2 session.

+ *

A {@link Stream} maps to an HTTP request/response cycle, and after the request/response cycle completed, + * the stream closed and removed from the session.

+ *

Like {@link Http2Connection}, {@link Stream} is the active part and by calling its API applications + * can generate events on the stream; conversely, {@link Stream.Listener} is the passive part, and + * its results invoked when events happen on the stream.

+ * + * @see Stream.Listener + */ +public interface Stream { + /** + * @return the stream's unique id + */ + int getId(); + + /** + * @return the HTTP 2 connection this stream associated to. + */ + Http2Connection getHttp2Connection(); + + /** + *

Sends the given HEADERS {@code frame} representing an HTTP response.

+ * + * @param frame The HEADERS frame to send. + * @param result The result that gets notified when the frame has been sent. + */ + void headers(HeadersFrame frame, Consumer> result); + + /** + *

Sends the given HEADERS {@code frame} representing an HTTP response.

+ * + * @param frame The HEADERS frame to send. + * @return The result that gets notified when the frame has been sent. + */ + default CompletableFuture headers(HeadersFrame frame) { + CompletableFuture future = new CompletableFuture<>(); + headers(frame, Result.futureToConsumer(future)); + return future; + } + + /** + *

Sends the given PUSH_PROMISE {@code frame}.

+ * + * @param frame The PUSH_PROMISE frame to send. + * @param promise The promise that gets notified of the pushed stream creation. + * @param listener The listener that gets notified of stream events. + */ + void push(PushPromiseFrame frame, Consumer> promise, Listener listener); + + /** + *

Sends the given PUSH_PROMISE {@code frame}.

+ * + * @param frame The PUSH_PROMISE frame to send. + * @param listener The listener that gets notified of stream events. + * @return The future which gets notified of the pushed stream creation. + */ + default CompletableFuture push(PushPromiseFrame frame, Listener listener) { + CompletableFuture future = new CompletableFuture<>(); + push(frame, Result.futureToConsumer(future), listener); + return future; + } + + /** + *

Sends the given DATA {@code frame}.

+ * + * @param frame The DATA frame to send. + * @param result The result that gets notified when the frame has been sent. + */ + void data(DataFrame frame, Consumer> result); + + /** + *

Sends the given DATA {@code frame}.

+ * + * @param frame The DATA frame to send. + * @return The result that gets notified when the frame has been sent. + */ + default CompletableFuture data(DataFrame frame) { + CompletableFuture future = new CompletableFuture<>(); + data(frame, Result.futureToConsumer(future)); + return future; + } + + /** + *

Sends the given RST_STREAM {@code frame}.

+ * + * @param frame The RST_FRAME to send. + * @param result The result that gets notified when the frame has been sent. + */ + void reset(ResetFrame frame, Consumer> result); + + /** + * @param key the attribute key. + * @return An object associated with the given key to this stream. + * or null if no object can be found for the given key. + * @see #setAttribute(String, Object) + */ + Object getAttribute(String key); + + /** + * @param key The attribute key. + * @param value An arbitrary object to associate with the given key to this stream. + * @see #getAttribute(String) + * @see #removeAttribute(String) + */ + void setAttribute(String key, Object value); + + /** + * @param key The attribute key. + * @return The object associated with the given key to this stream. + * @see #setAttribute(String, Object) + */ + Object removeAttribute(String key); + + /** + * @return If true this stream has been reset. + */ + boolean isReset(); + + /** + * @return If true this stream closed, both locally and remotely. + */ + boolean isClosed(); + + /** + * @return The stream idle timeout. + * @see #setIdleTimeout(long) + */ + long getIdleTimeout(); + + /** + * @param idleTimeout The stream idle timeout. + * @see #getIdleTimeout() + * @see Stream.Listener#onIdleTimeout(Stream, Throwable) + */ + void setIdleTimeout(long idleTimeout); + + /** + *

A {@link Stream.Listener} is the passive counterpart of a {@link Stream} and receives + * events happening on an HTTP/2 stream.

+ * + * @see Stream + */ + interface Listener { + /** + *

Callback method invoked when a HEADERS frame representing the HTTP response has been received.

+ * + * @param stream The stream. + * @param frame The HEADERS frame received. + */ + void onHeaders(Stream stream, HeadersFrame frame); + + /** + *

Callback method invoked when a PUSH_PROMISE frame has been received.

+ * + * @param stream The stream. + * @param frame The PUSH_PROMISE frame received. + * @return A Stream.Listener that will be notified of pushed stream events. + */ + Listener onPush(Stream stream, PushPromiseFrame frame); + + /** + *

Callback method invoked when a DATA frame has been received.

+ * + * @param stream The stream. + * @param frame The DATA frame received. + * @param result The result to complete when the bytes of the DATA frame have been consumed. + */ + void onData(Stream stream, DataFrame frame, Consumer> result); + + /** + *

Callback method invoked when an RST_STREAM frame has been received for this stream.

+ * + * @param stream The stream. + * @param frame The RST_FRAME received. + * @param result The result to complete when the reset has been handled. + */ + default void onReset(Stream stream, ResetFrame frame, Consumer> result) { + try { + onReset(stream, frame); + result.accept(Result.SUCCESS); + } catch (Throwable x) { + result.accept(Result.createFailedResult(x)); + } + } + + /** + *

Callback method invoked when an RST_STREAM frame has been received for this stream.

+ * + * @param stream The stream. + * @param frame The RST_FRAME received. + * @see Http2Connection.Listener#onReset(Http2Connection, ResetFrame) + */ + default void onReset(Stream stream, ResetFrame frame) { + } + + + /** + *

Callback method invoked when the stream exceeds its idle timeout.

+ * + * @param stream The stream. + * @param x The timeout failure. + * @return If true to reset the stream, false to ignore the idle timeout. + * @see #getIdleTimeout() + */ + default boolean onIdleTimeout(Stream stream, Throwable x) { + return true; + } + + /** + *

Callback method invoked when the stream failed.

+ * + * @param stream The stream. + * @param error An error code. + * @param reason An error reason, or null. + * @param result The result to complete when the failure has been handled. + */ + default void onFailure(Stream stream, int error, String reason, Consumer> result) { + result.accept(Result.SUCCESS); + } + + /** + *

Callback method invoked after the stream has been closed.

+ * + * @param stream The stream. + */ + default void onClosed(Stream stream) { + } + + /** + *

Callback method invoked after the connection is terminal.

+ * + * @param stream The stream. + */ + default void onTerminal(Stream stream) { + + } + + /** + *

Empty implementation of {@link Listener}

+ */ + class Adapter implements Listener { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) { + } + + @Override + public Listener onPush(Stream stream, PushPromiseFrame frame) { + return new Adapter(); + } + + @Override + public void onData(Stream stream, DataFrame frame, Consumer> result) { + result.accept(Result.SUCCESS); + } + + @Override + public void onReset(Stream stream, ResetFrame frame) { + } + + @Override + public boolean onIdleTimeout(Stream stream, Throwable x) { + return true; + } + } + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpProxy.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpProxy.java new file mode 100644 index 000000000..cf2af5be2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpProxy.java @@ -0,0 +1,26 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.lifecycle.LifeCycle; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public interface HttpProxy extends LifeCycle { + + /** + * Bind a server TCP address + * + * @param address The server TCP address. + */ + void listen(SocketAddress address); + + /** + * Bind the server host and port. + * + * @param host The server host. + * @param port The server port. + */ + default void listen(String host, int port) { + listen(new InetSocketAddress(host, port)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServer.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServer.java new file mode 100644 index 000000000..30e5acf50 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServer.java @@ -0,0 +1,188 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.lifecycle.LifeCycle; +import com.fireflysource.net.tcp.TcpConnection; +import com.fireflysource.net.tcp.secure.SecureEngineFactory; +import com.fireflysource.net.websocket.server.WebSocketServerConnectionBuilder; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * @author Pengtao Qiu + */ +public interface HttpServer extends LifeCycle, Cloneable { + + /** + * Register a new router. + * + * @return The router. + */ + Router router(); + + /** + * Register a new router. + * + * @param id The router id. + * @return The router. + */ + Router router(int id); + + /** + * Create a new websocket connection builder. + * + * @return The websocket connection builder. + */ + WebSocketServerConnectionBuilder websocket(); + + /** + * Create a new websocket connection builder. + * + * @param path The websocket url. + * @return The websocket connection builder. + */ + WebSocketServerConnectionBuilder websocket(String path); + + /** + * HTTP headers received callback. + * + * @param function The HTTP headers received callback. + * @return The HTTP server. + */ + HttpServer onHeaderComplete(Function> function); + + /** + * HTTP server exception callback. + * + * @param biFunction The HTTP server exception callback. + * @return The HTTP server. + */ + HttpServer onException(BiFunction> biFunction); + + /** + * The router not found callback. + * + * @param function Invoke this function when the HTTP server does not find the router. + * @return The HTTP server. + */ + HttpServer onRouterNotFound(Function> function); + + /** + * The router complete callback. + * + * @param function Invoke this function when the last router executes successfully. + * @return The HTTP server. + */ + HttpServer onRouterComplete(Function> function); + + /** + * The accept HTTP tunnel callback. + * + * @param function Invoke this function when the server accepts a HTTP tunnel request. + * @return The HTTP server. + */ + HttpServer onAcceptHttpTunnel(Function> function); + + /** + * Accept HTTP tunnel handshake response callback. The default response: HTTP/1.1 200 Connection Established. + * + * @param function Invoke this function after the server accepts a HTTP tunnel request and then the server will response the HTTP tunnel response. + * @return The HTTP server. + */ + HttpServer onAcceptHttpTunnelHandshakeResponse(Function> function); + + /** + * Refuse HTTP tunnel handshake response callback. The default response: HTTP/1.1 407 Proxy Authentication Required. + * + * @param function Invoke this function after the server refuses a HTTP tunnel request and then the server will response the HTTP tunnel response. + * @return The HTTP server. + */ + HttpServer onRefuseHttpTunnelHandshakeResponse(Function> function); + + /** + * The HTTP tunnel handshake complete callback. + * + * @param function Invoke this function when the HTTP tunnel handshake is complete. + * @return The HTTP server. + */ + HttpServer onHttpTunnelHandshakeComplete(BiFunction> function); + + /** + * Set the TLS engine factory. + * + * @param secureEngineFactory The TLS engine factory. + * @return The HTTP server. + */ + HttpServer secureEngineFactory(SecureEngineFactory secureEngineFactory); + + /** + * The supported application layer protocols. + * + * @param supportedProtocols The supported application layer protocols. + * @return The HTTP server. + */ + HttpServer supportedProtocols(List supportedProtocols); + + /** + * Create a TLS engine using advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param peerHost the non-authoritative name of the host. + * @return The HTTP server. + */ + HttpServer peerHost(String peerHost); + + /** + * Create a TLS engine using advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param peerPort the non-authoritative port. + * @return The HTTP server. + */ + HttpServer peerPort(int peerPort); + + /** + * Enable the TLS protocol over the TCP connection. + * + * @return The HTTP server. + */ + HttpServer enableSecureConnection(); + + /** + * Set the TCP idle timeout. The unit is second. + * + * @param timeout The TCP idle timeout. Time unit is second. + * @return The HTTP server. + */ + HttpServer timeout(Long timeout); + + /** + * Bind a server TCP address + * + * @param address The server TCP address. + */ + void listen(SocketAddress address); + + /** + * Bind the server host and port. + * + * @param host The server host. + * @param port The server port. + */ + default void listen(String host, int port) { + listen(new InetSocketAddress(host, port)); + } + + /** + * Copy the HTTP server instance. + * + * @return The new HTTP server instance. + */ + HttpServer copy(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerConnection.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerConnection.java new file mode 100644 index 000000000..a2e2338d4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerConnection.java @@ -0,0 +1,152 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.http.common.HttpConnection; +import com.fireflysource.net.tcp.TcpConnection; +import com.fireflysource.net.websocket.server.WebSocketServerConnectionHandler; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; + +/** + * The HTTP server connection. + * + * @author Pengtao Qiu + */ +public interface HttpServerConnection extends HttpConnection { + + Listener EMPTY_LISTENER = new Listener.Adapter(); + + /** + * Set HTTP server connection event listener. It receives the HTTP request or exception events. + * + * @param listener The HTTP server connection event listener. + * @return The HTTP server connection. + */ + HttpServerConnection setListener(Listener listener); + + /** + * Begin to receive HTTP request. + */ + void begin(); + + /** + * The HTTP server connection event listener. + */ + interface Listener { + + /** + * When the all HTTP headers receive, invokes this method. + * + * @param context The routing context. In this stage, the context cannot get the HTTP body. + * @return The future result. + */ + CompletableFuture onHeaderComplete(RoutingContext context); + + /** + * When the HTTP request is complete, invokes this method. it contains headers and body, + * + * @param context The routing context. + * @return The future result. + */ + CompletableFuture onHttpRequestComplete(RoutingContext context); + + /** + * When the connection parses the error HTTP message, invokes this method. + * + * @param context The routing context. The context may be null. + * @param throwable An exception. + * @return The future result. + */ + CompletableFuture onException(RoutingContext context, Throwable throwable); + + /** + * When the server accepts a Websocket handshake request, invokes this method. + * + * @param context The routing context. + * @return The Websocket connection handler. + */ + CompletableFuture onWebSocketHandshake(RoutingContext context); + + /** + * When the server accepts an HTTP tunnel request, invokes this method. + * + * @param request The HTTP request. + * @return If true, create an HTTP tunnel connection. + */ + CompletableFuture onAcceptHttpTunnel(HttpServerRequest request); + + /** + * After the server accepts a HTTP tunnel request and then the server will response the HTTP tunnel response, invokes this method. + * + * @param context The routing context. + * @return The future result. + */ + CompletableFuture onAcceptHttpTunnelHandshakeResponse(RoutingContext context); + + /** + * After the server refuses a HTTP tunnel request and then the server will response the HTTP tunnel response, invokes this method. + * + * @param context The routing context. + * @return The future result. + */ + CompletableFuture onRefuseHttpTunnelHandshakeResponse(RoutingContext context); + + /** + * When the HTTP tunnel handshake is complete, invokes this method. + * + * @param connection The client TCP connection. + * @param address The target address. + * @return The future result. + */ + CompletableFuture onHttpTunnelHandshakeComplete(TcpConnection connection, InetSocketAddress address); + + /** + * The empty listener implement. + */ + class Adapter implements Listener { + + @Override + public CompletableFuture onHeaderComplete(RoutingContext context) { + return Result.DONE; + } + + @Override + public CompletableFuture onHttpRequestComplete(RoutingContext context) { + return Result.DONE; + } + + @Override + public CompletableFuture onException(RoutingContext context, Throwable throwable) { + return Result.DONE; + } + + @Override + public CompletableFuture onWebSocketHandshake(RoutingContext context) { + return CompletableFuture.completedFuture(new WebSocketServerConnectionHandler()); + } + + @Override + public CompletableFuture onAcceptHttpTunnel(HttpServerRequest request) { + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture onAcceptHttpTunnelHandshakeResponse(RoutingContext context) { + return Result.DONE; + } + + @Override + public CompletableFuture onRefuseHttpTunnelHandshakeResponse(RoutingContext context) { + return Result.DONE; + } + + @Override + public CompletableFuture onHttpTunnelHandshakeComplete(TcpConnection connection, InetSocketAddress address) { + return Result.DONE; + } + } + } + + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentHandler.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentHandler.java new file mode 100644 index 000000000..ef8652577 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentHandler.java @@ -0,0 +1,6 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.net.http.common.content.handler.HttpContentHandler; + +public interface HttpServerContentHandler extends HttpContentHandler { +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentHandlerFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentHandlerFactory.java new file mode 100644 index 000000000..71a79a28c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentHandlerFactory.java @@ -0,0 +1,31 @@ +package com.fireflysource.net.http.server; + + +import com.fireflysource.net.http.server.impl.content.handler.ByteBufferContentHandler; +import com.fireflysource.net.http.server.impl.content.handler.FileContentHandler; +import com.fireflysource.net.http.server.impl.content.handler.StringContentHandler; + +import java.nio.file.OpenOption; +import java.nio.file.Path; + +abstract public class HttpServerContentHandlerFactory { + public static HttpServerContentHandler bytesHandler() { + return new ByteBufferContentHandler(); + } + + public static HttpServerContentHandler bytesHandler(long maxRequestBodySize) { + return new ByteBufferContentHandler(maxRequestBodySize); + } + + public static HttpServerContentHandler stringHandler() { + return new StringContentHandler(); + } + + public static HttpServerContentHandler stringHandler(long maxRequestBodySize) { + return new StringContentHandler(maxRequestBodySize); + } + + public static HttpServerContentHandler fileHandler(Path path, OpenOption... openOptions) { + return new FileContentHandler(path, openOptions); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentProvider.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentProvider.java new file mode 100644 index 000000000..fca43d629 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentProvider.java @@ -0,0 +1,26 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.net.http.common.content.provider.HttpContentProvider; + +import java.nio.ByteBuffer; + +/** + * @author Pengtao Qiu + */ +public interface HttpServerContentProvider extends HttpContentProvider { + + /** + * The content length. If the length is -1, the content is the data stream. + * + * @return The content length. + */ + long length(); + + /** + * Convert fixed length content to a ByteBuffer. If the content is the data stream, return an empty ByteBuffer. + * + * @return The ByteBuffer. + */ + ByteBuffer toByteBuffer(); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentProviderFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentProviderFactory.java new file mode 100644 index 000000000..b68154ca7 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerContentProviderFactory.java @@ -0,0 +1,30 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.net.http.server.impl.content.provider.ByteBufferContentProvider; +import com.fireflysource.net.http.server.impl.content.provider.FileContentProvider; +import com.fireflysource.net.http.server.impl.content.provider.StringContentProvider; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.util.Set; + +abstract public class HttpServerContentProviderFactory { + + public static HttpServerContentProvider bytesBody(ByteBuffer buffer) { + return new ByteBufferContentProvider(buffer); + } + + public static HttpServerContentProvider stringBody(String string, Charset charset) { + return new StringContentProvider(string, charset); + } + + public static HttpServerContentProvider fileBody(Path path, OpenOption... openOptions) { + return new FileContentProvider(path, openOptions); + } + + public static HttpServerContentProvider fileBody(Path path, Set openOptions, long position, long length) { + return new FileContentProvider(path, openOptions, position, length); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerFactory.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerFactory.java new file mode 100644 index 000000000..25a4c2183 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerFactory.java @@ -0,0 +1,24 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.net.http.common.HttpConfig; +import com.fireflysource.net.http.server.impl.AsyncHttpProxy; +import com.fireflysource.net.http.server.impl.AsyncHttpServer; + +abstract public class HttpServerFactory { + + public static HttpServer create(HttpConfig config) { + return new AsyncHttpServer(config); + } + + public static HttpServer create() { + return new AsyncHttpServer(); + } + + public static HttpProxy createHttpProxy() { + return new AsyncHttpProxy(); + } + + public static HttpProxy createHttpProxy(HttpConfig config) { + return new AsyncHttpProxy(config); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerOutputChannel.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerOutputChannel.java new file mode 100644 index 000000000..ba909e4d1 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerOutputChannel.java @@ -0,0 +1,68 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.io.OutputChannel; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface HttpServerOutputChannel extends OutputChannel { + + /** + * Commit the http response. + * + * @return The future result. + */ + CompletableFuture commit(); + + /** + * If true, the http response has committed. + * + * @return If true, the http response has committed. + */ + boolean isCommitted(); + + /** + * Write the message to the remote endpoint. + * + * @param byteBuffers The byte buffer array. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The future result. + */ + CompletableFuture write(ByteBuffer[] byteBuffers, int offset, int length); + + /** + * Write the message to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer list of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @return The future result. + */ + CompletableFuture write(List byteBufferList, int offset, int length); + + /** + * Write the message to the remote endpoint. + * + * @param string The string. + * @return The future result. + */ + CompletableFuture write(String string); + + /** + * Write the message to the remote endpoint. + * + * @param string The string. + * @param charset The charset. + * @return The future result. + */ + CompletableFuture write(String string, Charset charset); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerRequest.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerRequest.java new file mode 100644 index 000000000..ad346a8d8 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerRequest.java @@ -0,0 +1,180 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.net.http.common.model.Cookie; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpURI; +import com.fireflysource.net.http.common.model.HttpVersion; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * The HTTP request. + * + * @author Pengtao Qiu + */ +public interface HttpServerRequest { + + /** + * Get the HTTP method. + * + * @return The HTTP method. + */ + String getMethod(); + + /** + * Get the HTTP URI. + * + * @return The HTTP URI. + */ + HttpURI getURI(); + + /** + * Get the HTTP version. + * + * @return The HTTP version. + */ + HttpVersion getHttpVersion(); + + /** + * Get the URL query string. + * + * @param name The URL query parameter name. + * @return The URL query parameter value. + */ + String getQueryString(String name); + + /** + * Get the URL query strings. + * + * @param name The URL query parameter name. + * @return The URL query parameter values. + */ + List getQueryStrings(String name); + + /** + * Get all URL query strings. + * + * @return All URL query strings. + */ + Map> getQueryStrings(); + + /** + * Get the HTTP header fields. + * + * @return The HTTP header fields. + */ + HttpFields getHttpFields(); + + /** + * Get the HTTP cookies. + * + * @return The HTTP cookies. + */ + List getCookies(); + + /** + * Get the content length. + * + * @return The content length. + */ + long getContentLength(); + + /** + * Get the HTTP trailers. + * + * @return The HTTP trailers. + */ + Supplier getTrailerSupplier(); + + /** + * Set the HTTP request is complete. + * + * @param requestComplete If true, the HTTP request is complete. + */ + void setRequestComplete(boolean requestComplete); + + /** + * Get the HTTP request is complete. + * + * @return If true, the HTTP request is complete. + */ + boolean isRequestComplete(); + + /** + * Get the HTTP body and convert it to the UTF-8 string. + * + * @return The HTTP body string. + */ + String getStringBody(); + + /** + * Get the HTTP body and convert the specified charset string. + * + * @param charset The charset of the HTTP body string. + * @return The HTTP body string. + */ + String getStringBody(Charset charset); + + /** + * Get the HTTP body raw binary data. + * + * @return The HTTP body raw binary data. + */ + List getBody(); + + /** + * Get the web form input value. + * + * @param name The form input name. + * @return The value. + */ + String getFormInput(String name); + + /** + * Get the web form input values. + * + * @param name The web form input name. + * @return The values. + */ + List getFormInputs(String name); + + /** + * Get all web form inputs. + * + * @return All web form inputs. + */ + Map> getFormInputs(); + + /** + * Get HTTP request multi-part content. + * + * @param name The part name. + * @return The HTTP request multi-part content. + */ + MultiPart getPart(String name); + + /** + * Get all HTTP request multi-part content. + * + * @return All HTTP request multi-part content. + */ + List getParts(); + + /** + * Get HTTP request content handler. + * + * @return HTTP content handler. + */ + HttpServerContentHandler getContentHandler(); + + /** + * Set HTTP request content handler. + * + * @param contentHandler HTTP request content handler. + */ + void setContentHandler(HttpServerContentHandler contentHandler); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerResponse.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerResponse.java new file mode 100644 index 000000000..4f735de01 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/HttpServerResponse.java @@ -0,0 +1,154 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.io.AsyncCloseable; +import com.fireflysource.net.http.common.model.Cookie; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpVersion; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * The HTTP response. + * + * @author Pengtao Qiu + */ +public interface HttpServerResponse extends AsyncCloseable { + + /** + * Get the HTTP response status code. + * + * @return The HTTP response status code. + */ + int getStatus(); + + /** + * Set the HTTP response status code. + * + * @param status The HTTP response status code. + */ + void setStatus(int status); + + /** + * Get the textual description associated with the numeric status code. + * + * @return The textual description associated with the numeric status code. + */ + String getReason(); + + /** + * Set the textual description associated with the numeric status code. + * + * @param reason The textual description associated with the numeric status code. + */ + void setReason(String reason); + + /** + * Get the HTTP version of the current HTTP connection. + * + * @return The HTTP version of the current HTTP connection. + */ + HttpVersion getHttpVersion(); + + /** + * Set the HTTP version of the current HTTP connection. + * + * @param httpVersion The HTTP version of the current HTTP connection. + */ + void setHttpVersion(HttpVersion httpVersion); + + /** + * Get the HTTP header fields. + * + * @return The HTTP header fields. + */ + HttpFields getHttpFields(); + + /** + * Set the HTTP header fields. + * + * @param httpFields The HTTP header fields. + */ + void setHttpFields(HttpFields httpFields); + + /** + * Get the cookies. + * + * @return The cookies. + */ + List getCookies(); + + /** + * Set the cookies. + * + * @param cookies The cookies. + */ + void setCookies(List cookies); + + /** + * Get the HTTP trailer fields. + * + * @return The HTTP trailer fields. + */ + Supplier getTrailerSupplier(); + + /** + * Set the HTTP trailer fields. + * + * @param supplier The HTTP trailer fields supplier. + */ + void setTrailerSupplier(Supplier supplier); + + /** + * Get the content provider. + * + * @return the content provider. + */ + HttpServerContentProvider getContentProvider(); + + /** + * Set the content provider. When you commit the response, the HTTP server will send the data that read from the content provider. + * If you set content provider after commit response, this method will throw IllegalStateException. + * + * @param contentProvider When you commit the response, the HTTP server will send the data that read from the content provider. + */ + void setContentProvider(HttpServerContentProvider contentProvider); + + /** + * Get the output channel. It can write data to the client. + * If you get output channel before commit response, this method will throw IllegalStateException. + * + * @return The output channel. + */ + HttpServerOutputChannel getOutputChannel(); + + /** + * Commit the response. If you set the content provider, the server will output the data from the content provider, + * or else you can write data using the output channel. + * + * @return The future result. + */ + CompletableFuture commit(); + + /** + * If true, the http response has committed. + * + * @return If true, the http response has committed. + */ + boolean isCommitted(); + + /** + * Response 100 continue. + * + * @return The future result. + */ + CompletableFuture response100Continue(); + + /** + * Response 200 connection established. + * + * @return The future result. + */ + CompletableFuture response200ConnectionEstablished(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/Matcher.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/Matcher.java new file mode 100644 index 000000000..69a7eba93 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/Matcher.java @@ -0,0 +1,42 @@ +package com.fireflysource.net.http.server; + +import java.util.Collections; +import java.util.Map; +import java.util.SortedSet; + +public interface Matcher { + + enum MatchType { + PATH, METHOD, ACCEPT, CONTENT_TYPE + } + + class MatchResult { + private final SortedSet routers; + private final Map> parameters; + private final MatchType matchType; + + public MatchResult(SortedSet routers, Map> parameters, MatchType matchType) { + this.routers = routers; + this.parameters = Collections.unmodifiableMap(parameters); + this.matchType = matchType; + } + + public SortedSet getRouters() { + return routers; + } + + public Map> getParameters() { + return parameters; + } + + public MatchType getMatchType() { + return matchType; + } + } + + void add(String rule, Router router); + + MatchResult match(String value); + + MatchType getMatchType(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/MultiPart.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/MultiPart.java new file mode 100644 index 000000000..b2c795ff2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/MultiPart.java @@ -0,0 +1,62 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.io.InputChannel; +import com.fireflysource.net.http.common.model.HttpFields; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public interface MultiPart extends InputChannel { + + /** + * Gets the content type of this part. + * + * @return The content type of this part. + */ + String getContentType(); + + /** + * Gets the name of this part + * + * @return The name of this part as a String + */ + String getName(); + + /** + * Gets the file name + * + * @return The file name. + */ + String getFileName(); + + /** + * Returns the size of this file. + * + * @return a long specifying the size of this part, in bytes. + */ + long getSize(); + + /** + * Get this part headers. + * + * @return This part headers. + */ + HttpFields getHttpFields(); + + /** + * Get string body. If the content length not exceeded file threshold. + * + * @param charset The string charset. + * @return The string body. + */ + String getStringBody(Charset charset); + + /** + * Get string body. If the content length not exceeded file threshold. + * + * @return The string body. + */ + default String getStringBody() { + return getStringBody(StandardCharsets.UTF_8); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/Router.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/Router.java new file mode 100644 index 000000000..8c37ab55b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/Router.java @@ -0,0 +1,163 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.common.coroutine.CoroutineDispatchers; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.http.common.model.HttpMethod; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; + +public interface Router extends Comparable { + + Handler EMPTY_HANDLER = ctx -> Result.DONE; + + /** + * Get router id. + * + * @return The router id. + */ + int getId(); + + /** + * If true, the router is enable. + * + * @return If true, the router is enable. + */ + boolean isEnable(); + + /** + * Get matched types. + * + * @return The matched types. + */ + Set getMatchTypes(); + + /** + * Bind a URL for this router. + * + * @param url The URL. + * @return router. + */ + Router path(String url); + + /** + * Bind some URLs for this router. + * + * @param urlList The URL list. + * @return router. + */ + Router paths(List urlList); + + /** + * Bind URL using regex. + * + * @param regex The URL regex. + * @return router. + */ + Router pathRegex(String regex); + + /** + * Bind HTTP method. + * + * @param httpMethod The HTTP method. + * @return router. + */ + Router method(String httpMethod); + + /** + * Bind HTTP method. + * + * @param httpMethod The HTTP method. + * @return router. + */ + Router method(HttpMethod httpMethod); + + /** + * Bind get method and URL. + * + * @param url The URL. + * @return router. + */ + Router get(String url); + + /** + * Bind post method and URL. + * + * @param url The URL. + * @return router. + */ + Router post(String url); + + /** + * Bind put method and URL. + * + * @param url The URL. + * @return router. + */ + Router put(String url); + + /** + * Bind delete method and URL. + * + * @param url The URL. + * @return router. + */ + Router delete(String url); + + /** + * Bind the request content type. + * + * @param contentType The request content type. + * @return router. + */ + Router consumes(String contentType); + + /** + * Bind remote accepted content type. + * + * @param accept The remote accepted content type. + * @return router. + */ + Router produces(String accept); + + /** + * Set router handler. When the HTTP server accepted request, and the request match this router, + * the server will call this handler to process request. + * + * @param handler router handler. + * @return The HTTP server. + */ + HttpServer handler(Handler handler); + + /** + * Set router handler. The handler executes thread blocking task on the IO thread pool. + * + * @param consumer The thread blocking router handler. + * @return The HTTP server. + */ + default HttpServer blockingHandler(Consumer consumer) { + return this.handler(ctx -> CompletableFuture.runAsync(() -> consumer.accept(ctx), + CoroutineDispatchers.INSTANCE.getIoBlockingThreadPool())); + } + + /** + * Enable this router. + * + * @return router. + */ + Router enable(); + + /** + * Disable this router. + * + * @return router. + */ + Router disable(); + + interface Handler extends Function> { + + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/RouterManager.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/RouterManager.java new file mode 100644 index 000000000..9ab7d7425 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/RouterManager.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.http.server; + +import java.util.*; + +public interface RouterManager { + + class RouterMatchResult implements Comparable { + + private final Router router; + private final Map parameters; + private final Set matchTypes; + + public RouterMatchResult(Router router, Map parameters, Set matchTypes) { + this.router = router; + this.parameters = Collections.unmodifiableMap(parameters); + this.matchTypes = Collections.unmodifiableSet(matchTypes); + } + + public Router getRouter() { + return router; + } + + public Map getParameters() { + return parameters; + } + + public Set getMatchTypes() { + return matchTypes; + } + + @Override + public int compareTo(RouterMatchResult o) { + return router.compareTo(o.getRouter()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RouterMatchResult that = (RouterMatchResult) o; + return Objects.equals(router, that.router); + } + + @Override + public int hashCode() { + return Objects.hash(router); + } + } + + /** + * Register a router using automatic increase id. + * + * @return The new router. + */ + Router register(); + + /** + * Register a router. + * + * @param id The router id. + * @return The new router. + */ + Router register(Integer id); + + /** + * Find routers. + * + * @param context The routing context. + * @return The registered routers. + */ + SortedSet findRouters(RoutingContext context); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/http/server/RoutingContext.java b/firefly-net/src/main/java/com/fireflysource/net/http/server/RoutingContext.java new file mode 100644 index 000000000..5d736da64 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/http/server/RoutingContext.java @@ -0,0 +1,576 @@ +package com.fireflysource.net.http.server; + +import com.fireflysource.net.http.common.model.*; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * A new routing context instance creates when the server receives an HTTP request. + *

+ * You can visit the RoutingContext instance in the whole router chain. + * It provides HTTP request/response API and allows you to maintain data that lives for the lifetime of the context. + * Contexts discarded once they have been routed to the handler for the request. + *

+ * The context also provides access to the Session, cookies and body for the request, given the correct handlers in the application. + * + * @author Pengtao Qiu + */ +public interface RoutingContext { + + /** + * Get the attribute value. + * + * @param key The attribute key. + * @return The value. + */ + Object getAttribute(String key); + + /** + * Set the attribute value. + * + * @param key The attribute key. + * @param value The value. + * @return The old value if exists. + */ + Object setAttribute(String key, Object value); + + /** + * Remove the value. + * + * @param key The attribute key. + * @return The old value if exists. + */ + Object removeAttribute(String key); + + /** + * Get all attributes. + * + * @return All attributes. + */ + Map getAttributes(); + + /** + * Get HTTP request. + * + * @return The HTTP request. + */ + HttpServerRequest getRequest(); + + /** + * Get HTTP response. + * + * @return The HTTP response. + */ + HttpServerResponse getResponse(); + + + /** + * Get the parameter value. If you bind the parameter name for the path. + * + * @param name The path parameter name. + * @return The value. + */ + String getPathParameter(String name); + + /** + * Get the parameter value. If you bind the wildcard for the path. + * + * @param index The wildcard index. + * @return The value. + */ + String getPathParameter(int index); + + /** + * Get the path parameter by the regex group index. If you register the path using regex. + * + * @param index The regex group index. + * @return The value. + */ + String getPathParameterByRegexGroup(int index); + + /** + * Get the URL query string. + * + * @param name The URL query parameter name. + * @return The URL query parameter value. + */ + default String getQueryString(String name) { + return getRequest().getQueryString(name); + } + + /** + * Get the URL query strings. + * + * @param name The URL query parameter name. + * @return The URL query parameter values. + */ + default List getQueryStrings(String name) { + return getRequest().getQueryStrings(name); + } + + /** + * Get all URL query strings. + * + * @return All URL query strings. + */ + default Map> getQueryStrings() { + return getRequest().getQueryStrings(); + } + + /** + * Get the web form input value. + * + * @param name The form input name. + * @return The value. + */ + default String getFormInput(String name) { + return getRequest().getFormInput(name); + } + + /** + * Get the web form input values. + * + * @param name The web form input name. + * @return The values. + */ + default List getFormInputs(String name) { + return getRequest().getFormInputs(name); + } + + /** + * Get all web form inputs. + * + * @return All web form inputs. + */ + default Map> getFormInputs() { + return getRequest().getFormInputs(); + } + + /** + * Get the HTTP body and convert it to the UTF-8 string. + * + * @return The HTTP body string. + */ + default String getStringBody() { + return getRequest().getStringBody(); + } + + /** + * Get the HTTP body and convert the specified charset string. + * + * @param charset The charset of the HTTP body string. + * @return The HTTP body string. + */ + default String getStringBody(Charset charset) { + return getRequest().getStringBody(charset); + } + + /** + * Get the HTTP body raw binary data. + * + * @return The HTTP body raw binary data. + */ + default List getBody() { + return getRequest().getBody(); + } + + /** + * Get HTTP request multi-part content. + * + * @param name The part name. + * @return The HTTP request multi-part content. + */ + default MultiPart getPart(String name) { + return getRequest().getPart(name); + } + + /** + * Get all HTTP request multi-part content. + * + * @return All HTTP request multi-part content. + */ + default List getParts() { + return getRequest().getParts(); + } + + /** + * Set HTTP request content handler. + * + * @param contentHandler HTTP request content handler. + * @return The routing context. + */ + default RoutingContext contentHandler(HttpServerContentHandler contentHandler) { + getRequest().setContentHandler(contentHandler); + return this; + } + + /** + * Get HTTP request method. + * + * @return The HTTP request method. + */ + default String getMethod() { + return getRequest().getMethod(); + } + + /** + * Get HTTP request URI. + * + * @return The HTTP request URI. + */ + default HttpURI getURI() { + return getRequest().getURI(); + } + + /** + * Get HTTP request version. + * + * @return The HTTP request version. + */ + default HttpVersion getHttpVersion() { + return getRequest().getHttpVersion(); + } + + /** + * Get HTTP request headers. + * + * @return The HTTP request headers. + */ + default HttpFields getHttpFields() { + return getRequest().getHttpFields(); + } + + /** + * The request headers contain expect 100 continue. + * + * @return If true, the headers contain expect 100 continue. + */ + boolean expect100Continue(); + + /** + * Get HTTP request content length. + * + * @return The HTTP request content length. + */ + default long getContentLength() { + return getRequest().getContentLength(); + } + + /** + * Get HTTP request content type. + * + * @return The content type. + */ + default String getContentType() { + return getHttpFields().get(HttpHeader.CONTENT_TYPE); + } + + /** + * Get HTTP request cookies. + * + * @return The HTTP request cookies. + */ + default List getCookies() { + return getRequest().getCookies(); + } + + /** + * Set HTTP response status. + * + * @param status The HTTP response status. + * @return The routing context. + */ + default RoutingContext setStatus(int status) { + getResponse().setStatus(status); + return this; + } + + /** + * Set HTTP response reason. + * + * @param reason The HTTP response reason. + * @return The routing context. + */ + default RoutingContext setReason(String reason) { + getResponse().setReason(reason); + return this; + } + + /** + * Set HTTP response version. + * + * @param httpVersion The HTTP response version. + * @return The routing context. + */ + default RoutingContext setHttpVersion(HttpVersion httpVersion) { + getResponse().setHttpVersion(httpVersion); + return this; + } + + /** + * Put HTTP response header. + * + * @param header The HTTP header. + * @param value The value. + * @return The routing context. + */ + default RoutingContext put(HttpHeader header, String value) { + getResponse().getHttpFields().put(header, value); + return this; + } + + /** + * Put HTTP response header. + * + * @param header The HTTP header. + * @param value The value. + * @return The routing context. + */ + default RoutingContext put(HttpHeader header, HttpHeaderValue value) { + getResponse().getHttpFields().put(header, value); + return this; + } + + /** + * Put HTTP response header. + * + * @param header The HTTP header. + * @param value The value. + * @return The routing context. + */ + default RoutingContext put(String header, String value) { + getResponse().getHttpFields().put(header, value); + return this; + } + + /** + * Add HTTP response header. + * + * @param header The HTTP header. + * @param value The value. + * @return The routing context. + */ + default RoutingContext add(HttpHeader header, String value) { + getResponse().getHttpFields().add(header, value); + return this; + } + + /** + * Add HTTP response header. + * + * @param header The HTTP header. + * @param value The value. + * @return The routing context. + */ + default RoutingContext add(HttpHeader header, HttpHeaderValue value) { + getResponse().getHttpFields().add(header, value); + return this; + } + + /** + * Add HTTP response header. + * + * @param header The HTTP header. + * @param value The value. + * @return The routing context. + */ + default RoutingContext add(String header, String value) { + getResponse().getHttpFields().add(header, value); + return this; + } + + /** + * Add HTTP response header and CSV values. + * + * @param header The HTTP header. + * @param values The value. + * @return The routing context. + */ + default RoutingContext addCSV(HttpHeader header, String... values) { + getResponse().getHttpFields().addCSV(header, values); + return this; + } + + /** + * Add HTTP response header and CSV values. + * + * @param header The HTTP header. + * @param values The value. + * @return The routing context. + */ + default RoutingContext addCSV(String header, String... values) { + getResponse().getHttpFields().addCSV(header, values); + return this; + } + + /** + * Add HTTP response fields. + * + * @param fields The HTTP response fields. + * @return The routing context. + */ + default RoutingContext addAll(HttpFields fields) { + getResponse().getHttpFields().addAll(fields); + return this; + } + + /** + * Set the HTTP trailer fields. + * + * @param supplier The HTTP trailer fields supplier. + * @return The routing context. + */ + default RoutingContext setTrailerSupplier(Supplier supplier) { + getResponse().setTrailerSupplier(supplier); + return this; + } + + /** + * Set HTTP response cookies. + * + * @param cookies The HTTP response cookies. + * @return he routing context. + */ + default RoutingContext setCookies(List cookies) { + getResponse().setCookies(cookies); + return this; + } + + /** + * Set the HTTP response content provider. + * + * @param contentProvider HTTP response content provider. + * @return The routing context. + */ + default RoutingContext contentProvider(HttpServerContentProvider contentProvider) { + getResponse().setContentProvider(contentProvider); + return this; + } + + /** + * Response 100 continue. + * + * @return The future result. + */ + default CompletableFuture response100Continue() { + return getResponse().response100Continue(); + } + + /** + * Response 200 Connection Established. + * + * @return The future result. + */ + default CompletableFuture response200ConnectionEstablished() { + return getResponse().response200ConnectionEstablished(); + } + + /** + * Write string to the client. + * + * @param value The response content. + * @return The routing context. + */ + default RoutingContext write(String value) { + getResponse().commit().thenCompose(ignore -> getResponse().getOutputChannel().write(value)); + return this; + } + + /** + * Write the response content. + * + * @param byteBuffer The response content. + * @return The routing context. + */ + default RoutingContext write(ByteBuffer byteBuffer) { + getResponse().commit().thenCompose(ignore -> getResponse().getOutputChannel().write(byteBuffer)); + return this; + } + + /** + * Write the response content. + * + * @param byteBufferList The response content list. + * @param offset The offset within the buffer list of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The routing context. + */ + default RoutingContext write(List byteBufferList, int offset, int length) { + getResponse().commit().thenCompose(ignore -> getResponse().getOutputChannel().write(byteBufferList, offset, length)); + return this; + } + + /** + * Write the response content. + * + * @param byteBuffers The response content array. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The routing context. + */ + default RoutingContext write(ByteBuffer[] byteBuffers, int offset, int length) { + getResponse().commit().thenCompose(ignore -> getResponse().getOutputChannel().write(byteBuffers, offset, length)); + return this; + } + + /** + * End the HTTP response. + * + * @return The response future result. + */ + default CompletableFuture end() { + return getResponse().commit().thenCompose(ignore -> getResponse().closeAsync()); + } + + /** + * Write the value and end the HTTP response. + * + * @param value The HTTP response content. + * @return The response future result. + */ + default CompletableFuture end(String value) { + return write(value).end(); + } + + /** + * Write the redirect response to the client. + * + * @param url The redirect URL. + * @return The response future result. + */ + CompletableFuture redirect(String url); + + + /** + * If true, the router chain has next handler. + * + * @return If true, the router chain has next handler. + */ + boolean hasNext(); + + /** + * Execute the next handler of the router chain. + * + * @return The handler future result. + */ + CompletableFuture next(); + + /** + * Get the HTTP server connection. + * + * @return The HTTP server connection. + */ + HttpServerConnection getConnection(); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpChannelGroup.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpChannelGroup.java new file mode 100644 index 000000000..ea291e140 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpChannelGroup.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.common.lifecycle.LifeCycle; +import kotlinx.coroutines.CoroutineDispatcher; + +import java.nio.channels.AsynchronousChannelGroup; + +/** + * The asynchronous channel and message thread group. It manages the IO and message threads. + * + * @author Pengtao Qiu + */ +public interface TcpChannelGroup extends LifeCycle { + + /** + * Get the asynchronous channel group. It manages the IO thread. + * + * @return The asynchronous channel group. + */ + AsynchronousChannelGroup getAsynchronousChannelGroup(); + + /** + * Get the coroutine dispatcher. It is the message handler thread pool. + * + * @param connectionId The connection id. + * @return The coroutine dispatcher. + */ + CoroutineDispatcher getDispatcher(int connectionId); + + /** + * Get the next connection id. It is auto increment. + * + * @return The next connection id. + */ + int getNextId(); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClient.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClient.java new file mode 100644 index 000000000..b076964e3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClient.java @@ -0,0 +1,114 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.common.lifecycle.LifeCycle; +import com.fireflysource.net.tcp.secure.SecureEngineFactory; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * The TCP net client. + * + * @author Pengtao Qiu + */ +public interface TcpClient extends LifeCycle { + + /** + * Set the TCP channel group. + * + * @param group The TCP channel group. + * @return The TCP client. + */ + TcpClient tcpChannelGroup(TcpChannelGroup group); + + /** + * Stop the TCP group when the TCP client stops. + * + * @param stop If true, stop the TCP group when the TCP client stops. + * @return The TCP client. + */ + TcpClient stopTcpChannelGroup(boolean stop); + + /** + * Set the TLS engine factory. + * + * @param secureEngineFactory The TLS engine factory. + * @return The TCP client. + */ + TcpClient secureEngineFactory(SecureEngineFactory secureEngineFactory); + + /** + * Enable the TLS protocol over the TCP connection. + * + * @return The TCP client. + */ + TcpClient enableSecureConnection(); + + /** + * Set the TCP idle timeout. The unit is second. + * + * @param timeout The TCP idle timeout. The unit is second. + * @return The TCP client. + */ + TcpClient timeout(Long timeout); + + /** + * Enable output buffer. + * + * @return The TCP client. + */ + TcpClient enableOutputBuffer(); + + /** + * Set output buffer size. + * + * @param bufferSize The output buffer size. + * @return The TCP client. + */ + TcpClient bufferSize(int bufferSize); + + /** + * Create a TCP connection. + * + * @param address The server address. + * @return The TCP connection. + */ + CompletableFuture connect(SocketAddress address); + + /** + * Create a TCP connection to the server. + * + * @param host The server host. + * @param port The server port. + * @return The TCP connection. + */ + default CompletableFuture connect(String host, int port) { + return connect(new InetSocketAddress(host, port)); + } + + /** + * If you enable TLS connection, tt creates a TLS connection and set the supported application layer protocols. + * + * @param address The server address. + * @param supportedProtocols The supported application layer protocols. + * @return The TCP connection. + */ + CompletableFuture connect(SocketAddress address, List supportedProtocols); + + /** + * If you enable TLS connection, tt creates a TLS connection using advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param address The server address. + * @param peerHost the non-authoritative name of the host. + * @param peerPort the non-authoritative port. + * @param supportedProtocols The supported application layer protocols. + * @return The TCP connection. + */ + CompletableFuture connect(SocketAddress address, String peerHost, int peerPort, List supportedProtocols); + + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClientConnectionFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClientConnectionFactory.java new file mode 100644 index 000000000..98cc22b64 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClientConnectionFactory.java @@ -0,0 +1,126 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.common.collection.CollectionUtils; +import com.fireflysource.common.lifecycle.AbstractLifeCycle; +import com.fireflysource.common.object.Assert; +import com.fireflysource.net.tcp.secure.SecureEngineFactory; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TcpClientConnectionFactory extends AbstractLifeCycle { + + private TcpChannelGroup tcpChannelGroup; + private boolean stopTcpChannelGroup; + private long timeout; + private SecureEngineFactory secureEngineFactory; + private TcpClient tcpClient; + private TcpClient secureTcpClient; + + public TcpClientConnectionFactory() { + } + + public TcpClientConnectionFactory(TcpChannelGroup tcpChannelGroup, boolean stopTcpChannelGroup, long timeout, SecureEngineFactory secureEngineFactory) { + this.tcpChannelGroup = tcpChannelGroup; + this.stopTcpChannelGroup = stopTcpChannelGroup; + this.timeout = timeout; + this.secureEngineFactory = secureEngineFactory; + } + + public TcpChannelGroup getTcpChannelGroup() { + return tcpChannelGroup; + } + + public void setTcpChannelGroup(TcpChannelGroup tcpChannelGroup) { + this.tcpChannelGroup = tcpChannelGroup; + } + + public boolean isStopTcpChannelGroup() { + return stopTcpChannelGroup; + } + + public void setStopTcpChannelGroup(boolean stopTcpChannelGroup) { + this.stopTcpChannelGroup = stopTcpChannelGroup; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public SecureEngineFactory getSecureEngineFactory() { + return secureEngineFactory; + } + + public void setSecureEngineFactory(SecureEngineFactory secureEngineFactory) { + this.secureEngineFactory = secureEngineFactory; + } + + public TcpClientConnectionFactory isStopTcpChannelGroup(boolean isStopTcpChannelGroup) { + this.stopTcpChannelGroup = isStopTcpChannelGroup; + return this; + } + + public TcpClientConnectionFactory timeout(long timeout) { + this.timeout = timeout; + return this; + } + + public TcpClientConnectionFactory tcpChannelGroup(TcpChannelGroup tcpChannelGroup) { + this.tcpChannelGroup = tcpChannelGroup; + return this; + } + + public TcpClientConnectionFactory secureEngineFactory(SecureEngineFactory secureEngineFactory) { + this.secureEngineFactory = secureEngineFactory; + return this; + } + + public CompletableFuture connect(InetSocketAddress inetSocketAddress, boolean secure) { + return connect(inetSocketAddress, secure, Collections.emptyList()); + } + + public CompletableFuture connect(InetSocketAddress inetSocketAddress, boolean secure, List supportedProtocols) { + CompletableFuture future; + if (secure) { + if (CollectionUtils.isEmpty(supportedProtocols)) { + future = secureTcpClient.connect(inetSocketAddress); + } else { + future = secureTcpClient.connect(inetSocketAddress, supportedProtocols); + } + } else { + future = tcpClient.connect(inetSocketAddress); + } + return future; + } + + @Override + protected void init() { + Assert.notNull(tcpChannelGroup, "The tcp channel group must be not null"); + tcpClient = TcpClientFactory + .create() + .tcpChannelGroup(tcpChannelGroup) + .stopTcpChannelGroup(stopTcpChannelGroup) + .timeout(timeout); + secureTcpClient = TcpClientFactory + .create() + .tcpChannelGroup(tcpChannelGroup) + .stopTcpChannelGroup(stopTcpChannelGroup) + .timeout(timeout) + .enableSecureConnection(); + if (secureEngineFactory != null) { + secureTcpClient.secureEngineFactory(secureEngineFactory); + } + } + + @Override + protected void destroy() { + tcpClient.stop(); + secureTcpClient.stop(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClientFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClientFactory.java new file mode 100644 index 000000000..c966b5e2d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpClientFactory.java @@ -0,0 +1,15 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.net.tcp.aio.AioTcpClient; +import com.fireflysource.net.tcp.aio.TcpConfig; + +abstract public class TcpClientFactory { + + public static TcpClient create() { + return new AioTcpClient(); + } + + public static TcpClient create(TcpConfig config) { + return new AioTcpClient(config); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpConnection.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpConnection.java new file mode 100644 index 000000000..c18b1a284 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpConnection.java @@ -0,0 +1,292 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.common.func.Callback; +import com.fireflysource.common.io.AsyncCloseable; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.Connection; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import static com.fireflysource.common.sys.Result.futureToConsumer; + +/** + * The TCP connection. It reads or writes messages using the TCP (or TLS over the TCP) protocol. + * + * @author Pengtao Qiu + */ +public interface TcpConnection extends Connection, ApplicationProtocolSelector, TcpCoroutineDispatcher, AsyncCloseable { + + Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** + * Register a connection close event callback. When the connection close, the framework will invoke this function. + * + * @param callback The connection close event callback. + * @return The current connection. + */ + TcpConnection onClose(Callback callback); + + /** + * Close the current connection and wait the remaining messages of the channel have been sent completely. + * + * @param result When the connection close, the framework will invoke this function. + * @return The current connection. + */ + TcpConnection close(Consumer> result); + + /** + * Close the current connection and wait the remaining messages of the channel have been sent completely. + * + * @return The future result. + */ + default CompletableFuture closeAsync() { + CompletableFuture future = new CompletableFuture<>(); + close(futureToConsumer(future)); + return future; + } + + /** + * Close the current connection immediately. The remaining messages of the channel will not be sent. + * + * @return The current connection. + */ + TcpConnection closeNow(); + + /** + * If return true, the connection input channel has been closed. You can't receive any messages from the remote endpoint. + * + * @return If return true, the connection input channel has been closed. + */ + boolean isShutdownInput(); + + /** + * If return true, the connection output channel has been closed. You can't send any messages to the remote endpoint. + * + * @return If return true, the connection output channel has been closed. You can't send any messages to the remote endpoint. + */ + boolean isShutdownOutput(); + + /** + * Shutdown the connection for reading without closing the connection. + * + * @return The current connection. + */ + TcpConnection shutdownInput(); + + /** + * Shutdown the connection for writing without closing the connection. + * + * @return The current connection. + */ + TcpConnection shutdownOutput(); + + /** + * Read data from the remote endpoint. + * + * @return The future data. + */ + CompletableFuture read(); + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffer The byte buffer. + * @param result The handler for consuming the result. + * @return The current connection. + */ + TcpConnection write(ByteBuffer byteBuffer, Consumer> result); + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffers The byte buffer array. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @param result The handler for consuming the result. + * @return The current connection. + */ + TcpConnection write(ByteBuffer[] byteBuffers, int offset, int length, Consumer> result); + + /** + * Write the data to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @param result The handler for consuming the result. + * @return The current connection. + */ + TcpConnection write(List byteBufferList, int offset, int length, Consumer> result); + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffer The byte buffer. + * @return The future for consuming the result. + */ + default CompletableFuture write(ByteBuffer byteBuffer) { + CompletableFuture future = new CompletableFuture<>(); + write(byteBuffer, futureToConsumer(future)); + return future; + } + + /** + * Write and flush data to the remote endpoint. + * + * @param byteBuffer The byte buffer. + * @return The future for consuming the result. + */ + default CompletableFuture writeAndFlush(ByteBuffer byteBuffer) { + return write(byteBuffer).thenCompose(len -> flush().thenApply(n -> len)); + } + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffers The byte buffer array. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The future for consuming the result. + */ + default CompletableFuture write(ByteBuffer[] byteBuffers, int offset, int length) { + CompletableFuture future = new CompletableFuture<>(); + write(byteBuffers, offset, length, futureToConsumer(future)); + return future; + } + + /** + * Write the data to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @return The future for consuming the result. + */ + default CompletableFuture write(List byteBufferList, int offset, int length) { + CompletableFuture future = new CompletableFuture<>(); + write(byteBufferList, offset, length, futureToConsumer(future)); + return future; + } + + /** + * Write and flush data to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @return The future for consuming the result. + */ + default CompletableFuture writeAndFlush(List byteBufferList, int offset, int length) { + return write(byteBufferList, offset, length).thenCompose(len -> flush().thenApply(n -> len)); + } + + /** + * Write the data to the remote endpoint. + * + * @param bytes The byte array. + * @param result The handler for consuming the result. + * @return The current connection. + */ + default TcpConnection write(byte[] bytes, Consumer> result) { + return write(ByteBuffer.wrap(bytes), result); + } + + /** + * Write the data to the remote endpoint. + * + * @param string The string. + * @param result The handler for consuming the result. + * @return The current connection. + */ + default TcpConnection write(String string, Consumer> result) { + return write(ByteBuffer.wrap(string.getBytes(DEFAULT_CHARSET)), result); + } + + /** + * Flush output buffer to remote endpoint. + * + * @param result When flush data to remote endpoint, the framework will invoke this function. + * @return The current connection. + */ + TcpConnection flush(Consumer> result); + + /** + * Flush output buffer to remote endpoint. + * + * @return The future result. + */ + default CompletableFuture flush() { + CompletableFuture future = new CompletableFuture<>(); + flush(futureToConsumer(future)); + return future; + } + + /** + * Get output buffer size. + * + * @return The output buffer size. + */ + int getBufferSize(); + + /** + * If you enable the TLS protocol, it returns true. + * + * @return If you enable the TLS protocol, it returns true. + */ + boolean isSecureConnection(); + + /** + * If you enable the TLS protocol, it presents the TLS engine is client mode or server mode. + * + * @return The TLS engine is client mode or server mode. + */ + boolean isClientMode(); + + /** + * If return true, the TLS engine completes the handshake stage. + * + * @return If return true, the TLS engine completes the handshake stage. + */ + boolean isHandshakeComplete(); + + /** + * Listen the TLS handshake complete event. If the TLS handshake has finished, the framework will invoke the callback function. + * + * @param result The value is the negotiated application layer protocol. + * @return The current connection. + */ + TcpConnection beginHandshake(Consumer> result); + + /** + * Listen the TLS handshake complete event. + * + * @return The value is the negotiated application layer protocol. + */ + default CompletableFuture beginHandshake() { + CompletableFuture future = new CompletableFuture<>(); + beginHandshake(futureToConsumer(future)); + return future; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpCoroutineDispatcher.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpCoroutineDispatcher.java new file mode 100644 index 000000000..256162cf9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpCoroutineDispatcher.java @@ -0,0 +1,31 @@ +package com.fireflysource.net.tcp; + +import kotlinx.coroutines.CompletableJob; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + +import java.util.concurrent.Executor; + +public interface TcpCoroutineDispatcher extends Executor { + + /** + * Get the coroutine dispatcher of this connection. One TCP connection is always in the same coroutine context. + * + * @return The coroutine dispatcher of this connection. + */ + CoroutineDispatcher getCoroutineDispatcher(); + + /** + * Get the coroutine scope of this connection. + * + * @return The coroutine scope. + */ + CoroutineScope getCoroutineScope(); + + /** + * Get the supervisor job. + * + * @return The supervisor job. + */ + CompletableJob getSupervisorJob(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpServer.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpServer.java new file mode 100644 index 000000000..33a587bb7 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpServer.java @@ -0,0 +1,136 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.common.lifecycle.LifeCycle; +import com.fireflysource.net.tcp.secure.SecureEngineFactory; +import kotlinx.coroutines.channels.Channel; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import java.util.function.Consumer; + +/** + * The TCP net server. + * + * @author Pengtao Qiu + */ +public interface TcpServer extends LifeCycle, Cloneable { + + /** + * Set the TCP channel group. + * + * @param group The TCP channel group. + * @return The TCP server. + */ + TcpServer tcpChannelGroup(TcpChannelGroup group); + + /** + * Stop the TCP group when the TCP server stops. + * + * @param stop If true, stop the TCP group when the TCP server stops. + * @return The TCP client. + */ + TcpServer stopTcpChannelGroup(boolean stop); + + /** + * Set the TLS engine factory. + * + * @param secureEngineFactory The TLS engine factory. + * @return The TCP server. + */ + TcpServer secureEngineFactory(SecureEngineFactory secureEngineFactory); + + /** + * The supported application layer protocols. + * + * @param supportedProtocols The supported application layer protocols. + * @return The TCP server. + */ + TcpServer supportedProtocols(List supportedProtocols); + + /** + * Create a TLS engine using advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param peerHost the non-authoritative name of the host. + * @return The TCP server. + */ + TcpServer peerHost(String peerHost); + + /** + * Create a TLS engine using advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param peerPort the non-authoritative port. + * @return The TCP server. + */ + TcpServer peerPort(int peerPort); + + /** + * Enable the TLS protocol over the TCP connection. + * + * @return The TCP server. + */ + TcpServer enableSecureConnection(); + + /** + * Set the TCP idle timeout. The unit is second. + * + * @param timeout The TCP idle timeout. Time unit is second. + * @return The TCP server. + */ + TcpServer timeout(Long timeout); + + /** + * Enable output buffer. + * + * @return The TCP server. + */ + TcpServer enableOutputBuffer(); + + /** + * Set output buffer size. + * + * @param bufferSize The output buffer size. + * @return The TCP server. + */ + TcpServer bufferSize(int bufferSize); + + /** + * Accept the client TCP connection. + * + * @param consumer Accept the connection callback. + * @return The TCP server. + */ + TcpServer onAccept(Consumer consumer); + + /** + * If you don't set a callback for the connection accepting event. The server will accept connection + * and send it to the channel, and then, you can receive the connection from this channel. + * + * @return The TCP connection channel. + */ + Channel getTcpConnectionChannel(); + + /** + * Bind a server TCP address + * + * @param address The server TCP address. + * @return The TCP server. + */ + TcpServer listen(SocketAddress address); + + /** + * Bind the server host and port. + * + * @param host The server host. + * @param port The server port. + * @return The TCP server. + */ + default TcpServer listen(String host, int port) { + return listen(new InetSocketAddress(host, port)); + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpServerFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpServerFactory.java new file mode 100644 index 000000000..9e4e01fb6 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/TcpServerFactory.java @@ -0,0 +1,15 @@ +package com.fireflysource.net.tcp; + +import com.fireflysource.net.tcp.aio.AioTcpServer; +import com.fireflysource.net.tcp.aio.TcpConfig; + +abstract public class TcpServerFactory { + + public static TcpServer create() { + return new AioTcpServer(); + } + + public static TcpServer create(TcpConfig config) { + return new AioTcpServer(config); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/WrappedTcpConnection.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/WrappedTcpConnection.java new file mode 100644 index 000000000..8241d4d17 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/WrappedTcpConnection.java @@ -0,0 +1,16 @@ +package com.fireflysource.net.tcp; + +/** + * The wrapped TCP connection. + * + * @author Pengtao Qiu + */ +public interface WrappedTcpConnection { + + /** + * Get the raw TCP connection. + * + * @return The raw TCP connection. + */ + TcpConnection getRawTcpConnection(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/exception/UnknownProtocolException.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/exception/UnknownProtocolException.java new file mode 100644 index 000000000..429834e5f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/exception/UnknownProtocolException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.tcp.exception; + +public class UnknownProtocolException extends RuntimeException { + + public UnknownProtocolException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/ApplicationProtocolSelector.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/ApplicationProtocolSelector.java new file mode 100644 index 000000000..7894950cf --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/ApplicationProtocolSelector.java @@ -0,0 +1,28 @@ +package com.fireflysource.net.tcp.secure; + +import java.util.List; + +/** + * The TLS application layer protocol negotiation. + * + * @author Pengtao Qiu + */ +public interface ApplicationProtocolSelector { + + /** + * The protocol negotiation result. + * + * @return The protocol negotiation result. + */ + default String getApplicationProtocol() { + return ""; + } + + /** + * The current connection supports the protocols. + * + * @return The current connection supports the protocols. + */ + List getSupportedApplicationProtocols(); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/DefaultSecureEngineFactorySelector.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/DefaultSecureEngineFactorySelector.java new file mode 100644 index 000000000..2c44b1d4c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/DefaultSecureEngineFactorySelector.java @@ -0,0 +1,59 @@ +package com.fireflysource.net.tcp.secure; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.JavaVersion; +import com.fireflysource.net.tcp.secure.conscrypt.NoCheckConscryptSSLContextFactory; +import com.fireflysource.net.tcp.secure.conscrypt.SelfSignedCertificateConscryptSSLContextFactory; +import com.fireflysource.net.tcp.secure.jdk.NoCheckOpenJdkSSLContextFactory; +import com.fireflysource.net.tcp.secure.jdk.SelfSignedCertificateOpenJdkSSLContextFactory; + +public class DefaultSecureEngineFactorySelector { + + public static SecureEngineFactory createSecureEngineFactory(boolean client) { + SecureEngineFactory secureEngineFactory; + if (JavaVersion.VERSION.getPlatform() < 9) { + if (JavaVersion.VERSION.getPlatform() == 8) { + String[] update = StringUtils.split(JavaVersion.VERSION.getVersion(), '_'); + if (update.length == 2) { + try { + int u = Integer.parseInt(update[1]); + if (u >= 222) { + secureEngineFactory = createOpenJdkSecureEngineFactory(client); + } else { + secureEngineFactory = createConscryptSecureEngineFactory(client); + } + } catch (Exception e) { + secureEngineFactory = createConscryptSecureEngineFactory(client); + } + } else { + secureEngineFactory = createConscryptSecureEngineFactory(client); + } + } else { + secureEngineFactory = createConscryptSecureEngineFactory(client); + } + } else { + secureEngineFactory = createOpenJdkSecureEngineFactory(client); + } + return secureEngineFactory; + } + + private static SecureEngineFactory createOpenJdkSecureEngineFactory(boolean client) { + SecureEngineFactory secureEngineFactory; + if (client) { + secureEngineFactory = new NoCheckOpenJdkSSLContextFactory(); + } else { + secureEngineFactory = new SelfSignedCertificateOpenJdkSSLContextFactory(); + } + return secureEngineFactory; + } + + private static SecureEngineFactory createConscryptSecureEngineFactory(boolean client) { + SecureEngineFactory secureEngineFactory; + if (client) { + secureEngineFactory = new NoCheckConscryptSSLContextFactory(); + } else { + secureEngineFactory = new SelfSignedCertificateConscryptSSLContextFactory(); + } + return secureEngineFactory; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/HandshakeResult.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/HandshakeResult.java new file mode 100644 index 000000000..a209560f9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/HandshakeResult.java @@ -0,0 +1,22 @@ +package com.fireflysource.net.tcp.secure; + +import java.nio.ByteBuffer; +import java.util.List; + +public interface HandshakeResult { + + /** + * Get the stashed application buffers during the handshake process. + * + * @return The stashed application buffers. + */ + List getStashedAppBuffers(); + + /** + * The application protocol negotiation result. + * + * @return The application protocol negotiation result. + */ + String getApplicationProtocol(); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/SecureEngine.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/SecureEngine.java new file mode 100644 index 000000000..bbc8308f9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/SecureEngine.java @@ -0,0 +1,112 @@ +package com.fireflysource.net.tcp.secure; + +import com.fireflysource.common.sys.Result; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static com.fireflysource.common.sys.Result.futureToConsumer; + +/** + * The TLS engine. It can encrypt or decrypt the message. + * + * @author Pengtao Qiu + */ +public interface SecureEngine extends Closeable, ApplicationProtocolSelector { + + /** + * If return true, the TLS engine is client mode. + * + * @return If return true, the TLS engine is client mode. + */ + boolean isClientMode(); + + /** + * If return true, the TLS handshake stage is complete. + * + * @return If return true, the TLS handshake stage is complete. + */ + boolean isHandshakeComplete(); + + /** + * Begin the TLS handshake. + * + * @param result The TLS handshake result. + */ + void beginHandshake(Consumer> result); + + /** + * Begin the TLS handshake. + * + * @return The future for consuming the TLS handshake result. + */ + default CompletableFuture beginHandshake() { + CompletableFuture future = new CompletableFuture<>(); + beginHandshake(futureToConsumer(future)); + return future; + } + + /** + * Need read data in the handshake process. + * + * @param supplier The data supplier. + * @return The secure engine. + */ + SecureEngine onHandshakeRead(Supplier> supplier); + + /** + * Need write data in the handshake process. + * + * @param function The write function. + * @return The secure engine. + */ + SecureEngine onHandshakeWrite(Function> function); + + /** + * Decrypt the cipher text to the plain text. + * + * @param byteBuffer The cipher text data. + * @return The cipher text byte buffer. + */ + ByteBuffer decrypt(ByteBuffer byteBuffer); + + /** + * Encrypt the plain text to the cipher text. + * + * @param byteBuffer The plain text data. + * @return The cipher text byte buffer. + */ + ByteBuffer encrypt(ByteBuffer byteBuffer); + + /** + * Encrypt the plain text to the cipher text. + * + * @param byteBuffers The plain text data. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The cipher text byte buffer. + */ + ByteBuffer encrypt(ByteBuffer[] byteBuffers, int offset, int length); + + /** + * Encrypt the plain text to the cipher text. + * + * @param byteBuffers The plain text data. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The cipher text byte buffer. + */ + ByteBuffer encrypt(List byteBuffers, int offset, int length); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/SecureEngineFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/SecureEngineFactory.java new file mode 100644 index 000000000..665571b25 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/SecureEngineFactory.java @@ -0,0 +1,63 @@ +package com.fireflysource.net.tcp.secure; + +import kotlinx.coroutines.CoroutineScope; + +import java.util.List; + +/** + * The TLS engine factory. + * + * @author Pengtao Qiu + */ +public interface SecureEngineFactory { + + /** + * Create a TLS engine. + * + * @param coroutineScope The coroutine scope. + * @param clientMode If true, the current connection is the client tcp connection. + * @param supportedProtocols The supported application layer protocols. + * @return The TLS engine. + */ + SecureEngine create(CoroutineScope coroutineScope, boolean clientMode, List supportedProtocols); + + /** + * Create a TLS engine using advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param coroutineScope The coroutine scope. + * @param clientMode If true, the current connection is the client tcp connection. + * @param peerHost the non-authoritative name of the host. + * @param peerPort the non-authoritative port. + * @param supportedProtocols The supported application layer protocols. + * @return The TLS engine. + */ + SecureEngine create(CoroutineScope coroutineScope, boolean clientMode, String peerHost, int peerPort, List supportedProtocols); + + /** + * Create a TLS engine by default coroutine scope. + * + * @param clientMode If true, the current connection is the client tcp connection. + * @param supportedProtocols The supported application layer protocols. + * @return The TLS engine. + */ + default SecureEngine create(boolean clientMode, List supportedProtocols) { + return create(null, clientMode, supportedProtocols); + } + + /** + * Create a TLS engine by default coroutine scope and advisory peer information. + * Applications using this factory method are providing hints for an internal session reuse strategy. + * Some cipher suites (such as Kerberos) require remote hostname information, in which case peerHost needs to be specified. + * + * @param clientMode If true, the current connection is the client tcp connection. + * @param peerHost the non-authoritative name of the host. + * @param peerPort the non-authoritative port. + * @param supportedProtocols The supported application layer protocols. + * @return The TLS engine. + */ + default SecureEngine create(boolean clientMode, String peerHost, int peerPort, List supportedProtocols) { + return create(null, clientMode, peerHost, peerPort, supportedProtocols); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/common/AbstractSecureEngineFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/common/AbstractSecureEngineFactory.java new file mode 100644 index 000000000..386af7c57 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/common/AbstractSecureEngineFactory.java @@ -0,0 +1,109 @@ +package com.fireflysource.net.tcp.secure.common; + +import com.fireflysource.common.coroutine.CommonCoroutinePoolKt; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import com.fireflysource.net.tcp.secure.SecureEngine; +import com.fireflysource.net.tcp.secure.SecureEngineFactory; +import kotlinx.coroutines.CoroutineScope; + +import javax.net.ssl.*; +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Optional; + +import static com.fireflysource.net.tcp.secure.utils.SecureUtils.KEY_MANAGER_FACTORY_TYPE; +import static com.fireflysource.net.tcp.secure.utils.SecureUtils.TRUST_MANAGER_FACTORY_TYPE; + +abstract public class AbstractSecureEngineFactory implements SecureEngineFactory { + + protected static final LazyLogger LOG = SystemLogger.create(AbstractSecureEngineFactory.class); + + public SSLContext getSSLContextWithManager(KeyManager[] km, TrustManager[] tm, SecureRandom random) + throws NoSuchAlgorithmException, KeyManagementException, NoSuchProviderException { + long start = System.currentTimeMillis(); + + final SSLContext sslContext = SSLContext.getInstance(getSecureProtocol(), getProviderName()); + sslContext.init(km, tm, random); + + long end = System.currentTimeMillis(); + String protocol = sslContext.getProtocol(); + long time = end - start; + logCreatingSSLContent(time, protocol); + return sslContext; + } + + private void logCreatingSSLContent(long time, String protocol) { + LOG.info("Created SSL context in time {}ms. TLS protocol: {}", time, protocol); + } + + public SSLContext getSSLContext(InputStream in, String keystorePassword, String keyPassword, String keyStoreType) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, + UnrecoverableKeyException, KeyManagementException, NoSuchProviderException { + return getSSLContext(in, keystorePassword, keyPassword, keyStoreType, null, null, null); + } + + public SSLContext getSSLContext(InputStream in, String keystorePassword, String keyPassword, + String keyStoreType, + String keyManagerFactoryType, String trustManagerFactoryType, String sslProtocol) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, + UnrecoverableKeyException, KeyManagementException, NoSuchProviderException { + long start = System.currentTimeMillis(); + final SSLContext sslContext; + + KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(in, keystorePassword != null ? keystorePassword.toCharArray() : null); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyManagerFactoryType == null ? KEY_MANAGER_FACTORY_TYPE : keyManagerFactoryType); + kmf.init(ks, keyPassword != null ? keyPassword.toCharArray() : null); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(trustManagerFactoryType == null ? TRUST_MANAGER_FACTORY_TYPE : trustManagerFactoryType); + tmf.init(ks); + + sslContext = SSLContext.getInstance(sslProtocol == null ? getSecureProtocol() : sslProtocol, getProviderName()); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + long end = System.currentTimeMillis(); + String protocol = sslContext.getProtocol(); + long time = end - start; + logCreatingSSLContent(time, protocol); + return sslContext; + } + + @Override + public SecureEngine create(CoroutineScope coroutineScope, boolean clientMode, List supportedProtocols) { + SSLEngine sslEngine = getSSLContext().createSSLEngine(); + sslEngine.setUseClientMode(clientMode); + ApplicationProtocolSelector selector = createApplicationProtocolSelector(sslEngine, supportedProtocols); + CoroutineScope scope = Optional.ofNullable(coroutineScope).orElseGet(CommonCoroutinePoolKt::getApplicationScope); + return createSecureEngine(scope, sslEngine, selector); + } + + @Override + public SecureEngine create(CoroutineScope coroutineScope, boolean clientMode, String peerHost, int peerPort, + List supportedProtocols) { + SSLEngine sslEngine = getSSLContext().createSSLEngine(peerHost, peerPort); + sslEngine.setUseClientMode(clientMode); + ApplicationProtocolSelector selector = createApplicationProtocolSelector(sslEngine, supportedProtocols); + CoroutineScope scope = Optional.ofNullable(coroutineScope).orElseGet(CommonCoroutinePoolKt::getApplicationScope); + return createSecureEngine(scope, sslEngine, selector); + } + + abstract public SSLContext getSSLContext(); + + abstract public String getSecureProtocol(); + + abstract public String getProviderName(); + + abstract public SecureEngine createSecureEngine( + CoroutineScope coroutineScope, + SSLEngine sslEngine, + ApplicationProtocolSelector applicationProtocolSelector); + + abstract public ApplicationProtocolSelector createApplicationProtocolSelector( + SSLEngine sslEngine, List supportedProtocolList); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/AbstractConscryptSecureEngineFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/AbstractConscryptSecureEngineFactory.java new file mode 100644 index 000000000..c388cb758 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/AbstractConscryptSecureEngineFactory.java @@ -0,0 +1,53 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import com.fireflysource.net.tcp.secure.SecureEngine; +import com.fireflysource.net.tcp.secure.common.AbstractSecureEngineFactory; +import kotlinx.coroutines.CoroutineScope; +import org.conscrypt.Conscrypt; + +import javax.net.ssl.SSLEngine; +import java.security.Provider; +import java.security.Security; +import java.util.List; + +/** + * @author Pengtao Qiu + */ +abstract public class AbstractConscryptSecureEngineFactory extends AbstractSecureEngineFactory { + + private static final String SECURE_PROTOCOL = "TLSv1.3"; + private static final String PROVIDER_NAME; + + static { + Provider provider = Conscrypt.newProvider(); + PROVIDER_NAME = provider.getName(); + Security.addProvider(provider); + LOG.info("Add Conscrypt security provider. info: {}, name: {}", provider.getInfo(), PROVIDER_NAME); + } + + @Override + public String getProviderName() { + return PROVIDER_NAME; + } + + @Override + public String getSecureProtocol() { + return SECURE_PROTOCOL; + } + + @Override + public SecureEngine createSecureEngine( + CoroutineScope coroutineScope, + SSLEngine sslEngine, + ApplicationProtocolSelector applicationProtocolSelector) { + return new ConscryptSecureEngine(coroutineScope, sslEngine, applicationProtocolSelector); + } + + @Override + public ApplicationProtocolSelector createApplicationProtocolSelector( + SSLEngine sslEngine, List supportedProtocolList) { + return new ConscryptApplicationProtocolSelector(sslEngine, supportedProtocolList); + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/ConscryptApplicationProtocolSelector.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/ConscryptApplicationProtocolSelector.java new file mode 100644 index 000000000..7454dfc34 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/ConscryptApplicationProtocolSelector.java @@ -0,0 +1,70 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import org.conscrypt.Conscrypt; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import java.util.List; +import java.util.Optional; + +/** + * @author Pengtao Qiu + */ +public class ConscryptApplicationProtocolSelector implements ApplicationProtocolSelector { + + private static final LazyLogger LOG = SystemLogger.create(ConscryptApplicationProtocolSelector.class); + + private final String[] supportedProtocols; + private final List supportedProtocolList; + private final SSLEngine sslEngine; + + public ConscryptApplicationProtocolSelector(SSLEngine sslEngine, List supportedProtocolList) { + this.supportedProtocolList = supportedProtocolList; + supportedProtocols = this.supportedProtocolList.toArray(StringUtils.EMPTY_STRING_ARRAY); + this.sslEngine = sslEngine; + if (sslEngine.getUseClientMode()) { + Conscrypt.setApplicationProtocols(sslEngine, supportedProtocols); + } else { + Conscrypt.setApplicationProtocolSelector(sslEngine, new Selector()); + } + } + + @Override + public String getApplicationProtocol() { + return Optional.ofNullable(Conscrypt.getApplicationProtocol(sslEngine)).orElse(""); + } + + @Override + public List getSupportedApplicationProtocols() { + return supportedProtocolList; + } + + private final class Selector extends org.conscrypt.ApplicationProtocolSelector { + + @Override + public String selectApplicationProtocol(SSLEngine sslEngine, List list) { + return select(list); + } + + @Override + public String selectApplicationProtocol(SSLSocket sslSocket, List list) { + return select(list); + } + + String select(List clientProtocols) { + if (clientProtocols != null) { + for (String p : supportedProtocols) { + if (clientProtocols.contains(p)) { + LOG.debug(() -> "ALPN local server selected protocol -> " + p); + return p; + } + } + } + return null; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/ConscryptSecureEngine.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/ConscryptSecureEngine.java new file mode 100644 index 000000000..1ad23ff5d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/ConscryptSecureEngine.java @@ -0,0 +1,21 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + +import com.fireflysource.net.tcp.secure.AbstractAsyncSecureEngine; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import kotlinx.coroutines.CoroutineScope; + +import javax.net.ssl.SSLEngine; + +/** + * @author Pengtao Qiu + */ +public class ConscryptSecureEngine extends AbstractAsyncSecureEngine { + + public ConscryptSecureEngine( + CoroutineScope coroutineScope, + SSLEngine sslEngine, + ApplicationProtocolSelector applicationProtocolSelector) { + super(coroutineScope, sslEngine, applicationProtocolSelector); + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/DefaultConscryptSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/DefaultConscryptSSLContextFactory.java new file mode 100644 index 000000000..8adb9c167 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/DefaultConscryptSSLContextFactory.java @@ -0,0 +1,24 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + +import javax.net.ssl.SSLContext; + +/** + * @author Pengtao Qiu + */ +public class DefaultConscryptSSLContextFactory extends AbstractConscryptSecureEngineFactory { + + private SSLContext sslContext; + + public DefaultConscryptSSLContextFactory() { + try { + sslContext = getSSLContextWithManager(null, null, null); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/FileConscryptSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/FileConscryptSSLContextFactory.java new file mode 100644 index 000000000..78ee372b4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/FileConscryptSSLContextFactory.java @@ -0,0 +1,36 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + +import javax.net.ssl.SSLContext; +import java.io.InputStream; + +/** + * @author Pengtao Qiu + */ +public class FileConscryptSSLContextFactory extends AbstractConscryptSecureEngineFactory { + + private SSLContext sslContext; + + public FileConscryptSSLContextFactory(String path, String keystorePassword, String keyPassword, String keyStoreType) { + this(FileConscryptSSLContextFactory.class.getClassLoader().getResourceAsStream(path), keystorePassword, keyPassword, + keyStoreType, null, null, null); + } + + public FileConscryptSSLContextFactory( + InputStream inputStream, String keystorePassword, String keyPassword, + String keyStoreType, + String keyManagerFactoryType, + String trustManagerFactoryType, + String sslProtocol) { + try (InputStream in = inputStream) { + sslContext = getSSLContext(in, keystorePassword, keyPassword, + keyStoreType, keyManagerFactoryType, trustManagerFactoryType, sslProtocol); + } catch (Exception e) { + LOG.error("get SSL context exception", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/NoCheckConscryptSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/NoCheckConscryptSSLContextFactory.java new file mode 100644 index 000000000..0a4699324 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/NoCheckConscryptSSLContextFactory.java @@ -0,0 +1,28 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + + +import com.fireflysource.net.tcp.secure.utils.SecureUtils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * @author Pengtao Qiu + */ +public class NoCheckConscryptSSLContextFactory extends AbstractConscryptSecureEngineFactory { + + private SSLContext sslContext; + + public NoCheckConscryptSSLContextFactory() { + try { + sslContext = getSSLContextWithManager(null, new TrustManager[]{SecureUtils.createX509TrustManagerNoCheck()}, null); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/SelfSignedCertificateConscryptSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/SelfSignedCertificateConscryptSSLContextFactory.java new file mode 100644 index 000000000..a604b13da --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/conscrypt/SelfSignedCertificateConscryptSSLContextFactory.java @@ -0,0 +1,30 @@ +package com.fireflysource.net.tcp.secure.conscrypt; + + +import com.fireflysource.net.tcp.secure.utils.SecureUtils; + +import javax.net.ssl.SSLContext; +import java.io.InputStream; + +import static com.fireflysource.net.tcp.secure.utils.SecureUtils.*; + +/** + * @author Pengtao Qiu + */ +public class SelfSignedCertificateConscryptSSLContextFactory extends AbstractConscryptSecureEngineFactory { + + private SSLContext sslContext; + + public SelfSignedCertificateConscryptSSLContextFactory() { + try (InputStream in = SecureUtils.getSelfSignedCertificate()) { + sslContext = getSSLContext(in, SELF_SIGNED_KEY_STORE_PASSWORD, SELF_SIGNED_KEY_PASSWORD, SELF_SIGNED_KEY_STORE_TYPE); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/exception/SecureNetException.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/exception/SecureNetException.java new file mode 100644 index 000000000..1306e45f7 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/exception/SecureNetException.java @@ -0,0 +1,14 @@ +package com.fireflysource.net.tcp.secure.exception; + +/** + * @author Pengtao Qiu + */ +public class SecureNetException extends IllegalStateException { + public SecureNetException(String msg) { + super(msg); + } + + public SecureNetException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/AbstractOpenJdkSecureEngineFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/AbstractOpenJdkSecureEngineFactory.java new file mode 100644 index 000000000..02ae96276 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/AbstractOpenJdkSecureEngineFactory.java @@ -0,0 +1,61 @@ +package com.fireflysource.net.tcp.secure.jdk; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.JavaVersion; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import com.fireflysource.net.tcp.secure.SecureEngine; +import com.fireflysource.net.tcp.secure.common.AbstractSecureEngineFactory; +import kotlinx.coroutines.CoroutineScope; +import org.openjsse.net.ssl.OpenJSSE; + +import javax.net.ssl.SSLEngine; +import java.security.Provider; +import java.security.Security; +import java.util.List; + +abstract public class AbstractOpenJdkSecureEngineFactory extends AbstractSecureEngineFactory { + + protected static final LazyLogger LOG = SystemLogger.create(AbstractOpenJdkSecureEngineFactory.class); + + private static final String SECURE_PROTOCOL = "TLSv1.3"; + private static final String PROVIDER_NAME; + + static { + if (JavaVersion.VERSION.getPlatform() < 9) { + Provider provider = new OpenJSSE(); + PROVIDER_NAME = provider.getName(); + Security.addProvider(provider); + LOG.info("Add Openjsse security provider. info: {}, name: {}", provider.getInfo(), PROVIDER_NAME); + } else { + PROVIDER_NAME = "SunJSSE"; + Provider provider = Security.getProvider(PROVIDER_NAME); + LOG.info("Select {} security provider. info: {}", PROVIDER_NAME, provider.getInfo()); + } + } + + @Override + public String getProviderName() { + return PROVIDER_NAME; + } + + @Override + public String getSecureProtocol() { + return SECURE_PROTOCOL; + } + + @Override + public SecureEngine createSecureEngine( + CoroutineScope coroutineScope, + SSLEngine sslEngine, + ApplicationProtocolSelector applicationProtocolSelector) { + return new OpenJdkSecureEngine(coroutineScope, sslEngine, applicationProtocolSelector); + } + + @Override + public ApplicationProtocolSelector createApplicationProtocolSelector( + SSLEngine sslEngine, List supportedProtocolList) { + return new OpenJdkApplicationProtocolSelector(sslEngine, supportedProtocolList); + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/DefaultOpenJdkSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/DefaultOpenJdkSSLContextFactory.java new file mode 100644 index 000000000..6c09368fc --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/DefaultOpenJdkSSLContextFactory.java @@ -0,0 +1,24 @@ +package com.fireflysource.net.tcp.secure.jdk; + +import javax.net.ssl.SSLContext; + +/** + * @author Pengtao Qiu + */ +public class DefaultOpenJdkSSLContextFactory extends AbstractOpenJdkSecureEngineFactory { + + private SSLContext sslContext; + + public DefaultOpenJdkSSLContextFactory() { + try { + sslContext = getSSLContextWithManager(null, null, null); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/FileOpenJdkSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/FileOpenJdkSSLContextFactory.java new file mode 100644 index 000000000..64d957f6d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/FileOpenJdkSSLContextFactory.java @@ -0,0 +1,37 @@ +package com.fireflysource.net.tcp.secure.jdk; + +import javax.net.ssl.SSLContext; +import java.io.InputStream; + +/** + * @author Pengtao Qiu + */ +public class FileOpenJdkSSLContextFactory extends AbstractOpenJdkSecureEngineFactory { + + private SSLContext sslContext; + + public FileOpenJdkSSLContextFactory(String path, String keystorePassword, String keyPassword, String keyStoreType) { + this(FileOpenJdkSSLContextFactory.class.getClassLoader().getResourceAsStream(path), + keystorePassword, keyPassword, + keyStoreType, null, null, null); + } + + public FileOpenJdkSSLContextFactory( + InputStream inputStream, String keystorePassword, String keyPassword, + String keyStoreType, + String keyManagerFactoryType, + String trustManagerFactoryType, + String sslProtocol) { + try (InputStream in = inputStream) { + sslContext = getSSLContext(in, keystorePassword, keyPassword, + keyStoreType, keyManagerFactoryType, trustManagerFactoryType, sslProtocol); + } catch (Exception e) { + LOG.error("get SSL context exception", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/NoCheckOpenJdkSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/NoCheckOpenJdkSSLContextFactory.java new file mode 100644 index 000000000..c9aca1a5e --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/NoCheckOpenJdkSSLContextFactory.java @@ -0,0 +1,28 @@ +package com.fireflysource.net.tcp.secure.jdk; + + +import com.fireflysource.net.tcp.secure.utils.SecureUtils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * @author Pengtao Qiu + */ +public class NoCheckOpenJdkSSLContextFactory extends AbstractOpenJdkSecureEngineFactory { + + private SSLContext sslContext; + + public NoCheckOpenJdkSSLContextFactory() { + try { + sslContext = getSSLContextWithManager(null, new TrustManager[]{SecureUtils.createX509TrustManagerNoCheck()}, null); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/OpenJdkApplicationProtocolSelector.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/OpenJdkApplicationProtocolSelector.java new file mode 100644 index 000000000..93ce77721 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/OpenJdkApplicationProtocolSelector.java @@ -0,0 +1,95 @@ +package com.fireflysource.net.tcp.secure.jdk; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.JavaVersion; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.BiFunction; + +public class OpenJdkApplicationProtocolSelector implements ApplicationProtocolSelector { + + private static final LazyLogger LOG = SystemLogger.create(OpenJdkApplicationProtocolSelector.class); + private static Method setApplicationProtocols; + private static Method setHandshakeApplicationProtocolSelector; + private static Method getApplicationProtocol; + + private final List supportedProtocolList; + private final SSLEngine sslEngine; + + static { + try { + if (JavaVersion.VERSION.getPlatform() < 9) { + setApplicationProtocols = org.openjsse.javax.net.ssl.SSLParameters.class.getMethod("setApplicationProtocols", String[].class); + Class sslEngineImpl = Class.forName("org.openjsse.sun.security.ssl.SSLEngineImpl"); + setHandshakeApplicationProtocolSelector = sslEngineImpl.getMethod("setHandshakeApplicationProtocolSelector", BiFunction.class); + getApplicationProtocol = sslEngineImpl.getMethod("getApplicationProtocol"); + } else { + setApplicationProtocols = SSLParameters.class.getMethod("setApplicationProtocols", String[].class); + setHandshakeApplicationProtocolSelector = SSLEngine.class.getMethod("setHandshakeApplicationProtocolSelector", BiFunction.class); + getApplicationProtocol = SSLEngine.class.getMethod("getApplicationProtocol"); + } + setApplicationProtocols.setAccessible(true); + setHandshakeApplicationProtocolSelector.setAccessible(true); + getApplicationProtocol.setAccessible(true); + } catch (Exception e) { + LOG.error("Init openjsse application protocol selector exception", e); + } + } + + public OpenJdkApplicationProtocolSelector(SSLEngine sslEngine, List supportedProtocolList) { + this.sslEngine = sslEngine; + this.supportedProtocolList = supportedProtocolList; + + try { + if (sslEngine.getUseClientMode()) { + SSLParameters parameters; + if (JavaVersion.VERSION.getPlatform() < 9) { + parameters = new org.openjsse.javax.net.ssl.SSLParameters(); + } else { + parameters = new SSLParameters(); + } + setApplicationProtocols.invoke(parameters, new Object[]{supportedProtocolList.toArray(StringUtils.EMPTY_STRING_ARRAY)}); + sslEngine.setSSLParameters(parameters); + + } else { + BiFunction, String> selector = (serverEngine, clientProtocols) -> { + if (clientProtocols != null) { + for (String p : this.supportedProtocolList) { + if (clientProtocols.contains(p)) { + LOG.debug(() -> "ALPN local server selected protocol -> " + p); + return p; + } + } + } + return null; + }; + setHandshakeApplicationProtocolSelector.invoke(sslEngine, selector); + } + } catch (Exception e) { + LOG.error("Init openjsse application protocol selector exception", e); + } + } + + @Override + public String getApplicationProtocol() { + String protocol; + try { + protocol = (String) getApplicationProtocol.invoke(sslEngine); + } catch (Exception e) { + LOG.error("Get application protocol exception", e); + protocol = null; + } + return protocol; + } + + @Override + public List getSupportedApplicationProtocols() { + return supportedProtocolList; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/OpenJdkSecureEngine.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/OpenJdkSecureEngine.java new file mode 100644 index 000000000..53607f2b1 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/OpenJdkSecureEngine.java @@ -0,0 +1,17 @@ +package com.fireflysource.net.tcp.secure.jdk; + +import com.fireflysource.net.tcp.secure.AbstractAsyncSecureEngine; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import kotlinx.coroutines.CoroutineScope; + +import javax.net.ssl.SSLEngine; + +public class OpenJdkSecureEngine extends AbstractAsyncSecureEngine { + + public OpenJdkSecureEngine( + CoroutineScope coroutineScope, + SSLEngine sslEngine, + ApplicationProtocolSelector applicationProtocolSelector) { + super(coroutineScope, sslEngine, applicationProtocolSelector); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/SelfSignedCertificateOpenJdkSSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/SelfSignedCertificateOpenJdkSSLContextFactory.java new file mode 100644 index 000000000..f9a9715b9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/jdk/SelfSignedCertificateOpenJdkSSLContextFactory.java @@ -0,0 +1,30 @@ +package com.fireflysource.net.tcp.secure.jdk; + + +import com.fireflysource.net.tcp.secure.utils.SecureUtils; + +import javax.net.ssl.SSLContext; +import java.io.InputStream; + +import static com.fireflysource.net.tcp.secure.utils.SecureUtils.*; + +/** + * @author Pengtao Qiu + */ +public class SelfSignedCertificateOpenJdkSSLContextFactory extends AbstractOpenJdkSecureEngineFactory { + + private SSLContext sslContext; + + public SelfSignedCertificateOpenJdkSSLContextFactory() { + try (InputStream in = SecureUtils.getSelfSignedCertificate()) { + sslContext = getSSLContext(in, SELF_SIGNED_KEY_STORE_PASSWORD, SELF_SIGNED_KEY_PASSWORD, SELF_SIGNED_KEY_STORE_TYPE); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/utils/SecureUtils.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/utils/SecureUtils.java new file mode 100644 index 000000000..649177d52 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/utils/SecureUtils.java @@ -0,0 +1,38 @@ +package com.fireflysource.net.tcp.secure.utils; + +import javax.net.ssl.X509TrustManager; +import java.io.InputStream; +import java.security.cert.X509Certificate; + +/** + * @author Pengtao Qiu + */ +abstract public class SecureUtils { + + public static final String SELF_SIGNED_KEY_STORE_TYPE = "jks"; + public static final String SELF_SIGNED_KEY_STORE_PASSWORD = "123456"; + public static final String SELF_SIGNED_KEY_PASSWORD = "654321"; + public static final String KEY_MANAGER_FACTORY_TYPE = "SunX509"; // // PKIX, SunX509 + public static final String TRUST_MANAGER_FACTORY_TYPE = "SunX509"; + + public static InputStream getSelfSignedCertificate() { + return SecureUtils.class.getClassLoader().getResourceAsStream("fireflyKeystore.jks"); + } + + public static X509TrustManager createX509TrustManagerNoCheck() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/AbstractWildflySecureEngineFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/AbstractWildflySecureEngineFactory.java new file mode 100644 index 000000000..339f98639 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/AbstractWildflySecureEngineFactory.java @@ -0,0 +1,46 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import com.fireflysource.net.tcp.secure.SecureEngine; +import com.fireflysource.net.tcp.secure.common.AbstractSecureEngineFactory; +import kotlinx.coroutines.CoroutineScope; +import org.wildfly.openssl.OpenSSLProvider; + +import javax.net.ssl.SSLEngine; +import java.util.List; + +abstract public class AbstractWildflySecureEngineFactory extends AbstractSecureEngineFactory { + + protected static final LazyLogger LOG = SystemLogger.create(AbstractWildflySecureEngineFactory.class); + + private static final String SECURE_PROTOCOL = "openssl.TLS"; + private static final String PROVIDER_NAME; + + static { + OpenSSLProvider.register(); + PROVIDER_NAME = OpenSSLProvider.INSTANCE.getName(); + LOG.info("Add wildfly openssl security provider. info: {}, name: {}", OpenSSLProvider.INSTANCE.getInfo(), PROVIDER_NAME); + } + + @Override + public String getSecureProtocol() { + return SECURE_PROTOCOL; + } + + @Override + public String getProviderName() { + return PROVIDER_NAME; + } + + @Override + public SecureEngine createSecureEngine(CoroutineScope coroutineScope, SSLEngine sslEngine, ApplicationProtocolSelector applicationProtocolSelector) { + return new WildflySecureEngine(coroutineScope, sslEngine, applicationProtocolSelector); + } + + @Override + public ApplicationProtocolSelector createApplicationProtocolSelector(SSLEngine sslEngine, List supportedProtocolList) { + return new WildflyOpenSSLApplicationProtocolSelector(sslEngine, supportedProtocolList); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/DefaultWildflySSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/DefaultWildflySSLContextFactory.java new file mode 100644 index 000000000..b9aee7dfb --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/DefaultWildflySSLContextFactory.java @@ -0,0 +1,20 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import javax.net.ssl.SSLContext; + +public class DefaultWildflySSLContextFactory extends AbstractWildflySecureEngineFactory { + private SSLContext sslContext; + + public DefaultWildflySSLContextFactory() { + try { + sslContext = getSSLContextWithManager(null, null, null); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/FileWildflySSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/FileWildflySSLContextFactory.java new file mode 100644 index 000000000..a6d2bdf4e --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/FileWildflySSLContextFactory.java @@ -0,0 +1,35 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import com.fireflysource.net.tcp.secure.jdk.FileOpenJdkSSLContextFactory; + +import javax.net.ssl.SSLContext; +import java.io.InputStream; + +public class FileWildflySSLContextFactory extends AbstractWildflySecureEngineFactory { + private SSLContext sslContext; + + public FileWildflySSLContextFactory(String path, String keystorePassword, String keyPassword, String keyStoreType) { + this(FileOpenJdkSSLContextFactory.class.getClassLoader().getResourceAsStream(path), + keystorePassword, keyPassword, + keyStoreType, null, null, null); + } + + public FileWildflySSLContextFactory( + InputStream inputStream, String keystorePassword, String keyPassword, + String keyStoreType, + String keyManagerFactoryType, + String trustManagerFactoryType, + String sslProtocol) { + try (InputStream in = inputStream) { + sslContext = getSSLContext(in, keystorePassword, keyPassword, + keyStoreType, keyManagerFactoryType, trustManagerFactoryType, sslProtocol); + } catch (Exception e) { + LOG.error("get SSL context exception", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/NoCheckWildflySSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/NoCheckWildflySSLContextFactory.java new file mode 100644 index 000000000..657598d0a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/NoCheckWildflySSLContextFactory.java @@ -0,0 +1,24 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import com.fireflysource.net.tcp.secure.utils.SecureUtils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +public class NoCheckWildflySSLContextFactory extends AbstractWildflySecureEngineFactory{ + + private SSLContext sslContext; + + public NoCheckWildflySSLContextFactory() { + try { + sslContext = getSSLContextWithManager(null, new TrustManager[]{SecureUtils.createX509TrustManagerNoCheck()}, null); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/SelfSignedCertificateWildflySSLContextFactory.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/SelfSignedCertificateWildflySSLContextFactory.java new file mode 100644 index 000000000..5cf58326a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/SelfSignedCertificateWildflySSLContextFactory.java @@ -0,0 +1,26 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import com.fireflysource.net.tcp.secure.utils.SecureUtils; + +import javax.net.ssl.SSLContext; +import java.io.InputStream; + +import static com.fireflysource.net.tcp.secure.utils.SecureUtils.*; + +public class SelfSignedCertificateWildflySSLContextFactory extends AbstractWildflySecureEngineFactory { + + private SSLContext sslContext; + + public SelfSignedCertificateWildflySSLContextFactory() { + try (InputStream in = SecureUtils.getSelfSignedCertificate()) { + sslContext = getSSLContext(in, SELF_SIGNED_KEY_STORE_PASSWORD, SELF_SIGNED_KEY_PASSWORD, SELF_SIGNED_KEY_STORE_TYPE); + } catch (Throwable e) { + LOG.error("get SSL context error", e); + } + } + + @Override + public SSLContext getSSLContext() { + return sslContext; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/WildflyOpenSSLApplicationProtocolSelector.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/WildflyOpenSSLApplicationProtocolSelector.java new file mode 100644 index 000000000..0778a2535 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/WildflyOpenSSLApplicationProtocolSelector.java @@ -0,0 +1,79 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; + +import javax.net.ssl.SSLEngine; +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.BiFunction; + +public class WildflyOpenSSLApplicationProtocolSelector implements ApplicationProtocolSelector { + + private static final LazyLogger LOG = SystemLogger.create(WildflyOpenSSLApplicationProtocolSelector.class); + private static Method setApplicationProtocols; + private static Method setHandshakeApplicationProtocolSelector; + private static Method getApplicationProtocol; + + private final SSLEngine sslEngine; + private final List supportedProtocolList; + + static { + try { + Class sslEngineImpl = Class.forName("org.wildfly.openssl.OpenSSLEngine"); + setApplicationProtocols = sslEngineImpl.getMethod("setApplicationProtocols", String[].class); + setHandshakeApplicationProtocolSelector = sslEngineImpl.getMethod("setHandshakeApplicationProtocolSelector", BiFunction.class); + getApplicationProtocol = sslEngineImpl.getMethod("getApplicationProtocol"); + setApplicationProtocols.setAccessible(true); + setHandshakeApplicationProtocolSelector.setAccessible(true); + getApplicationProtocol.setAccessible(true); + } catch (Exception e) { + LOG.error("Get wildfly openssl application protocol selector methods exception.", e); + } + } + + public WildflyOpenSSLApplicationProtocolSelector(SSLEngine sslEngine, List supportedProtocolList) { + this.sslEngine = sslEngine; + this.supportedProtocolList = supportedProtocolList; + + try { + if (sslEngine.getUseClientMode()) { + setApplicationProtocols.invoke(sslEngine, new Object[]{supportedProtocolList.toArray(StringUtils.EMPTY_STRING_ARRAY)}); + } else { + BiFunction, String> selector = (serverEngine, clientProtocols) -> { + if (clientProtocols != null) { + for (String p : this.supportedProtocolList) { + if (clientProtocols.contains(p)) { + LOG.debug(() -> "ALPN local server selected protocol -> " + p); + return p; + } + } + } + return null; + }; + setHandshakeApplicationProtocolSelector.invoke(sslEngine, selector); + } + } catch (Exception e) { + LOG.error("Init wildfly openssl application protocol selector exception.", e); + } + } + + @Override + public String getApplicationProtocol() { + String protocol; + try { + protocol = (String) getApplicationProtocol.invoke(sslEngine); + } catch (Exception e) { + LOG.error("Get application protocol exception", e); + protocol = null; + } + return protocol; + } + + @Override + public List getSupportedApplicationProtocols() { + return supportedProtocolList; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/WildflySecureEngine.java b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/WildflySecureEngine.java new file mode 100644 index 000000000..738d7dc23 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/tcp/secure/wildfly/WildflySecureEngine.java @@ -0,0 +1,17 @@ +package com.fireflysource.net.tcp.secure.wildfly; + +import com.fireflysource.net.tcp.secure.AbstractAsyncSecureEngine; +import com.fireflysource.net.tcp.secure.ApplicationProtocolSelector; +import kotlinx.coroutines.CoroutineScope; + +import javax.net.ssl.SSLEngine; + +public class WildflySecureEngine extends AbstractAsyncSecureEngine { + + public WildflySecureEngine( + CoroutineScope coroutineScope, + SSLEngine sslEngine, + ApplicationProtocolSelector applicationProtocolSelector) { + super(coroutineScope, sslEngine, applicationProtocolSelector); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/udp/UdpClient.java b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpClient.java new file mode 100644 index 000000000..bf26a524b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpClient.java @@ -0,0 +1,14 @@ +package com.fireflysource.net.udp; + +import java.net.SocketAddress; + +public interface UdpClient { + + /** + * Create a UDP connection. + * + * @param address The server address. + * @return The UDP connection. + */ + UdpConnection connect(SocketAddress address); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/udp/UdpConnection.java b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpConnection.java new file mode 100644 index 000000000..f8fe12ef2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpConnection.java @@ -0,0 +1,185 @@ +package com.fireflysource.net.udp; + +import com.fireflysource.common.io.AsyncCloseable; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.Connection; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import static com.fireflysource.common.sys.Result.futureToConsumer; + +/** + * The UDP connection. It reads or writes messages using the UDP (or DTLS over the UDP) protocol. + */ +public interface UdpConnection extends Connection, AsyncCloseable { + + Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** + * Read data from the remote endpoint. + * + * @return The future data. + */ + CompletableFuture read(); + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffer The byte buffer. + * @param result The handler for consuming the result. + * @return The current connection. + */ + UdpConnection write(ByteBuffer byteBuffer, Consumer> result); + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffers The byte buffer array. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @param result The handler for consuming the result. + * @return The current connection. + */ + UdpConnection write(ByteBuffer[] byteBuffers, int offset, int length, Consumer> result); + + /** + * Write the data to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @param result The handler for consuming the result. + * @return The current connection. + */ + UdpConnection write(List byteBufferList, int offset, int length, Consumer> result); + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffer The byte buffer. + * @return The future for consuming the result. + */ + default CompletableFuture write(ByteBuffer byteBuffer) { + CompletableFuture future = new CompletableFuture<>(); + write(byteBuffer, futureToConsumer(future)); + return future; + } + + /** + * Write and flush data to the remote endpoint. + * + * @param byteBuffer The byte buffer. + * @return The future for consuming the result. + */ + default CompletableFuture writeAndFlush(ByteBuffer byteBuffer) { + return write(byteBuffer).thenCompose(len -> flush().thenApply(n -> len)); + } + + /** + * Write the data to the remote endpoint. + * + * @param byteBuffers The byte buffer array. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBuffers.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBuffers.length - offset. + * @return The future for consuming the result. + */ + default CompletableFuture write(ByteBuffer[] byteBuffers, int offset, int length) { + CompletableFuture future = new CompletableFuture<>(); + write(byteBuffers, offset, length, futureToConsumer(future)); + return future; + } + + /** + * Write the data to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @return The future for consuming the result. + */ + default CompletableFuture write(List byteBufferList, int offset, int length) { + CompletableFuture future = new CompletableFuture<>(); + write(byteBufferList, offset, length, futureToConsumer(future)); + return future; + } + + /** + * Write and flush data to the remote endpoint. + * + * @param byteBufferList The byte buffer list. + * @param offset The offset within the buffer array of the first buffer into which + * bytes are to be transferred; must be non-negative and no larger than + * byteBufferList.length. + * @param length The maximum number of buffers to be accessed; must be non-negative + * and no larger than byteBufferList.length - offset. + * @return The future for consuming the result. + */ + default CompletableFuture writeAndFlush(List byteBufferList, int offset, int length) { + return write(byteBufferList, offset, length).thenCompose(len -> flush().thenApply(n -> len)); + } + + /** + * Write the data to the remote endpoint. + * + * @param bytes The byte array. + * @param result The handler for consuming the result. + * @return The current connection. + */ + default UdpConnection write(byte[] bytes, Consumer> result) { + return write(ByteBuffer.wrap(bytes), result); + } + + /** + * Write the data to the remote endpoint. + * + * @param string The string. + * @param result The handler for consuming the result. + * @return The current connection. + */ + default UdpConnection write(String string, Consumer> result) { + return write(ByteBuffer.wrap(string.getBytes(DEFAULT_CHARSET)), result); + } + + /** + * Flush output buffer to remote endpoint. + * + * @param result When flush data to remote endpoint, the framework will invoke this function. + * @return The current connection. + */ + UdpConnection flush(Consumer> result); + + /** + * Flush output buffer to remote endpoint. + * + * @return The future result. + */ + default CompletableFuture flush() { + CompletableFuture future = new CompletableFuture<>(); + flush(futureToConsumer(future)); + return future; + } + + /** + * Get output buffer size. + * + * @return The output buffer size. + */ + int getBufferSize(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/udp/UdpCoroutineDispatcher.java b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpCoroutineDispatcher.java new file mode 100644 index 000000000..be592635f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpCoroutineDispatcher.java @@ -0,0 +1,31 @@ +package com.fireflysource.net.udp; + +import kotlinx.coroutines.CompletableJob; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + +import java.util.concurrent.Executor; + +public interface UdpCoroutineDispatcher extends Executor { + + /** + * Get the coroutine dispatcher of this connection. One TCP connection is always in the same coroutine context. + * + * @return The coroutine dispatcher of this connection. + */ + CoroutineDispatcher getCoroutineDispatcher(); + + /** + * Get the coroutine scope of this connection. + * + * @return The coroutine scope. + */ + CoroutineScope getCoroutineScope(); + + /** + * Get the supervisor job. + * + * @return The supervisor job. + */ + CompletableJob getSupervisorJob(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/udp/UdpServer.java b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpServer.java new file mode 100644 index 000000000..e8b6935ed --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/udp/UdpServer.java @@ -0,0 +1,35 @@ +package com.fireflysource.net.udp; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.function.Consumer; + +public interface UdpServer { + + /** + * The UDP connection open event listener. + * + * @param consumer The UDP connection. + * @return The UDP server. + */ + UdpServer onOpen(Consumer consumer); + + /** + * Bind a server UDP address + * + * @param address The server UDP address. + * @return The UDP server. + */ + UdpServer listen(SocketAddress address); + + /** + * Bind a server UDP address + * + * @param host The server host. + * @param port The server port. + * @return The UDP server. + */ + default UdpServer listen(String host, int port) { + return listen(new InetSocketAddress(host, port)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/udp/exception/UdpAttachmentTypeException.java b/firefly-net/src/main/java/com/fireflysource/net/udp/exception/UdpAttachmentTypeException.java new file mode 100644 index 000000000..9459b951c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/udp/exception/UdpAttachmentTypeException.java @@ -0,0 +1,7 @@ +package com.fireflysource.net.udp.exception; + +public class UdpAttachmentTypeException extends RuntimeException { + public UdpAttachmentTypeException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientConnectionBuilder.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientConnectionBuilder.java new file mode 100644 index 000000000..f3ef9c6bf --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientConnectionBuilder.java @@ -0,0 +1,61 @@ +package com.fireflysource.net.websocket.client; + +import com.fireflysource.net.websocket.common.WebSocketConnection; +import com.fireflysource.net.websocket.common.WebSocketMessageHandler; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @author Pengtao Qiu + */ +public interface WebSocketClientConnectionBuilder { + + /** + * Set the websocket url. + * + * @param url The websocket url. + * @return The websocket client connection builder. + */ + WebSocketClientConnectionBuilder url(String url); + + /** + * Set the websocket policy. + * + * @param policy The websocket policy. + * @return The websocket client connection builder. + */ + WebSocketClientConnectionBuilder policy(WebSocketPolicy policy); + + /** + * Put the websocket extensions. + * + * @param extensions The websocket extensions. + * @return The websocket client connection builder. + */ + WebSocketClientConnectionBuilder extensions(List extensions); + + /** + * Put the websocket sub protocols. + * + * @param subProtocols The websocket sub protocols. + * @return The websocket client connection builder. + */ + WebSocketClientConnectionBuilder subProtocols(List subProtocols); + + /** + * Set the websocket message handler. + * + * @param handler The websocket handler. + * @return The websocket client connection builder. + */ + WebSocketClientConnectionBuilder onMessage(WebSocketMessageHandler handler); + + /** + * Create the websocket connection. + * + * @return The future websocket connection. + */ + CompletableFuture connect(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientConnectionManager.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientConnectionManager.java new file mode 100644 index 000000000..c1b3ecc77 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientConnectionManager.java @@ -0,0 +1,20 @@ +package com.fireflysource.net.websocket.client; + +import com.fireflysource.net.websocket.common.WebSocketConnection; + +import java.util.concurrent.CompletableFuture; + +/** + * @author Pengtao Qiu + */ +public interface WebSocketClientConnectionManager { + + /** + * Create a websocket connection. + * + * @param request The websocket connection request. + * @return The websocket connection future. + */ + CompletableFuture connect(WebSocketClientRequest request); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientRequest.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientRequest.java new file mode 100644 index 000000000..9b4956667 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/client/WebSocketClientRequest.java @@ -0,0 +1,58 @@ +package com.fireflysource.net.websocket.client; + +import com.fireflysource.net.websocket.common.WebSocketMessageHandler; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +import java.util.List; + +/** + * @author Pengtao Qiu + */ +public class WebSocketClientRequest { + + private String url; + private WebSocketPolicy policy; + private List extensions; + private List subProtocols; + private WebSocketMessageHandler handler; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public WebSocketPolicy getPolicy() { + return policy; + } + + public void setPolicy(WebSocketPolicy policy) { + this.policy = policy; + } + + public List getExtensions() { + return extensions; + } + + public void setExtensions(List extensions) { + this.extensions = extensions; + } + + public List getSubProtocols() { + return subProtocols; + } + + public void setSubProtocols(List subProtocols) { + this.subProtocols = subProtocols; + } + + public WebSocketMessageHandler getHandler() { + return handler; + } + + public void setHandler(WebSocketMessageHandler handler) { + this.handler = handler; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketConnection.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketConnection.java new file mode 100644 index 000000000..68417e228 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketConnection.java @@ -0,0 +1,97 @@ +package com.fireflysource.net.websocket.common; + +import com.fireflysource.net.Connection; +import com.fireflysource.net.tcp.TcpCoroutineDispatcher; +import com.fireflysource.net.websocket.common.extension.ExtensionFactory; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * The websocket connection. + * + * @author Pengtao Qiu + */ +public interface WebSocketConnection extends Connection, TcpCoroutineDispatcher, WebSocketConnectionState { + + /** + * Get the absolute URL of the WebSocket. + * + * @return The absolute URL of the WebSocket. + */ + String getUrl(); + + /** + * Get the websocket extensions. + * + * @return The websocket extensions. + */ + List getExtensions(); + + /** + * Get the websocket sub protocols. + * + * @return The websocket sub protocols. + */ + List getSubProtocols(); + + /** + * Get the policy that the connection is running under. + * + * @return the policy for the connection + */ + WebSocketPolicy getPolicy(); + + /** + * Get the websocket extension factory. + * + * @return The websocket extension factory. + */ + ExtensionFactory getExtensionFactory(); + + /** + * Generate random 4bytes mask key + * + * @return the mask key + */ + byte[] generateMask(); + + /** + * Send text message. + * + * @param text The text message. + * @return The future result. + */ + CompletableFuture sendText(String text); + + /** + * Send binary message. + * + * @param data The binary message. + * @return The future result. + */ + CompletableFuture sendData(ByteBuffer data); + + /** + * Send websocket frame. + * + * @param frame The websocket frame. + * @return The future result. + */ + CompletableFuture sendFrame(Frame frame); + + /** + * Set the websocket message handler. + * + * @param handler The websocket message handler. + */ + void setWebSocketMessageHandler(WebSocketMessageHandler handler); + + /** + * Begin to receive websocket data. + */ + void begin(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketConnectionState.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketConnectionState.java new file mode 100644 index 000000000..0f9f7d4f7 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketConnectionState.java @@ -0,0 +1,23 @@ +package com.fireflysource.net.websocket.common; + +import com.fireflysource.net.websocket.common.stream.ConnectionState; + +/** + * @author Pengtao Qiu + */ +public interface WebSocketConnectionState { + + boolean isInputAvailable(); + + boolean isOutputAvailable(); + + ConnectionState getConnectionState(); + + boolean isRemoteCloseInitiated(); + + boolean isLocalCloseInitiated(); + + boolean isAbnormalClose(); + + boolean isCleanClose(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketMessageHandler.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketMessageHandler.java new file mode 100644 index 000000000..f9c9a0290 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/WebSocketMessageHandler.java @@ -0,0 +1,16 @@ +package com.fireflysource.net.websocket.common; + +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.util.concurrent.CompletableFuture; + +/** + * The websocket message handler. + * + * @author Pengtao Qiu + */ +public interface WebSocketMessageHandler { + + CompletableFuture handle(Frame frame, WebSocketConnection connection); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/Parser.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/Parser.java new file mode 100644 index 000000000..29bcfca53 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/Parser.java @@ -0,0 +1,529 @@ +package com.fireflysource.net.websocket.common.decoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.decoder.payload.DeMaskProcessor; +import com.fireflysource.net.websocket.common.decoder.payload.PayloadProcessor; +import com.fireflysource.net.websocket.common.exception.MessageTooLargeException; +import com.fireflysource.net.websocket.common.exception.ProtocolException; +import com.fireflysource.net.websocket.common.exception.WebSocketException; +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.model.*; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.atomic.LongAdder; + +import static com.fireflysource.net.websocket.common.extension.AbstractExtension.getFlags; + +/** + * Parsing of a frames in WebSocket land. + */ +public class Parser { + private enum State { + START, + PAYLOAD_LEN, + PAYLOAD_LEN_BYTES, + MASK, + MASK_BYTES, + PAYLOAD + } + + private static final LazyLogger LOG = SystemLogger.create(Parser.class); + private final WebSocketPolicy policy; + + // Stats (where a message is defined as a WebSocket frame) + private final LongAdder messagesIn = new LongAdder(); + + // State specific + private State state = State.START; + private int cursor = 0; + // Frame + private WebSocketFrame frame; + private boolean priorDataFrame; + // payload specific + private ByteBuffer payload; + private int payloadLength; + private final PayloadProcessor maskProcessor = new DeMaskProcessor(); + + /** + * Is there an extension using RSV flag? + *

+ * + *

+     *   0100_0000 (0x40) = rsv1
+     *   0010_0000 (0x20) = rsv2
+     *   0001_0000 (0x10) = rsv3
+     * 
+ */ + private byte flagsInUse = 0x00; + + private IncomingFrames incomingFramesHandler; + + public Parser(WebSocketPolicy policy) { + this.policy = policy; + } + + private void assertSanePayloadLength(long len) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Payload Length: {} - {}", policy.getBehavior(), len, this); + } + + // Since we use ByteBuffer so often, having lengths over Integer.MAX_VALUE is really impossible. + if (len > Integer.MAX_VALUE) { + // OMG! Sanity Check! DO NOT WANT! Won't anyone think of the memory! + throw new MessageTooLargeException("[int-sane!] cannot handle payload lengths larger than " + Integer.MAX_VALUE); + } + + switch (frame.getOpCode()) { + case OpCode.CLOSE: + if (len == 1) { + throw new ProtocolException("Invalid close frame payload length, [" + payloadLength + "]"); + } + case OpCode.PING: + case OpCode.PONG: + if (len > ControlFrame.MAX_CONTROL_PAYLOAD) { + throw new ProtocolException("Invalid control frame payload length, [" + payloadLength + "] cannot exceed [" + ControlFrame.MAX_CONTROL_PAYLOAD + "]"); + } + break; + case OpCode.TEXT: + policy.assertValidTextMessageSize((int) len); + break; + case OpCode.BINARY: + policy.assertValidBinaryMessageSize((int) len); + break; + } + } + + public void configureFromExtensions(List extensions) { + flagsInUse = getFlags(extensions); + } + + public IncomingFrames getIncomingFramesHandler() { + return incomingFramesHandler; + } + + public WebSocketPolicy getPolicy() { + return policy; + } + + public boolean isRsv1InUse() { + return (flagsInUse & 0x40) != 0; + } + + public boolean isRsv2InUse() { + return (flagsInUse & 0x20) != 0; + } + + public boolean isRsv3InUse() { + return (flagsInUse & 0x10) != 0; + } + + protected void notifyFrame(final Frame f) throws WebSocketException { + if (LOG.isDebugEnabled()) + LOG.debug("{} Notify {}", policy.getBehavior(), getIncomingFramesHandler()); + + if (policy.getBehavior() == WebSocketBehavior.SERVER) { + /* Parsing on server. + * + * Then you MUST make sure all incoming frames are masked! + * + * Technically, this test is in violation of RFC-6455, Section 5.1 + * http://tools.ietf.org/html/rfc6455#section-5.1 + * + * But we can't trust the client at this point, so Server opts to close + * the connection as a Protocol error. + */ + if (!f.isMasked()) { + throw new ProtocolException("Client MUST mask all frames (RFC-6455: Section 5.1)"); + } + } else if (policy.getBehavior() == WebSocketBehavior.CLIENT) { + // Required by RFC-6455 / Section 5.1 + if (f.isMasked()) { + throw new ProtocolException("Server MUST NOT mask any frames (RFC-6455: Section 5.1)"); + } + } + + if (incomingFramesHandler == null) { + if (LOG.isDebugEnabled()) + LOG.debug("No IncomingFrames Handler to notify"); + return; + } + try { + incomingFramesHandler.incomingFrame(f); + } catch (WebSocketException e) { + throw e; + } catch (Throwable t) { + throw new WebSocketException(t); + } + } + + public void parse(ByteBuffer buffer) throws WebSocketException { + while (buffer.hasRemaining()) { + parseSingleFrame(buffer); + } + } + + public void parseSingleFrame(ByteBuffer buffer) throws WebSocketException { + if (buffer.remaining() <= 0) + return; + + try { + // attempt to parse a frame from the buffer + if (parseFrame(buffer)) { + if (LOG.isDebugEnabled()) + LOG.debug("{} Parsed Frame: {}", policy.getBehavior(), frame); + + messagesIn.increment(); + notifyFrame(frame); + if (frame.isDataFrame()) { + priorDataFrame = !frame.isFin(); + } + reset(); + } + } catch (Throwable t) { + buffer.position(buffer.limit()); // consume remaining + reset(); + // need to throw for proper close behavior in connection + if (t instanceof WebSocketException) + throw t; + else + throw new WebSocketException(t); + } + } + + private void reset() { + frame = null; + payload = null; + } + + /** + * Parse the base framing protocol buffer. + *

+ * Note the first byte (fin,rsv1,rsv2,rsv3,opcode) are parsed by the {@link Parser#parse(ByteBuffer)} method + *

+ * Not overridable + * + * @param buffer the buffer to parse from. + * @return true if done parsing base framing protocol and ready for parsing of the payload. false if incomplete parsing of base framing protocol. + */ + private boolean parseFrame(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Parsing {} bytes", policy.getBehavior(), buffer.remaining()); + } + while (buffer.hasRemaining()) { + switch (state) { + case START: { + // peek at byte + byte b = buffer.get(); + boolean fin = ((b & 0x80) != 0); + + byte opcode = (byte) (b & 0x0F); + + if (!OpCode.isKnown(opcode)) { + throw new ProtocolException("Unknown opcode: " + opcode); + } + + if (LOG.isDebugEnabled()) + LOG.debug("{} OpCode {}, fin={} rsv={}{}{}", + policy.getBehavior(), + OpCode.name(opcode), + fin, + (((b & 0x40) != 0) ? '1' : '.'), + (((b & 0x20) != 0) ? '1' : '.'), + (((b & 0x10) != 0) ? '1' : '.')); + + // base framing flags + switch (opcode) { + case OpCode.TEXT: + frame = new TextFrame(); + // data validation + if (priorDataFrame) { + throw new ProtocolException("Unexpected " + OpCode.name(opcode) + " frame, was expecting CONTINUATION"); + } + break; + case OpCode.BINARY: + frame = new BinaryFrame(); + // data validation + if (priorDataFrame) { + throw new ProtocolException("Unexpected " + OpCode.name(opcode) + " frame, was expecting CONTINUATION"); + } + break; + case OpCode.CONTINUATION: + frame = new ContinuationFrame(); + // continuation validation + if (!priorDataFrame) { + throw new ProtocolException("CONTINUATION frame without prior !FIN"); + } + // Be careful to use the original opcode + break; + case OpCode.CLOSE: + frame = new CloseFrame(); + // control frame validation + if (!fin) { + throw new ProtocolException("Fragmented Close Frame [" + OpCode.name(opcode) + "]"); + } + break; + case OpCode.PING: + frame = new PingFrame(); + // control frame validation + if (!fin) { + throw new ProtocolException("Fragmented Ping Frame [" + OpCode.name(opcode) + "]"); + } + break; + case OpCode.PONG: + frame = new PongFrame(); + // control frame validation + if (!fin) { + throw new ProtocolException("Fragmented Pong Frame [" + OpCode.name(opcode) + "]"); + } + break; + } + + frame.setFin(fin); + + // Are any flags set? + if ((b & 0x70) != 0) { + /* + * RFC 6455 Section 5.2 + * + * MUST be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the + * negotiated extensions defines the meaning of such a nonzero value, the receiving endpoint MUST _Fail the WebSocket Connection_. + */ + if ((b & 0x40) != 0) { + if (isRsv1InUse()) + frame.setRsv1(true); + else { + String err = "RSV1 not allowed to be set"; + if (LOG.isDebugEnabled()) { + LOG.debug(err + ": Remaining buffer: {}", BufferUtils.toDetailString(buffer)); + } + throw new ProtocolException(err); + } + } + if ((b & 0x20) != 0) { + if (isRsv2InUse()) + frame.setRsv2(true); + else { + String err = "RSV2 not allowed to be set"; + if (LOG.isDebugEnabled()) { + LOG.debug(err + ": Remaining buffer: {}", BufferUtils.toDetailString(buffer)); + } + throw new ProtocolException(err); + } + } + if ((b & 0x10) != 0) { + if (isRsv3InUse()) + frame.setRsv3(true); + else { + String err = "RSV3 not allowed to be set"; + if (LOG.isDebugEnabled()) { + LOG.debug(err + ": Remaining buffer: {}", BufferUtils.toDetailString(buffer)); + } + throw new ProtocolException(err); + } + } + } + + state = State.PAYLOAD_LEN; + break; + } + + case PAYLOAD_LEN: { + byte b = buffer.get(); + frame.setMasked((b & 0x80) != 0); + payloadLength = (byte) (0x7F & b); + + if (payloadLength == 127) // 0x7F + { + // length 8 bytes (extended payload length) + payloadLength = 0; + state = State.PAYLOAD_LEN_BYTES; + cursor = 8; + break; // continue onto next state + } else if (payloadLength == 126) // 0x7E + { + // length 2 bytes (extended payload length) + payloadLength = 0; + state = State.PAYLOAD_LEN_BYTES; + cursor = 2; + break; // continue onto next state + } + + assertSanePayloadLength(payloadLength); + if (frame.isMasked()) { + state = State.MASK; + } else { + // special case for empty payloads (no more bytes left in buffer) + if (payloadLength == 0) { + state = State.START; + return true; + } + + maskProcessor.reset(frame); + state = State.PAYLOAD; + } + + break; + } + + case PAYLOAD_LEN_BYTES: { + byte b = buffer.get(); + --cursor; + payloadLength |= (b & 0xFF) << (8 * cursor); + if (cursor == 0) { + assertSanePayloadLength(payloadLength); + if (frame.isMasked()) { + state = State.MASK; + } else { + // special case for empty payloads (no more bytes left in buffer) + if (payloadLength == 0) { + state = State.START; + return true; + } + + maskProcessor.reset(frame); + state = State.PAYLOAD; + } + } + break; + } + + case MASK: { + byte[] m = new byte[4]; + frame.setMask(m); + if (buffer.remaining() >= 4) { + buffer.get(m, 0, 4); + // special case for empty payloads (no more bytes left in buffer) + if (payloadLength == 0) { + state = State.START; + return true; + } + + maskProcessor.reset(frame); + state = State.PAYLOAD; + } else { + state = State.MASK_BYTES; + cursor = 4; + } + break; + } + + case MASK_BYTES: { + byte b = buffer.get(); + frame.getMask()[4 - cursor] = b; + --cursor; + if (cursor == 0) { + // special case for empty payloads (no more bytes left in buffer) + if (payloadLength == 0) { + state = State.START; + return true; + } + + maskProcessor.reset(frame); + state = State.PAYLOAD; + } + break; + } + + case PAYLOAD: { + frame.assertValid(); + if (parsePayload(buffer)) { + // special check for close + if (frame.getOpCode() == OpCode.CLOSE) { + // TODO: yuck. Don't create an object to do validation checks! + new CloseInfo(frame); + } + state = State.START; + // we have a frame! + return true; + } + break; + } + } + } + + return false; + } + + /** + * Implementation specific parsing of a payload + * + * @param buffer the payload buffer + * @return true if payload is done reading, false if incomplete + */ + private boolean parsePayload(ByteBuffer buffer) { + if (payloadLength == 0) { + return true; + } + + if (buffer.hasRemaining()) { + // Create a small window of the incoming buffer to work with. + // this should only show the payload itself, and not any more + // bytes that could belong to the start of the next frame. + int bytesSoFar = payload == null ? 0 : payload.position(); + int bytesExpected = payloadLength - bytesSoFar; + int bytesAvailable = buffer.remaining(); + int windowBytes = Math.min(bytesAvailable, bytesExpected); + int limit = buffer.limit(); + buffer.limit(buffer.position() + windowBytes); + ByteBuffer window = buffer.slice(); + buffer.limit(limit); + buffer.position(buffer.position() + window.remaining()); + + if (LOG.isDebugEnabled()) { + LOG.debug("{} Window: {}", policy.getBehavior(), BufferUtils.toDetailString(window)); + } + + maskProcessor.process(window); + + if (window.remaining() == payloadLength) { + // We have the whole content, no need to copy. + frame.setPayload(window); + return true; + } else { + if (payload == null) { + payload = BufferUtils.allocate(payloadLength); + BufferUtils.clearToFill(payload); + } + // Copy the payload. + payload.put(window); + + if (payload.position() == payloadLength) { + BufferUtils.flipToFlush(payload, 0); + frame.setPayload(payload); + return true; + } + } + } + return false; + } + + public void setIncomingFramesHandler(IncomingFrames incoming) { + this.incomingFramesHandler = incoming; + } + + public long getMessagesIn() { + return messagesIn.longValue(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Parser@").append(Integer.toHexString(hashCode())); + builder.append("["); + if (incomingFramesHandler == null) { + builder.append("NO_HANDLER"); + } else { + builder.append(incomingFramesHandler.getClass().getSimpleName()); + } + builder.append(",s=").append(state); + builder.append(",c=").append(cursor); + builder.append(",len=").append(payloadLength); + builder.append(",f=").append(frame); + // builder.append(",p=").append(policy); + builder.append("]"); + return builder.toString(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/payload/DeMaskProcessor.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/payload/DeMaskProcessor.java new file mode 100644 index 000000000..7be90db76 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/payload/DeMaskProcessor.java @@ -0,0 +1,53 @@ +package com.fireflysource.net.websocket.common.decoder.payload; + +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.nio.ByteBuffer; + +public class DeMaskProcessor implements PayloadProcessor { + private byte[] maskBytes; + private int maskInt; + private int maskOffset; + + @Override + public void process(ByteBuffer payload) { + if (maskBytes == null) { + return; + } + + int maskInt = this.maskInt; + int start = payload.position(); + int end = payload.limit(); + int offset = this.maskOffset; + int remaining; + while ((remaining = end - start) > 0) { + if (remaining >= 4 && (offset & 3) == 0) { + payload.putInt(start, payload.getInt(start) ^ maskInt); + start += 4; + offset += 4; + } else { + payload.put(start, (byte) (payload.get(start) ^ maskBytes[offset & 3])); + ++start; + ++offset; + } + } + maskOffset = offset; + } + + public void reset(byte[] mask) { + this.maskBytes = mask; + int maskInt = 0; + if (mask != null) { + for (byte maskByte : mask) { + maskInt = (maskInt << 8) + (maskByte & 0xFF); + } + } + this.maskInt = maskInt; + this.maskOffset = 0; + } + + @Override + public void reset(Frame frame) { + reset(frame.getMask()); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/payload/PayloadProcessor.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/payload/PayloadProcessor.java new file mode 100644 index 000000000..c12ddadcb --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/decoder/payload/PayloadProcessor.java @@ -0,0 +1,22 @@ +package com.fireflysource.net.websocket.common.decoder.payload; + + +import com.fireflysource.net.websocket.common.exception.BadPayloadException; +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.nio.ByteBuffer; + +/** + * Process the payload (for demasking, validating, etc..) + */ +public interface PayloadProcessor { + /** + * Used to process payloads for in the spec. + * + * @param payload the payload to process + * @throws BadPayloadException the exception when the payload fails to validate properly + */ + void process(ByteBuffer payload); + + void reset(Frame frame); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/encoder/Generator.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/encoder/Generator.java new file mode 100644 index 000000000..d7db4e6ba --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/encoder/Generator.java @@ -0,0 +1,335 @@ +package com.fireflysource.net.websocket.common.encoder; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.exception.ProtocolException; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.*; + +import java.nio.ByteBuffer; +import java.util.List; + +import static com.fireflysource.net.websocket.common.extension.AbstractExtension.getFlags; + +/** + * Generating a frame in WebSocket land. + *

+ *

+ *    0                   1                   2                   3
+ *    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *   +-+-+-+-+-------+-+-------------+-------------------------------+
+ *   |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
+ *   |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
+ *   |N|V|V|V|       |S|             |   (if payload len==126/127)   |
+ *   | |1|2|3|       |K|             |                               |
+ *   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ *   |     Extended payload length continued, if payload len == 127  |
+ *   + - - - - - - - - - - - - - - - +-------------------------------+
+ *   |                               |Masking-key, if MASK set to 1  |
+ *   +-------------------------------+-------------------------------+
+ *   | Masking-key (continued)       |          Payload Data         |
+ *   +-------------------------------- - - - - - - - - - - - - - - - +
+ *   :                     Payload Data continued ...                :
+ *   + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ *   |                     Payload Data continued ...                |
+ *   +---------------------------------------------------------------+
+ * 
+ */ +public class Generator { + /** + * The overhead (maximum) for a framing header. Assuming a maximum sized payload with masking key. + */ + public static final int MAX_HEADER_LENGTH = 28; + + private final WebSocketBehavior behavior; + private final boolean validating; + private final boolean readOnly; + + /** + * Are any flags in use + *

+ *

+ *

+     *   0100_0000 (0x40) = rsv1
+     *   0010_0000 (0x20) = rsv2
+     *   0001_0000 (0x10) = rsv3
+     * 
+ */ + private byte flagsInUse = 0x00; + + /** + * Construct Generator with provided policy and bufferPool + * + * @param policy the policy to use + */ + public Generator(WebSocketPolicy policy) { + this(policy, true, false); + } + + /** + * Construct Generator with provided policy and bufferPool + * + * @param policy the policy to use + * @param validating true to enable RFC frame validation + */ + public Generator(WebSocketPolicy policy, boolean validating) { + this(policy, validating, false); + } + + /** + * Construct Generator with provided policy and bufferPool + * + * @param policy the policy to use + * @param validating true to enable RFC frame validation + * @param readOnly true if generator is to treat frames as read-only and not modify them. Useful for debugging purposes, but not generally for runtime use. + */ + public Generator(WebSocketPolicy policy, boolean validating, boolean readOnly) { + this.behavior = policy.getBehavior(); + this.validating = validating; + this.readOnly = readOnly; + } + + public void assertFrameValid(Frame frame) { + if (!validating) { + return; + } + + /* + * RFC 6455 Section 5.2 + * + * MUST be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the negotiated + * extensions defines the meaning of such a nonzero value, the receiving endpoint MUST _Fail the WebSocket Connection_. + */ + if (frame.isRsv1() && !isRsv1InUse()) { + throw new ProtocolException("RSV1 not allowed to be set"); + } + + if (frame.isRsv2() && !isRsv2InUse()) { + throw new ProtocolException("RSV2 not allowed to be set"); + } + + if (frame.isRsv3() && !isRsv3InUse()) { + throw new ProtocolException("RSV3 not allowed to be set"); + } + + if (OpCode.isControlFrame(frame.getOpCode())) { + /* + * RFC 6455 Section 5.5 + * + * All control frames MUST have a payload length of 125 bytes or less and MUST NOT be fragmented. + */ + if (frame.getPayloadLength() > 125) { + throw new ProtocolException("Invalid control frame payload length"); + } + + if (!frame.isFin()) { + throw new ProtocolException("Control Frames must be FIN=true"); + } + + /* + * RFC 6455 Section 5.5.1 + * + * close frame payload is specially formatted which is checked in CloseInfo + */ + if (frame.getOpCode() == OpCode.CLOSE) { + + ByteBuffer payload = frame.getPayload(); + if (payload != null) { + new CloseInfo(payload, true); + } + } + } + } + + public void configureFromExtensions(List extensions) { + flagsInUse = getFlags(extensions); + } + + public ByteBuffer generateHeaderBytes(Frame frame) { + ByteBuffer buffer = BufferUtils.allocate(MAX_HEADER_LENGTH); + generateHeaderBytes(frame, buffer); + return buffer; + } + + public void generateHeaderBytes(Frame frame, ByteBuffer buffer) { + int p = BufferUtils.flipToFill(buffer); + + // we need a framing header + assertFrameValid(frame); + + /* + * start the generation process + */ + byte b = 0x00; + + // Setup fin thru opcode + if (frame.isFin()) { + b |= 0x80; // 1000_0000 + } + + // Set the flags + if (frame.isRsv1()) { + b |= 0x40; // 0100_0000 + } + if (frame.isRsv2()) { + b |= 0x20; // 0010_0000 + } + if (frame.isRsv3()) { + b |= 0x10; // 0001_0000 + } + + // NOTE: using .getOpCode() here, not .getType().getOpCode() for testing reasons + byte opcode = frame.getOpCode(); + + if (frame.getOpCode() == OpCode.CONTINUATION) { + // Continuations are not the same OPCODE + opcode = OpCode.CONTINUATION; + } + + b |= opcode & 0x0F; + + buffer.put(b); + + // is masked + b = (frame.isMasked() ? (byte) 0x80 : (byte) 0x00); + + // payload lengths + int payloadLength = frame.getPayloadLength(); + + /* + * if length is over 65535 then its a 7 + 64 bit length + */ + if (payloadLength > 0xFF_FF) { + // we have a 64 bit length + b |= 0x7F; + buffer.put(b); // indicate 8 byte length + buffer.put((byte) 0); // + buffer.put((byte) 0); // anything over an + buffer.put((byte) 0); // int is just + buffer.put((byte) 0); // insane! + buffer.put((byte) ((payloadLength >> 24) & 0xFF)); + buffer.put((byte) ((payloadLength >> 16) & 0xFF)); + buffer.put((byte) ((payloadLength >> 8) & 0xFF)); + buffer.put((byte) (payloadLength & 0xFF)); + } + /* + * if payload is greater that 126 we have a 7 + 16 bit length + */ + else if (payloadLength >= 0x7E) { + b |= 0x7E; + buffer.put(b); // indicate 2 byte length + buffer.put((byte) (payloadLength >> 8)); + buffer.put((byte) (payloadLength & 0xFF)); + } + /* + * we have a 7 bit length + */ + else { + b |= (payloadLength & 0x7F); + buffer.put(b); + } + + // masking key + if (frame.isMasked() && !readOnly) { + byte[] mask = frame.getMask(); + buffer.put(mask); + int maskInt = 0; + for (byte maskByte : mask) + maskInt = (maskInt << 8) + (maskByte & 0xFF); + + // perform data masking here + ByteBuffer payload = frame.getPayload(); + if ((payload != null) && (payload.remaining() > 0)) { + int maskOffset = 0; + int start = payload.position(); + int end = payload.limit(); + int remaining; + while ((remaining = end - start) > 0) { + if (remaining >= 4) { + payload.putInt(start, payload.getInt(start) ^ maskInt); + start += 4; + } else { + payload.put(start, (byte) (payload.get(start) ^ mask[maskOffset & 3])); + ++start; + ++maskOffset; + } + } + } + } + + BufferUtils.flipToFlush(buffer, p); + } + + /** + * Generate the whole frame (header + payload copy) into a single ByteBuffer. + *

+ * Note: This is slow, moves lots of memory around. Only use this if you must (such as in unit testing). + * + * @param frame the frame to generate + * @param buf the buffer to output the generated frame to + */ + public void generateWholeFrame(Frame frame, ByteBuffer buf) { + buf.put(generateHeaderBytes(frame)); + if (frame.hasPayload()) { + if (readOnly) { + buf.put(frame.getPayload().slice()); + } else { + buf.put(frame.getPayload()); + } + } + } + + public void setRsv1InUse(boolean rsv1InUse) { + if (readOnly) { + throw new RuntimeException("Not allowed to modify read-only frame"); + } + flagsInUse = (byte) ((flagsInUse & 0xBF) | (rsv1InUse ? 0x40 : 0x00)); + } + + public void setRsv2InUse(boolean rsv2InUse) { + if (readOnly) { + throw new RuntimeException("Not allowed to modify read-only frame"); + } + flagsInUse = (byte) ((flagsInUse & 0xDF) | (rsv2InUse ? 0x20 : 0x00)); + } + + public void setRsv3InUse(boolean rsv3InUse) { + if (readOnly) { + throw new RuntimeException("Not allowed to modify read-only frame"); + } + flagsInUse = (byte) ((flagsInUse & 0xEF) | (rsv3InUse ? 0x10 : 0x00)); + } + + public boolean isRsv1InUse() { + return (flagsInUse & 0x40) != 0; + } + + public boolean isRsv2InUse() { + return (flagsInUse & 0x20) != 0; + } + + public boolean isRsv3InUse() { + return (flagsInUse & 0x10) != 0; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Generator["); + builder.append(behavior); + if (validating) { + builder.append(",validating"); + } + if (isRsv1InUse()) { + builder.append(",+rsv1"); + } + if (isRsv2InUse()) { + builder.append(",+rsv2"); + } + if (isRsv3InUse()) { + builder.append(",+rsv3"); + } + builder.append("]"); + return builder.toString(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/BadPayloadException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/BadPayloadException.java new file mode 100644 index 000000000..59dba6c1a --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/BadPayloadException.java @@ -0,0 +1,24 @@ +package com.fireflysource.net.websocket.common.exception; + +import com.fireflysource.net.websocket.common.model.StatusCode; + +/** + * Exception to terminate the connection because it has received data within a frame payload that was not consistent with the requirements of that frame + * payload. (eg: not UTF-8 in a text frame, or a unexpected data seen by an extension) + * + * @see StatusCode#BAD_PAYLOAD + */ +@SuppressWarnings("serial") +public class BadPayloadException extends CloseException { + public BadPayloadException(String message) { + super(StatusCode.BAD_PAYLOAD, message); + } + + public BadPayloadException(String message, Throwable t) { + super(StatusCode.BAD_PAYLOAD, message, t); + } + + public BadPayloadException(Throwable t) { + super(StatusCode.BAD_PAYLOAD, t); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/CloseException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/CloseException.java new file mode 100644 index 000000000..ccf4c29d9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/CloseException.java @@ -0,0 +1,25 @@ +package com.fireflysource.net.websocket.common.exception; + +@SuppressWarnings("serial") +public class CloseException extends WebSocketException { + private int statusCode; + + public CloseException(int closeCode, String message) { + super(message); + this.statusCode = closeCode; + } + + public CloseException(int closeCode, String message, Throwable cause) { + super(message, cause); + this.statusCode = closeCode; + } + + public CloseException(int closeCode, Throwable cause) { + super(cause); + this.statusCode = closeCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/EncodingAcceptHashKeyException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/EncodingAcceptHashKeyException.java new file mode 100644 index 000000000..606c8797f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/EncodingAcceptHashKeyException.java @@ -0,0 +1,15 @@ +package com.fireflysource.net.websocket.common.exception; + +/** + * @author Pengtao Qiu + */ +public class EncodingAcceptHashKeyException extends RuntimeException { + + public EncodingAcceptHashKeyException(String message) { + super(message); + } + + public EncodingAcceptHashKeyException(String message, Throwable e) { + super(message, e); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/MessageTooLargeException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/MessageTooLargeException.java new file mode 100644 index 000000000..79256cf4b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/MessageTooLargeException.java @@ -0,0 +1,23 @@ +package com.fireflysource.net.websocket.common.exception; + +import com.fireflysource.net.websocket.common.model.StatusCode; + +/** + * Exception when a message is too large for the internal buffers occurs and should trigger a connection close. + * + * @see StatusCode#MESSAGE_TOO_LARGE + */ +@SuppressWarnings("serial") +public class MessageTooLargeException extends CloseException { + public MessageTooLargeException(String message) { + super(StatusCode.MESSAGE_TOO_LARGE, message); + } + + public MessageTooLargeException(String message, Throwable t) { + super(StatusCode.MESSAGE_TOO_LARGE, message, t); + } + + public MessageTooLargeException(Throwable t) { + super(StatusCode.MESSAGE_TOO_LARGE, t); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/NextIncomingFramesNotSetException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/NextIncomingFramesNotSetException.java new file mode 100644 index 000000000..d710d2e87 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/NextIncomingFramesNotSetException.java @@ -0,0 +1,11 @@ +package com.fireflysource.net.websocket.common.exception; + +/** + * @author Pengtao Qiu + */ +public class NextIncomingFramesNotSetException extends RuntimeException { + + public NextIncomingFramesNotSetException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/ProtocolException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/ProtocolException.java new file mode 100644 index 000000000..46fc59a41 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/ProtocolException.java @@ -0,0 +1,21 @@ +package com.fireflysource.net.websocket.common.exception; + +import com.fireflysource.net.websocket.common.model.StatusCode; + +/** + * Per spec, a protocol error should result in a Close frame of status code 1002 (PROTOCOL_ERROR) + */ +@SuppressWarnings("serial") +public class ProtocolException extends CloseException { + public ProtocolException(String message) { + super(StatusCode.PROTOCOL, message); + } + + public ProtocolException(String message, Throwable t) { + super(StatusCode.PROTOCOL, message, t); + } + + public ProtocolException(Throwable t) { + super(StatusCode.PROTOCOL, t); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/UpgradeWebSocketConnectionException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/UpgradeWebSocketConnectionException.java new file mode 100644 index 000000000..e0f87e5ea --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/UpgradeWebSocketConnectionException.java @@ -0,0 +1,11 @@ +package com.fireflysource.net.websocket.common.exception; + +/** + * @author Pengtao Qiu + */ +public class UpgradeWebSocketConnectionException extends RuntimeException { + + public UpgradeWebSocketConnectionException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/WebSocketException.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/WebSocketException.java new file mode 100644 index 000000000..5b165dc15 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/exception/WebSocketException.java @@ -0,0 +1,23 @@ +package com.fireflysource.net.websocket.common.exception; + +/** + * A recoverable exception within the websocket framework. + */ +@SuppressWarnings("serial") +public class WebSocketException extends RuntimeException { + public WebSocketException() { + super(); + } + + public WebSocketException(String message) { + super(message); + } + + public WebSocketException(String message, Throwable cause) { + super(message, cause); + } + + public WebSocketException(Throwable cause) { + super(cause); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/AbstractExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/AbstractExtension.java new file mode 100644 index 000000000..bdc54d653 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/AbstractExtension.java @@ -0,0 +1,144 @@ +package com.fireflysource.net.websocket.common.extension; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.Result; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.*; + +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +@SuppressWarnings("unused") +public abstract class AbstractExtension implements Extension { + + private static final LazyLogger log = SystemLogger.create(AbstractExtension.class); + + private WebSocketPolicy policy; + private ExtensionConfig config; + private OutgoingFrames nextOutgoing; + private IncomingFrames nextIncoming; + + public AbstractExtension() { + } + + public void dump(Appendable out, String indent) throws IOException { + // incoming + dumpWithHeading(out, indent, "incoming", this.nextIncoming); + dumpWithHeading(out, indent, "outgoing", this.nextOutgoing); + } + + protected void dumpWithHeading(Appendable out, String indent, String heading, Object bean) throws IOException { + out.append(indent).append(" +- "); + out.append(heading).append(" : "); + out.append(bean.toString()); + } + + @Override + public ExtensionConfig getConfig() { + return config; + } + + @Override + public String getName() { + return config.getName(); + } + + public IncomingFrames getNextIncoming() { + return nextIncoming; + } + + public OutgoingFrames getNextOutgoing() { + return nextOutgoing; + } + + public WebSocketPolicy getPolicy() { + return policy; + } + + /** + * Used to indicate that the extension makes use of the RSV1 bit of the base websocket framing. + *

+ * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV1. + * + * @return true if extension uses RSV1 for its own purposes. + */ + @Override + public boolean isRsv1User() { + return false; + } + + /** + * Used to indicate that the extension makes use of the RSV2 bit of the base websocket framing. + *

+ * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV2. + * + * @return true if extension uses RSV2 for its own purposes. + */ + @Override + public boolean isRsv2User() { + return false; + } + + /** + * Used to indicate that the extension makes use of the RSV3 bit of the base websocket framing. + *

+ * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV3. + * + * @return true if extension uses RSV3 for its own purposes. + */ + @Override + public boolean isRsv3User() { + return false; + } + + protected void nextIncomingFrame(Frame frame) { + log.debug("nextIncomingFrame({})", frame); + this.nextIncoming.incomingFrame(frame); + } + + protected void nextOutgoingFrame(Frame frame, Consumer> result) { + log.debug("nextOutgoingFrame({})", frame); + this.nextOutgoing.outgoingFrame(frame, result); + } + + public void setConfig(ExtensionConfig config) { + this.config = config; + } + + @Override + public void setNextIncomingFrames(IncomingFrames nextIncoming) { + this.nextIncoming = nextIncoming; + } + + @Override + public void setNextOutgoingFrames(OutgoingFrames nextOutgoing) { + this.nextOutgoing = nextOutgoing; + } + + public void setPolicy(WebSocketPolicy policy) { + this.policy = policy; + } + + public static byte getFlags(List extensions) { + byte flags = 0x00; + for (Extension ext : extensions) { + if (ext.isRsv1User()) { + flags = (byte) (flags | 0x40); + } + if (ext.isRsv2User()) { + flags = (byte) (flags | 0x20); + } + if (ext.isRsv3User()) { + flags = (byte) (flags | 0x10); + } + } + return flags; + } + + @Override + public String toString() { + return String.format("%s[%s]", this.getClass().getSimpleName(), config.getParameterizedName()); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/ExtensionFactory.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/ExtensionFactory.java new file mode 100644 index 000000000..b3a6315e4 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/ExtensionFactory.java @@ -0,0 +1,66 @@ +package com.fireflysource.net.websocket.common.extension; + + +import com.fireflysource.common.collection.CollectionUtils; +import com.fireflysource.net.websocket.common.extension.compress.DeflateFrameExtension; +import com.fireflysource.net.websocket.common.extension.compress.PerMessageDeflateExtension; +import com.fireflysource.net.websocket.common.extension.compress.XWebkitDeflateFrameExtension; +import com.fireflysource.net.websocket.common.extension.fragment.FragmentExtension; +import com.fireflysource.net.websocket.common.extension.identity.IdentityExtension; +import com.fireflysource.net.websocket.common.model.Extension; +import com.fireflysource.net.websocket.common.model.ExtensionConfig; + +import java.util.*; + +@SuppressWarnings("unused") +public abstract class ExtensionFactory implements Iterable> { + private static final ServiceLoader extensionLoader = ServiceLoader.load(Extension.class); + private final Map> availableExtensions; + + public ExtensionFactory() { + availableExtensions = new HashMap<>(); + for (Extension ext : extensionLoader) { + if (ext != null) { + availableExtensions.put(ext.getName(), ext.getClass()); + } + } + if (CollectionUtils.isEmpty(availableExtensions)) { + availableExtensions.put(new DeflateFrameExtension().getName(), DeflateFrameExtension.class); + availableExtensions.put(new PerMessageDeflateExtension().getName(), PerMessageDeflateExtension.class); + availableExtensions.put(new XWebkitDeflateFrameExtension().getName(), XWebkitDeflateFrameExtension.class); + availableExtensions.put(new IdentityExtension().getName(), IdentityExtension.class); + availableExtensions.put(new FragmentExtension().getName(), FragmentExtension.class); + } + } + + public Map> getAvailableExtensions() { + return availableExtensions; + } + + public Class getExtension(String name) { + return availableExtensions.get(name); + } + + public Set getExtensionNames() { + return availableExtensions.keySet(); + } + + public boolean isAvailable(String name) { + return availableExtensions.containsKey(name); + } + + @Override + public Iterator> iterator() { + return availableExtensions.values().iterator(); + } + + public abstract Extension newInstance(ExtensionConfig config); + + public void register(String name, Class extension) { + availableExtensions.put(name, extension); + } + + public void unregister(String name) { + availableExtensions.remove(name); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/WebSocketExtensionFactory.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/WebSocketExtensionFactory.java new file mode 100644 index 000000000..b13815c6d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/WebSocketExtensionFactory.java @@ -0,0 +1,33 @@ +package com.fireflysource.net.websocket.common.extension; + + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.exception.WebSocketException; +import com.fireflysource.net.websocket.common.model.Extension; +import com.fireflysource.net.websocket.common.model.ExtensionConfig; + +public class WebSocketExtensionFactory extends ExtensionFactory { + + @Override + public Extension newInstance(ExtensionConfig config) { + if (config == null) { + return null; + } + + String name = config.getName(); + if (!StringUtils.hasText(name)) { + return null; + } + + Class extClass = getExtension(name); + if (extClass == null) { + return null; + } + + try { + return extClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new WebSocketException("Cannot instantiate extension: " + extClass, e); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/ByteAccumulator.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/ByteAccumulator.java new file mode 100644 index 000000000..f416abf44 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/ByteAccumulator.java @@ -0,0 +1,48 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.exception.MessageTooLargeException; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class ByteAccumulator { + private final List chunks = new ArrayList<>(); + private final int maxSize; + private int length = 0; + + public ByteAccumulator(int maxOverallBufferSize) { + this.maxSize = maxOverallBufferSize; + } + + public void copyChunk(byte[] buf, int offset, int length) { + if (this.length + length > maxSize) { + String err = String.format("Resulting message size [%,d] is too large for configured max of [%,d]", this.length + length, maxSize); + throw new MessageTooLargeException(err); + } + + byte[] copy = new byte[length - offset]; + System.arraycopy(buf, offset, copy, 0, length); + + chunks.add(copy); + this.length += length; + } + + public int getLength() { + return length; + } + + public void transferTo(ByteBuffer buffer) { + if (buffer.remaining() < length) { + throw new IllegalArgumentException(String.format("Not enough space in ByteBuffer remaining [%d] for accumulated buffers length [%d]", + buffer.remaining(), length)); + } + + int position = buffer.position(); + for (byte[] chunk : chunks) { + buffer.put(chunk, 0, chunk.length); + } + BufferUtils.flipToFlush(buffer, position); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/CompressExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/CompressExtension.java new file mode 100644 index 000000000..721f58938 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/CompressExtension.java @@ -0,0 +1,502 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +import com.fireflysource.common.concurrent.AutoLock; +import com.fireflysource.common.concurrent.IteratingCallback; +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.Result; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.extension.AbstractExtension; +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import java.util.zip.ZipException; + +import static com.fireflysource.common.coroutine.CommonCoroutinePoolKt.getApplicationCleaner; + +public abstract class CompressExtension extends AbstractExtension { + + private static final LazyLogger LOG = SystemLogger.create(CompressExtension.class); + + protected static final byte[] TAIL_BYTES = new byte[]{0x00, 0x00, (byte) 0xFF, (byte) 0xFF}; + protected static final ByteBuffer TAIL_BYTES_BUF = ByteBuffer.wrap(TAIL_BYTES); + + /** + * Never drop tail bytes 0000FFFF, from any frame type + */ + protected static final int TAIL_DROP_NEVER = 0; + /** + * Always drop tail bytes 0000FFFF, from all frame types + */ + protected static final int TAIL_DROP_ALWAYS = 1; + /** + * Only drop tail bytes 0000FFFF, from fin==true frames + */ + protected static final int TAIL_DROP_FIN_ONLY = 2; + + /** + * Always set RSV flag, on all frame types + */ + protected static final int RSV_USE_ALWAYS = 0; + /** + * Only set RSV flag on first frame in multi-frame messages. + *

+ * Note: this automatically means no-continuation frames have the RSV bit set + */ + protected static final int RSV_USE_ONLY_FIRST = 1; + + /** + * Inflater / Decompressed Buffer Size + */ + protected static final int INFLATE_BUFFER_SIZE = 8 * 1024; + + /** + * Deflater / Inflater: Maximum Input Buffer Size + */ + protected static final int INPUT_MAX_BUFFER_SIZE = 8 * 1024; + + /** + * Inflater : Output Buffer Size + */ + private static final int DECOMPRESS_BUF_SIZE = 8 * 1024; + + private final AutoLock lock = new AutoLock(); + private final Queue entries = new ArrayDeque<>(); + private final IteratingCallback flusher = new Flusher(); + private final Deflater deflaterImpl; + private final Inflater inflaterImpl; + protected AtomicInteger decompressCount = new AtomicInteger(0); + private int tailDrop = TAIL_DROP_NEVER; + private int rsvUse = RSV_USE_ALWAYS; + + protected CompressExtension() { + tailDrop = getTailDropMode(); + rsvUse = getRsvUseMode(); + deflaterImpl = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + inflaterImpl = new Inflater(true); + getApplicationCleaner().register(this, new CompressExtensionCleanTask(deflaterImpl, inflaterImpl)); + } + + public static class CompressExtensionCleanTask implements Runnable { + + private final Deflater deflaterImpl; + private final Inflater inflaterImpl; + + public CompressExtensionCleanTask(Deflater deflaterImpl, Inflater inflaterImpl) { + this.deflaterImpl = deflaterImpl; + this.inflaterImpl = inflaterImpl; + } + + @Override + public void run() { + deflaterImpl.end(); + inflaterImpl.end(); + } + } + + public Deflater getDeflater() { + return deflaterImpl; + } + + public Inflater getInflater() { + return inflaterImpl; + } + + /** + * Indicates use of RSV1 flag for indicating deflation is in use. + */ + @Override + public boolean isRsv1User() { + return true; + } + + /** + * Return the mode of operation for dropping (or keeping) tail bytes in frames generated by compress (outgoing) + * + * @return either {@link #TAIL_DROP_ALWAYS}, {@link #TAIL_DROP_FIN_ONLY}, or {@link #TAIL_DROP_NEVER} + */ + abstract int getTailDropMode(); + + /** + * Return the mode of operation for RSV flag use in frames generate by compress (outgoing) + * + * @return either {@link #RSV_USE_ALWAYS} or {@link #RSV_USE_ONLY_FIRST} + */ + abstract int getRsvUseMode(); + + protected void forwardIncoming(Frame frame, ByteAccumulator accumulator) { + DataFrame newFrame; + switch (frame.getType()) { + case TEXT: + newFrame = new TextFrame(frame); + break; + case BINARY: + newFrame = new BinaryFrame(frame); + break; + case CONTINUATION: + newFrame = new ContinuationFrame(frame); + break; + default: + newFrame = new DataFrame(frame); + break; + } + // Unset RSV1 since it's not compressed anymore. + newFrame.setRsv1(false); + + ByteBuffer buffer = BufferUtils.allocate(accumulator.getLength()); + BufferUtils.flipToFill(buffer); + accumulator.transferTo(buffer); + newFrame.setPayload(buffer); + nextIncomingFrame(newFrame); + } + + protected ByteAccumulator newByteAccumulator() { + int maxSize = Math.max(getPolicy().getMaxTextMessageSize(), getPolicy().getMaxBinaryMessageSize()); + return new ByteAccumulator(maxSize); + } + + protected void decompress(ByteAccumulator accumulator, ByteBuffer buf) throws DataFormatException { + if ((buf == null) || (!buf.hasRemaining())) { + return; + } + byte[] output = new byte[DECOMPRESS_BUF_SIZE]; + + Inflater inflater = getInflater(); + + while (buf.hasRemaining() && inflater.needsInput()) { + if (!supplyInput(inflater, buf)) { + LOG.debug("Needed input, but no buffer could supply input"); + return; + } + + int read; + while ((read = inflater.inflate(output)) >= 0) { + if (read == 0) { + LOG.debug("Decompress: read 0 {}", toDetail(inflater)); + break; + } else { + // do something with output + if (LOG.isDebugEnabled()) { + LOG.debug("Decompressed {} bytes: {}", read, toDetail(inflater)); + } + + accumulator.copyChunk(output, 0, read); + } + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Decompress: exiting {}", toDetail(inflater)); + } + } + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + // We use a queue and an IteratingCallback to handle concurrency. + // We must compress and write atomically, otherwise the compression + // context on the other end gets confused. + + if (flusher.isFailed()) { + notifyCallbackFailure(result, new ZipException()); + return; + } + + FrameEntry entry = new FrameEntry(frame, result); + if (LOG.isDebugEnabled()) + LOG.debug("Queuing {}", entry); + offerEntry(entry); + flusher.iterate(); + } + + private void offerEntry(FrameEntry entry) { + lock.lock(() -> entries.offer(entry)); + } + + private FrameEntry pollEntry() { + return lock.lock(entries::poll); + } + + protected void notifyCallbackSuccess(Consumer> result) { + try { + if (result != null) + result.accept(Result.SUCCESS); + } catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("Exception while notifying success", x); + } + } + + protected void notifyCallbackFailure(Consumer> result, Throwable failure) { + try { + if (result != null) + result.accept(Result.createFailedResult(failure)); + } catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("Exception while notifying failure", x); + } + } + + private static boolean supplyInput(Inflater inflater, ByteBuffer buf) { + if (buf == null || buf.remaining() <= 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("No data left left to supply to Inflater"); + } + return false; + } + + byte[] input; + int inputOffset; + int len; + + if (buf.hasArray()) { + // no need to create a new byte buffer, just return this one. + len = buf.remaining(); + input = buf.array(); + inputOffset = buf.position() + buf.arrayOffset(); + buf.position(buf.position() + len); + } else { + // Only create an return byte buffer that is reasonable in size + len = Math.min(INPUT_MAX_BUFFER_SIZE, buf.remaining()); + input = new byte[len]; + inputOffset = 0; + buf.get(input, 0, len); + } + + inflater.setInput(input, inputOffset, len); + if (LOG.isDebugEnabled()) { + LOG.debug("Supplied {} input bytes: {}", input.length, toDetail(inflater)); + } + return true; + } + + private static boolean supplyInput(Deflater deflater, ByteBuffer buf) { + if (buf == null || buf.remaining() <= 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("No data left left to supply to Deflater"); + } + return false; + } + + byte[] input; + int inputOffset; + int len; + + if (buf.hasArray()) { + // no need to create a new byte buffer, just return this one. + len = buf.remaining(); + input = buf.array(); + inputOffset = buf.position() + buf.arrayOffset(); + buf.position(buf.position() + len); + } else { + // Only create an return byte buffer that is reasonable in size + len = Math.min(INPUT_MAX_BUFFER_SIZE, buf.remaining()); + input = new byte[len]; + inputOffset = 0; + buf.get(input, 0, len); + } + + deflater.setInput(input, inputOffset, len); + if (LOG.isDebugEnabled()) { + LOG.debug("Supplied {} input bytes: {}", input.length, toDetail(deflater)); + } + return true; + } + + private static String toDetail(Inflater inflater) { + return String.format("Inflater[finished=%b,read=%d,written=%d,remaining=%d,in=%d,out=%d]", inflater.finished(), inflater.getBytesRead(), + inflater.getBytesWritten(), inflater.getRemaining(), inflater.getTotalIn(), inflater.getTotalOut()); + } + + private static String toDetail(Deflater deflater) { + return String.format("Deflater[finished=%b,read=%d,written=%d,in=%d,out=%d]", deflater.finished(), deflater.getBytesRead(), deflater.getBytesWritten(), + deflater.getTotalIn(), deflater.getTotalOut()); + } + + public static boolean endsWithTail(ByteBuffer buf) { + if ((buf == null) || (buf.remaining() < TAIL_BYTES.length)) { + return false; + } + int limit = buf.limit(); + for (int i = TAIL_BYTES.length; i > 0; i--) { + if (buf.get(limit - i) != TAIL_BYTES[TAIL_BYTES.length - i]) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + private static class FrameEntry { + private final Frame frame; + private final Consumer> result; + + private FrameEntry(Frame frame, Consumer> result) { + this.frame = frame; + this.result = result; + } + + @Override + public String toString() { + return frame.toString(); + } + } + + private class Flusher extends IteratingCallback { + private FrameEntry current; + private boolean finished = true; + + @Override + protected Action process() throws Exception { + if (finished) { + current = pollEntry(); + LOG.debug("Processing {}", current); + if (current == null) + return Action.IDLE; + deflate(current); + } else { + compress(current, false); + } + return Action.SCHEDULED; + } + + private void deflate(FrameEntry entry) { + Frame frame = entry.frame; + if (OpCode.isControlFrame(frame.getOpCode())) { + // Do not deflate control frames + nextOutgoingFrame(frame, this); + return; + } + + compress(entry, true); + } + + private void compress(FrameEntry entry, boolean first) { + // Get a chunk of the payload to avoid to blow + // the heap if the payload is a huge mapped file. + Frame frame = entry.frame; + ByteBuffer data = frame.getPayload(); + + if (data == null) + data = BufferUtils.EMPTY_BUFFER; + + int remaining = data.remaining(); + int outputLength = Math.max(256, data.remaining()); + if (LOG.isDebugEnabled()) + LOG.debug("Compressing {}: {} bytes in {} bytes chunk", entry, remaining, outputLength); + + boolean needsCompress = true; + + Deflater deflater = getDeflater(); + + if (deflater.needsInput() && !supplyInput(deflater, data)) { + // no input supplied + needsCompress = false; + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + byte[] output = new byte[outputLength]; + + boolean fin = frame.isFin(); + + // Compress the data + while (needsCompress) { + int compressed = deflater.deflate(output, 0, outputLength, Deflater.SYNC_FLUSH); + + // Append the output for the eventual frame. + if (LOG.isDebugEnabled()) + LOG.debug("Wrote {} bytes to output buffer", compressed); + out.write(output, 0, compressed); + + if (compressed < outputLength) { + needsCompress = false; + } + } + + ByteBuffer payload = ByteBuffer.wrap(out.toByteArray()); + + if (payload.remaining() > 0) { + // Handle tail bytes generated by SYNC_FLUSH. + if (LOG.isDebugEnabled()) + LOG.debug("compressed[] bytes = {}", BufferUtils.toDetailString(payload)); + + if (tailDrop == TAIL_DROP_ALWAYS) { + if (endsWithTail(payload)) { + payload.limit(payload.limit() - TAIL_BYTES.length); + } + if (LOG.isDebugEnabled()) + LOG.debug("payload (TAIL_DROP_ALWAYS) = {}", BufferUtils.toDetailString(payload)); + } else if (tailDrop == TAIL_DROP_FIN_ONLY) { + if (frame.isFin() && endsWithTail(payload)) { + payload.limit(payload.limit() - TAIL_BYTES.length); + } + if (LOG.isDebugEnabled()) + LOG.debug("payload (TAIL_DROP_FIN_ONLY) = {}", BufferUtils.toDetailString(payload)); + } + } else if (fin) { + // Special case: 7.2.3.6. Generating an Empty Fragment Manually + // https://tools.ietf.org/html/rfc7692#section-7.2.3.6 + payload = ByteBuffer.wrap(new byte[]{0x00}); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Compressed {}: input:{} -> payload:{}", entry, outputLength, payload.remaining()); + } + + boolean continuation = frame.getType().isContinuation() || !first; + DataFrame chunk = new DataFrame(frame, continuation); + if (rsvUse == RSV_USE_ONLY_FIRST) { + chunk.setRsv1(!continuation); + } else { + // always set + chunk.setRsv1(true); + } + chunk.setPayload(payload); + chunk.setFin(fin); + + nextOutgoingFrame(chunk, this); + } + + @Override + protected void onCompleteSuccess() { + // This IteratingCallback never completes. + } + + @Override + protected void onCompleteFailure(Throwable x) { + // Fail all the frames in the queue. + FrameEntry entry; + while ((entry = pollEntry()) != null) { + notifyCallbackFailure(entry.result, x); + } + } + + @Override + public void accept(Result result) { + if (result.isSuccess()) { + if (finished) + notifyCallbackSuccess(current.result); + } else { + notifyCallbackFailure(current.result, result.getThrowable()); + // If something went wrong, very likely the compression context + // will be invalid, so we need to fail this IteratingCallback. + LOG.warn("", result.getThrowable()); + } + super.accept(result); + } + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/DeflateFrameExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/DeflateFrameExtension.java new file mode 100644 index 000000000..8c4107d5c --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/DeflateFrameExtension.java @@ -0,0 +1,50 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +import com.fireflysource.net.websocket.common.exception.BadPayloadException; +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.util.zip.DataFormatException; + +/** + * Implementation of the + * deflate-frame + * extension seen out in the wild. + */ +public class DeflateFrameExtension extends CompressExtension { + @Override + public String getName() { + return "deflate-frame"; + } + + @Override + int getRsvUseMode() { + return RSV_USE_ALWAYS; + } + + @Override + int getTailDropMode() { + return TAIL_DROP_ALWAYS; + } + + @Override + public void incomingFrame(Frame frame) { + // Incoming frames are always non concurrent because + // they are read and parsed with a single thread, and + // therefore there is no need for synchronization. + + if (frame.getType().isControl() || !frame.isRsv1() || !frame.hasPayload()) { + nextIncomingFrame(frame); + return; + } + + try { + ByteAccumulator accumulator = newByteAccumulator(); + decompress(accumulator, frame.getPayload()); + decompress(accumulator, TAIL_BYTES_BUF.slice()); + forwardIncoming(frame, accumulator); + } catch (DataFormatException e) { + throw new BadPayloadException(e); + } + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/PerMessageDeflateExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/PerMessageDeflateExtension.java new file mode 100644 index 000000000..076ac9379 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/PerMessageDeflateExtension.java @@ -0,0 +1,160 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.Result; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.exception.BadPayloadException; +import com.fireflysource.net.websocket.common.exception.ProtocolException; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.ExtensionConfig; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; +import java.util.function.Consumer; +import java.util.zip.DataFormatException; + +/** + * Per Message Deflate Compression extension for WebSocket. + *

+ * Attempts to follow Compression Extensions for WebSocket + */ +public class PerMessageDeflateExtension extends CompressExtension { + + private static LazyLogger LOG = SystemLogger.create(PerMessageDeflateExtension.class); + + private ExtensionConfig configRequested; + private ExtensionConfig configNegotiated; + private boolean incomingContextTakeover = true; + private boolean outgoingContextTakeover = true; + private boolean incomingCompressed; + + @Override + public String getName() { + return "permessage-deflate"; + } + + @Override + public void incomingFrame(Frame frame) { + // Incoming frames are always non concurrent because + // they are read and parsed with a single thread, and + // therefore there is no need for synchronization. + + // This extension requires the RSV1 bit set only in the first frame. + // Subsequent continuation frames don't have RSV1 set, but are compressed. + if (frame.getType().isData()) { + incomingCompressed = frame.isRsv1(); + } + + if (OpCode.isControlFrame(frame.getOpCode()) || !incomingCompressed) { + nextIncomingFrame(frame); + return; + } + + if (frame.getOpCode() == OpCode.CONTINUATION && frame.isRsv1()) { + // Per RFC7692 we MUST Fail the websocket connection + throw new ProtocolException("Invalid RSV1 set on permessage-deflate CONTINUATION frame"); + } + + ByteAccumulator accumulator = newByteAccumulator(); + + try { + ByteBuffer payload = frame.getPayload(); + decompress(accumulator, payload); + if (frame.isFin()) { + decompress(accumulator, TAIL_BYTES_BUF.slice()); + } + + forwardIncoming(frame, accumulator); + } catch (DataFormatException e) { + throw new BadPayloadException(e); + } + + if (frame.isFin()) + incomingCompressed = false; + } + + @Override + protected void nextIncomingFrame(Frame frame) { + if (frame.isFin() && !incomingContextTakeover) { + LOG.debug("Incoming Context Reset"); + decompressCount.set(0); + getInflater().reset(); + } + super.nextIncomingFrame(frame); + } + + @Override + protected void nextOutgoingFrame(Frame frame, Consumer> result) { + if (frame.isFin() && !outgoingContextTakeover) { + LOG.debug("Outgoing Context Reset"); + getDeflater().reset(); + } + super.nextOutgoingFrame(frame, result); + } + + @Override + int getRsvUseMode() { + return RSV_USE_ONLY_FIRST; + } + + @Override + int getTailDropMode() { + return TAIL_DROP_FIN_ONLY; + } + + @Override + public void setConfig(final ExtensionConfig config) { + configRequested = new ExtensionConfig(config); + configNegotiated = new ExtensionConfig(config.getName()); + + for (String key : config.getParameterKeys()) { + key = key.trim(); + switch (key) { + case "client_max_window_bits": + case "server_max_window_bits": { + // Don't negotiate these parameters + break; + } + case "client_no_context_takeover": { + configNegotiated.setParameter("client_no_context_takeover"); + switch (getPolicy().getBehavior()) { + case CLIENT: + incomingContextTakeover = false; + break; + case SERVER: + outgoingContextTakeover = false; + break; + } + break; + } + case "server_no_context_takeover": { + configNegotiated.setParameter("server_no_context_takeover"); + switch (getPolicy().getBehavior()) { + case CLIENT: + outgoingContextTakeover = false; + break; + case SERVER: + incomingContextTakeover = false; + break; + } + break; + } + default: { + throw new IllegalArgumentException(); + } + } + } + + LOG.debug("config: outgoingContextTakeover={}, incomingContextTakeover={} : {}", outgoingContextTakeover, incomingContextTakeover, this); + + super.setConfig(configNegotiated); + } + + @Override + public String toString() { + return String.format("%s[requested=\"%s\", negotiated=\"%s\"]", + getClass().getSimpleName(), + configRequested.getParameterizedName(), + configNegotiated.getParameterizedName()); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/XWebkitDeflateFrameExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/XWebkitDeflateFrameExtension.java new file mode 100644 index 000000000..1b200d798 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/compress/XWebkitDeflateFrameExtension.java @@ -0,0 +1,12 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +/** + * Implementation of the x-webkit-deflate-frame extension seen out + * in the wild. Using the alternate extension identification + */ +public class XWebkitDeflateFrameExtension extends DeflateFrameExtension { + @Override + public String getName() { + return "x-webkit-deflate-frame"; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/fragment/FragmentExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/fragment/FragmentExtension.java new file mode 100644 index 000000000..2f7d0cb51 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/fragment/FragmentExtension.java @@ -0,0 +1,169 @@ +package com.fireflysource.net.websocket.common.extension.fragment; + +import com.fireflysource.common.concurrent.AutoLock; +import com.fireflysource.common.concurrent.IteratingCallback; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.Result; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.extension.AbstractExtension; +import com.fireflysource.net.websocket.common.frame.DataFrame; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.ExtensionConfig; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.function.Consumer; + +/** + * Fragment Extension + */ +public class FragmentExtension extends AbstractExtension { + private static LazyLogger LOG = SystemLogger.create(FragmentExtension.class); + + private final AutoLock lock = new AutoLock(); + private final Queue entries = new ArrayDeque<>(); + private final IteratingCallback flusher = new Flusher(); + private int maxLength; + + @Override + public String getName() { + return "fragment"; + } + + @Override + public void incomingFrame(Frame frame) { + nextIncomingFrame(frame); + } + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + ByteBuffer payload = frame.getPayload(); + int length = payload != null ? payload.remaining() : 0; + if (OpCode.isControlFrame(frame.getOpCode()) || maxLength <= 0 || length <= maxLength) { + nextOutgoingFrame(frame, result); + return; + } + + FrameEntry entry = new FrameEntry(frame, result); + if (LOG.isDebugEnabled()) + LOG.debug("Queuing {}", entry); + offerEntry(entry); + flusher.iterate(); + } + + @Override + public void setConfig(ExtensionConfig config) { + super.setConfig(config); + maxLength = config.getParameter("maxLength", -1); + } + + private void offerEntry(FrameEntry entry) { + lock.lock(() -> entries.offer(entry)); + } + + private FrameEntry pollEntry() { + return lock.lock(entries::poll); + } + + private static class FrameEntry { + private final Frame frame; + private final Consumer> result; + + private FrameEntry(Frame frame, Consumer> result) { + this.frame = frame; + this.result = result; + } + + @Override + public String toString() { + return frame.toString(); + } + } + + private class Flusher extends IteratingCallback { + private FrameEntry current; + private boolean finished = true; + + @Override + protected Action process() { + if (finished) { + current = pollEntry(); + LOG.debug("Processing {}", current); + if (current == null) + return Action.IDLE; + fragment(current, true); + } else { + fragment(current, false); + } + return Action.SCHEDULED; + } + + private void fragment(FrameEntry entry, boolean first) { + Frame frame = entry.frame; + ByteBuffer payload = frame.getPayload(); + int remaining = payload.remaining(); + int length = Math.min(remaining, maxLength); + finished = length == remaining; + + boolean continuation = frame.getType().isContinuation() || !first; + DataFrame fragment = new DataFrame(frame, continuation); + boolean fin = frame.isFin() && finished; + fragment.setFin(fin); + + int limit = payload.limit(); + int newLimit = payload.position() + length; + payload.limit(newLimit); + ByteBuffer payloadFragment = payload.slice(); + payload.limit(limit); + fragment.setPayload(payloadFragment); + if (LOG.isDebugEnabled()) + LOG.debug("Fragmented {}->{}", frame, fragment); + payload.position(newLimit); + + nextOutgoingFrame(fragment, this); + } + + @Override + protected void onCompleteSuccess() { + // This IteratingCallback never completes. + } + + @Override + protected void onCompleteFailure(Throwable x) { + // This IteratingCallback never fails. + // The callback are those provided by WriteCallback (implemented + // below) and even in case of writeFailed() we call succeeded(). + } + + @Override + public void accept(Result result) { + if (!result.isSuccess()) { + notifyCallbackFailure(current.result, result.getThrowable()); + } + notifyCallbackSuccess(current.result); + super.accept(result); + } + + private void notifyCallbackSuccess(Consumer> result) { + try { + if (result != null) + result.accept(Result.SUCCESS); + } catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("Exception while notifying success", x); + } + } + + private void notifyCallbackFailure(Consumer> result, Throwable failure) { + try { + if (result != null) + result.accept(Result.createFailedResult(failure)); + } catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("Exception while notifying failure", x); + } + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/identity/IdentityExtension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/identity/IdentityExtension.java new file mode 100644 index 000000000..7529b69dd --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/extension/identity/IdentityExtension.java @@ -0,0 +1,61 @@ +package com.fireflysource.net.websocket.common.extension.identity; + + +import com.fireflysource.common.string.QuotedStringTokenizer; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.extension.AbstractExtension; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.ExtensionConfig; + +import java.util.function.Consumer; + +public class IdentityExtension extends AbstractExtension { + + private String id; + + public String getParam(String key) { + return getConfig().getParameter(key, "?"); + } + + @Override + public String getName() { + return "identity"; + } + + @Override + public void incomingFrame(Frame frame) { + // pass through + nextIncomingFrame(frame); + } + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + // pass through + nextOutgoingFrame(frame, result); + } + + @Override + public void setConfig(ExtensionConfig config) { + super.setConfig(config); + StringBuilder s = new StringBuilder(); + s.append(config.getName()); + s.append("@").append(Integer.toHexString(hashCode())); + s.append("["); + boolean delim = false; + for (String param : config.getParameterKeys()) { + if (delim) { + s.append(';'); + } + s.append(param).append('=').append(QuotedStringTokenizer.quoteIfNeeded(config.getParameter(param, ""), ";=")); + delim = true; + } + s.append("]"); + id = s.toString(); + } + + @Override + public String toString() { + return id; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/BinaryFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/BinaryFrame.java new file mode 100644 index 000000000..197d1b4c0 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/BinaryFrame.java @@ -0,0 +1,38 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; + +public class BinaryFrame extends DataFrame { + + public BinaryFrame() { + super(OpCode.BINARY); + } + + public BinaryFrame(Frame basedOn) { + super(basedOn); + } + + @Override + public BinaryFrame setPayload(ByteBuffer buf) { + super.setPayload(buf); + return this; + } + + public BinaryFrame setPayload(byte[] buf) { + setPayload(ByteBuffer.wrap(buf)); + return this; + } + + public BinaryFrame setPayload(String payload) { + setPayload(StringUtils.getUtf8Bytes(payload)); + return this; + } + + @Override + public Type getType() { + return Type.BINARY; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/CloseFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/CloseFrame.java new file mode 100644 index 000000000..a19cfe7a8 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/CloseFrame.java @@ -0,0 +1,25 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +public class CloseFrame extends ControlFrame { + public CloseFrame() { + super(OpCode.CLOSE); + } + + @Override + public Type getType() { + return Type.CLOSE; + } + + /** + * Truncate arbitrary reason into something that will fit into the CloseFrame limits. + * + * @param reason the arbitrary reason to possibly truncate. + * @return the possibly truncated reason string. + */ + public static String truncate(String reason) { + return StringUtils.truncate(reason, (ControlFrame.MAX_CONTROL_PAYLOAD - 2)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ContinuationFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ContinuationFrame.java new file mode 100644 index 000000000..eab0c72c0 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ContinuationFrame.java @@ -0,0 +1,36 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; + +public class ContinuationFrame extends DataFrame { + + public ContinuationFrame() { + super(OpCode.CONTINUATION); + } + + public ContinuationFrame(Frame basedOn) { + super(basedOn); + } + + @Override + public ContinuationFrame setPayload(ByteBuffer buf) { + super.setPayload(buf); + return this; + } + + public ContinuationFrame setPayload(byte[] buf) { + return this.setPayload(ByteBuffer.wrap(buf)); + } + + public ContinuationFrame setPayload(String message) { + return this.setPayload(StringUtils.getUtf8Bytes(message)); + } + + @Override + public Type getType() { + return Type.CONTINUATION; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ControlFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ControlFrame.java new file mode 100644 index 000000000..55befc128 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ControlFrame.java @@ -0,0 +1,98 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.exception.ProtocolException; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public abstract class ControlFrame extends WebSocketFrame { + /** + * Maximum size of Control frame, per RFC 6455 + */ + public static final int MAX_CONTROL_PAYLOAD = 125; + + public ControlFrame(byte opcode) { + super(opcode); + } + + @Override + public void assertValid() { + if (isControlFrame()) { + if (getPayloadLength() > ControlFrame.MAX_CONTROL_PAYLOAD) { + throw new ProtocolException("Desired payload length [" + getPayloadLength() + + "] exceeds maximum control payload length [" + MAX_CONTROL_PAYLOAD + "]"); + } + + if ((finRsvOp & 0x80) == 0) { + throw new ProtocolException("Cannot have FIN==false on Control frames"); + } + + if ((finRsvOp & 0x40) != 0) { + throw new ProtocolException("Cannot have RSV1==true on Control frames"); + } + + if ((finRsvOp & 0x20) != 0) { + throw new ProtocolException("Cannot have RSV2==true on Control frames"); + } + + if ((finRsvOp & 0x10) != 0) { + throw new ProtocolException("Cannot have RSV3==true on Control frames"); + } + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ControlFrame other = (ControlFrame) obj; + if (data == null) { + if (other.data != null) { + return false; + } + } else if (!data.equals(other.data)) { + return false; + } + if (finRsvOp != other.finRsvOp) { + return false; + } + if (!Arrays.equals(mask, other.mask)) { + return false; + } + return masked == other.masked; + } + + @Override + public boolean isControlFrame() { + return true; + } + + @Override + public boolean isDataFrame() { + return false; + } + + @Override + public WebSocketFrame setPayload(ByteBuffer buf) { + if (buf != null && buf.remaining() > MAX_CONTROL_PAYLOAD) { + throw new ProtocolException("Control Payloads can not exceed " + MAX_CONTROL_PAYLOAD + " bytes in length."); + } + return super.setPayload(buf); + } + + @Override + public ByteBuffer getPayload() { + if (super.getPayload() == null) { + return BufferUtils.EMPTY_BUFFER; + } + return super.getPayload(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/DataFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/DataFrame.java new file mode 100644 index 000000000..00744829f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/DataFrame.java @@ -0,0 +1,55 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.net.websocket.common.model.OpCode; + +/** + * A Data Frame + */ +public class DataFrame extends WebSocketFrame { + protected DataFrame(byte opcode) { + super(opcode); + } + + /** + * Construct new DataFrame based on headers of provided frame. + *

+ * Useful for when working in extensions and a new frame needs to be created. + * + * @param basedOn the frame this one is based on + */ + public DataFrame(Frame basedOn) { + this(basedOn, false); + } + + /** + * Construct new DataFrame based on headers of provided frame, overriding for continuations if needed. + *

+ * Useful for when working in extensions and a new frame needs to be created. + * + * @param basedOn the frame this one is based on + * @param continuation true if this is a continuation frame + */ + public DataFrame(Frame basedOn, boolean continuation) { + super(basedOn.getOpCode()); + copyHeaders(basedOn); + if (continuation) { + setOpCode(OpCode.CONTINUATION); + } + } + + @Override + public void assertValid() { + /* no extra validation for data frames (yet) here */ + } + + @Override + public boolean isControlFrame() { + return false; + } + + @Override + public boolean isDataFrame() { + return true; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/Frame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/Frame.java new file mode 100644 index 000000000..901500b0d --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/Frame.java @@ -0,0 +1,80 @@ +package com.fireflysource.net.websocket.common.frame; + +import java.nio.ByteBuffer; + +/** + * An immutable websocket frame. + */ +public interface Frame { + enum Type { + CONTINUATION((byte) 0x00), + TEXT((byte) 0x01), + BINARY((byte) 0x02), + CLOSE((byte) 0x08), + PING((byte) 0x09), + PONG((byte) 0x0A); + + public static Type from(byte op) { + for (Type type : values()) { + if (type.opcode == op) { + return type; + } + } + throw new IllegalArgumentException("OpCode " + op + " is not a valid Frame.Type"); + } + + private byte opcode; + + Type(byte code) { + this.opcode = code; + } + + public byte getOpCode() { + return opcode; + } + + public boolean isControl() { + return (opcode >= CLOSE.getOpCode()); + } + + public boolean isData() { + return (opcode == TEXT.getOpCode()) || (opcode == BINARY.getOpCode()); + } + + public boolean isContinuation() { + return opcode == CONTINUATION.getOpCode(); + } + + @Override + public String toString() { + return this.name(); + } + } + + byte[] getMask(); + + byte getOpCode(); + + ByteBuffer getPayload(); + + /** + * The original payload length ({@link ByteBuffer#remaining()}) + * + * @return the original payload length ({@link ByteBuffer#remaining()}) + */ + int getPayloadLength(); + + Type getType(); + + boolean hasPayload(); + + boolean isFin(); + + boolean isMasked(); + + boolean isRsv1(); + + boolean isRsv2(); + + boolean isRsv3(); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/PingFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/PingFrame.java new file mode 100644 index 000000000..986483777 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/PingFrame.java @@ -0,0 +1,27 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; + +public class PingFrame extends ControlFrame { + public PingFrame() { + super(OpCode.PING); + } + + public PingFrame setPayload(byte[] bytes) { + setPayload(ByteBuffer.wrap(bytes)); + return this; + } + + public PingFrame setPayload(String payload) { + setPayload(ByteBuffer.wrap(StringUtils.getUtf8Bytes(payload))); + return this; + } + + @Override + public Type getType() { + return Type.PING; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/PongFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/PongFrame.java new file mode 100644 index 000000000..27ebbf2f3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/PongFrame.java @@ -0,0 +1,27 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; + +public class PongFrame extends ControlFrame { + public PongFrame() { + super(OpCode.PONG); + } + + public PongFrame setPayload(byte[] bytes) { + setPayload(ByteBuffer.wrap(bytes)); + return this; + } + + public PongFrame setPayload(String payload) { + setPayload(StringUtils.getUtf8Bytes(payload)); + return this; + } + + @Override + public Type getType() { + return Type.PONG; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ReadOnlyDelegatedFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ReadOnlyDelegatedFrame.java new file mode 100644 index 000000000..a689641c6 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/ReadOnlyDelegatedFrame.java @@ -0,0 +1,72 @@ +package com.fireflysource.net.websocket.common.frame; + +import java.nio.ByteBuffer; + +/** + * Immutable, Read-only, Frame implementation. + */ +public class ReadOnlyDelegatedFrame implements Frame { + private final Frame delegate; + + public ReadOnlyDelegatedFrame(Frame frame) { + this.delegate = frame; + } + + @Override + public byte[] getMask() { + return delegate.getMask(); + } + + @Override + public byte getOpCode() { + return delegate.getOpCode(); + } + + @Override + public ByteBuffer getPayload() { + if (!delegate.hasPayload()) { + return null; + } + return delegate.getPayload().asReadOnlyBuffer(); + } + + @Override + public int getPayloadLength() { + return delegate.getPayloadLength(); + } + + @Override + public Type getType() { + return delegate.getType(); + } + + @Override + public boolean hasPayload() { + return delegate.hasPayload(); + } + + @Override + public boolean isFin() { + return delegate.isFin(); + } + + @Override + public boolean isMasked() { + return delegate.isMasked(); + } + + @Override + public boolean isRsv1() { + return delegate.isRsv1(); + } + + @Override + public boolean isRsv2() { + return delegate.isRsv2(); + } + + @Override + public boolean isRsv3() { + return delegate.isRsv3(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/TextFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/TextFrame.java new file mode 100644 index 000000000..3c6ae1479 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/TextFrame.java @@ -0,0 +1,35 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; + +public class TextFrame extends DataFrame { + public TextFrame() { + super(OpCode.TEXT); + } + + public TextFrame(Frame basedOn) { + super(basedOn); + } + + @Override + public Type getType() { + return Type.TEXT; + } + + public TextFrame setPayload(String str) { + setPayload(ByteBuffer.wrap(StringUtils.getUtf8Bytes(str))); + return this; + } + + @Override + public String getPayloadAsUTF8() { + if (data == null) { + return null; + } + return BufferUtils.toUTF8String(data); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/WebSocketFrame.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/WebSocketFrame.java new file mode 100644 index 000000000..53950ed1b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/frame/WebSocketFrame.java @@ -0,0 +1,311 @@ +package com.fireflysource.net.websocket.common.frame; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.model.OpCode; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * A Base Frame as seen in RFC 6455. Sec 5.2 + * + *

+ *    0                   1                   2                   3
+ *    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *   +-+-+-+-+-------+-+-------------+-------------------------------+
+ *   |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
+ *   |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
+ *   |N|V|V|V|       |S|             |   (if payload len==126/127)   |
+ *   | |1|2|3|       |K|             |                               |
+ *   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ *   |     Extended payload length continued, if payload len == 127  |
+ *   + - - - - - - - - - - - - - - - +-------------------------------+
+ *   |                               |Masking-key, if MASK set to 1  |
+ *   +-------------------------------+-------------------------------+
+ *   | Masking-key (continued)       |          Payload Data         |
+ *   +-------------------------------- - - - - - - - - - - - - - - - +
+ *   :                     Payload Data continued ...                :
+ *   + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ *   |                     Payload Data continued ...                |
+ *   +---------------------------------------------------------------+
+ * 
+ */ +public abstract class WebSocketFrame implements Frame { + public static WebSocketFrame copy(Frame original) { + WebSocketFrame copy; + switch (original.getOpCode()) { + case OpCode.BINARY: + copy = new BinaryFrame(); + break; + case OpCode.TEXT: + copy = new TextFrame(); + break; + case OpCode.CLOSE: + copy = new CloseFrame(); + break; + case OpCode.CONTINUATION: + copy = new ContinuationFrame(); + break; + case OpCode.PING: + copy = new PingFrame(); + break; + case OpCode.PONG: + copy = new PongFrame(); + break; + default: + throw new IllegalArgumentException("Cannot copy frame with opcode " + original.getOpCode() + " - " + original); + } + + copy.copyHeaders(original); + ByteBuffer payload = original.getPayload(); + if (payload != null) { + ByteBuffer payloadCopy = ByteBuffer.allocate(payload.remaining()); + payloadCopy.put(payload.slice()).flip(); + copy.setPayload(payloadCopy); + } + return copy; + } + + /** + * Combined FIN + RSV1 + RSV2 + RSV3 + OpCode byte. + * + *
+     *   1000_0000 (0x80) = fin
+     *   0100_0000 (0x40) = rsv1
+     *   0010_0000 (0x20) = rsv2
+     *   0001_0000 (0x10) = rsv3
+     *   0000_1111 (0x0F) = opcode
+     * 
+ */ + protected byte finRsvOp; + protected boolean masked = false; + + protected byte[] mask; + /** + * The payload data. + *

+ * It is assumed to always be in FLUSH mode (ready to read) in this object. + */ + protected ByteBuffer data; + + /** + * Construct form opcode + * + * @param opcode the opcode the frame is based on + */ + protected WebSocketFrame(byte opcode) { + reset(); + setOpCode(opcode); + } + + public abstract void assertValid(); + + protected void copyHeaders(Frame frame) { + finRsvOp = 0x00; + finRsvOp |= frame.isFin() ? 0x80 : 0x00; + finRsvOp |= frame.isRsv1() ? 0x40 : 0x00; + finRsvOp |= frame.isRsv2() ? 0x20 : 0x00; + finRsvOp |= frame.isRsv3() ? 0x10 : 0x00; + finRsvOp |= frame.getOpCode() & 0x0F; + + masked = frame.isMasked(); + if (masked) { + mask = frame.getMask(); + } else { + mask = null; + } + } + + protected void copyHeaders(WebSocketFrame copy) { + finRsvOp = copy.finRsvOp; + masked = copy.masked; + mask = null; + if (copy.mask != null) + mask = Arrays.copyOf(copy.mask, copy.mask.length); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + WebSocketFrame other = (WebSocketFrame) obj; + if (data == null) { + if (other.data != null) { + return false; + } + } else if (!data.equals(other.data)) { + return false; + } + if (finRsvOp != other.finRsvOp) { + return false; + } + if (!Arrays.equals(mask, other.mask)) { + return false; + } + return masked == other.masked; + } + + @Override + public byte[] getMask() { + return mask; + } + + @Override + public final byte getOpCode() { + return (byte) (finRsvOp & 0x0F); + } + + /** + * Get the payload ByteBuffer. possible null. + */ + @Override + public ByteBuffer getPayload() { + return data; + } + + public String getPayloadAsUTF8() { + return BufferUtils.toUTF8String(getPayload()); + } + + @Override + public int getPayloadLength() { + if (data == null) { + return 0; + } + return data.remaining(); + } + + @Override + public Type getType() { + return Type.from(getOpCode()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((data == null) ? 0 : data.hashCode()); + result = (prime * result) + finRsvOp; + result = (prime * result) + Arrays.hashCode(mask); + return result; + } + + @Override + public boolean hasPayload() { + return ((data != null) && data.hasRemaining()); + } + + public abstract boolean isControlFrame(); + + public abstract boolean isDataFrame(); + + @Override + public boolean isFin() { + return (byte) (finRsvOp & 0x80) != 0; + } + + @Override + public boolean isMasked() { + return masked; + } + + @Override + public boolean isRsv1() { + return (byte) (finRsvOp & 0x40) != 0; + } + + @Override + public boolean isRsv2() { + return (byte) (finRsvOp & 0x20) != 0; + } + + @Override + public boolean isRsv3() { + return (byte) (finRsvOp & 0x10) != 0; + } + + public void reset() { + finRsvOp = (byte) 0x80; // FIN (!RSV, opcode 0) + masked = false; + data = null; + mask = null; + } + + public WebSocketFrame setFin(boolean fin) { + // set bit 1 + this.finRsvOp = (byte) ((finRsvOp & 0x7F) | (fin ? 0x80 : 0x00)); + return this; + } + + public Frame setMask(byte[] maskingKey) { + this.mask = maskingKey; + this.masked = (mask != null); + return this; + } + + public Frame setMasked(boolean mask) { + this.masked = mask; + return this; + } + + protected WebSocketFrame setOpCode(byte op) { + this.finRsvOp = (byte) ((finRsvOp & 0xF0) | (op & 0x0F)); + return this; + } + + /** + * Set the data payload. + *

+ * The provided buffer will be used as is, no copying of bytes performed. + *

+ * The provided buffer should be flipped and ready to READ from. + * + * @param buf the bytebuffer to set + * @return the frame itself + */ + public WebSocketFrame setPayload(ByteBuffer buf) { + data = buf; + return this; + } + + public WebSocketFrame setRsv1(boolean rsv1) { + // set bit 2 + this.finRsvOp = (byte) ((finRsvOp & 0xBF) | (rsv1 ? 0x40 : 0x00)); + return this; + } + + public WebSocketFrame setRsv2(boolean rsv2) { + // set bit 3 + this.finRsvOp = (byte) ((finRsvOp & 0xDF) | (rsv2 ? 0x20 : 0x00)); + return this; + } + + public WebSocketFrame setRsv3(boolean rsv3) { + // set bit 4 + this.finRsvOp = (byte) ((finRsvOp & 0xEF) | (rsv3 ? 0x10 : 0x00)); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append(OpCode.name((byte) (finRsvOp & 0x0F))); + b.append('['); + b.append("len=").append(getPayloadLength()); + b.append(",fin=").append((finRsvOp & 0x80) != 0); + b.append(",rsv="); + b.append(((finRsvOp & 0x40) != 0) ? '1' : '.'); + b.append(((finRsvOp & 0x20) != 0) ? '1' : '.'); + b.append(((finRsvOp & 0x10) != 0) ? '1' : '.'); + b.append(",masked=").append(masked); + b.append(']'); + return b.toString(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/AcceptHash.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/AcceptHash.java new file mode 100644 index 000000000..134b10aad --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/AcceptHash.java @@ -0,0 +1,38 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.codec.base64.Base64Utils; +import com.fireflysource.net.websocket.common.exception.EncodingAcceptHashKeyException; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +/** + * Logic for working with the Sec-WebSocket-Key and Sec-WebSocket-Accept headers. + *

+ * This is kept separate from Connection objects to facilitate difference in behavior between client and server, as well as making testing easier. + */ +public class AcceptHash { + /** + * Globally Unique Identifier for use in WebSocket handshake within Sec-WebSocket-Accept and Sec-WebSocket-Key http headers. + *

+ * See Opening Handshake (Section 1.3) + */ + private final static byte[] MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(StandardCharsets.ISO_8859_1); + + /** + * Concatenate the provided key with the Magic GUID and return the Base64 encoded form. + * + * @param key the key to hash + * @return the Sec-WebSocket-Accept header response (per opening handshake spec) + */ + public static String hashKey(String key) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.update(key.getBytes(StandardCharsets.UTF_8)); + md.update(MAGIC); + return new String(Base64Utils.encode(md.digest())); + } catch (Exception e) { + throw new EncodingAcceptHashKeyException("", e); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/CloseInfo.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/CloseInfo.java new file mode 100644 index 000000000..3b2299492 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/CloseInfo.java @@ -0,0 +1,177 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.string.Utf8Appendable; +import com.fireflysource.common.string.Utf8StringBuilder; +import com.fireflysource.net.websocket.common.exception.BadPayloadException; +import com.fireflysource.net.websocket.common.exception.ProtocolException; +import com.fireflysource.net.websocket.common.frame.CloseFrame; +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class CloseInfo { + private int statusCode = 0; + private byte[] reasonBytes; + + public CloseInfo() { + this(StatusCode.NO_CODE, null); + } + + /** + * Parse the Close Frame payload. + * + * @param payload the raw close frame payload. + * @param validate true if payload should be validated per WebSocket spec. + */ + public CloseInfo(ByteBuffer payload, boolean validate) { + this.statusCode = StatusCode.NO_CODE; + + if ((payload == null) || (payload.remaining() == 0)) { + return; // nothing to do + } + + ByteBuffer data = payload.slice(); + if ((data.remaining() == 1) && (validate)) { + throw new ProtocolException("Invalid 1 byte payload"); + } + + if (data.remaining() >= 2) { + // Status Code + statusCode = 0; // start with 0 + statusCode |= (data.get() & 0xFF) << 8; + statusCode |= (data.get() & 0xFF); + + if (validate) { + assertValidStatusCode(statusCode); + } + + if (data.remaining() > 0) { + // Reason (trimmed to max reason size) + int len = Math.min(data.remaining(), CloseStatus.MAX_REASON_PHRASE); + reasonBytes = new byte[len]; + data.get(reasonBytes, 0, len); + + // Spec Requirement : throw BadPayloadException on invalid UTF8 + if (validate) { + try { + Utf8StringBuilder utf = new Utf8StringBuilder(); + // if this throws, we know we have bad UTF8 + utf.append(reasonBytes, 0, reasonBytes.length); + } catch (Utf8Appendable.NotUtf8Exception e) { + throw new BadPayloadException("Invalid Close Reason", e); + } + } + } + } + } + + public CloseInfo(Frame frame) { + this(frame.getPayload(), false); + } + + public CloseInfo(Frame frame, boolean validate) { + this(frame.getPayload(), validate); + } + + public CloseInfo(int statusCode) { + this(statusCode, null); + } + + /** + * Create a CloseInfo, trimming the reason to {@link CloseStatus#MAX_REASON_PHRASE} UTF-8 bytes if needed. + * + * @param statusCode the status code + * @param reason the raw reason code + */ + public CloseInfo(int statusCode, String reason) { + this.statusCode = statusCode; + if (reason != null) { + byte[] utf8Bytes = reason.getBytes(StandardCharsets.UTF_8); + if (utf8Bytes.length > CloseStatus.MAX_REASON_PHRASE) { + this.reasonBytes = new byte[CloseStatus.MAX_REASON_PHRASE]; + System.arraycopy(utf8Bytes, 0, this.reasonBytes, 0, CloseStatus.MAX_REASON_PHRASE); + } else { + this.reasonBytes = utf8Bytes; + } + } + } + + private void assertValidStatusCode(int statusCode) { + // Status Codes outside of RFC6455 defined scope + if ((statusCode <= 999) || (statusCode >= 5000)) { + throw new ProtocolException("Out of range close status code: " + statusCode); + } + + // Status Codes not allowed to exist in a Close frame (per RFC6455) + if ((statusCode == StatusCode.NO_CLOSE) || (statusCode == StatusCode.NO_CODE) || (statusCode == StatusCode.FAILED_TLS_HANDSHAKE)) { + throw new ProtocolException("Frame forbidden close status code: " + statusCode); + } + + // Status Code is in defined "reserved space" and is declared (all others are invalid) + if ((statusCode >= 1000) && (statusCode <= 2999) && !StatusCode.isTransmittable(statusCode)) { + throw new ProtocolException("RFC6455 and IANA Undefined close status code: " + statusCode); + } + } + + private ByteBuffer asByteBuffer() { + if ((statusCode == StatusCode.NO_CLOSE) || (statusCode == StatusCode.NO_CODE) || (statusCode == (-1))) { + // codes that are not allowed to be used in endpoint. + return null; + } + + int len = 2; // status code + boolean hasReason = (this.reasonBytes != null) && (this.reasonBytes.length > 0); + if (hasReason) { + len += this.reasonBytes.length; + } + + ByteBuffer buf = BufferUtils.allocate(len); + BufferUtils.flipToFill(buf); + buf.put((byte) ((statusCode >>> 8) & 0xFF)); + buf.put((byte) ((statusCode >>> 0) & 0xFF)); + + if (hasReason) { + buf.put(this.reasonBytes, 0, this.reasonBytes.length); + } + BufferUtils.flipToFlush(buf, 0); + + return buf; + } + + public CloseFrame asFrame() { + CloseFrame frame = new CloseFrame(); + frame.setFin(true); + // Frame forbidden codes result in no status code (and no reason string) + if ((statusCode != StatusCode.NO_CLOSE) && (statusCode != StatusCode.NO_CODE) && (statusCode != StatusCode.FAILED_TLS_HANDSHAKE)) { + assertValidStatusCode(statusCode); + frame.setPayload(asByteBuffer()); + } + return frame; + } + + public String getReason() { + if (this.reasonBytes == null) { + return null; + } + return new String(this.reasonBytes, StandardCharsets.UTF_8); + } + + public int getStatusCode() { + return statusCode; + } + + public boolean isHarsh() { + return !((statusCode == StatusCode.NORMAL) || (statusCode == StatusCode.NO_CODE)); + } + + public boolean isAbnormal() { + return (statusCode != StatusCode.NORMAL); + } + + @Override + public String toString() { + return String.format("CloseInfo[code=%d,reason=%s]", statusCode, getReason()); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/CloseStatus.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/CloseStatus.java new file mode 100644 index 000000000..c23b333ad --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/CloseStatus.java @@ -0,0 +1,57 @@ +package com.fireflysource.net.websocket.common.model; + +import java.nio.charset.StandardCharsets; + +public class CloseStatus { + private static final int MAX_CONTROL_PAYLOAD = 125; + public static final int MAX_REASON_PHRASE = MAX_CONTROL_PAYLOAD - 2; + + /** + * Convenience method for trimming a long reason phrase at the maximum reason phrase length of 123 UTF-8 bytes (per WebSocket spec). + * + * @param reason the proposed reason phrase + * @return the reason phrase (trimmed if needed) + * @deprecated use of this method is strongly discouraged, as it creates too many new objects that are just thrown away to accomplish its goals. + */ + @Deprecated + public static String trimMaxReasonLength(String reason) { + if (reason == null) { + return null; + } + + byte[] reasonBytes = reason.getBytes(StandardCharsets.UTF_8); + if (reasonBytes.length > MAX_REASON_PHRASE) { + byte[] trimmed = new byte[MAX_REASON_PHRASE]; + System.arraycopy(reasonBytes, 0, trimmed, 0, MAX_REASON_PHRASE); + return new String(trimmed, StandardCharsets.UTF_8); + } + + return reason; + } + + private int code; + private String phrase; + + /** + * Creates a reason for closing a web socket connection with the given code and reason phrase. + * + * @param closeCode the close code + * @param reasonPhrase the reason phrase + * @see StatusCode + */ + public CloseStatus(int closeCode, String reasonPhrase) { + this.code = closeCode; + this.phrase = reasonPhrase; + if (reasonPhrase.length() > MAX_REASON_PHRASE) { + throw new IllegalArgumentException("Phrase exceeds maximum length of " + MAX_REASON_PHRASE); + } + } + + public int getCode() { + return code; + } + + public String getPhrase() { + return phrase; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/Extension.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/Extension.java new file mode 100644 index 000000000..4dc69028e --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/Extension.java @@ -0,0 +1,67 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.net.websocket.common.frame.Frame; + +/** + * Interface for WebSocket Extensions. + *

+ * That {@link Frame}s are passed through the Extension via the {@link IncomingFrames} and {@link OutgoingFrames} interfaces + */ +public interface Extension extends IncomingFrames, OutgoingFrames { + /** + * The active configuration for this extension. + * + * @return the configuration for this extension. never null. + */ + ExtensionConfig getConfig(); + + /** + * The Sec-WebSocket-Extensions name for this extension. + *

+ * Also known as the extension-token per Section 9.1. Negotiating Extensions. + * + * @return the name of the extension + */ + String getName(); + + /** + * Used to indicate that the extension makes use of the RSV1 bit of the base websocket framing. + *

+ * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV1. + * + * @return true if extension uses RSV1 for its own purposes. + */ + boolean isRsv1User(); + + /** + * Used to indicate that the extension makes use of the RSV2 bit of the base websocket framing. + *

+ * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV2. + * + * @return true if extension uses RSV2 for its own purposes. + */ + boolean isRsv2User(); + + /** + * Used to indicate that the extension makes use of the RSV3 bit of the base websocket framing. + *

+ * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV3. + * + * @return true if extension uses RSV3 for its own purposes. + */ + boolean isRsv3User(); + + /** + * Set the next {@link IncomingFrames} to call in the chain. + * + * @param nextIncoming the next incoming extension + */ + void setNextIncomingFrames(IncomingFrames nextIncoming); + + /** + * Set the next {@link OutgoingFrames} to call in the chain. + * + * @param nextOutgoing the next outgoing extension + */ + void setNextOutgoingFrames(OutgoingFrames nextOutgoing); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/ExtensionConfig.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/ExtensionConfig.java new file mode 100644 index 000000000..eb8879459 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/ExtensionConfig.java @@ -0,0 +1,191 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.net.websocket.common.utils.QuoteUtil; + +import java.util.*; + +/** + * Represents an Extension Configuration, as seen during the connection Handshake process. + */ +public class ExtensionConfig { + /** + * Parse a single parameterized name. + * + * @param parameterizedName the parameterized name + * @return the ExtensionConfig + */ + public static ExtensionConfig parse(String parameterizedName) { + return new ExtensionConfig(parameterizedName); + } + + /** + * Parse enumeration of Sec-WebSocket-Extensions header values into a {@link ExtensionConfig} list + * + * @param valuesEnum the raw header values enum + * @return the list of extension configs + */ + public static List parseEnum(Enumeration valuesEnum) { + List configs = new ArrayList<>(); + + if (valuesEnum != null) { + while (valuesEnum.hasMoreElements()) { + Iterator extTokenIter = QuoteUtil.splitAt(valuesEnum.nextElement(), ","); + while (extTokenIter.hasNext()) { + String extToken = extTokenIter.next(); + configs.add(ExtensionConfig.parse(extToken)); + } + } + } + + return configs; + } + + /** + * Parse 1 or more raw Sec-WebSocket-Extensions header values into a {@link ExtensionConfig} list + * + * @param rawSecWebSocketExtensions the raw header values + * @return the list of extension configs + */ + public static List parseList(List rawSecWebSocketExtensions) { + List configs = new ArrayList<>(); + + for (String rawValue : rawSecWebSocketExtensions) { + Iterator extTokenIter = QuoteUtil.splitAt(rawValue, ","); + while (extTokenIter.hasNext()) { + String extToken = extTokenIter.next(); + configs.add(ExtensionConfig.parse(extToken)); + } + } + + return configs; + } + + /** + * Convert a list of {@link ExtensionConfig} to a header value + * + * @param configs the list of extension configs + * @return the header value (null if no configs present) + */ + public static String toHeaderValue(List configs) { + if ((configs == null) || (configs.isEmpty())) { + return null; + } + StringBuilder parameters = new StringBuilder(); + boolean needsDelim = false; + for (ExtensionConfig ext : configs) { + if (needsDelim) { + parameters.append(", "); + } + parameters.append(ext.getParameterizedName()); + needsDelim = true; + } + return parameters.toString(); + } + + private final String name; + private final Map parameters; + + /** + * Copy constructor + * + * @param copy the extension config to copy + */ + public ExtensionConfig(ExtensionConfig copy) { + this.name = copy.name; + this.parameters = new HashMap<>(); + this.parameters.putAll(copy.parameters); + } + + public ExtensionConfig(String parameterizedName) { + Iterator extListIter = QuoteUtil.splitAt(parameterizedName, ";"); + this.name = extListIter.next(); + this.parameters = new HashMap<>(); + + // now for parameters + while (extListIter.hasNext()) { + String extParam = extListIter.next(); + Iterator extParamIter = QuoteUtil.splitAt(extParam, "="); + String key = extParamIter.next().trim(); + String value = null; + if (extParamIter.hasNext()) { + value = extParamIter.next(); + } + parameters.put(key, value); + } + } + + public String getName() { + return name; + } + + public final int getParameter(String key, int defValue) { + String val = parameters.get(key); + if (val == null) { + return defValue; + } + return Integer.parseInt(val); + } + + public final String getParameter(String key, String defValue) { + String val = parameters.get(key); + if (val == null) { + return defValue; + } + return val; + } + + public final String getParameterizedName() { + StringBuilder str = new StringBuilder(); + str.append(name); + for (String param : parameters.keySet()) { + str.append(';'); + str.append(param); + String value = parameters.get(param); + if (value != null) { + str.append('='); + QuoteUtil.quoteIfNeeded(str, value, ";="); + } + } + return str.toString(); + } + + public final Set getParameterKeys() { + return parameters.keySet(); + } + + /** + * Return parameters found in request URI. + * + * @return the parameter map + */ + public final Map getParameters() { + return parameters; + } + + /** + * Initialize the parameters on this config from the other configuration. + * + * @param other the other configuration. + */ + public final void init(ExtensionConfig other) { + this.parameters.clear(); + this.parameters.putAll(other.parameters); + } + + public final void setParameter(String key) { + parameters.put(key, null); + } + + public final void setParameter(String key, int value) { + parameters.put(key, Integer.toString(value)); + } + + public final void setParameter(String key, String value) { + parameters.put(key, value); + } + + @Override + public String toString() { + return getParameterizedName(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/IncomingFrames.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/IncomingFrames.java new file mode 100644 index 000000000..797fb8951 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/IncomingFrames.java @@ -0,0 +1,19 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.net.websocket.common.frame.Frame; + +/** + * Interface for dealing with Incoming Frames. + */ +public interface IncomingFrames { + /** + * Process the incoming frame. + *

+ * Note: if you need to hang onto any information from the frame, be sure + * to copy it, as the information contained in the Frame will be released + * and/or reused by the implementation. + * + * @param frame the frame to process + */ + void incomingFrame(Frame frame); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/OpCode.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/OpCode.java new file mode 100644 index 000000000..9d00a731b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/OpCode.java @@ -0,0 +1,89 @@ +package com.fireflysource.net.websocket.common.model; + +public final class OpCode { + /** + * OpCode for a Continuation Frame + * + * @see RFC 6455, Section 11.8 (WebSocket Opcode Registry + */ + public static final byte CONTINUATION = (byte) 0x00; + + /** + * OpCode for a Text Frame + * + * @see RFC 6455, Section 11.8 (WebSocket Opcode Registry + */ + public static final byte TEXT = (byte) 0x01; + + /** + * OpCode for a Binary Frame + * + * @see RFC 6455, Section 11.8 (WebSocket Opcode Registry + */ + public static final byte BINARY = (byte) 0x02; + + /** + * OpCode for a Close Frame + * + * @see RFC 6455, Section 11.8 (WebSocket Opcode Registry + */ + public static final byte CLOSE = (byte) 0x08; + + /** + * OpCode for a Ping Frame + * + * @see RFC 6455, Section 11.8 (WebSocket Opcode Registry + */ + public static final byte PING = (byte) 0x09; + + /** + * OpCode for a Pong Frame + * + * @see RFC 6455, Section 11.8 (WebSocket Opcode Registry + */ + public static final byte PONG = (byte) 0x0A; + + /** + * An undefined OpCode + */ + public static final byte UNDEFINED = (byte) -1; + + public static boolean isControlFrame(byte opcode) { + return (opcode >= CLOSE); + } + + public static boolean isDataFrame(byte opcode) { + return (opcode == TEXT) || (opcode == BINARY) || (opcode == CONTINUATION); + } + + /** + * Test for known opcodes (per the RFC spec) + * + * @param opcode the opcode to test + * @return true if known. false if unknown, undefined, or reserved + */ + public static boolean isKnown(byte opcode) { + return (opcode == CONTINUATION) || (opcode == TEXT) || (opcode == BINARY) || (opcode == CLOSE) || (opcode == PING) || (opcode == PONG); + } + + public static String name(byte opcode) { + switch (opcode) { + case -1: + return "NO-OP"; + case CONTINUATION: + return "CONTINUATION"; + case TEXT: + return "TEXT"; + case BINARY: + return "BINARY"; + case CLOSE: + return "CLOSE"; + case PING: + return "PING"; + case PONG: + return "PONG"; + default: + return "NON-SPEC[" + opcode + "]"; + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/OutgoingFrames.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/OutgoingFrames.java new file mode 100644 index 000000000..81d43984b --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/OutgoingFrames.java @@ -0,0 +1,25 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.util.function.Consumer; + +/** + * Interface for dealing with frames outgoing to (eventually) the network layer. + */ +public interface OutgoingFrames { + /** + * A frame, and optional callback, intended for the network layer. + *

+ * Note: the frame can undergo many transformations in the various + * layers and extensions present in the implementation. + *

+ * If you are implementing a mutation, you are obliged to handle + * the incoming WriteCallback appropriately. + * + * @param frame the frame to eventually write to the network layer. + * @param result the callback to notify when the frame is written. + */ + void outgoingFrame(Frame frame, Consumer> result); +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/StatusCode.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/StatusCode.java new file mode 100644 index 000000000..963f9c7d3 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/StatusCode.java @@ -0,0 +1,178 @@ +package com.fireflysource.net.websocket.common.model; + +/** + * The RFC 6455 specified status codes and IANA: WebSocket Close Code Number Registry + */ +public final class StatusCode { + /** + * 1000 indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int NORMAL = 1000; + + /** + * 1001 indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int SHUTDOWN = 1001; + + /** + * 1002 indicates that an endpoint is terminating the connection due to a protocol error. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int PROTOCOL = 1002; + + /** + * 1003 indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands + * only text data MAY send this if it receives a binary message). + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int BAD_DATA = 1003; + + /** + * Reserved. The specific meaning might be defined in the future. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int UNDEFINED = 1004; + + /** + * 1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting + * a status code to indicate that no status code was actually present. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int NO_CODE = 1005; + + /** + * 1006 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting + * a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int NO_CLOSE = 1006; + + /** + * Abnormal Close is a synonym for {@link #NO_CLOSE}, used to indicate a close + * condition where no close frame was processed from the remote side. + */ + public static final int ABNORMAL = NO_CLOSE; + + /** + * 1007 indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the + * message (e.g., non-UTF-8 [RFC3629] data within a text message). + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int BAD_PAYLOAD = 1007; + + /** + * 1008 indicates that an endpoint is terminating the connection because it has received a message that violates its policy. This is a generic status code + * that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the + * policy. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int POLICY_VIOLATION = 1008; + + /** + * 1009 indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int MESSAGE_TOO_LARGE = 1009; + + /** + * 1010 indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the + * server didn't return them in the response message of the WebSocket handshake. The list of extensions that are needed SHOULD appear in the /reason/ part + * of the Close frame. Note that this status code is not used by the server, because it can fail the WebSocket handshake instead. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int REQUIRED_EXTENSION = 1010; + + /** + * 1011 indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request. + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int SERVER_ERROR = 1011; + + /** + * 1012 indicates that the service is restarted. a client may reconnect, and if it chooses to do, should reconnect using a randomized delay of 5 - 30s. + *

+ * See [hybi] Additional WebSocket Close Error Codes + */ + public static final int SERVICE_RESTART = 1012; + + /** + * 1013 indicates that the service is experiencing overload. a client should only connect to a different IP (when there are multiple for the target) or + * reconnect to the same IP upon user action. + *

+ * See [hybi] Additional WebSocket Close Error Codes + */ + public static final int TRY_AGAIN_LATER = 1013; + + /** + * 1014 indicates that a gateway or proxy received and invalid upstream response. + *

+ * See [hybi] WebSocket Subprotocol Close Code: Bad Gateway + */ + public static final int INVALID_UPSTREAM_RESPONSE = 1014; + + /** + * 1015 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting + * a status code to indicate that the connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified). + *

+ * See RFC 6455, Section 7.4.1 Defined Status Codes. + */ + public static final int FAILED_TLS_HANDSHAKE = 1015; + + /** + * Test if provided status code is a fatal failure for bad protocol behavior. + * + * @param statusCode the status code to test + * @return true if fatal status code + */ + public static boolean isFatal(int statusCode) { + return (statusCode == ABNORMAL) || + (statusCode == PROTOCOL) || + (statusCode == MESSAGE_TOO_LARGE) || + (statusCode == BAD_DATA) || + (statusCode == BAD_PAYLOAD) || + (statusCode == POLICY_VIOLATION) || + (statusCode == REQUIRED_EXTENSION) || + (statusCode == SERVER_ERROR) || + (statusCode == SERVICE_RESTART); + } + + /** + * Test if provided status code can be sent/received on a WebSocket close. + *

+ * This honors the RFC6455 rules and IANA rules. + *

+ * + * @param statusCode the statusCode to test + * @return true if transmittable + */ + public static boolean isTransmittable(int statusCode) { + return (statusCode == NORMAL) || + (statusCode == SHUTDOWN) || + (statusCode == PROTOCOL) || + (statusCode == BAD_DATA) || + (statusCode == BAD_PAYLOAD) || + (statusCode == POLICY_VIOLATION) || + (statusCode == MESSAGE_TOO_LARGE) || + (statusCode == REQUIRED_EXTENSION) || + (statusCode == SERVER_ERROR) || + (statusCode == SERVICE_RESTART) || + (statusCode == TRY_AGAIN_LATER) || + (statusCode == INVALID_UPSTREAM_RESPONSE) || + ((statusCode >= 3000) && (statusCode <= 4999)); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/WebSocketBehavior.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/WebSocketBehavior.java new file mode 100644 index 000000000..9e901f1bf --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/WebSocketBehavior.java @@ -0,0 +1,12 @@ +package com.fireflysource.net.websocket.common.model; + +/** + * Behavior for how the WebSocket should operate. + *

+ * This dictated by the RFC 6455 spec in various places, where certain behavior must be performed depending on + * operation as a CLIENT vs a SERVER + */ +public enum WebSocketBehavior { + CLIENT, + SERVER +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/WebSocketPolicy.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/WebSocketPolicy.java new file mode 100644 index 000000000..53fe8db8f --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/model/WebSocketPolicy.java @@ -0,0 +1,465 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.net.websocket.common.exception.MessageTooLargeException; + +/** + * Settings for WebSocket operations. + */ +public class WebSocketPolicy { + private static final int KB = 1024; + + public static WebSocketPolicy newClientPolicy() { + return new WebSocketPolicy(WebSocketBehavior.CLIENT); + } + + public static WebSocketPolicy newServerPolicy() { + return new WebSocketPolicy(WebSocketBehavior.SERVER); + } + + /** + * The maximum size of a text message during parsing/generating. + *

+ * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} + *

+ * Default: 65536 (64 K) + */ + private int maxTextMessageSize = 64 * KB; + + /** + * The maximum size of a text message buffer. + *

+ * Used ONLY for stream based message writing. + *

+ * Default: 32768 (32 K) + */ + private int maxTextMessageBufferSize = 32 * KB; + + /** + * The maximum size of a binary message during parsing/generating. + *

+ * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} + *

+ * Default: 65536 (64 K) + */ + private int maxBinaryMessageSize = 64 * KB; + + /** + * The maximum size of a binary message buffer + *

+ * Used ONLY for for stream based message writing + *

+ * Default: 32768 (32 K) + */ + private int maxBinaryMessageBufferSize = 32 * KB; + + /** + * The timeout in ms (milliseconds) for async write operations. + *

+ * Negative values indicate a disabled timeout. + */ + private long asyncWriteTimeout = 60000; + + /** + * The time in ms (milliseconds) that a websocket may be idle before closing. + *

+ * Default: 300000 (ms) + */ + private long idleTimeout = 300000; + + /** + * The size of the input (read from network layer) buffer size. + *

+ * Default: 4096 (4 K) + */ + private int inputBufferSize = 4 * KB; + + /** + * Behavior of the websockets + */ + private final WebSocketBehavior behavior; + + public WebSocketPolicy(WebSocketBehavior behavior) { + this.behavior = behavior; + } + + private void assertLessThan(String name, long size, String otherName, long otherSize) { + if (size > otherSize) { + throw new IllegalArgumentException(String.format("%s [%d] must be less than %s [%d]", name, size, otherName, otherSize)); + } + } + + private void assertGreaterThan(String name, long size, long minSize) { + if (size < minSize) { + throw new IllegalArgumentException(String.format("%s [%d] must be a greater than or equal to " + minSize, name, size)); + } + } + + public void assertValidBinaryMessageSize(int requestedSize) { + if (maxBinaryMessageSize > 0) { + // validate it + if (requestedSize > maxBinaryMessageSize) { + throw new MessageTooLargeException("Binary message size [" + requestedSize + "] exceeds maximum size [" + maxBinaryMessageSize + "]"); + } + } + } + + public void assertValidTextMessageSize(int requestedSize) { + if (maxTextMessageSize > 0) { + // validate it + if (requestedSize > maxTextMessageSize) { + throw new MessageTooLargeException("Text message size [" + requestedSize + "] exceeds maximum size [" + maxTextMessageSize + "]"); + } + } + } + + /** + * Make a copy of the policy, with current values. + * + * @return the cloned copy of the policy. + */ + public WebSocketPolicy clonePolicy() { + WebSocketPolicy clone = new WebSocketPolicy(this.behavior); + clone.idleTimeout = this.getIdleTimeout(); + clone.maxTextMessageSize = this.getMaxTextMessageSize(); + clone.maxTextMessageBufferSize = this.getMaxTextMessageBufferSize(); + clone.maxBinaryMessageSize = this.getMaxBinaryMessageSize(); + clone.maxBinaryMessageBufferSize = this.getMaxBinaryMessageBufferSize(); + clone.inputBufferSize = this.getInputBufferSize(); + clone.asyncWriteTimeout = this.getAsyncWriteTimeout(); + return clone; + } + + /** + * Make a copy of the policy, with current values, but a different behavior. + * + * @param behavior the behavior to copy/clone + * @return the cloned policy with a new behavior. + * @deprecated use {@link #delegateAs(WebSocketBehavior)} instead + */ + @Deprecated + public WebSocketPolicy clonePolicy(WebSocketBehavior behavior) { + return delegateAs(behavior); + } + + public WebSocketPolicy delegateAs(WebSocketBehavior behavior) { + if (behavior == this.behavior) + return this; + + return new WebSocketPolicy.Delegated(this, behavior); + } + + /** + * The timeout in ms (milliseconds) for async write operations. + *

+ * Negative values indicate a disabled timeout. + * + * @return the timeout for async write operations. negative values indicate disabled timeout. + */ + @Deprecated + public long getAsyncWriteTimeout() { + return asyncWriteTimeout; + } + + public WebSocketBehavior getBehavior() { + return behavior; + } + + /** + * The time in ms (milliseconds) that a websocket connection may be idle before being closed automatically. + * + * @return the timeout in milliseconds for idle timeout. + */ + public long getIdleTimeout() { + return idleTimeout; + } + + /** + * The size of the input (read from network layer) buffer size. + *

+ * This is the raw read operation buffer size, before the parsing of the websocket frames. + * + * @return the raw network bytes read operation buffer size. + */ + public int getInputBufferSize() { + return inputBufferSize; + } + + /** + * Get the maximum size of a binary message buffer (for streaming writing) + * + * @return the maximum size of a binary message buffer + */ + public int getMaxBinaryMessageBufferSize() { + return maxBinaryMessageBufferSize; + } + + /** + * Get the maximum size of a binary message during parsing. + *

+ * This is a memory conservation option, memory over this limit will not be + * allocated by handling binary messages. This applies to individual frames, + * whole message handling, and partial message handling. + *

+ *

+ * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} + *

+ * + * @return the maximum size of a binary message + */ + public int getMaxBinaryMessageSize() { + return maxBinaryMessageSize; + } + + /** + * Get the maximum size of a text message buffer (for streaming writing) + * + * @return the maximum size of a text message buffer + */ + public int getMaxTextMessageBufferSize() { + return maxTextMessageBufferSize; + } + + /** + * Get the maximum size of a text message during parsing. + *

+ * This is a memory conservation option, memory over this limit will not be + * allocated by handling text messages. This applies to individual frames, + * whole message handling, and partial message handling. + *

+ *

+ * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} + *

+ * + * @return the maximum size of a text message. + */ + public int getMaxTextMessageSize() { + return maxTextMessageSize; + } + + /** + * The timeout in ms (milliseconds) for async write operations. + *

+ * Negative values indicate a disabled timeout. + * + * @param ms the timeout in milliseconds + */ + public void setAsyncWriteTimeout(long ms) { + assertLessThan("AsyncWriteTimeout", ms, "IdleTimeout", idleTimeout); + this.asyncWriteTimeout = ms; + } + + /** + * The time in ms (milliseconds) that a websocket may be idle before closing. + * + * @param ms the timeout in milliseconds + */ + public void setIdleTimeout(long ms) { + assertGreaterThan("IdleTimeout", ms, 0); + this.idleTimeout = ms; + } + + /** + * The size of the input (read from network layer) buffer size. + * + * @param size the size in bytes + */ + public void setInputBufferSize(int size) { + assertGreaterThan("InputBufferSize", size, 1); + this.inputBufferSize = size; + } + + /** + * The maximum size of a binary message buffer. + *

+ * Used ONLY for stream based binary message writing. + * + * @param size the maximum size of the binary message buffer + */ + public void setMaxBinaryMessageBufferSize(int size) { + assertGreaterThan("MaxBinaryMessageBufferSize", size, 1); + + this.maxBinaryMessageBufferSize = size; + } + + /** + * The maximum size of a binary message during parsing. + *

+ * This is a memory conservation option, memory over this limit will not be + * allocated by handling binary messages. This applies to individual frames, + * whole message handling, and partial message handling. + *

+ *

+ * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} + *

+ * + * @param size the maximum allowed size of a binary message. + */ + public void setMaxBinaryMessageSize(int size) { + assertGreaterThan("MaxBinaryMessageSize", size, -1); + + this.maxBinaryMessageSize = size; + } + + /** + * The maximum size of a text message buffer. + *

+ * Used ONLY for stream based text message writing. + * + * @param size the maximum size of the text message buffer + */ + public void setMaxTextMessageBufferSize(int size) { + assertGreaterThan("MaxTextMessageBufferSize", size, 1); + + this.maxTextMessageBufferSize = size; + } + + /** + * The maximum size of a text message during parsing. + *

+ * This is a memory conservation option, memory over this limit will not be + * allocated by handling text messages. This applies to individual frames, + * whole message handling, and partial message handling. + *

+ *

+ * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} + *

+ * + * @param size the maximum allowed size of a text message. + */ + public void setMaxTextMessageSize(int size) { + assertGreaterThan("MaxTextMessageSize", size, -1); + + this.maxTextMessageSize = size; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(this.getClass().getSimpleName()); + builder.append("@").append(Integer.toHexString(hashCode())); + builder.append("[behavior=").append(getBehavior()); + builder.append(",maxTextMessageSize=").append(getMaxTextMessageSize()); + builder.append(",maxTextMessageBufferSize=").append(getMaxTextMessageBufferSize()); + builder.append(",maxBinaryMessageSize=").append(getMaxBinaryMessageSize()); + builder.append(",maxBinaryMessageBufferSize=").append(getMaxTextMessageBufferSize()); + builder.append(",asyncWriteTimeout=").append(getAsyncWriteTimeout()); + builder.append(",idleTimeout=").append(getIdleTimeout()); + builder.append(",inputBufferSize=").append(getInputBufferSize()); + builder.append("]"); + return builder.toString(); + } + + /** + * Allows Behavior to be changed, but the settings to delegated. + *

+ * This rears its ugly head when a JSR356 Server Container is used as a + * JSR356 Client Container. + * The JSR356 Server Container is Behavior SERVER, but its container + * level Policy is shared with the JSR356 Client Container as well. + * This allows a delegate to the policy with a different behavior. + *

+ */ + private class Delegated extends WebSocketPolicy { + private final WebSocketPolicy delegated; + + public Delegated(WebSocketPolicy policy, WebSocketBehavior behavior) { + super(behavior); + this.delegated = policy; + } + + @Override + public void assertValidBinaryMessageSize(int requestedSize) { + delegated.assertValidBinaryMessageSize(requestedSize); + } + + @Override + public void assertValidTextMessageSize(int requestedSize) { + delegated.assertValidTextMessageSize(requestedSize); + } + + @Override + public WebSocketPolicy clonePolicy() { + return delegated.clonePolicy(); + } + + @Override + public WebSocketPolicy clonePolicy(WebSocketBehavior behavior) { + return delegated.clonePolicy(behavior); + } + + @Override + public WebSocketPolicy delegateAs(WebSocketBehavior behavior) { + return delegated.delegateAs(behavior); + } + + @Override + public long getAsyncWriteTimeout() { + return delegated.getAsyncWriteTimeout(); + } + + @Override + public long getIdleTimeout() { + return delegated.getIdleTimeout(); + } + + @Override + public int getInputBufferSize() { + return delegated.getInputBufferSize(); + } + + @Override + public int getMaxBinaryMessageBufferSize() { + return delegated.getMaxBinaryMessageBufferSize(); + } + + @Override + public int getMaxBinaryMessageSize() { + return delegated.getMaxBinaryMessageSize(); + } + + @Override + public int getMaxTextMessageBufferSize() { + return delegated.getMaxTextMessageBufferSize(); + } + + @Override + public int getMaxTextMessageSize() { + return delegated.getMaxTextMessageSize(); + } + + @Override + public void setAsyncWriteTimeout(long ms) { + delegated.setAsyncWriteTimeout(ms); + } + + @Override + public void setIdleTimeout(long ms) { + delegated.setIdleTimeout(ms); + } + + @Override + public void setInputBufferSize(int size) { + delegated.setInputBufferSize(size); + } + + @Override + public void setMaxBinaryMessageBufferSize(int size) { + delegated.setMaxBinaryMessageBufferSize(size); + } + + @Override + public void setMaxBinaryMessageSize(int size) { + delegated.setMaxBinaryMessageSize(size); + } + + @Override + public void setMaxTextMessageBufferSize(int size) { + delegated.setMaxTextMessageBufferSize(size); + } + + @Override + public void setMaxTextMessageSize(int size) { + delegated.setMaxTextMessageSize(size); + } + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/ConnectionState.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/ConnectionState.java new file mode 100644 index 000000000..fa3883064 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/ConnectionState.java @@ -0,0 +1,43 @@ +package com.fireflysource.net.websocket.common.stream; + +import com.fireflysource.net.websocket.common.model.CloseInfo; + +/** + * Connection states as outlined in RFC6455. + */ +public enum ConnectionState { + /** + * [RFC] Initial state of a connection, the upgrade request / response is in progress + */ + CONNECTING, + /** + * [Impl] Intermediate state between CONNECTING and OPEN, used to indicate that a upgrade request/response is successful, but the end-user provided socket's + * onOpen code has yet to run. + *

+ * This state is to allow the local socket to initiate messages and frames, but to NOT start reading yet. + */ + CONNECTED, + /** + * [RFC] The websocket connection is established and open. + *

+ * This indicates that the Upgrade has succeed, and the end-user provided socket's onOpen code has completed. + *

+ * It is now time to start reading from the remote endpoint. + */ + OPEN, + /** + * [RFC] The websocket closing handshake is started. + *

+ * This can be considered a half-closed state. + *

+ * When receiving this as an event on {@link IOState.ConnectionStateListener#onConnectionStateChange(ConnectionState)} a close frame should be sent using + * the {@link CloseInfo} available from {@link IOState#getCloseInfo()} + */ + CLOSING, + /** + * [RFC] The websocket connection is closed. + *

+ * Connection should be disconnected and no further reads or writes should occur. + */ + CLOSED +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/ExtensionNegotiator.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/ExtensionNegotiator.java new file mode 100644 index 000000000..836194222 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/ExtensionNegotiator.java @@ -0,0 +1,136 @@ +package com.fireflysource.net.websocket.common.stream; + + +import com.fireflysource.common.collection.CollectionUtils; +import com.fireflysource.common.object.Assert; +import com.fireflysource.net.websocket.common.decoder.Parser; +import com.fireflysource.net.websocket.common.encoder.Generator; +import com.fireflysource.net.websocket.common.extension.AbstractExtension; +import com.fireflysource.net.websocket.common.extension.ExtensionFactory; +import com.fireflysource.net.websocket.common.extension.WebSocketExtensionFactory; +import com.fireflysource.net.websocket.common.model.*; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author Pengtao Qiu + */ +public class ExtensionNegotiator { + + private ExtensionFactory factory; + private IncomingFrames nextIncomingFrames; + private OutgoingFrames nextOutgoingFrames; + private IncomingFrames incomingFrames; + private OutgoingFrames outgoingFrames; + + public ExtensionNegotiator() { + this(new WebSocketExtensionFactory()); + } + + public ExtensionNegotiator(ExtensionFactory factory) { + this.factory = factory; + } + + public ExtensionFactory getFactory() { + return factory; + } + + public void setFactory(ExtensionFactory factory) { + this.factory = factory; + } + + public List createExtensionConfigs(List rawSecWebSocketExtensions) { + return ExtensionConfig + .parseList(rawSecWebSocketExtensions) + .stream() + .filter(c -> factory.isAvailable(c.getName())) + .collect(Collectors.toList()); + } + + public void configureExtensions(List rawSecWebSocketExtensions, Parser parser, Generator generator, WebSocketPolicy policy) { + Assert.notNull(nextIncomingFrames, "The next incoming frames MUST be not null"); + Assert.notNull(nextOutgoingFrames, "The next outgoing frames MUST be not null"); + + List extensionConfigs = createExtensionConfigs(rawSecWebSocketExtensions); + if (CollectionUtils.isEmpty(extensionConfigs)) { + incomingFrames = nextIncomingFrames; + outgoingFrames = nextOutgoingFrames; + } else { + List incomingExtensions = createExtensions(extensionConfigs, policy); + List outgoingExtensions = createExtensions(extensionConfigs, policy); + + Collections.reverse(incomingExtensions); + + parser.configureFromExtensions(incomingExtensions); + generator.configureFromExtensions(outgoingExtensions); + + int lastIncoming = incomingExtensions.size() - 1; + for (int i = 0; i < incomingExtensions.size(); i++) { + int next = i + 1; + Extension extension = incomingExtensions.get(i); + if (next <= lastIncoming) { + extension.setNextIncomingFrames(incomingExtensions.get(next)); + } else { + extension.setNextIncomingFrames(nextIncomingFrames); + } + } + + int lastOutgoing = outgoingExtensions.size() - 1; + for (int i = 0; i < outgoingExtensions.size(); i++) { + int next = i + 1; + Extension extension = outgoingExtensions.get(i); + if (next <= lastOutgoing) { + extension.setNextOutgoingFrames(outgoingExtensions.get(next)); + } else { + extension.setNextOutgoingFrames(nextOutgoingFrames); + } + } + + incomingFrames = incomingExtensions.get(0); + outgoingFrames = outgoingExtensions.get(0); + } + } + + private List createExtensions(List extensionConfigs, WebSocketPolicy policy) { + return extensionConfigs + .stream() + .map(c -> { + Extension e = factory.newInstance(c); + if (e instanceof AbstractExtension) { + AbstractExtension abstractExtension = (AbstractExtension) e; + abstractExtension.setConfig(c); + abstractExtension.setPolicy(policy); + } + return e; + }) + .collect(Collectors.toList()); + } + + public IncomingFrames getNextIncomingFrames() { + return nextIncomingFrames; + } + + public void setNextIncomingFrames(IncomingFrames nextIncomingFrames) { + this.nextIncomingFrames = nextIncomingFrames; + } + + public OutgoingFrames getNextOutgoingFrames() { + return nextOutgoingFrames; + } + + public void setNextOutgoingFrames(OutgoingFrames nextOutgoingFrames) { + this.nextOutgoingFrames = nextOutgoingFrames; + } + + public IncomingFrames getIncomingFrames() { + return incomingFrames; + } + + public OutgoingFrames getOutgoingFrames() { + return outgoingFrames; + } + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/IOState.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/IOState.java new file mode 100644 index 000000000..bf14ca191 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/stream/IOState.java @@ -0,0 +1,491 @@ +package com.fireflysource.net.websocket.common.stream; + +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.WebSocketConnectionState; +import com.fireflysource.net.websocket.common.model.CloseInfo; +import com.fireflysource.net.websocket.common.model.StatusCode; + +import java.io.EOFException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Simple state tracker for Input / Output and {@link ConnectionState}. + *

+ * Use the various known .on*() methods to trigger a state change. + *

    + *
  • {@link #onOpen()} - connection has been opened
  • + *
+ */ +public class IOState implements WebSocketConnectionState { + /** + * The source of a close handshake. (ie: who initiated it). + */ + private enum CloseHandshakeSource { + /** + * No close handshake initiated (yet) + */ + NONE, + /** + * Local side initiated the close handshake + */ + LOCAL, + /** + * Remote side initiated the close handshake + */ + REMOTE, + /** + * An abnormal close situation (disconnect, timeout, etc...) + */ + ABNORMAL + } + + public interface ConnectionStateListener { + void onConnectionStateChange(ConnectionState state); + } + + private static LazyLogger LOG = SystemLogger.create(IOState.class); + + private ConnectionState state; + private final List listeners = new LinkedList<>(); + + /** + * Is input on websocket available (for reading frames). + * Used to determine close handshake completion, and track half-close states + */ + private boolean inputAvailable; + /** + * Is output on websocket available (for writing frames). + * Used to determine close handshake completion, and track half-closed states. + */ + private boolean outputAvailable; + /** + * Initiator of the close handshake. + * Used to determine who initiated a close handshake for reply reasons. + */ + private CloseHandshakeSource closeHandshakeSource; + /** + * The close info for the initiator of the close handshake. + * It is possible in abnormal close scenarios to have a different + * final close info that is used to notify the WS-Endpoint's onClose() + * events with. + */ + private CloseInfo closeInfo; + /** + * Atomic reference to the final close info. + * This can only be set once, and is used for the WS-Endpoint's onClose() + * event. + */ + private AtomicReference finalClose = new AtomicReference<>(); + /** + * Tracker for if the close handshake was completed successfully by + * both sides. False if close was sudden or abnormal. + */ + private boolean cleanClose; + + /** + * Create a new IOState, initialized to {@link ConnectionState#CONNECTING} + */ + public IOState() { + this.state = ConnectionState.CONNECTING; + this.inputAvailable = false; + this.outputAvailable = false; + this.closeHandshakeSource = CloseHandshakeSource.NONE; + this.closeInfo = null; + this.cleanClose = false; + } + + public void addListener(ConnectionStateListener listener) { + listeners.add(listener); + } + + public CloseInfo getCloseInfo() { + CloseInfo ci = finalClose.get(); + if (ci != null) { + return ci; + } + return closeInfo; + } + + public ConnectionState getConnectionState() { + return state; + } + + public boolean isClosed() { + return (state == ConnectionState.CLOSED); + } + + public boolean isInputAvailable() { + return inputAvailable; + } + + public boolean isOpen() { + return !isClosed(); + } + + public boolean isOutputAvailable() { + return outputAvailable; + } + + private void notifyStateListeners(ConnectionState state) { + if (LOG.isDebugEnabled()) + LOG.debug("Notify State Listeners: {}", state); + for (ConnectionStateListener listener : listeners) { + if (LOG.isDebugEnabled()) { + LOG.debug("{}.onConnectionStateChange({})", listener.getClass().getSimpleName(), state.name()); + } + try { + listener.onConnectionStateChange(state); + } catch (Exception e) { + LOG.error("handle websocket connection state change event exception.", e); + } + } + } + + /** + * A websocket connection has been disconnected for abnormal close reasons. + *

+ * This is the low level disconnect of the socket. It could be the result of a normal close operation, from an IO error, or even from a timeout. + * + * @param close the close information + */ + public void onAbnormalClose(CloseInfo close) { + if (LOG.isDebugEnabled()) + LOG.debug("onAbnormalClose({})", close); + + if (this.state == ConnectionState.CLOSED) { + // already closed + return; + } + + if (this.state == ConnectionState.OPEN) { + this.cleanClose = false; + } + + this.state = ConnectionState.CLOSED; + finalClose.compareAndSet(null, close); + this.inputAvailable = false; + this.outputAvailable = false; + this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL; + ConnectionState event = this.state; + notifyStateListeners(event); + } + + /** + * A close handshake has been issued from the local endpoint + * + * @param closeInfo the close information + */ + public void onCloseLocal(CloseInfo closeInfo) { + boolean open = false; + + ConnectionState initialState = this.state; + if (LOG.isDebugEnabled()) + LOG.debug("onCloseLocal({}) : {}", closeInfo, initialState); + if (initialState == ConnectionState.CLOSED) { + // already closed + if (LOG.isDebugEnabled()) + LOG.debug("already closed"); + return; + } + + if (initialState == ConnectionState.CONNECTED) { + // fast close. a local close request from end-user onConnect/onOpen method + if (LOG.isDebugEnabled()) + LOG.debug("FastClose in CONNECTED detected"); + open = true; + } + + if (open) + openAndCloseLocal(closeInfo); + else + closeLocal(closeInfo); + } + + private void openAndCloseLocal(CloseInfo closeInfo) { + // Force the state open (to allow read/write to endpoint) + onOpen(); + if (LOG.isDebugEnabled()) + LOG.debug("FastClose continuing with Closure"); + closeLocal(closeInfo); + } + + private void closeLocal(CloseInfo closeInfo) { + ConnectionState event = null; + ConnectionState abnormalEvent = null; + + if (LOG.isDebugEnabled()) + LOG.debug("onCloseLocal(), input={}, output={}", inputAvailable, outputAvailable); + + this.closeInfo = closeInfo; + + // Turn off further output. + outputAvailable = false; + + if (closeHandshakeSource == CloseHandshakeSource.NONE) { + closeHandshakeSource = CloseHandshakeSource.LOCAL; + } + + if (!inputAvailable) { + if (LOG.isDebugEnabled()) + LOG.debug("Close Handshake satisfied, disconnecting"); + cleanClose = true; + this.state = ConnectionState.CLOSED; + finalClose.compareAndSet(null, closeInfo); + event = this.state; + } else if (this.state == ConnectionState.OPEN) { + // We are now entering CLOSING (or half-closed). + this.state = ConnectionState.CLOSING; + event = this.state; + + // If abnormal, we don't expect an answer. + if (closeInfo.isAbnormal()) { + abnormalEvent = ConnectionState.CLOSED; + finalClose.compareAndSet(null, closeInfo); + cleanClose = false; + outputAvailable = false; + inputAvailable = false; + closeHandshakeSource = CloseHandshakeSource.ABNORMAL; + } + } + + // Only notify on state change events + if (event != null) { + notifyStateListeners(event); + if (abnormalEvent != null) { + notifyStateListeners(abnormalEvent); + } + } + } + + /** + * A close handshake has been received from the remote endpoint + * + * @param closeInfo the close information + */ + public void onCloseRemote(CloseInfo closeInfo) { + if (LOG.isDebugEnabled()) + LOG.debug("onCloseRemote({})", closeInfo); + + if (this.state == ConnectionState.CLOSED) { + // already closed + return; + } + + if (LOG.isDebugEnabled()) + LOG.debug("onCloseRemote(), input={}, output={}", inputAvailable, outputAvailable); + + this.closeInfo = closeInfo; + + // turn off further input + inputAvailable = false; + + if (closeHandshakeSource == CloseHandshakeSource.NONE) { + closeHandshakeSource = CloseHandshakeSource.REMOTE; + } + + ConnectionState event = null; + if (!outputAvailable) { + LOG.debug("Close Handshake satisfied, disconnecting"); + cleanClose = true; + state = ConnectionState.CLOSED; + finalClose.compareAndSet(null, closeInfo); + event = this.state; + } else if (this.state == ConnectionState.OPEN) { + // We are now entering CLOSING (or half-closed) + this.state = ConnectionState.CLOSING; + event = this.state; + } + + + // Only notify on state change events + if (event != null) { + notifyStateListeners(event); + } + } + + /** + * WebSocket has successfully upgraded, but the end-user onOpen call hasn't run yet. + *

+ * This is an intermediate state between the RFC's {@link ConnectionState#CONNECTING} and {@link ConnectionState#OPEN} + */ + public void onConnected() { + if (this.state != ConnectionState.CONNECTING) { + LOG.debug("Unable to set to connected, not in CONNECTING state: {}", this.state); + return; + } + + this.state = ConnectionState.CONNECTED; + inputAvailable = false; // cannot read (yet) + outputAvailable = true; // write allowed + ConnectionState event = this.state; + notifyStateListeners(event); + } + + /** + * A websocket connection has finished its upgrade handshake, and is now open. + */ + public void onOpen() { + if (LOG.isDebugEnabled()) + LOG.debug("onOpened()"); + + if (this.state == ConnectionState.OPEN) { + // already opened + return; + } + + if (this.state != ConnectionState.CONNECTED) { + LOG.debug("Unable to open, not in CONNECTED state: {}", this.state); + return; + } + + this.state = ConnectionState.OPEN; + this.inputAvailable = true; + this.outputAvailable = true; + ConnectionState event = this.state; + + notifyStateListeners(event); + } + + /** + * The local endpoint has reached a read failure. + *

+ * This could be a normal result after a proper close handshake, or even a premature close due to a connection disconnect. + * + * @param t the read failure + */ + public void onReadFailure(Throwable t) { + if (this.state == ConnectionState.CLOSED) { + // already closed + return; + } + + // Build out Close Reason + String reason = "WebSocket Read Failure"; + if (t instanceof EOFException) { + reason = "WebSocket Read EOF"; + Throwable cause = t.getCause(); + if ((cause != null) && (StringUtils.hasText(cause.getMessage()))) { + reason = "EOF: " + cause.getMessage(); + } + } else { + if (StringUtils.hasText(t.getMessage())) { + reason = t.getMessage(); + } + } + + CloseInfo close = new CloseInfo(StatusCode.ABNORMAL, reason); + finalClose.compareAndSet(null, close); + closeAndNotify(close); + } + + /** + * The local endpoint has reached a write failure. + *

+ * A low level I/O failure, or even a firefly side EndPoint close (from idle timeout) are a few scenarios + * + * @param t the throwable that caused the write failure + */ + public void onWriteFailure(Throwable t) { + if (this.state == ConnectionState.CLOSED) { + // already closed + return; + } + + // Build out Close Reason + String reason = "WebSocket Write Failure"; + if (t instanceof EOFException) { + reason = "WebSocket Write EOF"; + Throwable cause = t.getCause(); + if ((cause != null) && (StringUtils.hasText(cause.getMessage()))) { + reason = "EOF: " + cause.getMessage(); + } + } else { + if (StringUtils.hasText(t.getMessage())) { + reason = t.getMessage(); + } + } + + CloseInfo close = new CloseInfo(StatusCode.ABNORMAL, reason); + + finalClose.compareAndSet(null, close); + + this.cleanClose = false; + this.state = ConnectionState.CLOSED; + this.inputAvailable = false; + this.outputAvailable = false; + this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL; + ConnectionState event = this.state; + + notifyStateListeners(event); + } + + public void onDisconnected() { + if (this.state == ConnectionState.CLOSED) { + // already closed + return; + } + + CloseInfo close = new CloseInfo(StatusCode.ABNORMAL, "Disconnected"); + closeAndNotify(close); + } + + private void closeAndNotify(CloseInfo close) { + this.cleanClose = false; + this.state = ConnectionState.CLOSED; + this.closeInfo = close; + this.inputAvailable = false; + this.outputAvailable = false; + this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL; + ConnectionState event = this.state; + notifyStateListeners(event); + } + + public boolean isAbnormalClose() { + return closeHandshakeSource == CloseHandshakeSource.ABNORMAL; + } + + public boolean isCleanClose() { + return cleanClose; + } + + public boolean isLocalCloseInitiated() { + return closeHandshakeSource == CloseHandshakeSource.LOCAL; + } + + public boolean isRemoteCloseInitiated() { + return closeHandshakeSource == CloseHandshakeSource.REMOTE; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + str.append(this.getClass().getSimpleName()); + str.append("@").append(Integer.toHexString(hashCode())); + str.append("[").append(state); + str.append(','); + if (!inputAvailable) { + str.append('!'); + } + str.append("in,"); + if (!outputAvailable) { + str.append('!'); + } + str.append("out"); + if ((state == ConnectionState.CLOSED) || (state == ConnectionState.CLOSING)) { + CloseInfo ci = finalClose.get(); + if (ci != null) { + str.append(",finalClose=").append(ci); + } else { + str.append(",close=").append(closeInfo); + } + str.append(",clean=").append(cleanClose); + str.append(",closeSource=").append(closeHandshakeSource); + } + str.append(']'); + return str.toString(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/utils/QuoteUtil.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/utils/QuoteUtil.java new file mode 100644 index 000000000..463efef98 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/utils/QuoteUtil.java @@ -0,0 +1,375 @@ +package com.fireflysource.net.websocket.common.utils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Provide some consistent Http header value and Extension configuration parameter quoting support. + *

    + *
  • ABNF defined extension parameter parsing requirements of RFC-6455 (WebSocket) ABNF, is slightly different than the ABNF parsing defined in RFC-2616 + * (HTTP/1.1).
  • + *
  • Future HTTPbis ABNF changes for parsing will impact QuotedStringTokenizer
  • + *
+ * It was decided to keep this implementation separate for the above reasons. + */ +public class QuoteUtil { + private static class DeQuotingStringIterator implements Iterator { + private enum State { + START, + TOKEN, + QUOTE_SINGLE, + QUOTE_DOUBLE + } + + private final String input; + private final String delims; + private StringBuilder token; + private boolean hasToken = false; + private int i = 0; + + public DeQuotingStringIterator(String input, String delims) { + this.input = input; + this.delims = delims; + int len = input.length(); + token = new StringBuilder(len > 1024 ? 512 : len / 2); + } + + private void appendToken(char c) { + if (hasToken) { + token.append(c); + } else { + if (Character.isWhitespace(c)) { + return; // skip whitespace at start of token. + } else { + token.append(c); + hasToken = true; + } + } + } + + @Override + public boolean hasNext() { + // already found a token + if (hasToken) { + return true; + } + + State state = State.START; + boolean escape = false; + int inputLen = input.length(); + + while (i < inputLen) { + char c = input.charAt(i++); + + switch (state) { + case START: { + if (c == '\'') { + state = State.QUOTE_SINGLE; + appendToken(c); + } else if (c == '\"') { + state = State.QUOTE_DOUBLE; + appendToken(c); + } else { + appendToken(c); + state = State.TOKEN; + } + break; + } + case TOKEN: { + if (delims.indexOf(c) >= 0) { + // System.out.printf("hasNext/t: %b [%s]%n",hasToken,token); + return hasToken; + } else if (c == '\'') { + state = State.QUOTE_SINGLE; + } else if (c == '\"') { + state = State.QUOTE_DOUBLE; + } + appendToken(c); + break; + } + case QUOTE_SINGLE: { + if (escape) { + escape = false; + appendToken(c); + } else if (c == '\'') { + appendToken(c); + state = State.TOKEN; + } else if (c == '\\') { + escape = true; + } else { + appendToken(c); + } + break; + } + case QUOTE_DOUBLE: { + if (escape) { + escape = false; + appendToken(c); + } else if (c == '\"') { + appendToken(c); + state = State.TOKEN; + } else if (c == '\\') { + escape = true; + } else { + appendToken(c); + } + break; + } + } + // System.out.printf("%s <%s> : [%s]%n",state,c,token); + } + // System.out.printf("hasNext/e: %b [%s]%n",hasToken,token); + return hasToken; + } + + @Override + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + String ret = token.toString(); + token.setLength(0); + hasToken = false; + return QuoteUtil.dequote(ret.trim()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported with this iterator"); + } + } + + /** + * ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting. + */ + public static final String ABNF_REQUIRED_QUOTING = "\"'\\\n\r\t\f\b%+ ;="; + + private static final char UNICODE_TAG = 0xFFFF; + private static final char[] escapes = new char[32]; + + static { + Arrays.fill(escapes, UNICODE_TAG); + // non-unicode + escapes['\b'] = 'b'; + escapes['\t'] = 't'; + escapes['\n'] = 'n'; + escapes['\f'] = 'f'; + escapes['\r'] = 'r'; + } + + private static int dehex(byte b) { + if ((b >= '0') && (b <= '9')) { + return (byte) (b - '0'); + } + if ((b >= 'a') && (b <= 'f')) { + return (byte) ((b - 'a') + 10); + } + if ((b >= 'A') && (b <= 'F')) { + return (byte) ((b - 'A') + 10); + } + throw new IllegalArgumentException("!hex:" + Integer.toHexString(0xff & b)); + } + + /** + * Remove quotes from a string, only if the input string start with and end with the same quote character. + * + * @param str the string to remove surrounding quotes from + * @return the de-quoted string + */ + public static String dequote(String str) { + char start = str.charAt(0); + if ((start == '\'') || (start == '\"')) { + // possibly quoted + char end = str.charAt(str.length() - 1); + if (start == end) { + // dequote + return str.substring(1, str.length() - 1); + } + } + return str; + } + + public static void escape(StringBuilder buf, String str) { + for (char c : str.toCharArray()) { + if (c >= 32) { + // non special character + if ((c == '"') || (c == '\\')) { + buf.append('\\'); + } + buf.append(c); + } else { + // special characters, requiring escaping + char escaped = escapes[c]; + + // is this a unicode escape? + if (escaped == UNICODE_TAG) { + buf.append("\\u00"); + if (c < 0x10) { + buf.append('0'); + } + buf.append(Integer.toString(c, 16)); // hex + } else { + // normal escape + buf.append('\\').append(escaped); + } + } + } + } + + /** + * Simple quote of a string, escaping where needed. + * + * @param buf the StringBuilder to append to + * @param str the string to quote + */ + public static void quote(StringBuilder buf, String str) { + buf.append('"'); + escape(buf, str); + buf.append('"'); + } + + /** + * Append into buf the provided string, adding quotes if needed. + *

+ * Quoting is determined if any of the characters in the delim are found in the input str. + * + * @param buf the buffer to append to + * @param str the string to possibly quote + * @param delim the delimiter characters that will trigger automatic quoting + */ + public static void quoteIfNeeded(StringBuilder buf, String str, String delim) { + if (str == null) { + return; + } + // check for delimiters in input string + int len = str.length(); + if (len == 0) { + return; + } + int ch; + for (int i = 0; i < len; i++) { + ch = str.codePointAt(i); + if (delim.indexOf(ch) >= 0) { + // found a delimiter codepoint. we need to quote it. + quote(buf, str); + return; + } + } + + // no special delimiters used, no quote needed. + buf.append(str); + } + + /** + * Create an iterator of the input string, breaking apart the string at the provided delimiters, removing quotes and triming the parts of the string as + * needed. + * + * @param str the input string to split apart + * @param delims the delimiter characters to split the string on + * @return the iterator of the parts of the string, trimmed, with quotes around the string part removed, and unescaped + */ + public static Iterator splitAt(String str, String delims) { + return new DeQuotingStringIterator(str.trim(), delims); + } + + public static String unescape(String str) { + if (str == null) { + // nothing there + return null; + } + + int len = str.length(); + if (len <= 1) { + // impossible to be escaped + return str; + } + + StringBuilder ret = new StringBuilder(len - 2); + boolean escaped = false; + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (escaped) { + escaped = false; + switch (c) { + case 'n': + ret.append('\n'); + break; + case 'r': + ret.append('\r'); + break; + case 't': + ret.append('\t'); + break; + case 'f': + ret.append('\f'); + break; + case 'b': + ret.append('\b'); + break; + case '\\': + ret.append('\\'); + break; + case '/': + ret.append('/'); + break; + case '"': + ret.append('"'); + break; + case 'u': + ret.append((char) ((dehex((byte) str.charAt(i++)) << 24) + (dehex((byte) str.charAt(i++)) << 16) + (dehex((byte) str.charAt(i++)) << 8) + (dehex((byte) str + .charAt(i++))))); + break; + default: + ret.append(c); + } + } else if (c == '\\') { + escaped = true; + } else { + ret.append(c); + } + } + return ret.toString(); + } + + public static String join(Object[] objs, String delim) { + if (objs == null) { + return ""; + } + StringBuilder ret = new StringBuilder(); + int len = objs.length; + for (int i = 0; i < len; i++) { + if (i > 0) { + ret.append(delim); + } + if (objs[i] instanceof String) { + ret.append('"').append(objs[i]).append('"'); + } else { + ret.append(objs[i]); + } + } + return ret.toString(); + } + + public static String join(Collection objs, String delim) { + if (objs == null) { + return ""; + } + StringBuilder ret = new StringBuilder(); + boolean needDelim = false; + for (Object obj : objs) { + if (needDelim) { + ret.append(delim); + } + if (obj instanceof String) { + ret.append('"').append(obj).append('"'); + } else { + ret.append(obj); + } + needDelim = true; + } + return ret.toString(); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/common/utils/WSURI.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/utils/WSURI.java new file mode 100644 index 000000000..f2fe069d9 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/common/utils/WSURI.java @@ -0,0 +1,102 @@ +package com.fireflysource.net.websocket.common.utils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; + + +/** + * Utility methods for converting a {@link URI} between an HTTP(S) and WS(S) URI. + */ +public final class WSURI { + /** + * Convert to HTTP http or https scheme URIs. + *

+ * Converting ws and wss URIs to their HTTP equivalent + * + * @param inputUri the input URI + * @return the HTTP scheme URI for the input URI. + * @throws URISyntaxException if unable to convert the input URI + */ + public static URI toHttp(final URI inputUri) throws URISyntaxException { + Objects.requireNonNull(inputUri, "Input URI must not be null"); + String wsScheme = inputUri.getScheme(); + if ("http".equalsIgnoreCase(wsScheme) || "https".equalsIgnoreCase(wsScheme)) { + // leave alone + return inputUri; + } + + if ("ws".equalsIgnoreCase(wsScheme)) { + // convert to http + return new URI("http" + inputUri.toString().substring(wsScheme.length())); + } + + if ("wss".equalsIgnoreCase(wsScheme)) { + // convert to https + return new URI("https" + inputUri.toString().substring(wsScheme.length())); + } + + throw new URISyntaxException(inputUri.toString(), "Unrecognized WebSocket scheme"); + } + + /** + * Convert to WebSocket ws or wss scheme URIs + *

+ * Converting http and https URIs to their WebSocket equivalent + * + * @param inputUrl the input URI + * @return the WebSocket scheme URI for the input URI. + * @throws URISyntaxException if unable to convert the input URI + */ + public static URI toWebsocket(CharSequence inputUrl) throws URISyntaxException { + return toWebsocket(new URI(inputUrl.toString())); + } + + /** + * Convert to WebSocket ws or wss scheme URIs + *

+ * Converting http and https URIs to their WebSocket equivalent + * + * @param inputUrl the input URI + * @param query the optional query string + * @return the WebSocket scheme URI for the input URI. + * @throws URISyntaxException if unable to convert the input URI + */ + public static URI toWebsocket(CharSequence inputUrl, String query) throws URISyntaxException { + if (query == null) { + return toWebsocket(new URI(inputUrl.toString())); + } + return toWebsocket(new URI(inputUrl.toString() + '?' + query)); + } + + /** + * Convert to WebSocket ws or wss scheme URIs + * + *

+ * Converting http and https URIs to their WebSocket equivalent + * + * @param inputUri the input URI + * @return the WebSocket scheme URI for the input URI. + * @throws URISyntaxException if unable to convert the input URI + */ + public static URI toWebsocket(final URI inputUri) throws URISyntaxException { + Objects.requireNonNull(inputUri, "Input URI must not be null"); + String httpScheme = inputUri.getScheme(); + if ("ws".equalsIgnoreCase(httpScheme) || "wss".equalsIgnoreCase(httpScheme)) { + // keep as-is + return inputUri; + } + + if ("http".equalsIgnoreCase(httpScheme)) { + // convert to ws + return new URI("ws" + inputUri.toString().substring(httpScheme.length())); + } + + if ("https".equalsIgnoreCase(httpScheme)) { + // convert to wss + return new URI("wss" + inputUri.toString().substring(httpScheme.length())); + } + + throw new URISyntaxException(inputUri.toString(), "Unrecognized HTTP scheme"); + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/server/ExtensionSelector.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/ExtensionSelector.java new file mode 100644 index 000000000..e3a4fff90 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/ExtensionSelector.java @@ -0,0 +1,18 @@ +package com.fireflysource.net.websocket.server; + +import java.util.List; + +/** + * @author Pengtao Qiu + */ +public interface ExtensionSelector { + + /** + * Select the supported extensions. + * + * @param extensions The client supported extensions. + * @return The server selected extensions. + */ + List select(List extensions); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/server/SubProtocolSelector.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/SubProtocolSelector.java new file mode 100644 index 000000000..bfac743d5 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/SubProtocolSelector.java @@ -0,0 +1,18 @@ +package com.fireflysource.net.websocket.server; + +import java.util.List; + +/** + * @author Pengtao Qiu + */ +public interface SubProtocolSelector { + + /** + * Select the supported sub protocols. + * + * @param protocols The client supported sub protocols. + * @return The server selected sub protocols. + */ + List select(List protocols); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketManager.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketManager.java new file mode 100644 index 000000000..375768b83 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketManager.java @@ -0,0 +1,23 @@ +package com.fireflysource.net.websocket.server; + +/** + * @author Pengtao Qiu + */ +public interface WebSocketManager extends Cloneable { + + /** + * Find the websocket handler. + * + * @param path The request path. + * @return The websocket handler. + */ + WebSocketServerConnectionHandler findWebSocketHandler(String path); + + /** + * Register the websocket handler. + * + * @param connectionHandler The websocket handler. + */ + void register(WebSocketServerConnectionHandler connectionHandler); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionBuilder.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionBuilder.java new file mode 100644 index 000000000..b4093d8e2 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionBuilder.java @@ -0,0 +1,60 @@ +package com.fireflysource.net.websocket.server; + +import com.fireflysource.net.http.server.HttpServer; +import com.fireflysource.net.websocket.common.WebSocketMessageHandler; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +/** + * @author Pengtao Qiu + */ +public interface WebSocketServerConnectionBuilder { + + /** + * Set the websocket url. + * + * @param url The websocket url. + * @return The websocket server connection builder. + */ + WebSocketServerConnectionBuilder url(String url); + + /** + * Select the extensions. + * + * @param selector The websocket extension selector. + * @return The websocket server connection builder. + */ + WebSocketServerConnectionBuilder onExtensionSelect(ExtensionSelector selector); + + /** + * Select the sub protocols. + * + * @param selector The websocket sub protocol selector. + * @return The websocket server connection builder. + */ + WebSocketServerConnectionBuilder onSubProtocolSelect(SubProtocolSelector selector); + + /** + * Set the websocket policy. + * + * @param policy The websocket policy. + * @return The websocket server connection builder. + */ + WebSocketServerConnectionBuilder policy(WebSocketPolicy policy); + + /** + * Set the websocket message handler. + * + * @param handler The websocket message handler. + * @return The websocket server connection builder. + */ + WebSocketServerConnectionBuilder onMessage(WebSocketMessageHandler handler); + + /** + * Set the websocket connection listener. + * + * @param listener The websocket connection listener. + * @return The HTTP server. + */ + HttpServer onAccept(WebSocketServerConnectionListener listener); + +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionHandler.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionHandler.java new file mode 100644 index 000000000..2306c8312 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionHandler.java @@ -0,0 +1,65 @@ +package com.fireflysource.net.websocket.server; + +import com.fireflysource.net.websocket.common.WebSocketMessageHandler; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +/** + * @author Pengtao Qiu + */ +public class WebSocketServerConnectionHandler { + + private String url; + private ExtensionSelector extensionSelector; + private SubProtocolSelector subProtocolSelector; + private WebSocketPolicy policy; + private WebSocketServerConnectionListener connectionListener; + private WebSocketMessageHandler messageHandler; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public ExtensionSelector getExtensionSelector() { + return extensionSelector; + } + + public void setExtensionSelector(ExtensionSelector extensionSelector) { + this.extensionSelector = extensionSelector; + } + + public SubProtocolSelector getSubProtocolSelector() { + return subProtocolSelector; + } + + public void setSubProtocolSelector(SubProtocolSelector subProtocolSelector) { + this.subProtocolSelector = subProtocolSelector; + } + + public WebSocketPolicy getPolicy() { + return policy; + } + + public void setPolicy(WebSocketPolicy policy) { + this.policy = policy; + } + + public WebSocketServerConnectionListener getConnectionListener() { + return connectionListener; + } + + public void setConnectionListener(WebSocketServerConnectionListener connectionListener) { + this.connectionListener = connectionListener; + } + + public WebSocketMessageHandler getMessageHandler() { + return messageHandler; + } + + public void setMessageHandler(WebSocketMessageHandler messageHandler) { + this.messageHandler = messageHandler; + } +} diff --git a/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionListener.java b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionListener.java new file mode 100644 index 000000000..25f1d1629 --- /dev/null +++ b/firefly-net/src/main/java/com/fireflysource/net/websocket/server/WebSocketServerConnectionListener.java @@ -0,0 +1,14 @@ +package com.fireflysource.net.websocket.server; + +import com.fireflysource.net.websocket.common.WebSocketConnection; + +import java.util.concurrent.CompletableFuture; + +/** + * @author Pengtao Qiu + */ +public interface WebSocketServerConnectionListener { + + CompletableFuture accept(WebSocketConnection connection); + +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/FireflyExtensions.kt b/firefly-net/src/main/kotlin/com/fireflysource/FireflyExtensions.kt new file mode 100644 index 000000000..cd03f5e2c --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/FireflyExtensions.kt @@ -0,0 +1,9 @@ +package com.fireflysource + +/** + * @author Pengtao Qiu + */ +typealias fx = `$` +typealias future = `$`.future +typealias consumer = `$`.consumer +typealias logger = `$`.logger \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/CommonTcpChannelGroup.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/CommonTcpChannelGroup.kt new file mode 100644 index 000000000..bb6a7bbd3 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/CommonTcpChannelGroup.kt @@ -0,0 +1,65 @@ +package com.fireflysource.net + +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.lifecycle.ShutdownTasks +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.net.http.server.HttpServerFactory +import com.fireflysource.net.tcp.TcpClient +import com.fireflysource.net.tcp.TcpClientFactory +import com.fireflysource.net.tcp.TcpServer +import com.fireflysource.net.tcp.TcpServerFactory +import com.fireflysource.net.tcp.aio.AioTcpChannelGroup +import com.fireflysource.net.tcp.aio.TcpConfig + +/** + * @author Pengtao Qiu + */ +object CommonTcpChannelGroup : AbstractLifeCycle() { + + val group = AioTcpChannelGroup("common-tcp-channel-group") + val httpClient: HttpClient by lazy { createHttpClient() } + + init { + start() + } + + @JvmOverloads + fun createTcpServer(config: TcpConfig = TcpConfig()): TcpServer { + val server = TcpServerFactory.create(config) + server.tcpChannelGroup(group).stopTcpChannelGroup(false) + return server + } + + @JvmOverloads + fun createTcpClient(config: TcpConfig = TcpConfig()): TcpClient { + val client = TcpClientFactory.create(config) + client.tcpChannelGroup(group).stopTcpChannelGroup(false) + return client + } + + @JvmOverloads + fun createHttpServer(httpConfig: HttpConfig = HttpConfig()): HttpServer { + httpConfig.tcpChannelGroup = group + httpConfig.isStopTcpChannelGroup = false + return HttpServerFactory.create(httpConfig) + } + + @JvmOverloads + fun createHttpClient(httpConfig: HttpConfig = HttpConfig()): HttpClient { + httpConfig.tcpChannelGroup = group + httpConfig.isStopTcpChannelGroup = false + return HttpClientFactory.create(httpConfig) + } + + override fun destroy() { + httpClient.stop() + group.stop() + } + + override fun init() { + ShutdownTasks.register(CommonTcpChannelGroup::stop) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AbstractHttpClientConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AbstractHttpClientConnection.kt new file mode 100644 index 000000000..830c5e99d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AbstractHttpClientConnection.kt @@ -0,0 +1,16 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.net.http.client.HttpClientConnection +import com.fireflysource.net.http.client.HttpClientRequestBuilder +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.common.model.HttpVersion + +interface AbstractHttpClientConnection : HttpClientConnection { + + override fun request(method: String, httpURI: HttpURI): HttpClientRequestBuilder { + Assert.hasText(httpURI.path, "The http path must be not null.") + return AsyncHttpClientConnectionRequestBuilder(this, method, httpURI, HttpVersion.HTTP_1_1) + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AbstractHttpClientRequestBuilder.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AbstractHttpClientRequestBuilder.kt new file mode 100644 index 000000000..945776d57 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AbstractHttpClientRequestBuilder.kt @@ -0,0 +1,185 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.* +import com.fireflysource.net.http.client.impl.content.provider.ByteBufferContentProvider +import com.fireflysource.net.http.client.impl.content.provider.MultiPartContentProvider +import com.fireflysource.net.http.client.impl.content.provider.StringContentProvider +import com.fireflysource.net.http.common.model.* +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.function.BiConsumer +import java.util.function.Supplier + +@Suppress("ReplacePutWithAssignment") +abstract class AbstractHttpClientRequestBuilder( + method: String, + uri: HttpURI, + httpVersion: HttpVersion +) : HttpClientRequestBuilder { + + private val multiPartContentProvider: MultiPartContentProvider by lazy { MultiPartContentProvider() } + + val httpRequest: AsyncHttpClientRequest = AsyncHttpClientRequest() + + init { + httpRequest.method = method + httpRequest.uri = uri + httpRequest.httpVersion = httpVersion + } + + override fun cookies(cookies: MutableList?): HttpClientRequestBuilder { + httpRequest.cookies = cookies + return this + } + + override fun put(name: String, list: MutableList): HttpClientRequestBuilder { + httpRequest.httpFields.put(name, list) + return this + } + + override fun put(header: HttpHeader, value: String): HttpClientRequestBuilder { + httpRequest.httpFields.put(header, value) + return this + } + + override fun put(name: String, value: String): HttpClientRequestBuilder { + httpRequest.httpFields.put(name, value) + return this + } + + override fun put(field: HttpField): HttpClientRequestBuilder { + httpRequest.httpFields.put(field) + return this + } + + override fun addAll(fields: HttpFields): HttpClientRequestBuilder { + httpRequest.httpFields.addAll(fields) + return this + } + + override fun add(field: HttpField): HttpClientRequestBuilder { + httpRequest.httpFields.add(field) + return this + } + + override fun addCsv(header: HttpHeader, vararg values: String): HttpClientRequestBuilder { + httpRequest.httpFields.addCSV(header, *values) + return this + } + + override fun addCsv(header: String, vararg values: String): HttpClientRequestBuilder { + httpRequest.httpFields.addCSV(header, *values) + return this + } + + override fun trailerSupplier(trailerSupplier: Supplier?): HttpClientRequestBuilder { + httpRequest.trailerSupplier = trailerSupplier + return this + } + + override fun body(content: String): HttpClientRequestBuilder = body(content, StandardCharsets.UTF_8) + + override fun body(content: String, charset: Charset): HttpClientRequestBuilder = + contentProvider(StringContentProvider(content, charset)) + + override fun body(buffer: ByteBuffer): HttpClientRequestBuilder = contentProvider(ByteBufferContentProvider(buffer)) + + override fun contentProvider(contentProvider: HttpClientContentProvider?): HttpClientRequestBuilder { + httpRequest.contentProvider = contentProvider + return this + } + + override fun addPart( + name: String, + content: HttpClientContentProvider, + fields: HttpFields? + ): HttpClientRequestBuilder { + contentProvider(multiPartContentProvider) + multiPartContentProvider.addPart(name, content, fields) + return this + } + + override fun addFilePart( + name: String, + fileName: String, + content: HttpClientContentProvider, + fields: HttpFields? + ): HttpClientRequestBuilder { + contentProvider(multiPartContentProvider) + multiPartContentProvider.addFilePart(name, fileName, content, fields) + return this + } + + override fun addFormInput(name: String, value: String): HttpClientRequestBuilder { + httpRequest.formInputs.add(name, value) + return this + } + + override fun addFormInputs(name: String, values: MutableList): HttpClientRequestBuilder { + httpRequest.formInputs.addValues(name, values) + return this + } + + override fun putFormInput(name: String, value: String): HttpClientRequestBuilder { + httpRequest.formInputs.put(name, value) + return this + } + + override fun putFormInputs(name: String, values: MutableList): HttpClientRequestBuilder { + httpRequest.formInputs.putValues(name, values) + return this + } + + override fun removeFormInput(name: String): HttpClientRequestBuilder { + httpRequest.formInputs.remove(name) + return this + } + + override fun addQueryString(name: String, value: String): HttpClientRequestBuilder { + httpRequest.queryStrings.add(name, value) + return this + } + + override fun addQueryStrings(name: String, values: MutableList): HttpClientRequestBuilder { + httpRequest.queryStrings.addValues(name, values) + return this + } + + override fun putQueryString(name: String, value: String): HttpClientRequestBuilder { + httpRequest.queryStrings.put(name, value) + return this + } + + override fun putQueryStrings(name: String, values: MutableList): HttpClientRequestBuilder { + httpRequest.queryStrings[name] = values + return this + } + + override fun removeQueryString(name: String): HttpClientRequestBuilder { + httpRequest.queryStrings.remove(name) + return this + } + + override fun contentHandler(contentHandler: HttpClientContentHandler?): HttpClientRequestBuilder { + httpRequest.contentHandler = contentHandler + return this + } + + override fun http2Settings(http2Settings: Map?): HttpClientRequestBuilder { + httpRequest.http2Settings = http2Settings + return this + } + + override fun upgradeHttp2(): HttpClientRequestBuilder { + HttpProtocolNegotiator.addHttp2UpgradeHeader(httpRequest) + return this + } + + override fun onHeaderComplete(headerComplete: BiConsumer): HttpClientRequestBuilder { + httpRequest.headerComplete = headerComplete + return this + } + + override fun getHttpClientRequest(): HttpClientRequest = httpRequest +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClient.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClient.kt new file mode 100644 index 000000000..d8e4ceb92 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClient.kt @@ -0,0 +1,94 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.client.HttpClientConnection +import com.fireflysource.net.http.client.HttpClientRequestBuilder +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.common.model.HttpVersion +import com.fireflysource.net.tcp.TcpClientConnectionFactory +import com.fireflysource.net.tcp.aio.AioTcpChannelGroup +import com.fireflysource.net.websocket.client.WebSocketClientConnectionBuilder +import com.fireflysource.net.websocket.client.impl.AsyncWebSocketClientConnectionBuilder +import com.fireflysource.net.websocket.client.impl.AsyncWebSocketClientConnectionManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.concurrent.CompletableFuture + +class AsyncHttpClient(private val config: HttpConfig = HttpConfig()) : HttpClient, AbstractLifeCycle() { + + companion object { + private val log = SystemLogger.create(AsyncHttpClient::class.java) + } + + private val connectionFactory = TcpClientConnectionFactory( + createTcpChannelGroup(), + config.isStopTcpChannelGroup, + config.timeout, + config.secureEngineFactory + ) + private val httpClientConnectionManager = AsyncHttpClientConnectionManager(config, connectionFactory) + private val webSocketClientConnectionManager = AsyncWebSocketClientConnectionManager(config, connectionFactory) + + init { + start() + } + + private fun createTcpChannelGroup() = + if (config.tcpChannelGroup != null) config.tcpChannelGroup + else AioTcpChannelGroup("async-http-client") + + override fun request(method: String, httpURI: HttpURI): HttpClientRequestBuilder { + Assert.hasText(httpURI.path, "The http path must be not null.") + return AsyncHttpClientRequestBuilder(httpClientConnectionManager, method, httpURI, HttpVersion.HTTP_1_1) + } + + override fun createHttpClientConnection( + httpURI: HttpURI, + supportedProtocols: List + ): CompletableFuture { + return httpClientConnectionManager.createHttpClientConnection(httpURI, supportedProtocols) + } + + override fun websocket(): WebSocketClientConnectionBuilder { + return AsyncWebSocketClientConnectionBuilder(webSocketClientConnectionManager) + } + + override fun websocket(url: String): WebSocketClientConnectionBuilder { + return AsyncWebSocketClientConnectionBuilder(webSocketClientConnectionManager).url(url) + } + + override fun init() { + log.info { "AsyncHttpClient startup. Config: $config" } + } + + override fun destroy() { + httpClientConnectionManager.stop() + webSocketClientConnectionManager.stop() + } +} + +fun HttpClient.connectAsync(uri: String, block: suspend CoroutineScope.(HttpClientConnection) -> Unit) { + this.createHttpClientConnection(uri) + .thenAccept { connection -> connection.coroutineScope.launch { block(connection) } } +} + +fun HttpClient.connectAsync( + httpURI: HttpURI, + supportedProtocols: List, + block: suspend CoroutineScope.(HttpClientConnection) -> Unit +) { + this.createHttpClientConnection(httpURI, supportedProtocols) + .thenAccept { connection -> connection.coroutineScope.launch { block(connection) } } +} + +fun HttpClient.connectAsync( + httpURI: HttpURI, + block: suspend CoroutineScope.(HttpClientConnection) -> Unit +) { + this.createHttpClientConnection(httpURI) + .thenAccept { connection -> connection.coroutineScope.launch { block(connection) } } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientConnectionManager.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientConnectionManager.kt new file mode 100644 index 000000000..99b3687df --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientConnectionManager.kt @@ -0,0 +1,271 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.coroutine.CoroutineDispatchers +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.HttpClientConnection +import com.fireflysource.net.http.client.HttpClientConnectionManager +import com.fireflysource.net.http.client.HttpClientRequest +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.client.impl.exception.UnhandledRequestException +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.exception.MissingRemoteHostException +import com.fireflysource.net.http.common.exception.MissingRemotePortException +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.common.model.isCloseConnection +import com.fireflysource.net.tcp.TcpClientConnectionFactory +import com.fireflysource.net.tcp.aio.ApplicationProtocol.HTTP1 +import com.fireflysource.net.tcp.aio.defaultSupportedProtocols +import com.fireflysource.net.tcp.aio.isSecureProtocol +import com.fireflysource.net.tcp.aio.schemaDefaultPort +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.future.await +import java.net.InetSocketAddress +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import kotlin.math.absoluteValue + +class AsyncHttpClientConnectionManager( + private val config: HttpConfig, + private val connectionFactory: TcpClientConnectionFactory +) : HttpClientConnectionManager, AbstractLifeCycle() { + + companion object { + private val log = SystemLogger.create(AsyncHttpClientConnectionManager::class.java) + } + + private val connectionPoolMap = ConcurrentHashMap() + private val httpClientConnectionFactory = HttpClientConnectionFactory(config, connectionFactory) + private val scope = + CoroutineScope(CoroutineName("Firefly-HTTP-client-connection-manager") + CoroutineDispatchers.singleThread) + + init { + start() + } + + override fun send(request: HttpClientRequest): CompletableFuture { + val address = buildAddress(request.uri) + val nonPersistence = request.httpFields.isCloseConnection(request.httpVersion) + return if (nonPersistence) sendByNonPersistenceConnection(address, request) + else sendByPool(address, request) + } + + override fun createHttpClientConnection( + httpURI: HttpURI, + supportedProtocols: List + ): CompletableFuture { + return createHttpClientConnection(buildAddress(httpURI), supportedProtocols) + } + + private fun sendByPool(address: Address, request: HttpClientRequest): CompletableFuture { + val pool = connectionPoolMap.computeIfAbsent(address) { HttpClientConnectionPool(it) } + val message = RequestMessage(request, CompletableFuture()) + pool.sendMessage(message) + return message.response + } + + private fun sendByNonPersistenceConnection(address: Address, request: HttpClientRequest) = + createHttpClientConnection(address, listOf(HTTP1.value)).thenCompose { sendAndCloseConnection(it, request) } + + private fun sendAndCloseConnection(connection: HttpClientConnection, request: HttpClientRequest) = + connection.send(request).thenCompose { response -> connection.closeAsync().thenApply { response } } + + + private fun buildAddress(uri: HttpURI): Address { + if (uri.host.isNullOrBlank()) { + throw MissingRemoteHostException("The host is missing. uri: $uri") + } + val port: Int = if (uri.port > 0) { + uri.port + } else { + schemaDefaultPort[uri.scheme] ?: throw MissingRemotePortException("The address port is missing. uri: $uri") + } + val socketAddress = InetSocketAddress(uri.host, port) + val secure = isSecureProtocol(uri.scheme) + return Address(socketAddress, secure) + } + + private inner class HttpClientConnectionPool(val address: Address) : AbstractLifeCycle() { + private var i = 0 + private val httpClientConnections: Array = arrayOfNulls(config.connectionPoolSize) + private val channel: Channel = Channel(Channel.UNLIMITED) + private val checkJob = scope.launch { + delay(TimeUnit.SECONDS.toMillis(config.checkConnectionLiveInterval)) + sendMessage(CheckConnectionLiveMessage) + } + + init { + start() + } + + fun sendMessage(message: ClientRequestMessage) { + channel.trySend(message) + } + + override fun init() { + scope.launch { + requestLoop@ while (true) { + when (val message = channel.receive()) { + is RequestMessage -> handleRequest(message) + is UnhandledRequestMessage -> processUnhandledRequestInConnection(message) + is CheckConnectionLiveMessage -> checkConnectionLive() + is StopMessage -> { + onStop() + break@requestLoop + } + } + } + }.invokeOnCompletion { cause -> + if (cause != null) { + log.info { "The HTTP client connection pool job completion. cause: ${cause.message}" } + } + processUnhandledRequestInPool() + } + } + + override fun destroy() { + sendMessage(StopMessage) + checkJob.cancel() + } + + private fun onStop() { + httpClientConnections.forEach { it?.closeAsync() } + processUnhandledRequestInPool() + } + + private suspend fun handleRequest(message: RequestMessage) { + try { + val index = getIndex() + val connection = getConnection(index) + sendRequest(connection, message, index) + } catch (ex: Throwable) { + handleException(message, ex) + } + } + + private fun sendRequest(connection: HttpClientConnection, message: RequestMessage, index: Int) { + val request = message.request + val future = message.response + connection.send(request).handle { response, ex -> + if (ex != null) { + if (ex is UnhandledRequestException || ex.cause is UnhandledRequestException) { + sendMessage(UnhandledRequestMessage(request, future, connection.id, index)) + } else { + handleException(message, ex) + } + } else future.complete(response) + response + } + } + + private suspend fun processUnhandledRequestInConnection(message: UnhandledRequestMessage) { + val index = message.index + val connection = getConnection(index) + val newConnection = if (connection.id == message.connectionId) { + createConnection(index) + } else connection + val newMessage = RequestMessage(message.request, message.response) + sendRequest(newConnection, newMessage, index) + } + + private fun handleException(message: RequestMessage, ex: Throwable) { + val request = message.request + val future = message.response + if (future.isDone) return + + if (message.retry <= config.clientRetryCount) { + val retryCount = message.retry + 1 + log.warn { + "HTTP client request failure. The client will retry request. " + + "retryCount: $retryCount, " + + "url: ${request.uri}, " + + "info: ${ex.javaClass.name} ${ex.message}" + } + sendMessage(RequestMessage(request, future, retryCount)) + } else { + log.warn { + "HTTP client request failure. " + + "url: ${request.uri}, " + + "info: ${ex.javaClass.name} ${ex.message}" + } + future.completeExceptionally(ex) + } + } + + private fun getIndex(): Int { + val index = i.absoluteValue % config.connectionPoolSize + i++ + return index + } + + private suspend fun getConnection(index: Int): HttpClientConnection { + val oldConnection = httpClientConnections[index] + return if (oldConnection != null) { + if (oldConnection.isInvalid) createConnection(index) + else oldConnection + } else createConnection(index) + } + + private suspend fun createConnection(index: Int): HttpClientConnection { + val newConnection = createHttpClientConnection(address).await() + httpClientConnections[index] = newConnection + return newConnection + } + + private suspend fun checkConnectionLive() { + val maxIndex = config.connectionPoolSize - 1 + (0..maxIndex).forEach { index -> + try { + getConnection(index) + } catch (e: Exception) { + log.error(e) { "create http client connection failure. $address" } + } + } + } + + private fun processUnhandledRequestInPool() { + channel.consumeAll { message -> + if (message is RequestMessage) { + message.response.completeExceptionally(UnhandledRequestException("The HTTP client connection pool is shutdown. This request does not send.")) + } + } + } + } + + private fun createHttpClientConnection( + address: Address, + supportedProtocols: List = defaultSupportedProtocols + ): CompletableFuture { + return scope.async { httpClientConnectionFactory.createHttpClientConnection(address, supportedProtocols) } + .asCompletableFuture() + } + + override fun init() { + connectionFactory.start() + } + + override fun destroy() { + connectionPoolMap.values.forEach { it.stop() } + connectionPoolMap.clear() + connectionFactory.stop() + scope.cancel() + } +} + +sealed class ClientRequestMessage +class RequestMessage( + val request: HttpClientRequest, val response: CompletableFuture, + val retry: Int = 0 +) : ClientRequestMessage() + +class UnhandledRequestMessage( + val request: HttpClientRequest, val response: CompletableFuture, + val connectionId: Int, val index: Int +) : ClientRequestMessage() + +object CheckConnectionLiveMessage : ClientRequestMessage() +object StopMessage : ClientRequestMessage() \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientConnectionRequestBuilder.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientConnectionRequestBuilder.kt new file mode 100644 index 000000000..eab0eeff9 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientConnectionRequestBuilder.kt @@ -0,0 +1,18 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.HttpClientConnection +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.common.model.HttpVersion +import java.util.concurrent.CompletableFuture + +class AsyncHttpClientConnectionRequestBuilder( + private val connection: HttpClientConnection, + method: String, + uri: HttpURI, + httpVersion: HttpVersion +) : AbstractHttpClientRequestBuilder(method, uri, httpVersion) { + + override fun submit(): CompletableFuture = connection.send(httpRequest) + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientRequest.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientRequest.kt new file mode 100644 index 000000000..91da8ff5d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientRequest.kt @@ -0,0 +1,178 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.HttpClientContentHandler +import com.fireflysource.net.http.client.HttpClientContentProvider +import com.fireflysource.net.http.client.HttpClientRequest +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.client.impl.content.handler.StringContentHandler +import com.fireflysource.net.http.client.impl.content.provider.MultiPartContentProvider +import com.fireflysource.net.http.client.impl.content.provider.StringContentProvider +import com.fireflysource.net.http.common.codec.CookieGenerator +import com.fireflysource.net.http.common.codec.UrlEncoded +import com.fireflysource.net.http.common.model.* +import java.nio.charset.StandardCharsets +import java.util.function.BiConsumer +import java.util.function.Supplier + +class AsyncHttpClientRequest : HttpClientRequest { + + companion object { + private val defaultHttpUri = HttpURI("http://localhost:8080/") + private val defaultMethod = HttpMethod.GET.value + } + + private var method: String = defaultMethod + private var uri: HttpURI = defaultHttpUri + private var httpVersion: HttpVersion = HttpVersion.HTTP_1_1 + private var queryStrings: UrlEncoded? = null + private var formInputs: UrlEncoded? = null + private var httpFields: HttpFields = HttpFields() + private var cookies: MutableList? = null + private var trailerSupplier: Supplier? = null + private var contentProvider: HttpClientContentProvider? = null + private var contentHandler: HttpClientContentHandler? = null + private var http2Settings: Map? = null + private var headerComplete: BiConsumer = BiConsumer { _, _ -> } + + override fun getMethod(): String = method + + override fun setMethod(method: String) { + this.method = method + } + + override fun getURI(): HttpURI = uri + + override fun setURI(uri: HttpURI) { + this.uri = uri + } + + override fun getHttpVersion(): HttpVersion = httpVersion + + override fun setHttpVersion(httpVersion: HttpVersion) { + this.httpVersion = httpVersion + } + + override fun getQueryStrings(): UrlEncoded { + val query = this.queryStrings + return if (query != null) { + query + } else { + val urlEncoded = UrlEncoded() + this.queryStrings = urlEncoded + urlEncoded + } + } + + override fun setQueryStrings(queryStrings: UrlEncoded?) { + this.queryStrings = queryStrings + } + + override fun getFormInputs(): UrlEncoded { + val form = this.formInputs + return if (form != null) { + form + } else { + val urlEncoded = UrlEncoded() + this.formInputs = urlEncoded + urlEncoded + } + } + + override fun setFormInputs(formInputs: UrlEncoded?) { + this.formInputs = formInputs + } + + override fun getHttpFields(): HttpFields = httpFields + + override fun setHttpFields(httpFields: HttpFields) { + this.httpFields = httpFields + } + + override fun getCookies(): MutableList? = cookies + + override fun setCookies(cookies: MutableList?) { + this.cookies = cookies + } + + override fun getTrailerSupplier(): Supplier? = trailerSupplier + + override fun setTrailerSupplier(trailerSupplier: Supplier?) { + this.trailerSupplier = trailerSupplier + } + + override fun setContentProvider(contentProvider: HttpClientContentProvider?) { + this.contentProvider = contentProvider + } + + override fun getContentProvider(): HttpClientContentProvider? = contentProvider + + override fun setContentHandler(contentHandler: HttpClientContentHandler?) { + this.contentHandler = contentHandler + } + + override fun getContentHandler(): HttpClientContentHandler? = contentHandler + + override fun setHttp2Settings(http2Settings: Map?) { + this.http2Settings = http2Settings + } + + override fun getHttp2Settings(): Map? = http2Settings + + override fun setHeaderComplete(headerComplete: BiConsumer) { + this.headerComplete = headerComplete + } + + override fun getHeaderComplete(): BiConsumer { + return headerComplete + } +} + +fun toMetaDataRequest(request: HttpClientRequest): MetaData.Request { + if (request.formInputs.size > 0) { + val formStr = request.formInputs.encode(StandardCharsets.UTF_8, true) + val stringContentProvider = StringContentProvider(formStr, StandardCharsets.UTF_8) + request.contentProvider = stringContentProvider + request.httpFields.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.value) + request.httpFields.put(HttpHeader.CONTENT_LENGTH, stringContentProvider.length().toString()) + } else { + val provider = request.contentProvider + if (provider != null) { + if (provider is MultiPartContentProvider) { + request.httpFields.put(HttpHeader.CONTENT_TYPE, provider.contentType) + if (provider.length() >= 0) { + request.httpFields.put(HttpHeader.CONTENT_LENGTH, provider.length().toString()) + } + } else { + val contentLength = provider.length() + if (contentLength >= 0) { + request.httpFields.put(HttpHeader.CONTENT_LENGTH, contentLength.toString()) + } else { + request.httpFields.put(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.value) + } + } + } + } + + val uri = if (request.queryStrings.size > 0) { + val uri = HttpURI(request.uri) + if (request.uri.hasQuery()) { + uri.query = request.uri.query + "&" + request.queryStrings.encode(StandardCharsets.UTF_8, true) + } else { + uri.query = request.queryStrings.encode(StandardCharsets.UTF_8, true) + } + uri + } else request.uri + + if (request.cookies != null) { + request.httpFields.put(HttpHeader.COOKIE, CookieGenerator.generateCookies(request.cookies)) + } + + if (request.contentHandler == null) { + request.contentHandler = StringContentHandler(Long.MAX_VALUE) + } + + val len = request.contentProvider?.length() ?: -1 + val metaDataReq = MetaData.Request(request.method, uri, request.httpVersion, request.httpFields, len) + metaDataReq.trailerSupplier = request.trailerSupplier + return metaDataReq +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientRequestBuilder.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientRequestBuilder.kt new file mode 100644 index 000000000..36a61a78c --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientRequestBuilder.kt @@ -0,0 +1,18 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.HttpClientConnectionManager +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.common.model.HttpVersion +import java.util.concurrent.CompletableFuture + +class AsyncHttpClientRequestBuilder( + private val connectionManager: HttpClientConnectionManager, + method: String, + uri: HttpURI, + httpVersion: HttpVersion +) : AbstractHttpClientRequestBuilder(method, uri, httpVersion) { + + override fun submit(): CompletableFuture = connectionManager.send(httpRequest) + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientResponse.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientResponse.kt new file mode 100644 index 000000000..fc307805b --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/AsyncHttpClientResponse.kt @@ -0,0 +1,83 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.HttpClientContentHandler +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.client.impl.content.handler.ByteBufferContentHandler +import com.fireflysource.net.http.client.impl.content.handler.StringContentHandler +import com.fireflysource.net.http.common.codec.CookieParser +import com.fireflysource.net.http.common.model.* +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.function.Supplier + +class AsyncHttpClientResponse( + val response: MetaData.Response, + private var contentHandler: HttpClientContentHandler? +) : HttpClientResponse { + + private val cookieList: List by lazy { + httpFields.getValuesList(HttpHeader.SET_COOKIE).map { + CookieParser.parseSetCookie(it) + } + } + + override fun getStatus(): Int = response.status + + override fun getReason(): String = + Optional.ofNullable(response.reason).orElseGet { HttpStatus.getMessage(response.status) } + + override fun getHttpVersion(): HttpVersion = response.httpVersion + + override fun getHttpFields(): HttpFields = response.fields + + override fun getCookies(): List = cookieList + + override fun getContentLength(): Long = response.contentLength + + override fun getTrailerSupplier(): Supplier = + Optional.ofNullable(response.trailerSupplier).orElseGet { Supplier { HttpFields() } } + + override fun getStringBody(): String = getStringBody(StandardCharsets.UTF_8) + + override fun getStringBody(charset: Charset): String = Optional + .ofNullable(contentHandler) + .filter { it is StringContentHandler } + .map { it as StringContentHandler } + .map { it.toString(charset, getContentEncoding()) } + .orElse("") + + override fun getBody(): List = Optional + .ofNullable(contentHandler) + .filter { it is ByteBufferContentHandler } + .map { it as ByteBufferContentHandler } + .map { it.getByteBuffers(getContentEncoding()) } + .orElse(listOf()) + + override fun getContentHandler(): HttpClientContentHandler? { + return contentHandler + } + + override fun setContentHandler(contentHandler: HttpClientContentHandler?) { + this.contentHandler = contentHandler + } + + private fun getContentEncoding(): Optional { + return Optional.ofNullable(this.httpFields[HttpHeader.CONTENT_ENCODING]) + .map { it.trim() } + .map { it.lowercase(Locale.getDefault()) } + .flatMap { ContentEncoding.from(it) } + } + + override fun toString(): String { + return """ + |response: ----------------- + |$status $reason $httpVersion + |$httpFields + |$stringBody + |${trailerSupplier.get()} + |end response -------------- + """.trimMargin() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http1ClientConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http1ClientConnection.kt new file mode 100644 index 000000000..e2edc2411 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http1ClientConnection.kt @@ -0,0 +1,555 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.codec.base64.Base64Utils +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.io.useAwait +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.Connection +import com.fireflysource.net.http.client.HttpClientContentProvider +import com.fireflysource.net.http.client.HttpClientRequest +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.expectUpgradeHttp2 +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.expectUpgradeWebsocket +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.isUpgradeSuccess +import com.fireflysource.net.http.client.impl.exception.UnhandledRequestException +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.TcpBasedHttpConnection +import com.fireflysource.net.http.common.exception.Http1GeneratingResultException +import com.fireflysource.net.http.common.exception.NotSupportHttpVersionException +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.common.v1.decoder.HttpParser +import com.fireflysource.net.http.common.v1.decoder.parseAll +import com.fireflysource.net.http.common.v1.encoder.HttpGenerator +import com.fireflysource.net.http.common.v1.encoder.HttpGenerator.Result.* +import com.fireflysource.net.http.common.v1.encoder.HttpGenerator.State.* +import com.fireflysource.net.http.common.v1.encoder.assert +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpCoroutineDispatcher +import com.fireflysource.net.websocket.client.WebSocketClientRequest +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.exception.UpgradeWebSocketConnectionException +import com.fireflysource.net.websocket.common.impl.AsyncWebSocketConnection +import com.fireflysource.net.websocket.common.model.AcceptHash +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import java.io.IOException +import java.nio.ByteBuffer +import java.time.Duration +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ThreadLocalRandom + +class Http1ClientConnection( + private val config: HttpConfig, + private val tcpConnection: TcpConnection +) : Connection by tcpConnection, TcpCoroutineDispatcher by tcpConnection, TcpBasedHttpConnection, + AbstractHttpClientConnection { + + companion object { + private val log = SystemLogger.create(Http1ClientConnection::class.java) + } + + private val generator = HttpGenerator() + + private val headerBuffer = BufferUtils.allocateDirect(config.headerBufferSize) + private val contentBuffer: ByteBuffer by lazy(LazyThreadSafetyMode.NONE) { BufferUtils.allocateDirect(config.contentBufferSize) } + private val chunkBuffer: ByteBuffer by lazy(LazyThreadSafetyMode.NONE) { BufferUtils.allocateDirect(HttpGenerator.CHUNK_SIZE) } + + private val handler = Http1ClientResponseHandler() + private val parser = HttpParser(handler) + private val requestChannel = Channel(Channel.UNLIMITED) + + @Volatile + private var httpVersion: HttpVersion = HttpVersion.HTTP_1_1 + + @Volatile + private var http2ClientConnection: Http2ClientConnection? = null + + private var upgradeWebSocketSuccess: Boolean = false + + init { + handleMessage() + } + + private fun handleMessage() = coroutineScope.launch { + while (true) { + when (val message = requestChannel.receive()) { + is RequestMessage -> { + val exit = handleRequest(message) + if (exit) { + break + } + } + is Stop -> break + } + } + }.invokeOnCompletion { e -> + if (e != null) { + log.info { "The HTTP1 message job completion exception. id: $id info: ${e.javaClass.name} ${e.message}" } + } else { + log.info("The HTTP1 message job completion. id: $id") + } + processUnhandledRequest() + } + + private suspend fun handleRequest(message: RequestMessage): Boolean { + return try { + log.debug { + """ + |handle http1 request: + |${message.request.method} ${message.request.uri} ${message.request.httpVersion} + |${message.request.fields} + """.trimMargin() + } + + handler.init( + message.httpClientRequest, + message.expectServerAcceptsContent, + message.isHttpTunnel + ) + if (message.expectServerAcceptsContent) { + // flush content data after the server response 100 continue. + generateRequestAndWaitServerAccept(message) + waitResponse(message) + } else { + // avoid the request can not respond the server error code if the I/O exception happened during the client sends data. + val result = coroutineScope.async { waitResponse(message) } + generateRequest(message) + result.await() + } + } catch (e: IOException) { + log.info { "The TCP connection IO exception. id: $id info: ${e.javaClass.name} ${e.message}" } + completeResponseExceptionally(message, e) + true + } catch (e: Exception) { + log.error { "HTTP1 client handler exception. id: $id info: ${e.javaClass.name} ${e.message}" } + completeResponseExceptionally(message, e) + true + } finally { + handler.reset() + parser.reset() + generator.reset() + } + } + + private fun processUnhandledRequest() { + when { + isUpgradeToHttp2Success() -> requestChannel.consumeAll { message -> + if (message is RequestMessage && !message.response.isDone) { + log.info { "Client sends remaining request via HTTP2 protocol. id: $id, path: ${message.httpClientRequest.uri.path}" } + val future = message.response + sendRequestViaHttp2(message.httpClientRequest) + .thenAccept { future.complete(it) } + .exceptionallyAccept { future.completeExceptionally(it) } + } + } + else -> requestChannel.consumeAll { message -> + if (message is RequestMessage && !message.response.isDone) { + message.response.completeExceptionally( + UnhandledRequestException( + "The HTTP1 connection has closed. This request does not send." + ) + ) + } + } + } + } + + private fun completeResponseExceptionally(message: RequestMessage, e: Exception) { + if (!message.response.isDone) { + message.response.completeExceptionally(e) + } + closeAsync() + } + + private suspend fun waitResponse(message: RequestMessage): Boolean { + val response = parseResponse(message) + val isNonPersistence = complete(response, message) + return when { + isNonPersistence -> true + message.expectUpgradeHttp2 && isUpgradeToHttp2Success() -> true + message.expectUpgradeWebSocket && upgradeWebSocketSuccess -> true + message.isHttpTunnel -> true + else -> false + } + } + + private suspend fun complete(response: HttpClientResponse, message: RequestMessage): Boolean { + val request = message.request + val isCloseConnection = + response.httpFields.isCloseConnection(response.httpVersion) || request.fields.isCloseConnection(request.httpVersion) + val isNonPersistence = !message.isHttpTunnel && isCloseConnection + if (isNonPersistence) { + this.useAwait { message.response.complete(response) } + log.debug { "HTTP1 connection closed. id: $id, closed: ${this.isClosed}" } + } else { + message.response.complete(response) + } + return isNonPersistence + } + + private suspend fun parseResponse(message: RequestMessage): HttpClientResponse { + val remainingData = parser.parseAll(tcpConnection) + val response = handler.complete() + return when { + message.expectUpgradeHttp2 -> upgradeHttp2Connection(response, message, remainingData) + message.expectUpgradeWebSocket -> upgradeWebSocketConnection(response, message, remainingData) + else -> response + } + } + + private fun upgradeWebSocketConnection( + response: HttpClientResponse, + message: RequestMessage, + remainingData: ByteBuffer? + ): HttpClientResponse { + if (response.status != HttpStatus.SWITCHING_PROTOCOLS_101) { + val e = + UpgradeWebSocketConnectionException("The upgrade response status is not 101. status: ${response.status}") + message.webSocketClientConnection?.completeExceptionally(e) + return response + } + + if (!response.httpFields.contains(HttpHeader.CONNECTION, "Upgrade")) { + val e = + UpgradeWebSocketConnectionException("The upgrade response does not contain the Connection Upgrade field.") + message.webSocketClientConnection?.completeExceptionally(e) + return response + } + + if (!response.httpFields.contains(HttpHeader.UPGRADE, "websocket")) { + val e = + UpgradeWebSocketConnectionException("The upgrade response does not contain the UPGRADE websocket field.") + message.webSocketClientConnection?.completeExceptionally(e) + return response + } + + if (!response.httpFields.contains(HttpHeader.SEC_WEBSOCKET_ACCEPT)) { + val e = + UpgradeWebSocketConnectionException("The upgrade response does not contain the Sec-WebSocket-Accept.") + message.webSocketClientConnection?.completeExceptionally(e) + return response + } + + val clientKey = message.httpClientRequest.httpFields[HttpHeader.SEC_WEBSOCKET_KEY] + val serverKey = AcceptHash.hashKey(clientKey) + if (response.httpFields[HttpHeader.SEC_WEBSOCKET_ACCEPT] != serverKey) { + val e = UpgradeWebSocketConnectionException("The upgrade response SEC_WEBSOCKET_ACCEPT is illegal.") + message.webSocketClientConnection?.completeExceptionally(e) + return response + } + + log.info { "Upgrade websocket. Client received 101 Switching Protocols. id: $id" } + val webSocketClientRequest = message.webSocketClientRequest + requireNotNull(webSocketClientRequest) + val serverExtensions = response.httpFields.getValuesList(HttpHeader.SEC_WEBSOCKET_EXTENSIONS) + val serverSubProtocols = response.httpFields.getValuesList(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL) + val webSocketConnection = AsyncWebSocketConnection( + tcpConnection, + webSocketClientRequest.policy, + webSocketClientRequest.url, + serverExtensions ?: listOf(), + AsyncWebSocketConnection.defaultExtensionFactory, + serverSubProtocols ?: listOf(), + remainingData = remainingData + ) + webSocketConnection.setWebSocketMessageHandler(webSocketClientRequest.handler) + webSocketConnection.begin() + upgradeWebSocketSuccess = true + message.webSocketClientConnection?.complete(webSocketConnection) + return response + } + + private suspend fun upgradeHttp2Connection( + response: HttpClientResponse, + message: RequestMessage, + remainingData: ByteBuffer? + ): HttpClientResponse { + return if (isUpgradeSuccess(response)) { + log.info { "Upgrade HTTP2. Client received 101 Switching Protocols. id: $id" } + + val http2Connection = Http2ClientConnection(config, tcpConnection, priorKnowledge = false) + val responseFuture = + http2Connection.upgradeHttp2(message.httpClientRequest, remainingData) + http2ClientConnection = http2Connection + httpVersion = HttpVersion.HTTP_2 + responseFuture.await().also { log.info { "Client upgrades HTTP2 success. id: $id" } } + } else response.also { log.info { "Client upgrades HTTP2 failure. id: $id" } } + } + + + private fun isUpgradeToHttp2Success(): Boolean { + return httpVersion == HttpVersion.HTTP_2 && http2ClientConnection != null + } + + private suspend fun waitServerResponse100Continue(): Boolean { + val accepted = try { + withTimeout(Duration.ofSeconds(config.waitResponse100ContinueTimeout).toMillis()) { + parser.parseAll(tcpConnection) + handler.isServerAcceptedContent() + } + } catch (e: TimeoutCancellationException) { + log.info { "Wait server response 100 continue timeout. The client will send data." } + true + } catch (e: Exception) { + log.error { "Wait server response 100 continue failure. The client will not send data. id: $id info: ${e.javaClass.name} ${e.message}" } + false + } + if (accepted) { + parser.reset() + } + return accepted + } + + private suspend fun generateRequestAndWaitServerAccept(requestMessage: RequestMessage) { + var accepted = false + generateRequestLoop@ while (true) { + when (generator.state) { + START -> generateHeader(requestMessage) + COMMITTED -> { + if (accepted) { + generateContent(requestMessage) + } else { + if (waitServerResponse100Continue()) { + accepted = true + generateContent(requestMessage) + log.debug("HTTP1 client receives 100 continue and generates content complete. id: $id") + } else { + requestMessage.contentProvider?.closeAsync()?.await() + break@generateRequestLoop + } + } + } + COMPLETING -> completeContent() + END -> { + completeRequest(requestMessage) + break@generateRequestLoop + } + else -> throw Http1GeneratingResultException("The HTTP client generator state error. ${generator.state}") + } + } + } + + private suspend fun generateRequest(requestMessage: RequestMessage) { + generateRequestLoop@ while (true) { + when (generator.state) { + START -> generateHeader(requestMessage) + COMMITTED -> generateContent(requestMessage) + COMPLETING -> completeContent() + END -> { + completeRequest(requestMessage) + break@generateRequestLoop + } + else -> throw Http1GeneratingResultException("The HTTP client generator state error. ${generator.state}") + } + } + } + + private suspend fun generateHeader(requestMessage: RequestMessage) { + val hasContent = (requestMessage.contentProvider != null) + generator.generateRequest(requestMessage.request, headerBuffer, null, null, !hasContent) + .assert(FLUSH) + flushHeaderBuffer() + } + + private suspend fun generateContent(requestMessage: RequestMessage) { + requireNotNull(requestMessage.contentProvider) + + val pos = contentBuffer.flipToFill() + val len = requestMessage.contentProvider.read(contentBuffer).await() + contentBuffer.flipToFlush(pos) + + val last = (len == -1) + + if (generator.isChunking) { + when (val result = generator.generateRequest(null, null, chunkBuffer, contentBuffer, last)) { + FLUSH -> flushChunkedContentBuffer() + CONTINUE -> { // ignore the generator result continue. + } + SHUTDOWN_OUT, DONE -> completeContent() + else -> throw Http1GeneratingResultException("The HTTP client generator result error. $result") + } + } else { + when (val result = generator.generateRequest(null, null, null, contentBuffer, last)) { + FLUSH -> flushContentBuffer() + CONTINUE -> { // ignore the generator result continue. + } + SHUTDOWN_OUT, DONE -> completeContent() + else -> throw Http1GeneratingResultException("The HTTP client generator result error. $result") + } + } + } + + private suspend fun completeContent() { + if (generator.isChunking) { + when (val result = generator.generateRequest(null, null, chunkBuffer, null, true)) { + FLUSH -> flushChunkBuffer() + CONTINUE, SHUTDOWN_OUT, DONE -> { // ignore the generator result done. + } + else -> throw Http1GeneratingResultException("The HTTP client generator result error. $result") + } + } else { + generator.generateRequest(null, null, null, null, true) + .assert(setOf(DONE, SHUTDOWN_OUT, CONTINUE)) + } + } + + private suspend fun completeRequest(requestMessage: RequestMessage) { + tcpConnection.flush().await() + requestMessage.contentProvider?.closeAsync()?.await() + } + + private suspend fun flushHeaderBuffer() { + if (headerBuffer.hasRemaining()) { + val size = tcpConnection.write(headerBuffer).await() + log.debug { "flush header bytes: $size" } + } + BufferUtils.clear(headerBuffer) + } + + private suspend fun flushContentBuffer() { + if (contentBuffer.hasRemaining()) { + val size = tcpConnection.write(contentBuffer).await() + log.debug { "flush content bytes: $size" } + } + BufferUtils.clear(contentBuffer) + } + + private suspend fun flushChunkedContentBuffer() { + val bufArray = arrayOf(chunkBuffer, contentBuffer) + val remaining = bufArray.sumOf { it.remaining().toLong() } + if (remaining > 0) { + val size = tcpConnection.write(bufArray, 0, bufArray.size).await() + log.debug { "flush chunked content bytes: $size" } + } + bufArray.forEach(BufferUtils::clear) + } + + private suspend fun flushChunkBuffer() { + if (chunkBuffer.hasRemaining()) { + val size = tcpConnection.write(chunkBuffer).await() + log.debug { "flush chunked bytes: $size" } + } + BufferUtils.clear(chunkBuffer) + } + + override fun getHttpVersion(): HttpVersion = httpVersion + + override fun isSecureConnection(): Boolean = tcpConnection.isSecureConnection + + override fun getTcpConnection(): TcpConnection = tcpConnection + + override fun send(request: HttpClientRequest): CompletableFuture { + return when (httpVersion) { + HttpVersion.HTTP_2 -> sendRequestViaHttp2(request) + HttpVersion.HTTP_1_1 -> sendRequestViaHttp1(request) + else -> throw NotSupportHttpVersionException("HTTP version not support. $httpVersion") + } + } + + private fun sendRequestViaHttp1(request: HttpClientRequest): CompletableFuture { + log.debug { "Send request via HTTP1 protocol. id: $id" } + if (config.isAutoGeneratedClientHttp1Headers) { + prepareHttp1Headers(request) { this.remoteAddress.hostName } + } + val future = CompletableFuture() + requestChannel.trySend(RequestMessage(request, future)) + return future + } + + private fun sendRequestViaHttp2(request: HttpClientRequest): CompletableFuture { + val http2Connection = http2ClientConnection + requireNotNull(http2Connection) + log.debug { "Send request via HTTP2 protocol. id: $id" } + return http2Connection.send(request) + } + + fun upgradeWebSocket(webSocketClientRequest: WebSocketClientRequest): CompletableFuture { + val request = AsyncHttpClientRequest() + request.method = HttpMethod.GET.value + request.uri = HttpURI(webSocketClientRequest.url) + request.httpFields = HttpFields() + request.httpFields.put(HttpHeader.HOST, request.uri.host) + request.httpFields.put(HttpHeader.CONNECTION, "Upgrade") + request.httpFields.put(HttpHeader.UPGRADE, "websocket") + request.httpFields.put(HttpHeader.SEC_WEBSOCKET_VERSION, "13") + request.httpFields.put(HttpHeader.SEC_WEBSOCKET_KEY, genRandomWebSocketKey()) + if (!webSocketClientRequest.extensions.isNullOrEmpty()) { + request.httpFields.put( + HttpHeader.SEC_WEBSOCKET_EXTENSIONS, + webSocketClientRequest.extensions.joinToString(", ") + ) + } + if (!webSocketClientRequest.subProtocols.isNullOrEmpty()) { + request.httpFields.put( + HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, + webSocketClientRequest.subProtocols.joinToString(", ") + ) + } + + val websocketFuture = CompletableFuture() + val responseFuture = CompletableFuture() + val message = RequestMessage( + httpClientRequest = request, + response = responseFuture, + webSocketClientConnection = websocketFuture, + webSocketClientRequest = webSocketClientRequest + ) + requestChannel.trySend(message) + return websocketFuture + } + + fun dispose() { + requestChannel.trySend(Stop) + } + + private fun genRandomWebSocketKey(): String { + val bytes = ByteArray(16) + ThreadLocalRandom.current().nextBytes(bytes) + return String(Base64Utils.encode(bytes)) + } + + sealed interface Http1ClientConnectionMessage + + private data class RequestMessage( + val httpClientRequest: HttpClientRequest, + val response: CompletableFuture, + val request: MetaData.Request = toMetaDataRequest(httpClientRequest), + val contentProvider: HttpClientContentProvider? = httpClientRequest.contentProvider, + val expectServerAcceptsContent: Boolean = httpClientRequest.httpFields.expectServerAcceptsContent(), + val expectUpgradeHttp2: Boolean = expectUpgradeHttp2(httpClientRequest), + val expectUpgradeWebSocket: Boolean = expectUpgradeWebsocket(httpClientRequest), + val isHttpTunnel: Boolean = httpClientRequest.method.equals(HttpMethod.CONNECT.value), + val webSocketClientConnection: CompletableFuture? = null, + val webSocketClientRequest: WebSocketClientRequest? = null + ) : Http1ClientConnectionMessage + + private object Stop : Http1ClientConnectionMessage +} + +fun prepareHttp1Headers(request: HttpClientRequest, defaultHost: () -> String) { + if (request.httpFields.getValuesList(HttpHeader.HOST.value).isEmpty()) { + val host = if (request.uri.host.isNullOrBlank()) defaultHost.invoke() else request.uri.host + request.httpFields.put(HttpHeader.HOST, host) + } + + val connectionValues = request.httpFields.getCSV(HttpHeader.CONNECTION, false) + if (connectionValues.isNotEmpty()) { + if (!connectionValues.contains(HttpHeaderValue.KEEP_ALIVE.value)) { + val newValues = mutableListOf() + newValues.addAll(connectionValues) + newValues.add(HttpHeaderValue.KEEP_ALIVE.value) + request.httpFields.remove(HttpHeader.CONNECTION) + request.httpFields.addCSV(HttpHeader.CONNECTION, *newValues.toTypedArray()) + } + } else { + request.httpFields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.value) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http1ClientResponseHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http1ClientResponseHandler.kt new file mode 100644 index 000000000..8fc580f6e --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http1ClientResponseHandler.kt @@ -0,0 +1,114 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.HttpClientRequest +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.common.v1.decoder.HttpParser +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import java.nio.ByteBuffer +import java.util.function.Supplier + +class Http1ClientResponseHandler : HttpParser.ResponseHandler { + + private val response: MetaData.Response = MetaData.Response(HttpVersion.HTTP_1_1, 0, HttpFields()) + private var expectServerAcceptsContent = false + private var httpClientResponse: AsyncHttpClientResponse? = null + private val trailers = HttpFields() + private val responseChannel: Channel = Channel(Channel.UNLIMITED) + private var isServerAcceptedContent: Boolean = false + private var isHttpTunnel: Boolean = false + private var httpRequest: HttpClientRequest? = null + + fun init(httpRequest: HttpClientRequest, expectServerAcceptsContent: Boolean, isHttpTunnel: Boolean) { + this.httpRequest = httpRequest + this.expectServerAcceptsContent = expectServerAcceptsContent + this.isHttpTunnel = isHttpTunnel + } + + override fun getHeaderCacheSize(): Int { + return 4096 + } + + override fun startResponse(version: HttpVersion, status: Int, reason: String): Boolean { + fun updateResponseLine() { + response.httpVersion = version + response.status = status + response.reason = reason + } + + if (expectServerAcceptsContent) { + if (status == HttpStatus.CONTINUE_100) { + isServerAcceptedContent = true + } else { + isServerAcceptedContent = false + updateResponseLine() + } + expectServerAcceptsContent = false + } else updateResponseLine() + return status == HttpStatus.CONTINUE_100 + } + + override fun parsedHeader(field: HttpField) { + response.fields.add(field) + } + + override fun headerComplete(): Boolean { + val httpClientResponse = AsyncHttpClientResponse(MetaData.Response(response), httpRequest?.contentHandler) + this.httpClientResponse = httpClientResponse + return if (isHttpTunnel) { + responseChannel.trySend(httpClientResponse) + true + } else { + val request = httpRequest + requireNotNull(request) + request.headerComplete.accept(request, httpClientResponse) + false + } + } + + override fun content(buffer: ByteBuffer): Boolean { + httpRequest?.contentHandler?.accept(buffer, httpClientResponse) + return false + } + + override fun contentComplete(): Boolean { + return false + } + + override fun parsedTrailer(field: HttpField) { + trailers.add(field) + } + + override fun messageComplete(): Boolean { + val clientResponse = httpClientResponse + requireNotNull(clientResponse) + val trailer = HttpFields(trailers) + clientResponse.response.trailerSupplier = Supplier { trailer } + responseChannel.trySend(clientResponse) + return true + } + + override fun badMessage(failure: BadMessageException) { + throw failure + } + + override fun earlyEOF() { + throw BadMessageException(HttpStatus.BAD_REQUEST_400) + } + + suspend fun complete(): HttpClientResponse { + val response = responseChannel.receive() + httpRequest?.contentHandler?.closeAsync()?.await() + return response + } + + fun isServerAcceptedContent(): Boolean = isServerAcceptedContent + + fun reset() { + response.recycle() + httpRequest = null + trailers.clear() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http2ClientConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http2ClientConnection.kt new file mode 100644 index 000000000..588bd1755 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http2ClientConnection.kt @@ -0,0 +1,228 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.io.useAwait +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.* +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.HttpConfig.DEFAULT_WINDOW_SIZE +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v2.decoder.Parser +import com.fireflysource.net.http.common.v2.frame.* +import com.fireflysource.net.http.common.v2.stream.* +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.aio.AdaptiveBufferSize +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.future.await +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.UnaryOperator + +class Http2ClientConnection( + config: HttpConfig, + tcpConnection: TcpConnection, + flowControl: FlowControl = BufferedFlowControlStrategy(), + listener: Http2Connection.Listener = defaultHttp2ConnectionListener, + priorKnowledge: Boolean = true +) : AsyncHttp2Connection(1, config, tcpConnection, flowControl, listener), AbstractHttpClientConnection { + + companion object { + private val log = SystemLogger.create(Http2ClientConnection::class.java) + } + + private val parser: Parser = Parser(this, config.maxDynamicTableSize, config.maxHeaderSize) + private val adaptiveBufferSize = AdaptiveBufferSize() + + init { + if (priorKnowledge) { + sendConnectionPreface() + parser.init(UnaryOperator.identity()) + launchParserJob(parser) + } + } + + fun upgradeHttp2(request: HttpClientRequest, frameBytes: ByteBuffer?): CompletableFuture { + val streamId = getNextStreamId() + val future = CompletableFuture() + val streamListener = Http2ClientStreamListener(request, future) + createLocalStream(streamId, streamListener) + sendConnectionPreface() + parser.init(UnaryOperator.identity()) + if (frameBytes != null) { + log.debug { "Upgrade HTTP2 and received frame data. id: $id, remaining: ${frameBytes.remaining()}" } + while (frameBytes.hasRemaining()) { + parser.parse(frameBytes) + } + } + launchParserJob(parser) + return future + } + + private fun sendConnectionPreface() { + val settings = notifyPreface() + + val maxFrameLength = settings[SettingsFrame.MAX_FRAME_SIZE] + if (maxFrameLength != null) { + parser.maxFrameLength = maxFrameLength + } + + val prefaceFrame = PrefaceFrame() + val settingsFrame = SettingsFrame(settings, false) + val windowDelta = initialSessionRecvWindow - DEFAULT_WINDOW_SIZE + if (windowDelta > 0) { + val windowUpdateFrame = WindowUpdateFrame(0, windowDelta) + updateRecvWindow(windowDelta) + sendControlFrame(null, prefaceFrame, settingsFrame, windowUpdateFrame) + .thenAccept { log.info { "send connection preface success. id: $id, settings: $settingsFrame" } } + .exceptionallyAccept { log.error(it) { "send connection preface exception. id: $id" } } + } else { + sendControlFrame(null, prefaceFrame, settingsFrame) + .thenAccept { log.info { "send connection preface success. id: $id, settings: $settingsFrame" } } + .exceptionallyAccept { log.error(it) { "send connection preface exception. id: $id" } } + } + } + + override fun onHeaders(frame: HeadersFrame) { + log.debug { "Received $frame" } + + // HEADERS can be received for normal and pushed responses. + val streamId = frame.streamId + val stream = getStream(streamId) + if (stream != null && stream is AsyncHttp2Stream) { + val metaData = frame.metaData + if (metaData.isRequest) { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_response") + } else { + stream.process(frame, discard()) + notifyHeaders(stream, frame) + } + } else { + log.debug { "Stream: $streamId not found" } + if (isClientStream(streamId)) { + // The normal stream. Headers or trailers arriving after the stream has been reset are ignored. + if (!isLocalStreamClosed(streamId)) { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame") + } + } else { + // The pushed stream. Headers or trailers arriving after the stream has been reset are ignored. + if (!isRemoteStreamClosed(streamId)) { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame") + } + } + } + } + + + // promise frame + override fun onPushPromise(frame: PushPromiseFrame) { + log.debug { "Received $frame" } + + val stream = getStream(frame.streamId) + if (stream == null) { + log.debug { "Ignoring $frame, stream: ${frame.streamId} not found" } + } else { + val pushStream = createRemoteStream(frame.promisedStreamId) + if (pushStream != null && pushStream is AsyncHttp2Stream) { + pushStream.process(frame, discard()) + pushStream.listener = notifyPush(stream, pushStream, frame) + } + } + } + + private fun notifyPush(stream: Stream, pushStream: Stream, frame: PushPromiseFrame): Stream.Listener { + return try { + val listener = (stream as AsyncHttp2Stream).listener + listener.onPush(pushStream, frame) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + AsyncHttp2Stream.defaultStreamListener + } + } + + override fun onResetForUnknownStream(frame: ResetFrame) { + val streamId = frame.streamId + val closed = if (isClientStream(streamId)) isLocalStreamClosed(streamId) else isRemoteStreamClosed(streamId) + if (closed) { + notifyReset(this, frame) + } else { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_rst_stream_frame") + } + } + + override fun send(request: HttpClientRequest): CompletableFuture { + val metaDataRequest: MetaData.Request = toMetaDataRequest(request) + val contentProvider: HttpClientContentProvider? = request.contentProvider + val lastHeaders = contentProvider == null && metaDataRequest.trailerSupplier == null + val headersFrame = HeadersFrame(metaDataRequest, null, lastHeaders) + + val future = CompletableFuture() + val streamListener = Http2ClientStreamListener(request, future) + val serverAccepted = streamListener.serverAccepted + newStream(headersFrame, streamListener) + .thenCompose { newStream -> serverAccepted.thenApply { Pair(newStream, it) } } + .thenCompose { generateContent(contentProvider, metaDataRequest, it.first, it.second) } + .thenAccept { generateTrailer(metaDataRequest, it.first, it.second) } + .exceptionallyAccept { + log.error(it) { "The HTTP2 client connection creates local stream failure. id: $id " } + future.completeExceptionally(it) + } + return future + } + + private fun generateContent( + contentProvider: HttpClientContentProvider?, + metaDataRequest: MetaData.Request, + newStream: Stream, + serverAccept: Boolean + ) = tcpConnection.coroutineScope.async { + if (contentProvider != null && serverAccept) { + contentProvider.useAwait { + val byteBuffers = LinkedList() + readLoop@ while (true) { + val contentBuffer = BufferUtils.allocate(adaptiveBufferSize.getBufferSize()) + val pos = contentBuffer.flipToFill() + val length = contentProvider.read(contentBuffer).await() + contentBuffer.flipToFlush(pos) + adaptiveBufferSize.update(length) + + when { + length > 0 -> byteBuffers.offer(contentBuffer) + length < 0 -> break@readLoop + } + + if (byteBuffers.size > 1) { + val dataFrame = DataFrame(newStream.id, byteBuffers.poll(), false) + newStream.data(dataFrame) + } + } + + val last = metaDataRequest.trailerSupplier == null + if (byteBuffers.isNotEmpty()) { + val dataFrame = DataFrame(newStream.id, byteBuffers.poll(), last) + newStream.data(dataFrame) + } else { + val empty = ByteBuffer.allocate(0) + val dataFrame = DataFrame(newStream.id, empty, last) + newStream.data(dataFrame) + } + } + } + Pair(newStream, serverAccept) + }.asCompletableFuture() + + private fun generateTrailer(metaDataRequest: MetaData.Request, newStream: Stream, serverAccept: Boolean) { + val trailerSupplier = metaDataRequest.trailerSupplier + if (trailerSupplier != null && serverAccept) { + val trailerMetaData = MetaData.Request(trailerSupplier.get()) + trailerMetaData.isOnlyTrailer = true + val headersFrameTrailer = HeadersFrame(newStream.id, trailerMetaData, null, true) + newStream.headers(headersFrameTrailer, discard()) + } + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http2ClientStreamListener.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http2ClientStreamListener.kt new file mode 100644 index 000000000..11e7c3841 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/Http2ClientStreamListener.kt @@ -0,0 +1,148 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.HttpClientRequest +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.common.v2.frame.DataFrame +import com.fireflysource.net.http.common.v2.frame.ErrorCode +import com.fireflysource.net.http.common.v2.frame.HeadersFrame +import com.fireflysource.net.http.common.v2.frame.ResetFrame +import com.fireflysource.net.http.common.v2.stream.Stream +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +class Http2ClientStreamListener( + private val request: HttpClientRequest, + private val future: CompletableFuture +) : Stream.Listener.Adapter() { + + companion object { + private val log = SystemLogger.create(Http2ClientStreamListener::class.java) + private val defaultServerAccepted: CompletableFuture by lazy { + val future = CompletableFuture() + future.complete(true) + future + } + } + + private val expectServerAcceptsContent: Boolean = request.httpFields.expectServerAcceptsContent() + private val response = + AsyncHttpClientResponse(MetaData.Response(HttpVersion.HTTP_2, 0, HttpFields()), request.contentHandler) + private val metaDataResponse = response.response + val serverAccepted = if (expectServerAcceptsContent) CompletableFuture() else defaultServerAccepted + private val trailer = HttpFields() + private var theFirstHeader = true + private var receivedData = false + + private fun onMessageComplete() { + val contentHandler = request.contentHandler + if (contentHandler != null) { + contentHandler.closeAsync() + .thenAccept { future.complete(response) } + .exceptionallyAccept { future.completeExceptionally(it) } + } else future.complete(response) + } + + private fun handleResponseHeaders(stream: Stream, frame: HeadersFrame) { + when (val metaData = frame.metaData) { + is MetaData.Response -> { + metaDataResponse.status = metaData.status + metaDataResponse.reason = metaData.reason + metaDataResponse.fields.addAll(metaData.fields) + request.headerComplete.accept(request, response) + } + is MetaData.Request -> handleError(stream, "The HTTP2 client must receive response metadata.") + else -> { + if (receivedData) { + if (metaDataResponse.trailerSupplier == null) { + metaDataResponse.setTrailerSupplier { trailer } + } + trailer.addAll(metaData.fields) + } else metaDataResponse.fields.addAll(metaData.fields) + } + } + if (frame.isEndStream) onMessageComplete() + } + + private fun handleError(stream: Stream, message: String) { + val resetFrame = ResetFrame(stream.id, ErrorCode.INTERNAL_ERROR.code) + stream.reset(resetFrame) { + val exception = IllegalStateException(message) + future.completeExceptionally(exception) + } + } + + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + if (theFirstHeader) { + theFirstHeader = false + val metaData = frame.metaData + if (metaData is MetaData.Response) { + if (expectServerAcceptsContent) { + if (metaData.status == HttpStatus.CONTINUE_100) { + log.debug { "Client received 100 continue response. stream: $stream" } + if (frame.isEndStream) { + serverAccepted.complete(false) + handleError(stream, "The remote stream closed. id: ${stream.id}") + } else serverAccepted.complete(true) + } else { + serverAccepted.complete(false) + handleResponseHeaders(stream, frame) + } + } else { + serverAccepted.complete(true) + handleResponseHeaders(stream, frame) + } + } else { + serverAccepted.complete(false) + handleError(stream, "The HTTP2 client must receive response metadata.") + } + } else { + handleResponseHeaders(stream, frame) + } + } + + override fun onData(stream: Stream, frame: DataFrame, result: Consumer>) { + try { + receivedData = true + request.contentHandler?.accept(frame.data, response) + if (frame.isEndStream) onMessageComplete() + } finally { + result.accept(Result.SUCCESS) + } + } + + override fun onReset(stream: Stream, frame: ResetFrame) { + if (!future.isDone) { + val error = ErrorCode.toString(frame.error, "http2_request_error") + val exception = IllegalStateException(error) + future.completeExceptionally(exception) + } + } + + override fun onIdleTimeout(stream: Stream, x: Throwable): Boolean { + if (!future.isDone) { + val exception = IllegalStateException("http2_stream_timeout") + future.completeExceptionally(exception) + } + return true + } + + override fun onTerminal(stream: Stream) { + if (!future.isDone) { + val exception = IllegalStateException("The stream is closed") + future.completeExceptionally(exception) + } + } + + override fun onFailure(stream: Stream, error: Int, reason: String, result: Consumer>) { + if (!future.isDone) { + val exception = IllegalStateException("The stream is failure") + future.completeExceptionally(exception) + } + result.accept(Result.SUCCESS) + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/HttpClientConnectionFactory.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/HttpClientConnectionFactory.kt new file mode 100644 index 000000000..0571d52b4 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/HttpClientConnectionFactory.kt @@ -0,0 +1,135 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.common.codec.base64.Base64 +import com.fireflysource.common.string.StringUtils +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.HttpClientConnection +import com.fireflysource.net.http.client.impl.exception.HttpTunnelHandshakeException +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.tcp.TcpClientConnectionFactory +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.aio.ApplicationProtocol +import com.fireflysource.net.tcp.aio.createSecureTcpConnection +import com.fireflysource.net.tcp.aio.defaultSupportedProtocols +import com.fireflysource.net.tcp.secure.DefaultSecureEngineFactorySelector +import kotlinx.coroutines.future.await +import java.net.InetSocketAddress +import java.util.concurrent.CompletableFuture + +class HttpClientConnectionFactory( + private val httpConfig: HttpConfig, + private val connectionFactory: TcpClientConnectionFactory +) { + + companion object { + private val log = SystemLogger.create(HttpClientConnectionFactory::class.java) + } + + suspend fun createHttpClientConnection( + address: Address, + supportedProtocols: List = defaultSupportedProtocols + ): HttpClientConnection { + return if (isProxyEnabled()) { + createProxyHttpClientConnection(address, supportedProtocols.ifEmpty { defaultSupportedProtocols }) + } else { + createDirectHttpClientConnection(address, supportedProtocols.ifEmpty { defaultSupportedProtocols }) + } + } + + private fun isProxyEnabled(): Boolean { + return httpConfig.proxyConfig != null && StringUtils.hasText(httpConfig.proxyConfig.host) && httpConfig.proxyConfig.port > 0 + } + + private suspend fun createDirectHttpClientConnection( + address: Address, + supportedProtocols: List = defaultSupportedProtocols + ): HttpClientConnection { + return connectionFactory.connect(address.socketAddress, address.secure, supportedProtocols) + .thenCompose { createHttpClientConnection(it) }.await() + } + + private suspend fun createProxyHttpClientConnection( + address: Address, + supportedProtocols: List = defaultSupportedProtocols + ): HttpClientConnection { + val proxyAddress = getProxyAddress() + val proxyTcpConnection = connectionFactory.connect(proxyAddress, false).await() + val httpConnection = createHttp1ClientConnection(proxyTcpConnection) + return if (address.secure) { + try { + val success = + beginHttpTunnelHandshake(address.socketAddress.hostName, address.socketAddress.port, httpConnection) + if (success) { + val secureEngineFactory = if (connectionFactory.secureEngineFactory == null) + DefaultSecureEngineFactorySelector.createSecureEngineFactory(true) + else connectionFactory.secureEngineFactory + + val secureTcpConnection = createSecureTcpConnection( + proxyTcpConnection, + "", + 0, + true, + supportedProtocols.ifEmpty { defaultSupportedProtocols }, + secureEngineFactory + ) + createHttpClientConnection(secureTcpConnection).await() + } else { + throw HttpTunnelHandshakeException("HTTP tunnel handshake failure.") + } + } finally { + httpConnection.dispose() + } + } else httpConnection + } + + private fun getProxyAddress(): InetSocketAddress { + val proxyConfig = httpConfig.proxyConfig + requireNotNull(proxyConfig) + Assert.hasText(proxyConfig.host, "The proxy host must be not null") + Assert.isTrue(proxyConfig.port > 0, "The proxy port must be greater than 0") + + return InetSocketAddress(proxyConfig.host, proxyConfig.port) + } + + private suspend fun beginHttpTunnelHandshake( + host: String, + port: Int, + httpConnection: HttpClientConnection + ): Boolean { + val request = AsyncHttpClientRequest() + request.method = HttpMethod.CONNECT.value + request.uri = HttpURI("$host:$port") + request.httpFields.put(HttpHeader.HOST, host) + request.httpFields.put(HttpHeader.PROXY_CONNECTION, "keep-alive") + val auth = httpConfig.proxyConfig.proxyAuthentication + if (auth != null && auth.username != null && auth.password != null) { + val authStr = Base64.encodeBase64String("${auth.username}:${auth.password}".toByteArray()) + request.httpFields.put(HttpHeader.PROXY_AUTHORIZATION, "Basic $authStr") + } + val response = httpConnection.send(request).await() + log.info("HTTP tunnel handshake result: ${response.status}, host: $host, port: $port") + return response.status == HttpStatus.OK_200 + } + + private fun createHttpClientConnection(connection: TcpConnection): CompletableFuture { + return if (connection.isSecureConnection) { + connection.beginHandshake().thenApply { applicationProtocol -> + when (applicationProtocol) { + ApplicationProtocol.HTTP2.value -> createHttp2ClientConnection(connection) + else -> createHttp1ClientConnection(connection) + } + } + } else CompletableFuture.completedFuture(createHttp1ClientConnection(connection)) + } + + private fun createHttp1ClientConnection(connection: TcpConnection) = Http1ClientConnection(httpConfig, connection) + + private fun createHttp2ClientConnection(connection: TcpConnection) = Http2ClientConnection(httpConfig, connection) +} + +data class Address(val socketAddress: InetSocketAddress, val secure: Boolean) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/HttpProtocolNegotiator.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/HttpProtocolNegotiator.kt new file mode 100644 index 000000000..b5906474c --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/HttpProtocolNegotiator.kt @@ -0,0 +1,79 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.codec.base64.Base64Utils +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.client.HttpClientRequest +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpHeaderValue +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.v2.encoder.SettingsGenerator.generateSettingsBody +import com.fireflysource.net.http.common.v2.frame.SettingsFrame +import com.fireflysource.net.http.server.HttpServerRequest + +object HttpProtocolNegotiator { + val defaultSettingsFrameBytes: ByteArray = + BufferUtils.toArray(generateSettingsBody(SettingsFrame.DEFAULT_SETTINGS_FRAME.settings)) + + fun addHttp2UpgradeHeader(request: HttpClientRequest) { + // detect the protocol version using Connection and Upgrade HTTP headers + val oldValues: List = request.httpFields.getCSV(HttpHeader.CONNECTION, false) + if (oldValues.isNotEmpty()) { + val newValues = mutableListOf() + newValues.addAll(oldValues) + newValues.add("Upgrade") + newValues.add("HTTP2-Settings") + request.httpFields.remove(HttpHeader.CONNECTION) + request.httpFields.addCSV(HttpHeader.CONNECTION, *newValues.toTypedArray()) + } else { + request.httpFields.addCSV(HttpHeader.CONNECTION, "Upgrade", "HTTP2-Settings") + } + request.httpFields.put(HttpHeader.UPGRADE, "h2c") + + // generate http2 settings base64 + val bytes = if (request.http2Settings.isNullOrEmpty()) { + defaultSettingsFrameBytes + } else { + BufferUtils.toArray(generateSettingsBody(request.http2Settings)) + } + + val base64 = Base64Utils.encodeToUrlSafeString(bytes) + request.httpFields.put(HttpHeader.HTTP2_SETTINGS, base64) + } + + fun removeHttp2UpgradeHeader(request: HttpClientRequest) { + request.httpFields.remove(HttpHeader.HTTP2_SETTINGS) + request.httpFields.remove(HttpHeader.UPGRADE) + request.httpFields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.value) + } + + fun isUpgradeSuccess(response: HttpClientResponse): Boolean { + return response.status == HttpStatus.SWITCHING_PROTOCOLS_101 + } + + fun expectUpgradeHttp2(request: HttpClientRequest): Boolean { + return request.httpFields.contains(HttpHeader.CONNECTION, "Upgrade") + && request.httpFields.contains(HttpHeader.UPGRADE, "h2c") + } + + fun expectUpgradeHttp2(request: HttpServerRequest): Boolean { + return request.httpFields.contains(HttpHeader.CONNECTION, "Upgrade") + && request.httpFields.contains(HttpHeader.UPGRADE, "h2c") + && request.httpFields.contains(HttpHeader.HTTP2_SETTINGS) + } + + fun expectUpgradeWebsocket(request: HttpClientRequest): Boolean { + return request.method == HttpMethod.GET.value + && request.httpFields.contains(HttpHeader.UPGRADE, "websocket") + && request.httpFields.contains(HttpHeader.SEC_WEBSOCKET_VERSION, "13") + && request.httpFields.contains(HttpHeader.SEC_WEBSOCKET_KEY) + } + + fun expectUpgradeWebsocket(request: HttpServerRequest): Boolean { + return request.method == HttpMethod.GET.value + && request.httpFields.contains(HttpHeader.UPGRADE, "websocket") + && request.httpFields.contains(HttpHeader.SEC_WEBSOCKET_VERSION, "13") + && request.httpFields.contains(HttpHeader.SEC_WEBSOCKET_KEY) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/ByteBufferContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/ByteBufferContentHandler.kt new file mode 100644 index 000000000..ab463f868 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/ByteBufferContentHandler.kt @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.client.impl.content.handler + +import com.fireflysource.net.http.client.HttpClientContentHandler +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.content.handler.AbstractByteBufferContentHandler + +open class ByteBufferContentHandler(maxRequestBodySize: Long = 200 * 1024 * 1024) : + AbstractByteBufferContentHandler(maxRequestBodySize), HttpClientContentHandler \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/FileContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/FileContentHandler.kt new file mode 100644 index 000000000..f029d112d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/FileContentHandler.kt @@ -0,0 +1,10 @@ +package com.fireflysource.net.http.client.impl.content.handler + +import com.fireflysource.net.http.client.HttpClientContentHandler +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.content.handler.AbstractFileContentHandler +import java.nio.file.OpenOption +import java.nio.file.Path + +class FileContentHandler(path: Path, vararg options: OpenOption) : + AbstractFileContentHandler(path, *options), HttpClientContentHandler \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/StringContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/StringContentHandler.kt new file mode 100644 index 000000000..4383e0458 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/handler/StringContentHandler.kt @@ -0,0 +1,3 @@ +package com.fireflysource.net.http.client.impl.content.handler + +class StringContentHandler(maxRequestBodySize: Long = 200 * 1024 * 1024) : ByteBufferContentHandler(maxRequestBodySize) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/ByteBufferContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/ByteBufferContentProvider.kt new file mode 100644 index 000000000..ea09f59b1 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/ByteBufferContentProvider.kt @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.net.http.client.HttpClientContentProvider +import com.fireflysource.net.http.common.content.provider.AbstractByteBufferContentProvider +import java.nio.ByteBuffer + +open class ByteBufferContentProvider(content: ByteBuffer) : + AbstractByteBufferContentProvider(content), HttpClientContentProvider \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/FileContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/FileContentProvider.kt new file mode 100644 index 000000000..c9c15252b --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/FileContentProvider.kt @@ -0,0 +1,27 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.net.http.client.HttpClientContentProvider +import com.fireflysource.net.http.common.content.provider.AbstractFileContentProvider +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import java.nio.file.Files +import java.nio.file.OpenOption +import java.nio.file.Path + +class FileContentProvider( + path: Path, + options: Set, + position: Long, + length: Long, + scope: CoroutineScope = CoroutineScope(CoroutineName("Firefly-file-content-provider")) +) : AbstractFileContentProvider(path, options, position, length, scope), HttpClientContentProvider { + + constructor(path: Path, vararg options: OpenOption) : this(path, options.toSet(), 0, Files.size(path)) + + constructor( + path: Path, + options: Set, + position: Long, + length: Long + ) : this(path, options, position, length, CoroutineScope(CoroutineName("Firefly-file-content-provider"))) +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/MultiPartContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/MultiPartContentProvider.kt new file mode 100644 index 000000000..a9e3acc68 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/MultiPartContentProvider.kt @@ -0,0 +1,302 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.common.coroutine.event +import com.fireflysource.common.exception.UnsupportedOperationException +import com.fireflysource.common.io.AsyncCloseable +import com.fireflysource.net.http.client.HttpClientContentProvider +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.HttpHeader +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.CompletableFuture + + +class MultiPartContentProvider : HttpClientContentProvider { + + companion object { + private const val newLine = "\r\n" + private val colonSpaceBytes: ByteArray = byteArrayOf(':'.code.toByte(), ' '.code.toByte()) + private val newLineBytes: ByteArray = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte()) + } + + val contentType: String + private val firstBoundary: ByteArray + private val middleBoundary: ByteArray + private val onlyBoundary: ByteArray + private val lastBoundary: ByteArray + private val parts: MutableList = LinkedList() + + private var index = 0 + private var state = State.FIRST_BOUNDARY + private var open = true + + private val multiPartChannel: Channel = Channel(Channel.UNLIMITED) + private val generatingJob: Job + + init { + val boundary = makeBoundary() + this.contentType = "multipart/form-data; boundary=$boundary" + + val firstBoundaryLine = "--$boundary$newLine" + this.firstBoundary = firstBoundaryLine.toByteArray(StandardCharsets.US_ASCII) + + val middleBoundaryLine = newLine + firstBoundaryLine + this.middleBoundary = middleBoundaryLine.toByteArray(StandardCharsets.US_ASCII) + + val onlyBoundaryLine = "--$boundary--$newLine" + this.onlyBoundary = onlyBoundaryLine.toByteArray(StandardCharsets.US_ASCII) + + val lastBoundaryLine = newLine + onlyBoundaryLine + this.lastBoundary = lastBoundaryLine.toByteArray(StandardCharsets.US_ASCII) + + generatingJob = event { + readMessageLoop@ while (true) { + when (val readMultiPartMessage = multiPartChannel.receive()) { + is GenerateMultiPart -> { + val (buf, future) = readMultiPartMessage + try { + val len = generate(buf) + future.complete(len) + } catch (e: Exception) { + future.completeExceptionally(e) + } + } + is EndMultiPartProvider -> { + open = false + state = State.COMPLETE + parts.forEach { part -> part.closeAsync().await() } + break@readMessageLoop + } + } + + } + } + } + + /** + *

Adds a field part with the given {@code name} as field name, and the given + * {@code content} as part content.

+ * + * @param name the part name + * @param content the part content + * @param fields the headers associated with this part + */ + fun addPart(name: String, content: HttpClientContentProvider, fields: HttpFields?) { + parts.add(Part(name, null, content, fields, "text/plain")) + } + + /** + *

Adds a file part with the given {@code name} as field name, the given + * {@code fileName} as file name, and the given {@code content} as part content.

+ * + * @param name the part name + * @param fileName the file name associated to this part + * @param content the part content + * @param fields the headers associated with this part + */ + fun addFilePart(name: String, fileName: String?, content: HttpClientContentProvider, fields: HttpFields?) { + parts.add(Part(name, fileName, content, fields, "application/octet-stream")) + } + + override fun length(): Long { + // Compute the length, if possible. + if (parts.isEmpty()) { + return onlyBoundary.size.toLong() + } else { + var result: Long = 0 + for (i in 0 until parts.size) { + result += if (i == 0) firstBoundary.size.toLong() else middleBoundary.size.toLong() + val part = parts[i] + val partLength = part.length + result += partLength + if (partLength < 0) { + result = -1 + break + } + } + if (result > 0) { + result += lastBoundary.size.toLong() + } + return result + } + } + + override fun isOpen(): Boolean = open + + override fun toByteBuffer(): ByteBuffer { + throw UnsupportedOperationException("The multi part content does not support this method") + } + + private suspend fun closeAwait() { + close() + generatingJob.join() + } + + override fun closeAsync(): CompletableFuture { + val future = CompletableFuture() + event { + closeAwait() + future.complete(null) + } + return future + } + + override fun close() { + multiPartChannel.trySend(EndMultiPartProvider) + } + + override fun read(byteBuffer: ByteBuffer): CompletableFuture { + if (!isOpen) { + return endStream() + } + + if (state == State.COMPLETE) { + return endStream() + } + + val future = CompletableFuture() + multiPartChannel.trySend(GenerateMultiPart(byteBuffer, future)) + return future + } + + private suspend fun generate(byteBuffer: ByteBuffer): Int { + while (true) { + when (state) { + State.FIRST_BOUNDARY -> { + return if (parts.isEmpty()) { + state = State.COMPLETE + byteBuffer.put(onlyBoundary) + onlyBoundary.size + } else { + state = State.HEADERS + byteBuffer.put(firstBoundary) + firstBoundary.size + } + } + State.HEADERS -> { + val part = parts[index] + state = State.CONTENT + byteBuffer.put(part.headers) + return part.headers.size + } + State.CONTENT -> { + val part = parts[index] + val len = part.content.read(byteBuffer).await() + if (len >= 0) { + return len + } else { + ++index + state = if (index == parts.size) State.LAST_BOUNDARY else State.MIDDLE_BOUNDARY + } + } + State.MIDDLE_BOUNDARY -> { + state = State.HEADERS + byteBuffer.put(middleBoundary) + return middleBoundary.size + } + State.LAST_BOUNDARY -> { + state = State.COMPLETE + byteBuffer.put(lastBoundary) + return lastBoundary.size + } + State.COMPLETE -> { + open = false + return -1 + } + } + } + } + + private fun makeBoundary(): String { + val random = Random() + val builder = StringBuilder("FireflyHttpClientBoundary") + val length = builder.length + while (builder.length < length + 16) { + val rnd = random.nextLong() + builder.append((if (rnd < 0) -rnd else rnd).toString(36)) + } + builder.setLength(length + 16) + return builder.toString() + } + + private class Part( + name: String, + fileName: String?, + val content: HttpClientContentProvider, + fields: HttpFields?, + val contentType: String + ) : AsyncCloseable { + val headers: ByteArray + val length: Long + + init { + // Compute the Content-Disposition. + var contentDisposition = "Content-Disposition: form-data; name=\"$name\"" + if (fileName != null) { + contentDisposition += "; filename=\"$fileName\"" + } + contentDisposition += newLine + + // Compute the Content-Type. + var contentType = fields?.get(HttpHeader.CONTENT_TYPE) + if (contentType == null) { + contentType = this.contentType + } + contentType = "Content-Type: $contentType$newLine" + + // Compute the headers + if (fields == null || fields.size() == 0) { + var headers = contentDisposition + headers += contentType + headers += newLine + this.headers = headers.toByteArray(StandardCharsets.UTF_8) + } else { + val buffer = ByteArrayOutputStream((fields.size() + 1) * contentDisposition.length) + buffer.write(contentDisposition.toByteArray(StandardCharsets.UTF_8)) + buffer.write(contentType.toByteArray(StandardCharsets.UTF_8)) + for (field in fields) { + if (HttpHeader.CONTENT_TYPE == field.header) { + continue + } + buffer.write(field.name.toByteArray(StandardCharsets.US_ASCII)) + buffer.write(colonSpaceBytes) + val value = field.value + if (value != null) { + buffer.write(value.toByteArray(StandardCharsets.UTF_8)) + } + buffer.write(newLineBytes) + } + buffer.write(newLineBytes) + headers = buffer.toByteArray() + } + + length = if (content.length() >= 0) headers.size + content.length() else -1 + } + + override fun closeAsync(): CompletableFuture { + return content.closeAsync() + } + + override fun close() { + content.close() + } + + } + + private enum class State { + FIRST_BOUNDARY, HEADERS, CONTENT, MIDDLE_BOUNDARY, LAST_BOUNDARY, COMPLETE + } +} + +sealed class MultiPartProviderMessage + +data class GenerateMultiPart( + val byteBuffer: ByteBuffer, val future: CompletableFuture +) : MultiPartProviderMessage() + +object EndMultiPartProvider : MultiPartProviderMessage() \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/StringContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/StringContentProvider.kt new file mode 100644 index 000000000..bd599f3c6 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/content/provider/StringContentProvider.kt @@ -0,0 +1,9 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.client.HttpClientContentProvider +import com.fireflysource.net.http.common.content.provider.AbstractByteBufferContentProvider +import java.nio.charset.Charset + +class StringContentProvider(val content: String, val charset: Charset) : + AbstractByteBufferContentProvider(BufferUtils.toBuffer(content, charset)), HttpClientContentProvider \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/exception/HttpTunnelHandshakeException.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/exception/HttpTunnelHandshakeException.kt new file mode 100644 index 000000000..fa5e4cf6a --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/exception/HttpTunnelHandshakeException.kt @@ -0,0 +1,3 @@ +package com.fireflysource.net.http.client.impl.exception + +class HttpTunnelHandshakeException(message: String) : IllegalStateException(message) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/exception/UnhandledRequestException.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/exception/UnhandledRequestException.kt new file mode 100644 index 000000000..86e3eb69a --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/client/impl/exception/UnhandledRequestException.kt @@ -0,0 +1,6 @@ +package com.fireflysource.net.http.client.impl.exception + +/** + * @author Pengtao Qiu + */ +class UnhandledRequestException(message: String) : IllegalStateException(message) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/handler/AbstractByteBufferContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/handler/AbstractByteBufferContentHandler.kt new file mode 100644 index 000000000..cbead7f82 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/handler/AbstractByteBufferContentHandler.kt @@ -0,0 +1,58 @@ +package com.fireflysource.net.http.common.content.handler + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.codec.ContentEncoded +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.ContentEncoding +import com.fireflysource.net.http.common.model.HttpStatus +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.CompletableFuture + +abstract class AbstractByteBufferContentHandler( + private val maxRequestBodySize: Long = 200 * 1024 * 1024 +) : HttpContentHandler { + + private val byteBufferList = LinkedList() + private var requestSize: Long = 0 + private val utf8String: String by lazy { toString(StandardCharsets.UTF_8) } + + override fun accept(byteBuffer: ByteBuffer, u: T) { + requestSize += byteBuffer.remaining() + if (requestSize > maxRequestBodySize) { + throw BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413) + } + + byteBufferList.add(byteBuffer) + } + + override fun closeAsync(): CompletableFuture = Result.DONE + + override fun close() { + } + + fun getByteBuffers(encoding: Optional = Optional.empty()): List { + return if (encoding.isPresent) { + val data = decode(encoding.get()) + listOf(ByteBuffer.wrap(data)) + } else byteBufferList.map { it.duplicate() } + } + + fun toString(charset: Charset, encoding: Optional = Optional.empty()): String { + return if (encoding.isPresent) { + val data = decode(encoding.get()) + String(data, charset) + } else BufferUtils.toString(byteBufferList.map { it.duplicate() }, charset) + } + + private fun decode(encoding: ContentEncoding): ByteArray { + val buffer = BufferUtils.merge(byteBufferList.map { it.duplicate() }) + val bytes = BufferUtils.toArray(buffer) + return ContentEncoded.decode(bytes, encoding) + } + + override fun toString(): String = utf8String +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/handler/AbstractFileContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/handler/AbstractFileContentHandler.kt new file mode 100644 index 000000000..175e92c0d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/handler/AbstractFileContentHandler.kt @@ -0,0 +1,91 @@ +package com.fireflysource.net.http.common.content.handler + +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.common.io.closeAsync +import com.fireflysource.common.io.openFileChannelAsync +import com.fireflysource.common.io.writeAwait +import com.fireflysource.common.sys.SystemLogger +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.nio.ByteBuffer +import java.nio.file.OpenOption +import java.nio.file.Path +import java.util.concurrent.CompletableFuture + +abstract class AbstractFileContentHandler(val path: Path, vararg options: OpenOption) : HttpContentHandler { + + companion object { + private val log = SystemLogger.create(AbstractFileContentHandler::class.java) + } + + private val inputChannel: Channel = Channel(Channel.UNLIMITED) + private val scope: CoroutineScope = CoroutineScope(CoroutineName("Firefly-file-content-handler")) + private val writeJob: Job = scope.launch { + val fileChannel = openFileChannelAsync(path, *options).await() + var pos = 0L + + suspend fun closeFileChannel() { + try { + fileChannel.closeAsync().join() + } catch (e: Exception) { + log.error(e) { "close file channel exception." } + } + } + + suspend fun write(): Boolean { + var closed = false + when (val writeFileMessage = inputChannel.receive()) { + is WriteFileRequest -> { + val buf = writeFileMessage.buffer + flushDataLoop@ while (buf.hasRemaining()) { + val len = fileChannel.writeAwait(buf, pos) + if (len < 0) { + closeFileChannel() + closed = true + } + pos += len + } + } + is EndWriteFile -> { + closeFileChannel() + closed = true + } + } + return closed + } + + writeMessageLoop@ while (true) { + val closed = try { + write() + } catch (e: Exception) { + log.error(e) { "read file exception." } + closeFileChannel() + true + } + if (closed) break@writeMessageLoop + } + } + + + override fun accept(buffer: ByteBuffer, t: T) { + inputChannel.trySend(WriteFileRequest(buffer)) + } + + override fun closeAsync(): CompletableFuture = + scope.launch { closeAwait() }.asVoidFuture().thenAccept { scope.cancel() } + + override fun close() { + inputChannel.trySend(EndWriteFile) + } + + private suspend fun closeAwait() { + close() + writeJob.join() + } + +} + +sealed interface WriteFileMessage +@JvmInline +value class WriteFileRequest(val buffer: ByteBuffer) : WriteFileMessage +object EndWriteFile : WriteFileMessage diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/provider/AbstractByteBufferContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/provider/AbstractByteBufferContentProvider.kt new file mode 100644 index 000000000..6bda2525c --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/provider/AbstractByteBufferContentProvider.kt @@ -0,0 +1,53 @@ +package com.fireflysource.net.http.common.content.provider + +import com.fireflysource.common.io.InputChannel +import com.fireflysource.common.sys.Result +import java.nio.ByteBuffer +import java.util.concurrent.CompletableFuture +import kotlin.math.min + +abstract class AbstractByteBufferContentProvider(private val content: ByteBuffer) : InputChannel { + private val buffer = content.duplicate() + private val length = content.remaining().toLong() + private var open = true + + override fun isOpen(): Boolean = open + + override fun close() { + open = false + } + + open fun length(): Long = length + + open fun toByteBuffer(): ByteBuffer = buffer.duplicate() + + override fun read(byteBuffer: ByteBuffer): CompletableFuture { + if (!isOpen) { + return endStream() + } + + if (!content.hasRemaining()) { + return endStream() + } + + if (!byteBuffer.hasRemaining()) { + val future = CompletableFuture() + future.complete(0) + return future + } + + val len = min(content.remaining(), byteBuffer.remaining()) + val to = ByteArray(len) + content.get(to) + byteBuffer.put(to) + + val future = CompletableFuture() + future.complete(len) + return future + } + + override fun closeAsync(): CompletableFuture { + close() + return Result.DONE + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/provider/AbstractFileContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/provider/AbstractFileContentProvider.kt new file mode 100644 index 000000000..356546a43 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/content/provider/AbstractFileContentProvider.kt @@ -0,0 +1,108 @@ +package com.fireflysource.net.http.common.content.provider + +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.common.coroutine.clear +import com.fireflysource.common.exception.UnsupportedOperationException +import com.fireflysource.common.io.InputChannel +import com.fireflysource.common.io.closeAsync +import com.fireflysource.common.io.openFileChannelAsync +import com.fireflysource.common.io.readAwait +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.nio.ByteBuffer +import java.nio.file.Files +import java.nio.file.OpenOption +import java.nio.file.Path +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean + +abstract class AbstractFileContentProvider( + val path: Path, + val options: Set, + var position: Long, + val length: Long, + val scope: CoroutineScope = CoroutineScope(CoroutineName("Firefly-file-content-provider")) +) : InputChannel { + + private val readChannel: Channel = Channel(Channel.UNLIMITED) + private val readJob: Job + private val closed = AtomicBoolean(false) + private val lastPosition = position + length + + constructor(path: Path, vararg options: OpenOption) : this(path, options.toSet(), 0, Files.size(path)) + + init { + readJob = scope.launch { + val fileChannel = openFileChannelAsync(path, options).await() + + readMessageLoop@ while (true) { + when (val readFileMessage = readChannel.receive()) { + is ReadFileRequest -> { + val (buf, future) = readFileMessage + + suspend fun endRead() { + fileChannel.closeAsync().join() + closed.set(true) + future.complete(-1) + } + + if (position >= lastPosition) { + endRead() + break@readMessageLoop + } + try { + val len = fileChannel.readAwait(buf, position) + if (len < 0) { + endRead() + break@readMessageLoop + } else { + position += len + future.complete(len) + } + } catch (e: Exception) { + future.completeExceptionally(e) + } + } + is EndReadFile -> { + fileChannel.closeAsync().join() + closed.set(true) + break@readMessageLoop + } + } + } + + readChannel.clear() + } + } + + fun length(): Long = length + + override fun isOpen(): Boolean = !closed.get() + + fun toByteBuffer(): ByteBuffer { + throw UnsupportedOperationException("The file content does not support this method") + } + + override fun closeAsync(): CompletableFuture = + scope.launch { closeAwait() }.asVoidFuture().thenAccept { scope.cancel() } + + override fun close() { + if (isOpen) readChannel.trySend(EndReadFile) + } + + private suspend fun closeAwait() { + close() + readJob.join() + } + + override fun read(byteBuffer: ByteBuffer): CompletableFuture { + val future = CompletableFuture() + readChannel.trySend(ReadFileRequest(byteBuffer, future)) + return future + } + +} + +sealed class ReadFileMessage +data class ReadFileRequest(val buffer: ByteBuffer, val future: CompletableFuture) : ReadFileMessage() +object EndReadFile : ReadFileMessage() \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/Http1GeneratingResultException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/Http1GeneratingResultException.java new file mode 100644 index 000000000..58422a173 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/Http1GeneratingResultException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class Http1GeneratingResultException extends RuntimeException { + + public Http1GeneratingResultException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/Http2StreamFrameProcessException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/Http2StreamFrameProcessException.java new file mode 100644 index 000000000..56fc218b5 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/Http2StreamFrameProcessException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class Http2StreamFrameProcessException extends RuntimeException { + + public Http2StreamFrameProcessException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/HttpServerConnectionListenerNotSetException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/HttpServerConnectionListenerNotSetException.java new file mode 100644 index 000000000..2c1bcd256 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/HttpServerConnectionListenerNotSetException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class HttpServerConnectionListenerNotSetException extends RuntimeException { + + public HttpServerConnectionListenerNotSetException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/HttpServerResponseNotCommitException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/HttpServerResponseNotCommitException.java new file mode 100644 index 000000000..a0acaeca3 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/HttpServerResponseNotCommitException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class HttpServerResponseNotCommitException extends RuntimeException { + + public HttpServerResponseNotCommitException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/MissingRemoteHostException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/MissingRemoteHostException.java new file mode 100644 index 000000000..931ec8ff6 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/MissingRemoteHostException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class MissingRemoteHostException extends RuntimeException { + + public MissingRemoteHostException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/MissingRemotePortException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/MissingRemotePortException.java new file mode 100644 index 000000000..0be296ecb --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/MissingRemotePortException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class MissingRemotePortException extends RuntimeException { + + public MissingRemotePortException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/NotSupportHttpVersionException.java b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/NotSupportHttpVersionException.java new file mode 100644 index 000000000..4df3a1139 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/exception/NotSupportHttpVersionException.java @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.common.exception; + +public class NotSupportHttpVersionException extends RuntimeException { + + public NotSupportHttpVersionException(String message) { + super(message); + } +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/model/HttpFieldsExtension.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/model/HttpFieldsExtension.kt new file mode 100644 index 000000000..7faed74c3 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/model/HttpFieldsExtension.kt @@ -0,0 +1,17 @@ +package com.fireflysource.net.http.common.model + +import com.fireflysource.net.http.common.model.HttpHeader.CONNECTION +import com.fireflysource.net.http.common.model.HttpHeader.EXPECT +import com.fireflysource.net.http.common.model.HttpHeaderValue.CLOSE +import com.fireflysource.net.http.common.model.HttpHeaderValue.CONTINUE +import com.fireflysource.net.http.common.model.HttpVersion.HTTP_0_9 +import com.fireflysource.net.http.common.model.HttpVersion.HTTP_1_0 + +fun HttpFields.expectServerAcceptsContent(): Boolean { + return this.contains(EXPECT, CONTINUE.value) +} + +fun HttpFields.isCloseConnection(version: HttpVersion): Boolean = when (version) { + HTTP_0_9, HTTP_1_0 -> !this.contains(CONNECTION, HttpHeaderValue.KEEP_ALIVE.value) + else -> this.contains(CONNECTION, CLOSE.value) +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v1/decoder/HttpParserExtension.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v1/decoder/HttpParserExtension.kt new file mode 100644 index 000000000..17bde44e9 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v1/decoder/HttpParserExtension.kt @@ -0,0 +1,25 @@ +package com.fireflysource.net.http.common.v1.decoder + +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.tcp.TcpConnection +import kotlinx.coroutines.future.await +import java.nio.ByteBuffer + +suspend fun HttpParser.parseAll(tcpConnection: TcpConnection): ByteBuffer? { + var lastBuffer: ByteBuffer? = null + readLoop@ while (!isState(HttpParser.State.END)) { + val buffer = tcpConnection.read().await() + lastBuffer = buffer + parseLoop@ while (buffer.remaining() > 0) { + val beforeRemaining = buffer.remaining() + val exit = this.parseNext(buffer) + val afterRemaining = buffer.remaining() + when { + exit -> break@readLoop + isState(HttpParser.State.END) -> break@readLoop + beforeRemaining == afterRemaining -> throw BadMessageException("The received data cannot be consumed") + } + } + } + return lastBuffer +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorExtension.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorExtension.kt new file mode 100644 index 000000000..0ee6fda97 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorExtension.kt @@ -0,0 +1,15 @@ +package com.fireflysource.net.http.common.v1.encoder + +import com.fireflysource.net.http.common.exception.Http1GeneratingResultException + +fun HttpGenerator.Result.assert(expectResult: HttpGenerator.Result) { + if (this != expectResult) { + throw Http1GeneratingResultException("The HTTP generator result is $this, but expect $expectResult") + } +} + +fun HttpGenerator.Result.assert(expectResults: Set) { + if (!expectResults.contains(this)) { + throw Http1GeneratingResultException("The HTTP generator result is $this, but expect $expectResults") + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AbstractFlowControlStrategy.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AbstractFlowControlStrategy.kt new file mode 100644 index 000000000..687df9dbe --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AbstractFlowControlStrategy.kt @@ -0,0 +1,95 @@ +package com.fireflysource.net.http.common.v2.stream + +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.HttpConfig.DEFAULT_WINDOW_SIZE +import com.fireflysource.net.http.common.v2.frame.WindowUpdateFrame + +abstract class AbstractFlowControlStrategy( + protected var initialStreamRecvWindow: Int = DEFAULT_WINDOW_SIZE +) : FlowControl { + + companion object { + private val log = SystemLogger.create(AbstractFlowControlStrategy::class.java) + } + + private var initialStreamSendWindow: Int = DEFAULT_WINDOW_SIZE + + override fun onStreamCreated(stream: Stream) { + val http2Stream = stream as AsyncHttp2Stream + http2Stream.updateSendWindow(initialStreamSendWindow) + http2Stream.updateRecvWindow(initialStreamRecvWindow) + } + + override fun onStreamDestroyed(stream: Stream) { + } + + override fun updateInitialStreamWindow( + http2Connection: Http2Connection, + initialStreamWindow: Int, + local: Boolean + ) { + val previousInitialStreamWindow: Int + if (local) { + previousInitialStreamWindow = initialStreamRecvWindow + initialStreamRecvWindow = initialStreamWindow + } else { + previousInitialStreamWindow = initialStreamSendWindow + initialStreamSendWindow = initialStreamWindow + } + + val delta = initialStreamWindow - previousInitialStreamWindow + if (delta == 0) return + + http2Connection.streams.forEach { stream -> + if (local) { + val http2Stream = stream as AsyncHttp2Stream + http2Stream.updateRecvWindow(delta) + log.debug { "Updated initial stream recv window $previousInitialStreamWindow -> $initialStreamWindow for $http2Stream" } + } else { + (http2Connection as AsyncHttp2Connection).onWindowUpdate(stream, WindowUpdateFrame(stream.id, delta)) + } + } + } + + override fun onWindowUpdate(http2Connection: Http2Connection, stream: Stream?, frame: WindowUpdateFrame) { + if (frame.isStreamWindowUpdate) { + // The stream may have been removed concurrently. + if (stream != null && stream is AsyncHttp2Stream) { + stream.updateSendWindow(frame.windowDelta) + } + } else { + (http2Connection as AsyncHttp2Connection).updateSendWindow(frame.windowDelta) + } + } + + override fun onDataReceived(http2Connection: Http2Connection, stream: Stream?, length: Int) { + val connection = http2Connection as AsyncHttp2Connection + var oldSize: Int = connection.updateRecvWindow(-length) + log.debug { "Data received, $length bytes, updated session recv window $oldSize -> ${oldSize - length} for $connection" } + + if (stream != null && stream is AsyncHttp2Stream) { + oldSize = stream.updateRecvWindow(-length) + log.debug { "Data received, $length bytes, updated stream recv window $oldSize -> ${oldSize - length} for $stream" } + } + } + + override fun onDataSending(stream: Stream, length: Int) { + if (length == 0) return + + val http2Stream = stream as AsyncHttp2Stream + val connection = http2Stream.http2Connection as AsyncHttp2Connection + val oldSessionWindow = connection.updateSendWindow(-length) + val newSessionWindow = oldSessionWindow - length + log.debug { "Sending, session send window $oldSessionWindow -> $newSessionWindow for $connection" } + + val oldStreamWindow = http2Stream.updateSendWindow(-length) + val newStreamWindow = oldStreamWindow - length + log.debug { "Sending, stream send window $oldStreamWindow -> $newStreamWindow for $stream" } + } + + override fun onDataSent(stream: Stream, length: Int) { + } + + override fun windowUpdate(http2Connection: Http2Connection, stream: Stream?, frame: WindowUpdateFrame) { + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AsyncHttp2Connection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AsyncHttp2Connection.kt new file mode 100644 index 000000000..7a5f8bc1b --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AsyncHttp2Connection.kt @@ -0,0 +1,943 @@ +package com.fireflysource.net.http.common.v2.stream + +import com.fireflysource.common.concurrent.AtomicBiInteger +import com.fireflysource.common.concurrent.Atomics +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.math.MathUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.* +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.Connection +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.TcpBasedHttpConnection +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.HttpVersion +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v2.decoder.Parser +import com.fireflysource.net.http.common.v2.encoder.Generator +import com.fireflysource.net.http.common.v2.frame.* +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpCoroutineDispatcher +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Consumer +import kotlin.math.min + +abstract class AsyncHttp2Connection( + private val initStreamId: Int, + val config: HttpConfig, + private val tcpConnection: TcpConnection, + private val flowControl: FlowControl, + private val listener: Http2Connection.Listener +) : Connection by tcpConnection, TcpCoroutineDispatcher by tcpConnection, Http2Connection, TcpBasedHttpConnection, + Parser.Listener { + + companion object { + private val log = SystemLogger.create(AsyncHttp2Connection::class.java) + val defaultHttp2ConnectionListener = object : Http2Connection.Listener.Adapter() { + override fun onReset(http2Connection: Http2Connection, frame: ResetFrame) { + log.info { "HTTP2 connection received reset frame. $frame" } + } + + override fun onFailure(http2Connection: Http2Connection, e: Throwable) { + log.error(e) { "HTTP2 connection exception. ${http2Connection.id}" } + } + } + } + + private val localStreamId = AtomicInteger(initStreamId) + private val http2StreamMap = ConcurrentHashMap() + private val localStreamCount = AtomicInteger() + private val remoteStreamCount = AtomicBiInteger() + private val lastRemoteStreamId = AtomicInteger() + private val closeState = AtomicReference(CloseState.NOT_CLOSED) + + private val sendWindow = AtomicInteger(HttpConfig.DEFAULT_WINDOW_SIZE) + private val recvWindow = AtomicInteger(HttpConfig.DEFAULT_WINDOW_SIZE) + + private val generator = Generator(config.maxDynamicTableSize, config.maxHeaderBlockFragment) + + private var maxLocalStreams: Int = -1 + private var maxRemoteStreams: Int = -1 + private var streamIdleTimeout: Long = config.streamIdleTimeout + private var pushEnabled: Boolean = false + private var closeFrame: GoAwayFrame? = null + protected val initialSessionRecvWindow: Int = config.initialSessionRecvWindow + + private val flusher = FrameEntryFlusher() + + init { + tcpConnection.onClose { clearStreams() } + } + + private inner class FrameEntryFlusher { + + private val frameEntryChannel = Channel(Channel.UNLIMITED) + + init { + launchEntryFlushJob() + } + + private fun launchEntryFlushJob() = tcpConnection.coroutineScope.launch { + while (true) { + when (val frameEntry = frameEntryChannel.receive()) { + is ControlFrameEntry -> flushControlFrameEntry(frameEntry) + is DataFrameEntry -> flushOrStashDataFrameEntry(frameEntry) + is OnWindowUpdateMessage -> onWindowUpdateMessage(frameEntry) + } + } + }.invokeOnCompletion { terminate() } + + private suspend fun flushOrStashDataFrameEntry(frameEntry: DataFrameEntry) { + try { + val http2Stream = frameEntry.stream as AsyncHttp2Stream + val isEmpty = http2Stream.flushStashedDataFrameEntries() + if (isEmpty) { + val success = flushDataFrame(frameEntry) + if (!success) { + http2Stream.stashFrameEntry(frameEntry) + val dataRemaining = frameEntry.dataRemaining + log.debug { "Send data frame failure. Stash a data frame. remaining: $dataRemaining, stream: $http2Stream" } + } + } else { + http2Stream.stashFrameEntry(frameEntry) + log.debug { "Stashed data frame is not empty. Stash a data frame. stream: $http2Stream" } + } + } catch (e: Exception) { + log.error { "flush data frame exception. ${e.javaClass.name} ${e.message}" } + frameEntry.result.accept(createFailedResult(-1L, e)) + } + } + + private fun AsyncHttp2Stream.stashFrameEntry(frameEntry: DataFrameEntry) { + this.stashedDataFrames.offer(frameEntry) + } + + private suspend fun AsyncHttp2Stream.flushStashedDataFrameEntries(): Boolean { + val stashedDataFrames = this.stashedDataFrames + flush@ while (stashedDataFrames.isNotEmpty()) { + val stashedFrameEntry = stashedDataFrames.peek() + if (stashedFrameEntry != null) { + val success = flushDataFrame(stashedFrameEntry) + if (success) { + val entry = stashedDataFrames.poll() + val dataRemaining = entry.dataRemaining + val writtenBytes = entry.writtenBytes + log.debug { "Poll a stashed data frame. remaining: ${dataRemaining}, written: $writtenBytes" } + } else break@flush + } else break@flush + } + return stashedDataFrames.isEmpty() + } + + private suspend fun flushDataFrame(frameEntry: DataFrameEntry): Boolean { + val dataFrame = frameEntry.frame + val stream = frameEntry.stream as AsyncHttp2Stream + val dataRemaining = frameEntry.dataRemaining + + val connectionSendWindow = getSendWindow() + val streamSendWindow = stream.getSendWindow() + val window = min(streamSendWindow, connectionSendWindow) + + log.debug { "Flush data frame. window: $window, remaining: $dataRemaining" } + if (window <= 0 && dataRemaining > 0) { + log.debug { "The sending window not enough. stream: $stream" } + return false + } + + val length = min(dataRemaining, window) + val frameBytes = generator.data(dataFrame, length) + val dataLength = frameBytes.dataLength + log.debug { "Before flush data frame. window: $window, remaining: $dataRemaining, dataLength: $dataLength" } + + flowControl.onDataSending(stream, dataLength) + stream.updateClose(dataFrame.isEndStream, CloseState.Event.BEFORE_SEND) + + val writtenBytes = writeAndFlush(frameBytes.byteBuffers, dataFrame) + frameEntry.dataRemaining -= dataLength + frameEntry.writtenBytes += writtenBytes + + flowControl.onDataSent(stream, dataLength) + val currentRemaining = frameEntry.dataRemaining + log.debug { "After flush data frame. window: $window, remaining: $currentRemaining, dataLength: $dataLength" } + + return if (currentRemaining == 0) { + // Only now we can update the close state and eventually remove the stream. + if (stream.updateClose(dataFrame.isEndStream, CloseState.Event.AFTER_SEND)) { + removeStream(stream) + } + frameEntry.result.accept(Result(true, frameEntry.writtenBytes, null)) + log.debug { "Flush all data success. stream: $stream" } + true + } else { + log.debug { "Flush data success. stream: $stream, remaining: $currentRemaining" } + false + } + } + + fun onWindowUpdate(stream: Stream?, frame: WindowUpdateFrame) { + frameEntryChannel.trySend(OnWindowUpdateMessage(stream, frame)) + } + + private suspend fun onWindowUpdateMessage(onWindowUpdateMessage: OnWindowUpdateMessage) { + val (stream, frame) = onWindowUpdateMessage + flowControl.onWindowUpdate(this@AsyncHttp2Connection, stream, frame) + if (stream != null) { + val http2Stream = stream as AsyncHttp2Stream + log.debug { "Flush stream stashed data frames. stream: $http2Stream" } + http2Stream.flushStashedDataFrameEntries() + } else { + if (frame.isSessionWindowUpdate) { + log.debug { "Flush all streams stashed data frames. id: ${tcpConnection.id}" } + streams.map { it as AsyncHttp2Stream }.forEach { it.flushStashedDataFrameEntries() } + } + } + log.debug { "Update send window and flush stashed data frame success. frame: $frame" } + } + + private suspend fun flushControlFrameEntry(frameEntry: ControlFrameEntry) { + try { + val length = flushControlFrames(frameEntry) + frameEntry.result.accept(Result(true, length, null)) + } catch (e: Exception) { + frameEntry.result.accept(createFailedResult(-1, e)) + } + } + + private suspend fun flushControlFrames(frameEntry: ControlFrameEntry): Long { + val stream = frameEntry.stream + var writtenBytes = 0L + frameLoop@ for (frame in frameEntry.frames) { + when (frame.type) { + FrameType.HEADERS -> { + val headersFrame = frame as HeadersFrame + if (stream != null && stream is AsyncHttp2Stream) { + stream.updateClose(headersFrame.isEndStream, CloseState.Event.BEFORE_SEND) + } + } + FrameType.SETTINGS -> { + val settingsFrame = frame as SettingsFrame + val initialWindow = settingsFrame.settings[SettingsFrame.INITIAL_WINDOW_SIZE] + if (initialWindow != null) { + flowControl.updateInitialStreamWindow(this@AsyncHttp2Connection, initialWindow, true) + } + } + FrameType.DISCONNECT -> { + terminate() + break@frameLoop + } + else -> { + // ignore the other control frame types + } + } + + val byteBuffers = generator.control(frame).byteBuffers + val bytes = writeAndFlush(byteBuffers, frame) + writtenBytes += bytes + + when (frame.type) { + FrameType.HEADERS -> { + val headersFrame = frame as HeadersFrame + if (stream != null && stream is AsyncHttp2Stream) { + onStreamOpened(stream) + if (stream.updateClose(headersFrame.isEndStream, CloseState.Event.AFTER_SEND)) { + removeStream(stream) + } + } + } + FrameType.RST_STREAM -> { + if (stream != null && stream is AsyncHttp2Stream) { + stream.close() + removeStream(stream) + } + } + FrameType.PUSH_PROMISE -> { + if (stream != null && stream is AsyncHttp2Stream) { + stream.updateClose(true, CloseState.Event.RECEIVED) + } + } + FrameType.GO_AWAY -> { + tcpConnection.close { + log.info { "Send go away frame and close TCP connection success" } + } + } + FrameType.WINDOW_UPDATE -> { + flowControl.windowUpdate(this@AsyncHttp2Connection, stream, frame as WindowUpdateFrame) + } + else -> { + // ignore the other control frame types + } + } + } + + return writtenBytes + } + + private suspend fun writeAndFlush(byteBuffers: List, frame: Frame): Long { + val flush = when (frame) { + is HeadersFrame -> { + if (frame.isEndStream) true + else { + val metaData = frame.metaData + metaData is MetaData.Response && metaData.status == HttpStatus.CONTINUE_100 + } + } + is DataFrame -> frame.isEndStream + else -> true + } + return if (flush) tcpConnection.writeAndFlush(byteBuffers, 0, byteBuffers.size).await() + else tcpConnection.write(byteBuffers, 0, byteBuffers.size).await() + } + + fun sendControlFrame(stream: Stream?, vararg frames: Frame): CompletableFuture { + val future = CompletableFuture() + frameEntryChannel.trySend(ControlFrameEntry(stream, arrayOf(*frames), futureToConsumer(future))) + return future + } + + fun sendDataFrame(stream: Stream, frame: DataFrame): CompletableFuture { + val future = CompletableFuture() + frameEntryChannel.trySend(DataFrameEntry(stream, frame, futureToConsumer(future))) + return future + } + } + + fun sendControlFrame(stream: Stream?, vararg frames: Frame): CompletableFuture = + flusher.sendControlFrame(stream, *frames) + + fun launchParserJob(parser: Parser) = tcpConnection.coroutineScope.launch { + recvLoop@ while (true) { + val buffer = try { + tcpConnection.read().await() + } catch (e: Exception) { + break@recvLoop + } + parsingLoop@ while (buffer.hasRemaining()) { + parser.parse(buffer) + } + } + } + + override fun isSecureConnection(): Boolean = tcpConnection.isSecureConnection + + override fun getHttpVersion(): HttpVersion = HttpVersion.HTTP_2 + + override fun getTcpConnection(): TcpConnection = tcpConnection + + + fun notifyPreface(): MutableMap { + return try { + val settings = listener.onPreface(this) ?: newDefaultSettings() + val initialWindowSize = settings[SettingsFrame.INITIAL_WINDOW_SIZE] ?: config.initialStreamRecvWindow + flowControl.updateInitialStreamWindow(this, initialWindowSize, true) + settings + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + newDefaultSettings() + } + } + + private fun newDefaultSettings(): MutableMap { + val settings = HashMap(SettingsFrame.DEFAULT_SETTINGS_FRAME.settings) + settings[SettingsFrame.INITIAL_WINDOW_SIZE] = config.initialStreamRecvWindow + settings[SettingsFrame.MAX_CONCURRENT_STREAMS] = config.maxConcurrentStreams + return settings + } + + // stream management + override fun getStreams(): MutableCollection = http2StreamMap.values + + override fun getStream(streamId: Int): Stream? = http2StreamMap[streamId] + + override fun newStream(headersFrame: HeadersFrame, promise: Consumer>, listener: Stream.Listener) { + try { + val frameStreamId = headersFrame.streamId + if (frameStreamId == 0) { + val newHeadersFrame = copyHeadersFrameAndSetCurrentStreamId(headersFrame) + val stream = createLocalStream(newHeadersFrame.streamId, listener) + sendNewHeadersFrame(stream, newHeadersFrame, promise) + } else { + val stream = createLocalStream(frameStreamId, listener) + sendNewHeadersFrame(stream, headersFrame, promise) + } + } catch (e: Exception) { + promise.accept(Result(false, null, e)) + } + } + + private fun copyHeadersFrameAndSetCurrentStreamId(headersFrame: HeadersFrame): HeadersFrame { + val nextStreamId = getNextStreamId() + val priority = headersFrame.priority + val priorityFrame = if (priority != null) { + PriorityFrame(nextStreamId, priority.parentStreamId, priority.weight, priority.isExclusive) + } else null + return HeadersFrame(nextStreamId, headersFrame.metaData, priorityFrame, headersFrame.isEndStream) + } + + private fun sendNewHeadersFrame(stream: Stream, newHeadersFrame: HeadersFrame, promise: Consumer>) { + sendControlFrame(stream, newHeadersFrame) + .thenAccept { promise.accept(Result(true, stream, null)) } + .exceptionallyAccept { promise.accept(createFailedResult(null, it)) } + } + + fun removeStream(stream: Stream) { + val removed = http2StreamMap.remove(stream.id) + if (removed != null) { + onStreamClosed(stream) + flowControl.onStreamDestroyed(stream) + log.debug { "Removed $stream" } + } + } + + protected fun getNextStreamId(): Int = getAndIncreaseStreamId(localStreamId, initStreamId) + + private fun getCurrentLocalStreamId(): Int = localStreamId.get() + + protected fun createLocalStream(streamId: Int, listener: Stream.Listener): Stream { + checkMaxLocalStreams() + val stream = AsyncHttp2Stream(this, streamId, true, listener) + if (http2StreamMap.putIfAbsent(streamId, stream) == null) { + stream.idleTimeout = streamIdleTimeout + flowControl.onStreamCreated(stream) // TODO before preface + log.debug { "Created local $stream" } + return stream + } else { + localStreamCount.decrementAndGet() + throw IllegalStateException("Duplicate stream $streamId") + } + } + + private fun checkMaxLocalStreams() { + val maxCount = maxLocalStreams + if (maxCount > 0) { + while (true) { + val localCount = localStreamCount.get() + if (localCount >= maxCount) { + throw IllegalStateException("Max local stream count $localCount exceeded $maxCount") + } + if (localStreamCount.compareAndSet(localCount, localCount + 1)) { + break + } + } + } else { + localStreamCount.incrementAndGet() + } + } + + protected fun createRemoteStream(streamId: Int): Stream? { + // SPEC: exceeding max concurrent streams treated as stream error. + if (!checkMaxRemoteStreams(streamId)) { + return null + } + val stream: Stream = AsyncHttp2Stream(this, streamId, false) + // SPEC: duplicate stream treated as connection error. + return if (http2StreamMap.putIfAbsent(streamId, stream) == null) { + updateLastRemoteStreamId(streamId) + stream.idleTimeout = streamIdleTimeout + flowControl.onStreamCreated(stream) + log.debug { "Created remote $stream" } + stream + } else { + remoteStreamCount.addAndGetHi(-1) + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream") + null + } + } + + private fun checkMaxRemoteStreams(streamId: Int): Boolean { + while (true) { + val encoded = remoteStreamCount.get() + val remoteCount = AtomicBiInteger.getHi(encoded) + val remoteClosing = AtomicBiInteger.getLo(encoded) + val maxCount: Int = maxRemoteStreams + if (maxCount >= 0 && remoteCount - remoteClosing >= maxCount) { + reset(ResetFrame(streamId, ErrorCode.REFUSED_STREAM_ERROR.code), discard()) + return false + } + if (remoteStreamCount.compareAndSet(encoded, remoteCount + 1, remoteClosing)) { + break + } + } + return true + } + + protected open fun onStreamOpened(stream: Stream) {} + + protected open fun onStreamClosed(stream: Stream) {} + + protected open fun notifyNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + return try { + listener.onNewStream(stream, frame) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + AsyncHttp2Stream.defaultStreamListener + } + } + + private fun updateLastRemoteStreamId(streamId: Int) { + Atomics.updateMax(lastRemoteStreamId, streamId) + } + + fun updateStreamCount(local: Boolean, deltaStreams: Int, deltaClosing: Int) { + if (local) { + localStreamCount.addAndGet(deltaStreams) + } else { + remoteStreamCount.add(deltaStreams, deltaClosing) + } + } + + fun isClientStream(streamId: Int) = (streamId and 1 == 1) + + private fun getLastRemoteStreamId(): Int { + return lastRemoteStreamId.get() + } + + protected fun isLocalStreamClosed(streamId: Int): Boolean { + return streamId <= getCurrentLocalStreamId() + } + + protected fun isRemoteStreamClosed(streamId: Int): Boolean { + return streamId <= getLastRemoteStreamId() + } + + override fun onStreamFailure(streamId: Int, error: Int, reason: String) { + val stream = getStream(streamId) + if (stream != null && stream is AsyncHttp2Stream) { + stream.process(FailureFrame(error, reason), discard()) + } else { + reset(ResetFrame(streamId, error), discard()) + } + } + + + // reset frame + override fun onReset(frame: ResetFrame) { + log.debug { "Received $frame" } + val stream = getStream(frame.streamId) + if (stream != null && stream is AsyncHttp2Stream) { + stream.process(frame, discard()) + } else { + onResetForUnknownStream(frame) + } + } + + protected fun notifyReset(http2Connection: Http2Connection, frame: ResetFrame) { + try { + listener.onReset(http2Connection, frame) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + protected abstract fun onResetForUnknownStream(frame: ResetFrame) + + protected fun reset(frame: ResetFrame, result: Consumer>) { + sendControlFrame(getStream(frame.streamId), frame) + .thenAccept { result.accept(SUCCESS) } + .exceptionallyAccept { result.accept(createFailedResult(it)) } + } + + + // headers frame + abstract override fun onHeaders(frame: HeadersFrame) + + protected fun notifyHeaders(stream: AsyncHttp2Stream, frame: HeadersFrame) { + try { + stream.listener.onHeaders(stream, frame) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + fun push(frame: PushPromiseFrame, promise: Consumer>, listener: Stream.Listener) { + val promiseStreamId = getNextStreamId() + val pushStream = createLocalStream(promiseStreamId, listener) + val pushPromiseFrame = PushPromiseFrame(frame.streamId, promiseStreamId, frame.metaData) + sendControlFrame(pushStream, pushPromiseFrame) + .thenAccept { promise.accept(Result(true, pushStream, null)) } + .exceptionallyAccept { promise.accept(Result(false, null, it)) } + } + + + // data frame + override fun onData(frame: DataFrame) { + onData(frame, discard()) + } + + private fun onData(frame: DataFrame, result: Consumer>) { + log.debug { "Received $frame" } + val streamId = frame.streamId + val stream = getStream(streamId) + + // SPEC: the session window must be updated even if the stream is null. + // The flow control length includes the padding bytes. + val flowControlLength = frame.remaining() + frame.padding() + flowControl.onDataReceived(this, stream, flowControlLength) + + if (stream != null) { + if (getRecvWindow() < 0) { + onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "session_window_exceeded", result) + } else { + val dataResult = Consumer> { r -> + flowControl.onDataConsumed(this@AsyncHttp2Connection, stream, flowControlLength) + result.accept(r) + } + val http2Stream = stream as AsyncHttp2Stream + http2Stream.process(frame, dataResult) + } + } else { + log.debug("Stream #{} not found", streamId) + // We must enlarge the session flow control window, + // otherwise, the other requests will be stalled. + flowControl.onDataConsumed(this, null, flowControlLength) + val local = (streamId and 1) == (getCurrentLocalStreamId() and 1) + val closed = if (local) isLocalStreamClosed(streamId) else isRemoteStreamClosed(streamId) + if (closed) reset(ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR.code), result) + else onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_data_frame", result) + } + } + + fun sendDataFrame(stream: Stream, frame: DataFrame) = flusher.sendDataFrame(stream, frame) + + + // priority frame + override fun priority(frame: PriorityFrame, result: Consumer>): Int { + val stream = http2StreamMap[frame.streamId] + return if (stream == null) { + val newStreamId = getNextStreamId() + val newFrame = PriorityFrame(newStreamId, frame.parentStreamId, frame.weight, frame.isExclusive) + sendControlFrame(null, newFrame) + .thenAccept { result.accept(SUCCESS) } + .exceptionallyAccept { result.accept(createFailedResult(it)) } + newStreamId + } else { + sendControlFrame(stream, frame) + .thenAccept { result.accept(SUCCESS) } + .exceptionallyAccept { result.accept(createFailedResult(it)) } + stream.id + } + } + + override fun onPriority(frame: PriorityFrame) { + log.debug { "Received $frame" } + } + + + // settings frame + override fun settings(frame: SettingsFrame, result: Consumer>) { + sendControlFrame(null, frame) + .thenAccept { result.accept(SUCCESS) } + .exceptionallyAccept { result.accept(createFailedResult(it)) } + } + + override fun onSettings(frame: SettingsFrame) { + // SPEC: SETTINGS frame MUST be replied. + onSettings(frame, true) + } + + fun onSettings(frame: SettingsFrame, reply: Boolean) { + log.debug { "received frame: $frame" } + if (frame.isReply) { + return + } + + frame.settings.forEach { (key, value) -> + when (key) { + SettingsFrame.HEADER_TABLE_SIZE -> { + log.debug { "Updating HPACK header table size to $value for $this" } + generator.setHeaderTableSize(value) + } + SettingsFrame.ENABLE_PUSH -> { + val enabled = value == 1 + log.debug { "${if (enabled) "Enabling" else "Disabling"} push for $this" } + pushEnabled = enabled + } + SettingsFrame.MAX_CONCURRENT_STREAMS -> { + log.debug { "Updating max local concurrent streams to $value for $this" } + maxLocalStreams = value + } + SettingsFrame.INITIAL_WINDOW_SIZE -> { + log.debug { "Updating initial window size to $value for $this" } + flowControl.updateInitialStreamWindow(this, value, false) + } + SettingsFrame.MAX_FRAME_SIZE -> { + log.debug { "Updating max frame size to $value for $this" } + generator.setMaxFrameSize(value) + } + SettingsFrame.MAX_HEADER_LIST_SIZE -> { + log.debug { "Updating max header list size to $value for $this" } + generator.setMaxHeaderListSize(value) + } + else -> { + log.debug { "Unknown setting $key:$value for $this" } + } + } + } + + notifySettings(this, frame) + + if (reply) { + val replyFrame = SettingsFrame(emptyMap(), true) + settings(replyFrame, discard()) + } + } + + private fun notifySettings(connection: Http2Connection, frame: SettingsFrame) { + try { + listener.onSettings(connection, frame) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + + // window update + override fun onWindowUpdate(frame: WindowUpdateFrame) { + log.debug { "Received $frame" } + val streamId = frame.streamId + val windowDelta = frame.windowDelta + if (frame.isStreamWindowUpdate) { + val stream = getStream(streamId) + if (stream != null && stream is AsyncHttp2Stream) { + val streamSendWindow: Int = stream.updateSendWindow(0) + if (MathUtils.sumOverflows(streamSendWindow, windowDelta)) { + reset(ResetFrame(streamId, ErrorCode.FLOW_CONTROL_ERROR.code), discard()) + } else { + stream.process(frame, discard()) + onWindowUpdate(stream, frame) + } + } else { + if (!isRemoteStreamClosed(streamId)) { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame") + } + } + } else { + val sessionSendWindow = getSendWindow() + if (MathUtils.sumOverflows(sessionSendWindow, windowDelta)) { + onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_flow_control_window") + } else { + onWindowUpdate(null, frame) + } + } + } + + fun onWindowUpdate(stream: Stream?, frame: WindowUpdateFrame) { + flusher.onWindowUpdate(stream, frame) + } + + fun updateRecvWindow(delta: Int): Int { + return recvWindow.getAndAdd(delta) + } + + fun updateSendWindow(delta: Int): Int { + return sendWindow.getAndAdd(delta) + } + + fun getSendWindow(): Int { + return sendWindow.get() + } + + fun getRecvWindow(): Int { + return recvWindow.get() + } + + + // ping frame + override fun ping(frame: PingFrame, result: Consumer>) { + if (frame.isReply) { + result.accept(createFailedResult(IllegalArgumentException("The reply must be false"))) + } else { + sendControlFrame(null, frame) + .thenAccept { result.accept(SUCCESS) } + .exceptionallyAccept { result.accept(createFailedResult(it)) } + } + } + + override fun onPing(frame: PingFrame) { + log.debug { "Received $frame" } + if (frame.isReply) { + notifyPing(this, frame) + } else { + sendControlFrame(null, PingFrame(frame.payload, true)) + } + } + + private fun notifyPing(connection: Http2Connection, frame: PingFrame) { + try { + listener.onPing(connection, frame) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + + // close connection + override fun close(error: Int, reason: String, result: Consumer>): Boolean { + while (true) { + when (val current: CloseState = closeState.get()) { + CloseState.NOT_CLOSED -> { + if (closeState.compareAndSet(current, CloseState.LOCALLY_CLOSED)) { + val goAwayFrame = newGoAwayFrame(CloseState.LOCALLY_CLOSED, error, reason) + closeFrame = goAwayFrame + sendControlFrame(null, goAwayFrame) + .thenAccept { result.accept(SUCCESS) } + .exceptionallyAccept { result.accept(createFailedResult(it)) } + return true + } + } + else -> { + log.debug { "Ignoring close $error/$reason, already closed" } + result.accept(SUCCESS) + return false + } + } + } + } + + suspend fun close(error: Int, reason: String): Boolean { + val future = CompletableFuture() + val success = close(error, reason, futureToConsumer(future)) + future.await() + return success + } + + private fun terminate() { + terminateLoop@ while (true) { + when (val current = closeState.get()) { + CloseState.NOT_CLOSED, CloseState.LOCALLY_CLOSED, CloseState.REMOTELY_CLOSED -> { + if (closeState.compareAndSet(current, CloseState.CLOSED)) { + clearStreams() + disconnect() + break@terminateLoop + } + } + else -> { + // ignore the other close states + break@terminateLoop + } + } + } + } + + private fun disconnect() { + log.debug { "Disconnecting $this" } + tcpConnection.close() + } + + private fun clearStreams() { + log.debug { "HTTP2 connection terminated. id: $id, stream size: ${http2StreamMap.size}" } + http2StreamMap.values.map { it as AsyncHttp2Stream }.forEach { + it.notifyTerminal(it) + it.close() + } + http2StreamMap.clear() + } + + override fun closeAsync(): CompletableFuture { + val future = CompletableFuture() + close(ErrorCode.NO_ERROR.code, "no_error", futureToConsumer(future)) + return future + } + + override fun close() { + closeAsync() + } + + + // go away frame + override fun onGoAway(frame: GoAwayFrame) { + log.debug { "Received $frame" } + closeLoop@ while (true) { + when (val current: CloseState = closeState.get()) { + CloseState.NOT_CLOSED -> { + if (closeState.compareAndSet(current, CloseState.REMOTELY_CLOSED)) { + // We received a GO_AWAY, so try to write what's in the queue and then disconnect. + closeFrame = frame + notifyClose(this, frame) { + val goAwayFrame = newGoAwayFrame(CloseState.CLOSED, ErrorCode.NO_ERROR.code, null) + val disconnectFrame = DisconnectFrame() + sendControlFrame(null, goAwayFrame, disconnectFrame) + } + break@closeLoop + } + } + else -> { + log.debug { "Ignored $frame, already closed" } + break@closeLoop + } + } + } + } + + private fun notifyClose(connection: Http2Connection, frame: GoAwayFrame, consumer: Consumer>) { + try { + listener.onClose(connection, frame, consumer) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + private fun newGoAwayFrame(closeState: CloseState, error: Int, reason: String?): GoAwayFrame { + var payload: ByteArray? = null + if (reason != null) { // Trim the reason to avoid attack vectors. + payload = reason.substring(0, min(reason.length, 32)).toByteArray(StandardCharsets.UTF_8) + } + return GoAwayFrame(closeState, getLastRemoteStreamId(), error, payload) + } + + override fun onConnectionFailure(error: Int, reason: String) { + onConnectionFailure(error, reason, discard()) + } + + protected fun onConnectionFailure(error: Int, reason: String, result: Consumer>) { + notifyFailure(this, IOException(String.format("%d/%s", error, reason))) { close(error, reason, result) } + } + + private fun notifyFailure(connection: Http2Connection, throwable: Throwable, consumer: Consumer>) { + try { + listener.onFailure(connection, throwable, consumer) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + override fun toString(): String { + return java.lang.String.format( + "%s@%x{l:%s <-> r:%s,sendWindow=%s,recvWindow=%s,streams=%d,%s,%s}", + this::class.java.simpleName, + hashCode(), + tcpConnection.localAddress, + tcpConnection.remoteAddress, + sendWindow, + recvWindow, + streams.size, + closeState.get(), + closeFrame?.toString() + ) + } +} + +fun getAndIncreaseStreamId(id: AtomicInteger, initStreamId: Int) = id.getAndUpdate { prev -> + val currentId = prev + 2 + if (currentId <= 0) initStreamId else currentId +} + +sealed class FlushFrameMessage +class ControlFrameEntry(val stream: Stream?, val frames: Array, val result: Consumer>) : + FlushFrameMessage() + +class DataFrameEntry(val stream: Stream, val frame: DataFrame, val result: Consumer>) : + FlushFrameMessage() { + var dataRemaining = frame.remaining() + var writtenBytes: Long = 0 +} + +data class OnWindowUpdateMessage(val stream: Stream?, val frame: WindowUpdateFrame) : FlushFrameMessage() \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AsyncHttp2Stream.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AsyncHttp2Stream.kt new file mode 100644 index 000000000..9bd7400ca --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/AsyncHttp2Stream.kt @@ -0,0 +1,411 @@ +package com.fireflysource.net.http.common.v2.stream + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.exception.Http2StreamFrameProcessException +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v2.frame.* +import com.fireflysource.net.http.common.v2.frame.CloseState.* +import com.fireflysource.net.http.common.v2.frame.CloseState.Event.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.io.Closeable +import java.io.IOException +import java.time.Duration +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Consumer + +class AsyncHttp2Stream( + private val asyncHttp2Connection: AsyncHttp2Connection, + private val id: Int, + private val local: Boolean, + var listener: Stream.Listener = defaultStreamListener +) : Stream, Closeable { + + companion object { + private val log = SystemLogger.create(AsyncHttp2Stream::class.java) + val defaultStreamListener = Stream.Listener.Adapter() + } + + private val attributes: ConcurrentMap by lazy { ConcurrentHashMap() } + + private val sendWindow = AtomicInteger() + private val recvWindow = AtomicInteger() + private val level = AtomicInteger() + + private val closeState = AtomicReference(NOT_CLOSED) + private var localReset = false + private var remoteReset = false + + private val createTime = System.currentTimeMillis() + private var lastActiveTime = createTime + private var idleTimeout: Long = 0 + private var idleCheckJob: Job? = null + + private var dataLength = Long.MIN_VALUE + + val stashedDataFrames = LinkedList() + + + override fun getId(): Int = id + + override fun getHttp2Connection(): Http2Connection = asyncHttp2Connection + + override fun getIdleTimeout(): Long = idleTimeout + + override fun setIdleTimeout(idleTimeout: Long) { + if (idleTimeout > 0) { + this.idleTimeout = idleTimeout + val job = idleCheckJob + idleCheckJob = if (job == null) { + launchIdleCheckJob() + } else { + job.cancel(CancellationException("Set the new idle timeout. id: $id")) + launchIdleCheckJob() + } + } + } + + private fun noIdle() { + lastActiveTime = System.currentTimeMillis() + } + + private fun launchIdleCheckJob() = asyncHttp2Connection.coroutineScope.launch { + while (true) { + val timeout = Duration.ofSeconds(idleTimeout).toMillis() + val delayTime = (timeout - getIdleTime()).coerceAtLeast(0) + log.debug { "Stream idle check delay: $delayTime" } + if (delayTime > 0) { + delay(delayTime) + } + val idle = getIdleTime() + if (idle >= timeout) { + notifyIdleTimeout() + break + } + } + } + + private fun getIdleTime() = System.currentTimeMillis() - lastActiveTime + + private fun notifyIdleTimeout() { + try { + val reset = listener.onIdleTimeout(this, TimeoutException("Stream idle timeout")) + if (reset) { + val frame = ResetFrame(id, ErrorCode.CANCEL_STREAM_ERROR.code) + reset(frame, discard()) + } + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + override fun getAttribute(key: String): Any? = attributes[key] + + override fun setAttribute(key: String, value: Any) { + attributes[key] = value + } + + override fun removeAttribute(key: String): Any? = attributes.remove(key) + + + fun process(frame: Frame, result: Consumer>) { + when (frame.type) { + FrameType.HEADERS -> onHeaders(frame as HeadersFrame, result) + FrameType.DATA -> onData(frame as DataFrame, result) + FrameType.RST_STREAM -> onReset(frame as ResetFrame, result) + FrameType.PUSH_PROMISE -> { + // They are closed when receiving an end-stream DATA frame. + // Pushed streams implicitly locally closed. + // They are closed when receiving an end-stream DATA frame. + updateClose(true, AFTER_SEND) + result.accept(Result.SUCCESS) + } + FrameType.WINDOW_UPDATE -> result.accept(Result.SUCCESS) + FrameType.FAILURE -> notifyFailure(this, frame as FailureFrame, result) + else -> throw Http2StreamFrameProcessException("Process frame type error. ${frame.type}") + } + } + + // header frame + override fun headers(frame: HeadersFrame, result: Consumer>) { + try { + noIdle() + Assert.isTrue(frame.streamId == id, "The headers frame id must equal the stream id") + sendControlFrame(frame, result) + } catch (e: Exception) { + result.accept(Result.createFailedResult(e)) + } + } + + private fun onHeaders(frame: HeadersFrame, result: Consumer>) { + noIdle() + val metaData: MetaData = frame.metaData + if (metaData.isRequest || metaData.isResponse) { + val fields = metaData.fields + var length: Long = -1 + if (fields != null) { + length = fields.getLongField(HttpHeader.CONTENT_LENGTH.value) + } + dataLength = if (length >= 0) length else Long.MIN_VALUE + } + if (updateClose(frame.isEndStream, RECEIVED)) { + asyncHttp2Connection.removeStream(this) + } + result.accept(Result.SUCCESS) + } + + + // push promise frame + override fun push(frame: PushPromiseFrame, promise: Consumer>, listener: Stream.Listener) { + noIdle() + asyncHttp2Connection.push(frame, promise, listener) + } + + + // data frame + override fun data(frame: DataFrame, result: Consumer>) { + noIdle() + asyncHttp2Connection.sendDataFrame(this, frame) + .thenAccept { result.accept(Result.SUCCESS) } + .exceptionallyAccept { result.accept(Result.createFailedResult(it)) } + } + + private fun onData(frame: DataFrame, result: Consumer>) { + noIdle() + if (getRecvWindow() < 0) { + // It's a bad client, it does not deserve to be treated gently by just resetting the stream. + asyncHttp2Connection.close(ErrorCode.FLOW_CONTROL_ERROR.code, "stream_window_exceeded", discard()) + result.accept(Result.createFailedResult(IOException("stream_window_exceeded"))) + return + } + + // SPEC: remotely closed streams must be replied with a reset. + if (isRemotelyClosed()) { + reset(ResetFrame(id, ErrorCode.STREAM_CLOSED_ERROR.code), discard()) + result.accept(Result.createFailedResult(IOException("stream_closed"))) + return + } + + if (isReset) { // Just drop the frame. + result.accept(Result.createFailedResult(IOException("stream_reset"))) + return + } + + if (dataLength != Long.MIN_VALUE) { + dataLength -= frame.remaining() + if (frame.isEndStream && dataLength != 0L) { + reset(ResetFrame(id, ErrorCode.PROTOCOL_ERROR.code), discard()) + result.accept(Result.createFailedResult(IOException("invalid_data_length"))) + return + } + } + + if (updateClose(frame.isEndStream, RECEIVED)) { + asyncHttp2Connection.removeStream(this) + } + + notifyData(this, frame, result) + } + + fun isRemotelyClosed(): Boolean { + val state = closeState.get() + return state === REMOTELY_CLOSED || state === CLOSING + } + + private fun notifyData(stream: Stream, frame: DataFrame, result: Consumer>) { + try { + listener.onData(stream, frame, result) + } catch (e: Throwable) { + log.error(e) { "Failure while notifying listener $listener" } + result.accept(Result.createFailedResult(e)) + } + } + + + // window update + fun updateSendWindow(delta: Int): Int = sendWindow.getAndAdd(delta) + + fun getSendWindow(): Int = sendWindow.get() + + fun updateRecvWindow(delta: Int): Int = recvWindow.getAndAdd(delta) + + fun getRecvWindow(): Int = recvWindow.get() + + fun addAndGetLevel(delta: Int): Int = level.addAndGet(delta) + + fun setLevel(level: Int) { + this.level.set(level) + } + + + // reset frame + override fun reset(frame: ResetFrame, result: Consumer>) { + if (isReset) { + result.accept(Result.createFailedResult(IllegalStateException("The stream: $id is reset"))) + return + } + localReset = true + sendControlFrame(frame, result) + } + + override fun isReset(): Boolean { + return localReset || remoteReset + } + + private fun onReset(frame: ResetFrame, result: Consumer>) { + remoteReset = true + close() + asyncHttp2Connection.removeStream(this) + notifyReset(this, frame, result) + } + + private fun notifyReset(stream: Stream, frame: ResetFrame, result: Consumer>) { + try { + listener.onReset(stream, frame, result) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + } + } + + private fun notifyFailure(stream: Stream, frame: FailureFrame, result: Consumer>) { + try { + listener.onFailure(stream, frame.error, frame.reason, result) + } catch (e: Exception) { + log.error(e) { "failure while notifying listener" } + result.accept(Result.createFailedResult(e)) + } + } + + + private fun sendControlFrame(frame: Frame, result: Consumer>) { + asyncHttp2Connection.sendControlFrame(this, frame) + .thenAccept { result.accept(Result.SUCCESS) } + .exceptionallyAccept { result.accept(Result.createFailedResult(it)) } + } + + + // close frame + override fun close() { + val oldState = closeState.getAndSet(CLOSED) + if (oldState != CLOSED) { + idleCheckJob?.cancel(CancellationException("The stream closed. id: $id")) + val deltaClosing = if (oldState == CLOSING) -1 else 0 + asyncHttp2Connection.updateStreamCount(local, -1, deltaClosing) + notifyClosed(this) + } + } + + fun updateClose(update: Boolean, event: Event): Boolean { + log.debug { "Update close for $this update=$update event=$event" } + + if (!update) { + return false + } + + return when (event) { + RECEIVED -> updateCloseAfterReceived() + BEFORE_SEND -> updateCloseBeforeSend() + AFTER_SEND -> updateCloseAfterSend() + } + } + + override fun isClosed(): Boolean { + return closeState.get() == CLOSED + } + + private fun updateCloseAfterReceived(): Boolean { + while (true) { + when (val current = closeState.get()) { + NOT_CLOSED -> if (closeState.compareAndSet(current, REMOTELY_CLOSED)) { + return false + } + LOCALLY_CLOSING -> { + if (closeState.compareAndSet(current, CLOSING)) { + asyncHttp2Connection.updateStreamCount(local, 0, 1) + return false + } + } + LOCALLY_CLOSED -> { + close() + return true + } + else -> return false + } + } + } + + private fun updateCloseBeforeSend(): Boolean { + while (true) { + when (val current = closeState.get()) { + NOT_CLOSED -> if (closeState.compareAndSet(current, LOCALLY_CLOSING)) { + return false + } + REMOTELY_CLOSED -> if (closeState.compareAndSet(current, CLOSING)) { + asyncHttp2Connection.updateStreamCount(local, 0, 1) + return false + } + else -> return false + } + } + } + + private fun updateCloseAfterSend(): Boolean { + while (true) { + when (val current = closeState.get()) { + NOT_CLOSED, LOCALLY_CLOSING -> if (closeState.compareAndSet(current, LOCALLY_CLOSED)) { + return false + } + REMOTELY_CLOSED, CLOSING -> { + close() + return true + } + else -> return false + } + } + } + + private fun notifyClosed(stream: Stream) { + try { + listener.onClosed(stream) + } catch (x: Throwable) { + log.info("Failure while notifying listener $listener", x) + } + } + + fun notifyTerminal(stream: Stream) { + try { + listener.onTerminal(stream) + } catch (x: Throwable) { + log.info("Failure while notifying listener $listener", x) + } + } + + + override fun toString(): String { + return String.format( + "%s@%x#%d{sendWindow=%s,recvWindow=%s,local=%b,reset=%b/%b,%s,age=%d}", + "AsyncHttp2Stream", + hashCode(), + getId(), + sendWindow, + recvWindow, + local, + localReset, + remoteReset, + closeState, + (System.currentTimeMillis() - createTime) + ) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/BufferedFlowControlStrategy.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/BufferedFlowControlStrategy.kt new file mode 100644 index 000000000..00bf579fd --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/BufferedFlowControlStrategy.kt @@ -0,0 +1,65 @@ +package com.fireflysource.net.http.common.v2.stream + +import com.fireflysource.common.concurrent.Atomics +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.v2.frame.WindowUpdateFrame +import java.util.concurrent.atomic.AtomicInteger + +class BufferedFlowControlStrategy( + private val ratio: Float = 0.5f, + initialStreamRecvWindow: Int = HttpConfig.DEFAULT_WINDOW_SIZE +) : AbstractFlowControlStrategy(initialStreamRecvWindow) { + + companion object { + private val log = SystemLogger.create(BufferedFlowControlStrategy::class.java) + } + + private val maxConnectionRecvWindow = AtomicInteger(HttpConfig.DEFAULT_WINDOW_SIZE) + private val connectionLevel = AtomicInteger() + + override fun onDataConsumed(http2Connection: Http2Connection, stream: Stream?, length: Int) { + if (length <= 0) return + + val connection = http2Connection as AsyncHttp2Connection + val level = connectionLevel.addAndGet(length) + val maxLevel = (maxConnectionRecvWindow.get() * ratio).toInt() + if (level >= maxLevel) { + if (connectionLevel.compareAndSet(level, 0)) { + connection.updateRecvWindow(level) + log.debug { "Data consumed, $length bytes, updated session recv window by $level/$maxLevel for $http2Connection" } + connection.sendControlFrame(null, WindowUpdateFrame(0, level)) + } else { + log.debug { "Data consumed, $length bytes, concurrent session recv window level $level/$maxLevel for $http2Connection" } + } + } else { + log.debug { "Data consumed, $length bytes, session recv window level $level/$maxLevel for $http2Connection" } + } + + if (stream != null && stream is AsyncHttp2Stream) { + if (stream.isRemotelyClosed()) { + log.debug { "Data consumed, $length bytes, ignoring update stream recv window for remotely closed $stream" } + } else { + val streamLevel = stream.addAndGetLevel(length) + val maxStreamLevel = (initialStreamRecvWindow * ratio).toInt() + if (streamLevel >= maxStreamLevel) { + stream.setLevel(0) + stream.updateRecvWindow(streamLevel) + log.debug { "Data consumed, $length bytes, updated stream recv window by $streamLevel/$maxStreamLevel for $stream" } + connection.sendControlFrame(stream, WindowUpdateFrame(stream.getId(), streamLevel)) + } else { + log.debug { "Data consumed, $length bytes, stream recv window level $streamLevel/$maxStreamLevel for $stream" } + } + } + } + } + + override fun windowUpdate(http2Connection: Http2Connection, stream: Stream?, frame: WindowUpdateFrame) { + super.windowUpdate(http2Connection, stream, frame) + if (frame.streamId == 0) { + val connection = http2Connection as AsyncHttp2Connection + val recvWindow = connection.getRecvWindow() + Atomics.updateMax(maxConnectionRecvWindow, recvWindow) + } + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/SimpleFlowControlStrategy.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/SimpleFlowControlStrategy.kt new file mode 100644 index 000000000..f1fe801bc --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/common/v2/stream/SimpleFlowControlStrategy.kt @@ -0,0 +1,44 @@ +package com.fireflysource.net.http.common.v2.stream + +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.v2.frame.WindowUpdateFrame + +class SimpleFlowControlStrategy( + initialStreamRecvWindow: Int = HttpConfig.DEFAULT_WINDOW_SIZE +) : AbstractFlowControlStrategy(initialStreamRecvWindow) { + + companion object { + private val log = SystemLogger.create(SimpleFlowControlStrategy::class.java) + } + + override fun onDataConsumed(http2Connection: Http2Connection, stream: Stream?, length: Int) { + log.debug { "Data consumed. length: $length, id: ${http2Connection.id}, stream: $stream" } + if (length <= 0) return + + // This is the simple algorithm for flow control. + // This method called when a whole flow controlled frame has been consumed. + // We send a WindowUpdate every time, even if the frame was very small. + val connection = http2Connection as AsyncHttp2Connection + val sessionFrame = WindowUpdateFrame(0, length) + connection.updateRecvWindow(length) + log.debug { "Data consumed, increased session recv window by $length for $connection" } + + var streamFrame: WindowUpdateFrame? = null + if (stream != null && stream is AsyncHttp2Stream) { + if (stream.isRemotelyClosed()) { + log.debug { "Data consumed, ignoring update stream recv window by $length for remotely closed $stream" } + } else { + streamFrame = WindowUpdateFrame(stream.id, length) + stream.updateRecvWindow(length) + log.debug { "Data consumed, increased stream recv window by $length for $stream" } + } + } + if (streamFrame != null) { + connection.sendControlFrame(stream, sessionFrame, streamFrame) + } else { + connection.sendControlFrame(stream, sessionFrame) + } + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AbstractHttpServerResponse.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AbstractHttpServerResponse.kt new file mode 100644 index 000000000..e8e95fd72 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AbstractHttpServerResponse.kt @@ -0,0 +1,192 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.io.useAwait +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.codec.CookieGenerator +import com.fireflysource.net.http.common.exception.HttpServerResponseNotCommitException +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.http.server.HttpServerContentProvider +import com.fireflysource.net.http.server.HttpServerOutputChannel +import com.fireflysource.net.http.server.HttpServerResponse +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Supplier + +abstract class AbstractHttpServerResponse(private val httpServerConnection: HttpServerConnection) : HttpServerResponse { + + companion object { + const val contentProviderBufferSize = 8 * 1024 + } + + val response: MetaData.Response = MetaData.Response(HttpVersion.HTTP_1_1, HttpStatus.OK_200, HttpFields()) + private var contentProvider: HttpServerContentProvider? = null + private var cookieList: List? = null + private val committed = AtomicBoolean(false) + private val callCommit = AtomicBoolean(false) + private var serverOutputChannel: HttpServerOutputChannel? = null + private val mutex = Mutex() + + override fun getStatus(): Int = response.status + + override fun setStatus(status: Int) { + response.status = status + } + + override fun getReason(): String = response.reason + + override fun setReason(reason: String) { + response.reason = reason + } + + override fun getHttpVersion(): HttpVersion = response.httpVersion + + override fun setHttpVersion(httpVersion: HttpVersion) { + response.httpVersion = httpVersion + } + + override fun getHttpFields(): HttpFields = response.fields + + override fun setHttpFields(httpFields: HttpFields) { + response.fields.clear() + response.fields.addAll(httpFields) + } + + override fun getCookies(): List = cookieList ?: listOf() + + override fun setCookies(cookies: List) { + cookieList = cookies + } + + override fun getContentProvider(): HttpServerContentProvider? = contentProvider + + override fun setContentProvider(contentProvider: HttpServerContentProvider) { + Assert.state(!isCommitted, "Set content provider must before commit response.") + this.contentProvider = contentProvider + } + + override fun getTrailerSupplier(): Supplier = response.trailerSupplier + + override fun setTrailerSupplier(supplier: Supplier) { + response.trailerSupplier = supplier + } + + override fun isCommitted(): Boolean = callCommit.get() + + override fun commit(): CompletableFuture { + callCommit.set(true) + return httpServerConnection.coroutineScope + .launch { commitAwait() } + .asCompletableFuture() + .thenCompose { Result.DONE } + } + + private suspend fun commitAwait() { + if (committed.get()) return + + mutex.withLock { + if (committed.get()) return@commitAwait + + createOutputChannelAndCommit() + committed.set(true) + } + } + + private suspend fun createOutputChannelAndCommit() { + if (response.fields[HttpHeader.CONNECTION] == null && httpVersion == HttpVersion.HTTP_1_1) { + response.fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE) + } + + cookies.map { CookieGenerator.generateSetCookie(it) } + .forEach { response.fields.add(HttpHeader.SET_COOKIE, it) } + + val provider = contentProvider + if (provider != null && provider.length() >= 0) { + response.fields.put(HttpHeader.CONTENT_LENGTH, provider.length().toString()) + } + + val contentEncoding = Optional + .ofNullable(response.fields[HttpHeader.CONTENT_ENCODING]) + .flatMap { ContentEncoding.from(it) } + + val output = if (contentEncoding.isPresent) { + val out = createHttpServerOutputChannel(response) + CompressedServerOutputChannel(out, contentEncoding.get()) + } else createHttpServerOutputChannel(response) + + if (provider != null) { + output.useAwait { + it.commit().await() + writeContent(provider, it) + } + } else { + output.commit().await() + } + this.serverOutputChannel = output + } + + /** + * Create the HTTP server output channel. It outputs the HTTP response. + * + * @return The HTTP server output channel. + */ + abstract fun createHttpServerOutputChannel(response: MetaData.Response): HttpServerOutputChannel + + private suspend fun writeContent(provider: HttpServerContentProvider, outputChannel: HttpServerOutputChannel) { + val size = provider.getContentProviderBufferSize() + writeLoop@ while (true) { + val buffer = BufferUtils.allocate(size) + val position = buffer.flipToFill() + val length = provider.read(buffer).await() + buffer.flipToFlush(position) + when { + length > 0 -> outputChannel.write(buffer).await() + length < 0 -> break@writeLoop + } + } + provider.closeAsync().await() + } + + private fun HttpServerContentProvider.getContentProviderBufferSize(): Int { + return if (this.length() > 0) this.length().coerceAtMost(contentProviderBufferSize.toLong()).toInt() + else contentProviderBufferSize + } + + override fun getOutputChannel(): HttpServerOutputChannel { + val outputChannel = serverOutputChannel + if (outputChannel == null) { + throw HttpServerResponseNotCommitException("The response not commit") + } else { + Assert.state( + contentProvider == null, + "The content provider is not null. The server has used content provider to output content." + ) + return outputChannel + } + } + + override fun closeAsync(): CompletableFuture { + val provider = contentProvider + return if (provider == null) { + httpServerConnection.coroutineScope.launch { + commit().await() + outputChannel.closeAsync().await() + }.asVoidFuture() + } else Result.DONE + } + + override fun close() { + closeAsync() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpProxy.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpProxy.kt new file mode 100644 index 000000000..187f37fd4 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpProxy.kt @@ -0,0 +1,249 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.deleteIfExistsAsync +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.* +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpHeaderValue +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.server.* +import com.fireflysource.net.http.server.impl.content.handler.ByteBufferContentHandler +import com.fireflysource.net.http.server.impl.content.handler.FileContentHandler +import com.fireflysource.net.http.server.impl.router.asyncHandler +import com.fireflysource.net.tcp.TcpClientFactory +import com.fireflysource.net.tcp.TcpConnection +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.net.SocketAddress +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.util.* +import java.util.concurrent.CompletableFuture + +class AsyncHttpProxy(httpConfig: HttpConfig = HttpConfig()) : AbstractLifeCycle(), HttpProxy { + + companion object { + private val log = SystemLogger.create(AsyncHttpProxy::class.java) + private val tempPath = System.getProperty("java.io.tmpdir") + private const val httpBodyDir = "com.fireflysource.http.proxy" + private const val httpProxyServerContentPathKey = "httpProxyServerContentPath" + private const val httpProxyClientContentPathKey = "httpProxyClientContentPath" + } + + private val server = HttpServerFactory.create(httpConfig) + private val tcpClient = TcpClientFactory.create() + private val httpClient = HttpClientFactory.create(httpConfig) + private val httpProxyBodySizeThreshold = httpConfig.httpProxyBodySizeThreshold + + init { + server + .onAcceptHttpTunnel { request -> + log.info("Accept http tunnel handshake. uri: ${request.uri}") + CompletableFuture.completedFuture(true) + } + .onHttpTunnelHandshakeComplete { connection, targetAddress -> + log.info("HTTP tunnel handshake success. target: $targetAddress") + connection.coroutineScope.launch { buildHttpTunnel(this, connection, targetAddress) }.asVoidFuture() + } + .onHeaderComplete { ctx -> + if (ctx.expect100Continue()) { + ctx.response100Continue() + } else if (ctx.method == HttpMethod.CONNECT.value) { + Result.DONE + } else { + setServerContentHandler(ctx) + Result.DONE + } + } + .router().path("*").asyncHandler { buildNonSecureHttpHandler(it) } + val tempDir = Paths.get(tempPath, httpBodyDir) + if (!Files.exists(tempDir)) { + Files.createDirectory(tempDir) + } + log.info("HTTP proxy content temp file path: $tempDir") + start() + } + + private suspend fun buildNonSecureHttpHandler(ctx: RoutingContext) { + try { + val response = httpClient.request(ctx.method, ctx.uri) + .addAll(ctx.httpFields) + .contentProvider(createClientContentProvider(ctx.request.contentHandler)) + .onHeaderComplete { request, response -> setClientContentHandler(request, response, ctx) } + .submit().await() + + ctx.setStatus(response.status) + .addAll(response.httpFields) + .contentProvider(createServerContentProvider(response.contentHandler)) + .end().await() + } finally { + deleteContentTempFile(ctx) + } + } + + private fun setClientContentHandler( + request: HttpClientRequest, + response: HttpClientResponse, + ctx: RoutingContext + ) { + val clientBodyContentHandler = createClientContentHandler(response, ctx) + request.contentHandler = clientBodyContentHandler + response.contentHandler = clientBodyContentHandler + } + + private fun createClientContentHandler( + response: HttpClientResponse, + ctx: RoutingContext + ): HttpClientContentHandler { + fun createFileHandler(): HttpClientContentHandler { + val path = + Paths.get(tempPath, httpBodyDir, "client-body-${UUID.randomUUID()}") + ctx.setAttribute(httpProxyClientContentPathKey, path) + return HttpClientContentHandlerFactory.fileHandler( + path, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.READ, + StandardOpenOption.WRITE + ) + } + return if (response.contentLength > httpProxyBodySizeThreshold) { + createFileHandler() + } else if (response.httpFields.contains(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.value)) { + createFileHandler() + } else { + HttpClientContentHandlerFactory.bytesHandler(httpProxyBodySizeThreshold) + } + } + + private fun createClientContentProvider(serverContentHandler: HttpServerContentHandler) = + when (serverContentHandler) { + is FileContentHandler -> HttpClientContentProviderFactory.fileBody( + serverContentHandler.path, + StandardOpenOption.READ + ) + is ByteBufferContentHandler -> HttpClientContentProviderFactory.bytesBody( + BufferUtils.merge(serverContentHandler.getByteBuffers()) + ) + else -> throw IllegalStateException("The HTTP proxy content handler type error.") + } + + private fun createServerContentProvider(clientContentHandler: HttpClientContentHandler) = + when (clientContentHandler) { + is com.fireflysource.net.http.client.impl.content.handler.FileContentHandler -> HttpServerContentProviderFactory.fileBody( + clientContentHandler.path, + StandardOpenOption.READ + ) + is com.fireflysource.net.http.client.impl.content.handler.ByteBufferContentHandler -> HttpServerContentProviderFactory.bytesBody( + BufferUtils.merge(clientContentHandler.getByteBuffers()) + ) + else -> throw IllegalStateException("The HTTP client response content handler type error.") + } + + + private fun setServerContentHandler(ctx: RoutingContext) { + fun setFileHandler() { + val path = + Paths.get(tempPath, httpBodyDir, "server-body-${UUID.randomUUID()}") + val handler = HttpServerContentHandlerFactory.fileHandler( + path, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.READ, + StandardOpenOption.WRITE + ) + ctx.setAttribute(httpProxyServerContentPathKey, path) + ctx.contentHandler(handler) + } + + fun setBytesHandler() { + ctx.contentHandler(HttpServerContentHandlerFactory.bytesHandler(httpProxyBodySizeThreshold)) + } + + if (ctx.contentLength > httpProxyBodySizeThreshold) { + setFileHandler() + } else if (ctx.httpFields.contains(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.value)) { + setFileHandler() + } else { + setBytesHandler() + } + } + + private fun deleteContentTempFile(ctx: RoutingContext) { + ctx.getAttribute(httpProxyServerContentPathKey)?.let { + deleteIfExistsAsync(it as Path).asCompletableFuture() + .thenAccept { success -> log.debug("delete server body temp file. $success") } + .exceptionallyAccept { e -> log.error(e) { "delete server body temp file exception." } } + } + ctx.getAttribute(httpProxyClientContentPathKey)?.let { + deleteIfExistsAsync(it as Path).asCompletableFuture() + .thenAccept { success -> log.debug("delete client body temp file. $success") } + .exceptionallyAccept { e -> log.error(e) { "delete client body temp file exception." } } + } + } + + private suspend fun buildHttpTunnel( + coroutineScope: CoroutineScope, + connection: TcpConnection, + targetAddress: InetSocketAddress + ) { + val targetConnection = tcpClient.connect(targetAddress).await() + val readFromClientJob = coroutineScope.launch { + while (true) { + val r = this.runCatching { + val data = connection.read().await() + val size = targetConnection.write(data).await() + log.debug("write to target: $size") + } + if (r.isFailure) { + log.error("read from client job failure", r.exceptionOrNull()) + break + } + } + } + val writeToClientJob = coroutineScope.launch { + while (true) { + val r = this.runCatching { + val data = targetConnection.read().await() + val size = connection.write(data).await() + connection.flush().await() + log.debug("write to client: $size") + } + if (r.isFailure) { + log.error("write to client job failure", r.exceptionOrNull()) + break + } + } + } + coroutineScope.runCatching { + readFromClientJob.join() + log.info("HTTP tunnel read job exit.") + writeToClientJob.join() + log.info("HTTP tunnel write job exit.") + } + targetConnection.closeAsync().await() + connection.closeAsync().await() + } + + override fun listen(address: SocketAddress) { + server.listen(address) + } + + override fun init() { + } + + override fun destroy() { + server.stop() + tcpClient.stop() + httpClient.stop() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServer.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServer.kt new file mode 100644 index 000000000..c19d13999 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServer.kt @@ -0,0 +1,282 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.ProjectVersion +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpHeaderValue +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.* +import com.fireflysource.net.http.server.impl.content.provider.DefaultContentProvider +import com.fireflysource.net.http.server.impl.exception.ProxyAuthException +import com.fireflysource.net.http.server.impl.exception.RouterNotCommitException +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpServer +import com.fireflysource.net.tcp.aio.AioTcpServer +import com.fireflysource.net.tcp.aio.ApplicationProtocol.HTTP1 +import com.fireflysource.net.tcp.aio.ApplicationProtocol.HTTP2 +import com.fireflysource.net.tcp.secure.SecureEngineFactory +import com.fireflysource.net.websocket.server.WebSocketManager +import com.fireflysource.net.websocket.server.WebSocketServerConnectionBuilder +import com.fireflysource.net.websocket.server.impl.AsyncWebSocketManager +import com.fireflysource.net.websocket.server.impl.AsyncWebSocketServerConnectionBuilder +import java.net.InetSocketAddress +import java.net.SocketAddress +import java.util.concurrent.CompletableFuture +import java.util.function.BiFunction +import java.util.function.Function +import kotlin.system.measureTimeMillis + +class AsyncHttpServer(val config: HttpConfig = HttpConfig()) : HttpServer, AbstractLifeCycle() { + + companion object { + private val log = SystemLogger.create(AsyncHttpServer::class.java) + } + + private var routerManager: RouterManager = AsyncRouterManager(this) + private var webSocketManager: WebSocketManager = AsyncWebSocketManager() + private var tcpServer: TcpServer = AioTcpServer() + private var address: SocketAddress? = null + private var onHeaderComplete: Function> = Function { ctx -> + if (ctx.expect100Continue()) ctx.response100Continue() else Result.DONE + } + private var onException: BiFunction> = BiFunction { ctx, e -> + if (ctx != null && !ctx.response.isCommitted) { + if (e is BadMessageException) { + ctx.setStatus(e.code) + .put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE) + .contentProvider(DefaultContentProvider(e.code, e, ctx)) + .end() + .thenCompose { ctx.connection.closeAsync() } + } else { + ctx.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) + .setReason(HttpStatus.Code.INTERNAL_SERVER_ERROR.message) + .contentProvider(DefaultContentProvider(HttpStatus.INTERNAL_SERVER_ERROR_500, e, ctx)) + .end() + } + } else Result.DONE + } + private var onRouterNotFound: Function> = Function { ctx -> + ctx.setStatus(HttpStatus.NOT_FOUND_404) + .setReason(HttpStatus.Code.NOT_FOUND.message) + .contentProvider(DefaultContentProvider(HttpStatus.NOT_FOUND_404, null, ctx)) + .end() + } + private var onRouterComplete: Function> = Function { ctx -> + if (ctx.response.isCommitted) Result.DONE + else ctx.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) + .setReason(HttpStatus.Code.INTERNAL_SERVER_ERROR.message) + .contentProvider( + DefaultContentProvider( + HttpStatus.INTERNAL_SERVER_ERROR_500, + RouterNotCommitException("The response does not commit"), + ctx + ) + ) + .end() + } + private var onAcceptHttpTunnel: Function> = Function { + CompletableFuture.completedFuture(false) + } + private var onHttpTunnelHandshakeComplete: BiFunction> = BiFunction { connection, _ -> + connection.closeAsync() + } + private var onAcceptHttpTunnelHandshakeResponse: Function> = + Function { ctx -> ctx.response200ConnectionEstablished() } + private var onRefuseHttpTunnelHandshakeResponse: Function> = + Function { ctx -> + ctx.setStatus(HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407) + .setReason(HttpStatus.Code.PROXY_AUTHENTICATION_REQUIRED.message) + .put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE) + .contentProvider( + DefaultContentProvider( + HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407, + ProxyAuthException("The proxy authentication must be required"), + ctx + ) + ) + .end() + .thenCompose { ctx.connection.closeAsync() } + } + + + override fun router(): Router = routerManager.register() + + override fun router(id: Int): Router = routerManager.register(id) + + override fun websocket(): WebSocketServerConnectionBuilder { + return AsyncWebSocketServerConnectionBuilder(this, webSocketManager) + } + + override fun websocket(path: String): WebSocketServerConnectionBuilder { + return AsyncWebSocketServerConnectionBuilder(this, webSocketManager).url(path) + } + + override fun onHeaderComplete(function: Function>): HttpServer { + this.onHeaderComplete = function + return this + } + + override fun onException(biFunction: BiFunction>): HttpServer { + this.onException = biFunction + return this + } + + override fun onRouterComplete(function: Function>): HttpServer { + this.onRouterComplete = function + return this + } + + override fun onRouterNotFound(function: Function>): HttpServer { + this.onRouterNotFound = function + return this + } + + override fun onAcceptHttpTunnel(function: Function>): HttpServer { + this.onAcceptHttpTunnel = function + return this + } + + override fun onAcceptHttpTunnelHandshakeResponse(function: Function>): HttpServer { + this.onAcceptHttpTunnelHandshakeResponse = function + return this + } + + override fun onRefuseHttpTunnelHandshakeResponse(function: Function>): HttpServer { + this.onRefuseHttpTunnelHandshakeResponse = function + return this + } + + override fun onHttpTunnelHandshakeComplete(function: BiFunction>): HttpServer { + this.onHttpTunnelHandshakeComplete = function + return this + } + + override fun timeout(timeout: Long): HttpServer { + tcpServer.timeout(timeout) + return this + } + + override fun secureEngineFactory(secureEngineFactory: SecureEngineFactory): HttpServer { + tcpServer.secureEngineFactory(secureEngineFactory) + return this + } + + override fun peerHost(peerHost: String): HttpServer { + tcpServer.peerHost(peerHost) + return this + } + + override fun peerPort(peerPort: Int): HttpServer { + tcpServer.peerPort(peerPort) + return this + } + + override fun supportedProtocols(supportedProtocols: MutableList): HttpServer { + tcpServer.supportedProtocols(supportedProtocols) + return this + } + + override fun enableSecureConnection(): HttpServer { + tcpServer.enableSecureConnection() + return this + } + + override fun listen(address: SocketAddress) { + this.address = address + start() + } + + override fun init() { + val time = measureTimeMillis { startupHttpServer() } + log.info(ProjectVersion.logo()) + log.info("Started Firefly HTTP server in {}ms. Address: {}", time, this.address) + } + + private fun startupHttpServer() { + require(config.maxRequestBodySize >= config.maxUploadFileSize) { "The max request size must be greater than the max file size." } + require(config.maxUploadFileSize >= config.uploadFileSizeThreshold) { "The max file size must be greater than the file size threshold." } + + val address = this.address + requireNotNull(address) + + if (config.tcpChannelGroup != null) { + tcpServer.tcpChannelGroup(config.tcpChannelGroup) + } + + tcpServer.stopTcpChannelGroup(config.isStopTcpChannelGroup) + + if (config.secureEngineFactory != null) { + tcpServer.secureEngineFactory(config.secureEngineFactory) + } + + val listener = AsyncHttpServerConnectionListener( + routerManager, + onHeaderComplete, + onException, + onRouterNotFound, + onRouterComplete, + webSocketManager, + onAcceptHttpTunnel, + onAcceptHttpTunnelHandshakeResponse, + onRefuseHttpTunnelHandshakeResponse, + onHttpTunnelHandshakeComplete + ) + + tcpServer.onAccept { connection -> + if (connection.isSecureConnection) { + connection.beginHandshake().thenAccept { protocol -> + when (protocol) { + HTTP2.value -> createHttp2Connection(connection, listener) + HTTP1.value -> createHttp1Connection(connection, listener) + else -> createHttp1Connection(connection, listener) + } + }.exceptionallyAccept { e -> + log.error(e) { "TLS handshake exception. id: ${connection.id}" } + connection.close() + } + } else createHttp1Connection(connection, listener) + } + + tcpServer.enableOutputBuffer().listen(address) + } + + private fun createHttp2Connection(connection: TcpConnection, listener: HttpServerConnection.Listener.Adapter) { + val http2Connection = Http2ServerConnection(config, connection) + http2Connection.setListener(listener).begin() + } + + private fun createHttp1Connection(connection: TcpConnection, listener: HttpServerConnection.Listener.Adapter) { + val http1Connection = Http1ServerConnection(config, connection) + http1Connection.setListener(listener).begin() + } + + override fun destroy() { + tcpServer.stop() + } + + override fun clone(): HttpServer { + val server = AsyncHttpServer(this.config.clone()) + server.routerManager = (this.routerManager as AsyncRouterManager).copy(server) + server.webSocketManager = (this.webSocketManager as AsyncWebSocketManager).clone() + server.tcpServer = (this.tcpServer as AioTcpServer).clone() + server.onHeaderComplete = this.onHeaderComplete + server.onException = this.onException + server.onRouterNotFound = this.onRouterNotFound + server.onRouterComplete = this.onRouterComplete + server.onAcceptHttpTunnel = this.onAcceptHttpTunnel + server.onHttpTunnelHandshakeComplete = this.onHttpTunnelHandshakeComplete + server.onAcceptHttpTunnelHandshakeResponse = this.onAcceptHttpTunnelHandshakeResponse + server.onRefuseHttpTunnelHandshakeResponse = this.onRefuseHttpTunnelHandshakeResponse + return server + } + + override fun copy(): HttpServer { + return this.clone() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServerConnectionListener.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServerConnectionListener.kt new file mode 100644 index 000000000..01dc467e0 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServerConnectionListener.kt @@ -0,0 +1,110 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.annotation.NoArg +import com.fireflysource.common.concurrent.exceptionallyCompose +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.codec.URIUtils +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.http.server.HttpServerRequest +import com.fireflysource.net.http.server.RouterManager +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRoutingContext +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.websocket.server.WebSocketManager +import com.fireflysource.net.websocket.server.WebSocketServerConnectionHandler +import java.net.InetSocketAddress +import java.util.concurrent.CompletableFuture +import java.util.function.BiFunction +import java.util.function.Function + +/** + * @author Pengtao Qiu + */ +@NoArg +class AsyncHttpServerConnectionListener( + private val routerManager: RouterManager, + private val onHeaderComplete: Function>, + private val onException: BiFunction>, + private val onRouterNotFound: Function>, + private val onRouterComplete: Function>, + private val webSocketManager: WebSocketManager, + private val onAcceptHttpTunnel: Function>, + private val onAcceptHttpTunnelHandshakeResponse: Function>, + private val onRefuseHttpTunnelHandshakeResponse: Function>, + private val onHttpTunnelHandshakeComplete: BiFunction> +) : HttpServerConnection.Listener.Adapter() { + + override fun onHeaderComplete(ctx: RoutingContext): CompletableFuture { + return onHeaderComplete.apply(ctx) + } + + override fun onHttpRequestComplete(ctx: RoutingContext): CompletableFuture { + if (ctx.response.isCommitted) return Result.DONE + + val results = routerManager.findRouters(ctx) + val asyncCtx = ctx as AsyncRoutingContext + val iterator = results.iterator() + return if (iterator.hasNext()) { + val result = iterator.next() + asyncCtx.routerMatchResult = result + asyncCtx.routerIterator = iterator + (result.router as AsyncRouter).getHandler() + .apply(ctx) + .thenCompose { handleRouterComplete(ctx) } + .exceptionallyCompose { handleRouterException(ctx, it) } + } else handleRouterNotFound(ctx) + } + + override fun onException(ctx: RoutingContext?, e: Throwable): CompletableFuture { + return onException.apply(ctx, e) + } + + override fun onWebSocketHandshake(ctx: RoutingContext): CompletableFuture { + val path = URIUtils.canonicalPath(ctx.uri.decodedPath) + val handler = webSocketManager.findWebSocketHandler(path) + val future = CompletableFuture() + if (handler != null) { + future.complete(handler) + } else { + future.completeExceptionally( + BadMessageException( + HttpStatus.BAD_REQUEST_400, + "The websocket handler is not register" + ) + ) + } + return future + } + + override fun onAcceptHttpTunnel(request: HttpServerRequest): CompletableFuture { + return onAcceptHttpTunnel.apply(request) + } + + override fun onAcceptHttpTunnelHandshakeResponse(context: RoutingContext): CompletableFuture { + return onAcceptHttpTunnelHandshakeResponse.apply(context) + } + + override fun onRefuseHttpTunnelHandshakeResponse(context: RoutingContext): CompletableFuture { + return onRefuseHttpTunnelHandshakeResponse.apply(context) + } + + override fun onHttpTunnelHandshakeComplete(connection: TcpConnection, address: InetSocketAddress): CompletableFuture { + return onHttpTunnelHandshakeComplete.apply(connection, address) + } + + private fun handleRouterNotFound(ctx: RoutingContext): CompletableFuture { + return onRouterNotFound.apply(ctx) + } + + private fun handleRouterException(ctx: RoutingContext, e: Throwable): CompletableFuture { + return onException.apply(ctx, e) + } + + private fun handleRouterComplete(ctx: RoutingContext): CompletableFuture { + return onRouterComplete.apply(ctx) + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServerRequest.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServerRequest.kt new file mode 100644 index 000000000..2e469672e --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/AsyncHttpServerRequest.kt @@ -0,0 +1,175 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.codec.CookieParser +import com.fireflysource.net.http.common.codec.UrlEncoded +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.server.HttpServerContentHandler +import com.fireflysource.net.http.server.HttpServerRequest +import com.fireflysource.net.http.server.MultiPart +import com.fireflysource.net.http.server.impl.content.handler.ByteBufferContentHandler +import com.fireflysource.net.http.server.impl.content.handler.FormInputsContentHandler +import com.fireflysource.net.http.server.impl.content.handler.MultiPartContentHandler +import com.fireflysource.net.http.server.impl.content.handler.StringContentHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Supplier + +class AsyncHttpServerRequest( + val request: MetaData.Request, + config: HttpConfig, + scope: CoroutineScope = CoroutineScope(CoroutineName("Firefly-HTTP-server-request")) +) : HttpServerRequest { + + private var cookieList: List? = null + private var queryStringMap: UrlEncoded? = null + private val requestComplete = AtomicBoolean(false) + private var contentHandler: HttpServerContentHandler + + init { + val query: String? = request.uri.query + if (!query.isNullOrBlank()) { + queryStringMap = UrlEncoded(query) + } + val contentType = request.fields[HttpHeader.CONTENT_TYPE] + contentHandler = if (contentType != null) { + when { + contentType.contains("x-www-form-urlencoded", true) -> + FormInputsContentHandler(config.maxRequestBodySize) + contentType.contains("multipart/form-data", true) -> + MultiPartContentHandler( + config.maxUploadFileSize, + config.maxRequestBodySize, + config.uploadFileSizeThreshold, + scope + ) + else -> StringContentHandler(config.maxRequestBodySize) + } + } else StringContentHandler(config.maxRequestBodySize) + + } + + override fun getMethod(): String = request.method + + override fun getURI(): HttpURI = request.uri + + override fun getHttpVersion(): HttpVersion = request.httpVersion + + override fun getQueryString(name: String): String = queryStringMap?.getString(name) ?: "" + + override fun getQueryStrings(name: String): List = queryStringMap?.get(name) ?: listOf() + + override fun getQueryStrings(): Map> = queryStringMap ?: mapOf() + + override fun getHttpFields(): HttpFields = request.fields + + override fun getContentLength(): Long = request.contentLength + + override fun getCookies(): List { + val cookies = cookieList + return if (cookies == null) { + val list = Optional.ofNullable(httpFields[HttpHeader.COOKIE]) + .filter { it.isNotBlank() } + .map { CookieParser.parseCookie(it) } + .orElse(listOf()) + cookieList = list + list + } else { + cookies + } + } + + override fun getContentHandler(): HttpServerContentHandler = this.contentHandler + + override fun setContentHandler(contentHandler: HttpServerContentHandler) { + this.contentHandler = contentHandler + } + + override fun isRequestComplete(): Boolean = requestComplete.get() + + override fun setRequestComplete(requestComplete: Boolean) { + this.requestComplete.set(requestComplete) + } + + override fun getStringBody(): String = getStringBody(StandardCharsets.UTF_8) + + override fun getStringBody(charset: Charset): String = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is StringContentHandler } + .map { it as StringContentHandler } + .map { it.toString(charset, getContentEncoding()) } + .orElse("") + + override fun getBody(): List = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is ByteBufferContentHandler } + .map { it as ByteBufferContentHandler } + .map { it.getByteBuffers(getContentEncoding()) } + .orElse(listOf()) + + override fun getFormInput(name: String): String = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is FormInputsContentHandler } + .map { it as FormInputsContentHandler } + .map { it.getFormInput(name, getContentEncoding()) } + .orElse("") + + override fun getFormInputs(name: String): List = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is FormInputsContentHandler } + .map { it as FormInputsContentHandler } + .map { it.getFormInputs(name, getContentEncoding()) } + .orElse(listOf()) + + override fun getFormInputs(): Map> = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is FormInputsContentHandler } + .map { it as FormInputsContentHandler } + .map { it.getFormInputs(getContentEncoding()) } + .orElse(mapOf()) + + override fun getPart(name: String): MultiPart? = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is MultiPartContentHandler } + .map { it as MultiPartContentHandler } + .map { it.getPart(name) } + .orElse(null) + + override fun getParts(): List = Optional + .ofNullable(contentHandler) + .filter { isRequestComplete } + .filter { it is MultiPartContentHandler } + .map { it as MultiPartContentHandler } + .map { it.getParts() } + .orElse(listOf()) + + override fun getTrailerSupplier(): Supplier = request.trailerSupplier + + private fun getContentEncoding(): Optional { + return Optional.ofNullable(this.httpFields[HttpHeader.CONTENT_ENCODING]) + .map { it.trim() } + .map { it.lowercase(Locale.getDefault()) } + .flatMap { ContentEncoding.from(it) } + } + + override fun toString(): String { + return """ + |request: ----------------- + |$method $uri $httpVersion + |$httpFields + |$stringBody + |end request -------------- + """.trimMargin() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/CompressedServerOutputChannel.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/CompressedServerOutputChannel.kt new file mode 100644 index 000000000..297a13c84 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/CompressedServerOutputChannel.kt @@ -0,0 +1,101 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.net.http.common.codec.ContentEncoded +import com.fireflysource.net.http.common.model.ContentEncoding +import com.fireflysource.net.http.server.HttpServerOutputChannel +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture + +class CompressedServerOutputChannel( + private val outputChannel: HttpServerOutputChannel, + private val contentEncoding: ContentEncoding, + private val bufferSize: Int = 512 +) : HttpServerOutputChannel { + + private var compressedOutputStream: OutputStream? = null + private var outputStreamAdapter: ServerOutputStreamAdapter? = null + + private inner class ServerOutputStreamAdapter : OutputStream() { + + override fun write(b: Int) { + val buffer = BufferUtils.allocate(1) + val pos = buffer.flipToFill() + buffer.put(b.toByte()) + buffer.flipToFlush(pos) + outputChannel.write(buffer) + } + + override fun write(b: ByteArray) { + outputChannel.write(ByteBuffer.wrap(b)) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + outputChannel.write(ByteBuffer.wrap(b, off, len)) + } + + } + + override fun isCommitted(): Boolean = outputChannel.isCommitted + + override fun commit(): CompletableFuture { + return outputChannel.commit().thenAccept { + val adapter = ServerOutputStreamAdapter() + compressedOutputStream = ContentEncoded.createEncodingOutputStream(adapter, contentEncoding, bufferSize) + } + } + + override fun write(byteBuffer: ByteBuffer): CompletableFuture { + val bytes = BufferUtils.toArray(byteBuffer) + getOutput().write(bytes) + val future = CompletableFuture() + future.complete(bytes.size) + return future + } + + override fun write(byteBuffers: Array, offset: Int, length: Int): CompletableFuture { + val last = offset + length - 1 + val buffer = BufferUtils.merge((offset..last).map { byteBuffers[it].duplicate() }) + val bytes = BufferUtils.toArray(buffer) + getOutput().write(bytes) + val future = CompletableFuture() + future.complete(bytes.size.toLong()) + return future + } + + override fun write(byteBufferList: List, offset: Int, length: Int): CompletableFuture { + return write(byteBufferList.toTypedArray(), offset, length) + } + + override fun write(string: String): CompletableFuture { + return write(string, StandardCharsets.UTF_8) + } + + override fun write(string: String, charset: Charset): CompletableFuture { + val buffer = BufferUtils.toBuffer(string, charset) + return write(buffer) + } + + private fun getOutput(): OutputStream { + val output = compressedOutputStream + requireNotNull(output) { "The compressed output stream not create" } + return output + } + + override fun closeAsync(): CompletableFuture { + compressedOutputStream?.close() + outputStreamAdapter?.close() + return outputChannel.closeAsync() + } + + override fun close() { + closeAsync() + } + + override fun isOpen(): Boolean = outputChannel.isOpen +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerConnection.kt new file mode 100644 index 000000000..6d909501f --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerConnection.kt @@ -0,0 +1,124 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.Connection +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.TcpBasedHttpConnection +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.exception.HttpServerConnectionListenerNotSetException +import com.fireflysource.net.http.common.model.HttpVersion +import com.fireflysource.net.http.common.v1.decoder.HttpParser +import com.fireflysource.net.http.common.v1.decoder.parseAll +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpCoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import java.io.IOException +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicBoolean + +class Http1ServerConnection( + val config: HttpConfig, + private val tcpConnection: TcpConnection +) : Connection by tcpConnection, TcpCoroutineDispatcher by tcpConnection, TcpBasedHttpConnection, HttpServerConnection { + + companion object { + private val log = SystemLogger.create(Http1ServerConnection::class.java) + } + + private val requestHandler = Http1ServerRequestHandler(this) + private val parser = HttpParser(requestHandler) + private val responseHandler = Http1ServerResponseHandler(this) + private val beginning = AtomicBoolean(false) + private val channel: Channel = Channel(Channel.UNLIMITED) + private var parseRequestJob: Job? = null + private var generateResponseJob: Job? = null + + private fun parseRequestJob() = coroutineScope.launch { + parseLoop@ while (true) { + when (channel.receive()) { + is ParseNextHttpPacket -> parseNextHttpPacket() + is ExitHttpParser -> { + log.info { "Exit the HTTP server parser. id: $id" } + break@parseLoop + } + } + } + } + + private suspend fun parseNextHttpPacket() { + try { + parser.parseAll(tcpConnection) + } catch (e: BadMessageException) { + requestHandler.badMessage(e) + channel.trySend(ExitHttpParser) + } catch (e: IOException) { + log.info { "The TCP connection IO exception. message: ${e.message ?: e.javaClass.name}, id: $id" } + channel.trySend(ExitHttpParser) + } catch (e: CancellationException) { + log.info { "Cancel HTTP1 parsing. message: ${e.message} id: $id" } + channel.trySend(ExitHttpParser) + } catch (e: Exception) { + log.error(e) { "Parse HTTP1 request exception. id: $id" } + } finally { + resetParser() + } + } + + fun parseNextRequest() { + channel.trySend(ParseNextHttpPacket) + } + + suspend fun endHttpParser() { + channel.trySend(ExitHttpParser) + parseRequestJob?.join() + } + + fun resetParser() { + parser.reset() + } + + private fun generateResponseJob() = responseHandler.generateResponseJob() + + fun getHeaderBufferSize() = config.headerBufferSize + + fun sendResponseMessage(message: Http1ResponseMessage) = responseHandler.sendResponseMessage(message) + + suspend fun endResponseHandler() { + responseHandler.endResponseHandler() + generateResponseJob?.join() + } + + override fun begin() { + if (beginning.compareAndSet(false, true)) { + if (requestHandler.connectionListener === HttpServerConnection.EMPTY_LISTENER) { + throw HttpServerConnectionListenerNotSetException("Please set connection listener before begin parsing.") + } + parseRequestJob = parseRequestJob() + generateResponseJob = generateResponseJob() + parseNextRequest() + } + } + + override fun setListener(listener: HttpServerConnection.Listener): HttpServerConnection { + Assert.state( + !beginning.get(), + "The HTTP request parser has started. Please set listener before begin parsing." + ) + requestHandler.connectionListener = listener + return this + } + + override fun getHttpVersion(): HttpVersion = HttpVersion.HTTP_1_1 + + override fun isSecureConnection(): Boolean = tcpConnection.isSecureConnection + + override fun getTcpConnection(): TcpConnection = tcpConnection +} + +sealed interface ParseHttpPacketMessage +object ParseNextHttpPacket : ParseHttpPacketMessage +object ExitHttpParser : ParseHttpPacketMessage \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerOutputChannel.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerOutputChannel.kt new file mode 100644 index 000000000..7ca4e58ef --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerOutputChannel.kt @@ -0,0 +1,79 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.server.HttpServerOutputChannel +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean + +class Http1ServerOutputChannel( + private val http1ServerConnection: Http1ServerConnection, + private val response: MetaData.Response, + private val closeConnection: Boolean +) : HttpServerOutputChannel { + + private val committed = AtomicBoolean(false) + private val closed = AtomicBoolean(false) + + override fun isCommitted(): Boolean = committed.get() + + override fun commit(): CompletableFuture { + return if (committed.compareAndSet(false, true)) { + val header = Header(response, CompletableFuture()) + http1ServerConnection.sendResponseMessage(header) + header.future + } else { + Result.DONE + } + } + + override fun write(byteBuffers: Array, offset: Int, length: Int): CompletableFuture { + val future = CompletableFuture() + val buffers = Http1OutputBuffers(byteBuffers, offset, length, Result.futureToConsumer(future)) + http1ServerConnection.sendResponseMessage(buffers) + return future + } + + override fun write(byteBufferList: List, offset: Int, length: Int): CompletableFuture { + val future = CompletableFuture() + val buffers = Http1OutputBufferList(byteBufferList, offset, length, Result.futureToConsumer(future)) + http1ServerConnection.sendResponseMessage(buffers) + return future + } + + override fun write(string: String): CompletableFuture { + return write(string, StandardCharsets.UTF_8) + } + + override fun write(string: String, charset: Charset): CompletableFuture { + val buffer = BufferUtils.toBuffer(string, charset) + return write(buffer) + } + + override fun write(byteBuffer: ByteBuffer): CompletableFuture { + val future = CompletableFuture() + val buffer = Http1OutputBuffer(byteBuffer, Result.futureToConsumer(future)) + http1ServerConnection.sendResponseMessage(buffer) + return future + } + + override fun closeAsync(): CompletableFuture { + return if (closed.compareAndSet(false, true)) { + val message = EndResponse(CompletableFuture(), closeConnection) + http1ServerConnection.sendResponseMessage(message) + message.future + } else { + Result.DONE + } + } + + override fun isOpen(): Boolean = !closed.get() + + override fun close() { + closeAsync() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerRequestHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerRequestHandler.kt new file mode 100644 index 000000000..468f3d030 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerRequestHandler.kt @@ -0,0 +1,355 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.codec.base64.Base64Utils +import com.fireflysource.common.io.toBuffer +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.common.v1.decoder.HttpParser +import com.fireflysource.net.http.common.v2.decoder.SettingsBodyParser +import com.fireflysource.net.http.common.v2.frame.SettingsFrame +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.router.AsyncRoutingContext +import com.fireflysource.net.websocket.common.impl.AsyncWebSocketConnection +import com.fireflysource.net.websocket.common.model.AcceptHash +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.nio.ByteBuffer + +class Http1ServerRequestHandler(private val connection: Http1ServerConnection) : HttpParser.RequestHandler { + + companion object { + private val log = SystemLogger.create(Http1ServerRequestHandler::class.java) + } + + var connectionListener: HttpServerConnection.Listener = HttpServerConnection.EMPTY_LISTENER + private val parserChannel: Channel = Channel(Channel.UNLIMITED) + private var expectUpgradeHttp2 = false + private var settingsFrame: SettingsFrame? = null + private var expectUpgradeWebsocket = false + + init { + handleParserMessageJob() + } + + private fun handleParserMessageJob() = connection.coroutineScope.launch { + var request: MetaData.Request? = null + var context: AsyncRoutingContext? = null + parserLoop@ while (true) { + val message = parserChannel.receive() + try { + when (message) { + is StartRequest -> request = newRequest(message) + is ParsedHeader -> addHeader(request, message) + is HeaderComplete -> context = newContextAndNotifyHeaderComplete(request) + is Content -> acceptContent(context, message) + is ContentComplete -> closeContentHandler(context) + is MessageComplete -> notifyHttpRequestComplete(context) + is BadMessage -> notifyException(request, context, message.exception) + is EarlyEOF -> notifyException(request, context, IllegalStateException("Parser early EOF")) + is EndRequestHandler -> { + log.info { "Exit the server request handler. id: ${connection.id}" } + break@parserLoop + } + } + } catch (e: Exception) { + notifyException(request, context, e) + } + } + } + + private fun newRequest(message: StartRequest): MetaData.Request { + val httpURI = if (message.method == HttpMethod.CONNECT.value) { + if (!message.uri.contains("://")) HttpURI("https://${message.uri}") + else HttpURI(message.uri) + } else HttpURI(message.uri) + return MetaData.Request(message.method, httpURI, message.version, HttpFields()) + } + + private fun addHeader(request: MetaData.Request?, message: ParsedHeader) { + requireNotNull(request) + request.fields.add(message.field) + } + + private suspend fun newContextAndNotifyHeaderComplete(request: MetaData.Request?): AsyncRoutingContext { + requireNotNull(request) + val httpServerRequest = AsyncHttpServerRequest( + request, + connection.config, + CoroutineScope(CoroutineName("Firefly-HTTP-server-request") + connection.coroutineDispatcher) + ) + + this.expectUpgradeHttp2 = HttpProtocolNegotiator.expectUpgradeHttp2(httpServerRequest) + if (expectUpgradeHttp2) { + val settingsBody = Base64Utils.decodeFromUrlSafeString(request.fields[HttpHeader.HTTP2_SETTINGS]) + val settings = SettingsBodyParser.parseBody(ByteBuffer.wrap(settingsBody)) + this.settingsFrame = settings + this.expectUpgradeHttp2 = settings != null + } + this.expectUpgradeWebsocket = HttpProtocolNegotiator.expectUpgradeWebsocket(httpServerRequest) + + + val ctx = newContext(httpServerRequest) + notifyHeaderComplete(ctx) + return ctx + } + + private fun newContext(request: MetaData.Request?): AsyncRoutingContext? { + return if (request != null) { + val httpServerRequest = AsyncHttpServerRequest( + request, + connection.config, + CoroutineScope(CoroutineName("Firefly-HTTP-server-request") + connection.coroutineDispatcher) + ) + newContext(httpServerRequest) + } else null + } + + private fun newContext(request: AsyncHttpServerRequest): AsyncRoutingContext { + val expect100 = request.httpFields.expectServerAcceptsContent() + val closeConnection = request.httpFields.isCloseConnection(request.httpVersion) + return AsyncRoutingContext( + request, + Http1ServerResponse(connection, expect100, closeConnection), + connection + ) + } + + private suspend fun notifyHeaderComplete(context: RoutingContext) { + connectionListener.onHeaderComplete(context).await() + } + + private fun acceptContent(context: AsyncRoutingContext?, message: Content) { + requireNotNull(context) + context.request.contentHandler.accept(message.byteBuffer, context) + } + + private suspend fun closeContentHandler(context: AsyncRoutingContext?) { + requireNotNull(context) + context.request.contentHandler.closeAsync().await() + } + + private suspend fun notifyHttpRequestComplete(context: RoutingContext?) { + requireNotNull(context) + context.request.isRequestComplete = true + when { + isHttpTunnel(context) -> { + val accept = connectionListener.onAcceptHttpTunnel(context.request).await() + if (accept) { + endHttpParser() + switchHttpTunnel(context) + endResponseHandler() + log.info { "Establish HTTP tunnel success. id: ${connection.id}" } + } else { + refuseHttpTunnelRequest(context) + } + } + isUpgradeHttp2(context) -> { + endHttpParser() + endResponseHandler() + switchToHttp2(context) + log.info { "Upgrade to HTTP2 success. id: ${connection.id}" } + } + isUpgradeWebsocket(context) -> { + endHttpParser() + endResponseHandler() + switchToWebSocket(context) + log.info { "Upgrade to Websocket success. id: ${connection.id}" } + } + else -> { + connection.parseNextRequest() + connectionListener.onHttpRequestComplete(context).await() + } + } + log.debug { "HTTP1 server handles request success. id: ${connection.id}" } + } + + private suspend fun endHttpParser() { + parserChannel.trySend(EndRequestHandler) + connection.endHttpParser() + log.info { "Upgrade protocol success. Exit HTTP1 parser. id: ${connection.id}" } + } + + private suspend fun endResponseHandler() { + connection.endResponseHandler() + } + + private fun isHttpTunnel(ctx: RoutingContext): Boolean { + return HttpMethod.CONNECT.`is`(ctx.method) && !connection.isSecureConnection + } + + private suspend fun switchHttpTunnel(ctx: RoutingContext) { + connectionListener.onAcceptHttpTunnelHandshakeResponse(ctx).await() + val address = InetSocketAddress(ctx.request.uri.host, ctx.request.uri.port) + connectionListener.onHttpTunnelHandshakeComplete(connection.tcpConnection, address) + } + + private suspend fun refuseHttpTunnelRequest(ctx: RoutingContext) { + connectionListener.onRefuseHttpTunnelHandshakeResponse(ctx).await() + } + + private suspend fun switchToHttp2(ctx: RoutingContext) { + val settings = settingsFrame + requireNotNull(settings) + + writeHttp2UpgradeResponse() + val http2ServerConnection = createHttp2Connection() + val stream = http2ServerConnection.upgradeHttp2(settings) + val response = Http2ServerResponse(http2ServerConnection, stream) + val http2Context = AsyncRoutingContext( + ctx.request, + response, + http2ServerConnection + ) + connectionListener.onHttpRequestComplete(http2Context).await() + } + + private fun isUpgradeWebsocket(context: RoutingContext) = + expectUpgradeWebsocket && !context.response.isCommitted + + private fun isUpgradeHttp2(context: RoutingContext) = + expectUpgradeHttp2 && !context.response.isCommitted + + private suspend fun switchToWebSocket(ctx: RoutingContext) { + val handler = connectionListener.onWebSocketHandshake(ctx).await() + + val clientKey = ctx.httpFields[HttpHeader.SEC_WEBSOCKET_KEY] + val serverAccept = AcceptHash.hashKey(clientKey) + + val clientExtensions = ctx.httpFields.getValuesList(HttpHeader.SEC_WEBSOCKET_EXTENSIONS) + val serverExtensions = handler.extensionSelector.select(clientExtensions) + + val clientSubProtocols = ctx.httpFields.getValuesList(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL) + val serverSubProtocols = handler.subProtocolSelector.select(clientSubProtocols) + + val message = buildString { + append("HTTP/1.1 101 Switching Protocols\r\n") + append("Connection: Upgrade\r\n") + append("Upgrade: websocket\r\n") + append("${HttpHeader.SEC_WEBSOCKET_ACCEPT.value}: ${serverAccept}\r\n") + if (!serverExtensions.isNullOrEmpty()) { + append("${HttpHeader.SEC_WEBSOCKET_EXTENSIONS.value}: ${serverExtensions.joinToString(", ")}\r\n") + } + if (!serverSubProtocols.isNullOrEmpty()) { + append("${HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.value}: ${serverSubProtocols.joinToString(", ")}\r\n") + } + append("\r\n") + }.toBuffer() + connection.tcpConnection.writeAndFlush(message).await() + log.info { "Server response 101 Switching Protocols. upgrade: websocket, id: ${connection.id}" } + + val webSocketConnection = AsyncWebSocketConnection( + connection.tcpConnection, + handler.policy, + handler.url, + serverExtensions ?: listOf(), + AsyncWebSocketConnection.defaultExtensionFactory, + serverSubProtocols ?: listOf() + ) + webSocketConnection.setWebSocketMessageHandler(handler.messageHandler) + webSocketConnection.begin() + handler.connectionListener.accept(webSocketConnection).await() + } + + private fun createHttp2Connection() = + Http2ServerConnection(connection.config, connection.tcpConnection) + .also { it.setListener(connectionListener).begin() } + + private suspend fun writeHttp2UpgradeResponse() { + val message = ("HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: h2c\r\n\r\n").toBuffer() + connection.tcpConnection.writeAndFlush(message).await() + log.info { "Server response 101 Switching Protocols. upgrade: h2c, id: ${connection.id}" } + } + + private suspend fun notifyException(request: MetaData.Request?, context: RoutingContext?, exception: Throwable) { + val ctx = context ?: newContext(request) + try { + log.error(exception) { "HTTP1 server parser exception. id: ${connection.id}" } + connectionListener.onException(ctx, exception).await() + } catch (e: Exception) { + log.error(e) { "HTTP1 server handles exception failure. id: ${connection.id}" } + } + when { + exception is BadMessageException -> closeConnection() + ctx == null -> closeConnection() + !ctx.request.isRequestComplete -> connection.parseNextRequest() + } + } + + private suspend fun closeConnection() { + endHttpParser() + endResponseHandler() + connection.closeAsync() + } + + override fun startRequest(method: String, uri: String, version: HttpVersion): Boolean { + parserChannel.trySend(StartRequest(method, uri, version)) + return false + } + + override fun getHeaderCacheSize(): Int = 4096 + + override fun parsedHeader(field: HttpField) { + parserChannel.trySend(ParsedHeader(field)) + } + + override fun headerComplete(): Boolean { + parserChannel.trySend(HeaderComplete) + return false + } + + override fun content(byteBuffer: ByteBuffer): Boolean { + parserChannel.trySend(Content(byteBuffer)) + return false + } + + override fun contentComplete(): Boolean { + parserChannel.trySend(ContentComplete) + return false + } + + override fun messageComplete(): Boolean { + parserChannel.trySend(MessageComplete) + return true + } + + override fun earlyEOF() { + parserChannel.trySend(EarlyEOF) + } + + override fun badMessage(failure: BadMessageException) { + parserChannel.trySend(BadMessage(failure)) + } + +} + +sealed interface ParserMessage + +data class StartRequest(val method: String, val uri: String, val version: HttpVersion) : ParserMessage + +@JvmInline +value class ParsedHeader(val field: HttpField) : ParserMessage + +object HeaderComplete : ParserMessage + +@JvmInline +value class Content(val byteBuffer: ByteBuffer) : ParserMessage + +object ContentComplete : ParserMessage + +object MessageComplete : ParserMessage + +object EarlyEOF : ParserMessage + +@JvmInline +value class BadMessage(val exception: Exception) : ParserMessage + +object EndRequestHandler : ParserMessage \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerResponse.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerResponse.kt new file mode 100644 index 000000000..9faa26d58 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerResponse.kt @@ -0,0 +1,44 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.io.toBuffer +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.server.HttpServerOutputChannel +import java.nio.ByteBuffer +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean + +class Http1ServerResponse( + private val http1ServerConnection: Http1ServerConnection, + private val expect100Continue: Boolean, + private val closeConnection: Boolean +) : AbstractHttpServerResponse(http1ServerConnection) { + + private val write100Continue = AtomicBoolean(false) + private val write200ConnectionEstablished = AtomicBoolean(false) + + override fun createHttpServerOutputChannel(response: MetaData.Response): HttpServerOutputChannel { + if (expect100Continue && !write100Continue.get()) { + http1ServerConnection.resetParser() + } + return Http1ServerOutputChannel(http1ServerConnection, response, closeConnection) + } + + override fun response100Continue(): CompletableFuture { + return if (write100Continue.compareAndSet(false, true)) { + writeAndFlushHttpMessage("HTTP/1.1 100 Continue\r\n".toBuffer()) + } else Result.DONE + } + + override fun response200ConnectionEstablished(): CompletableFuture { + return if (write200ConnectionEstablished.compareAndSet(false, true)) { + writeAndFlushHttpMessage("HTTP/1.1 200 Connection Established\r\n\r\n".toBuffer()) + } else Result.DONE + } + + private fun writeAndFlushHttpMessage(message: ByteBuffer): CompletableFuture { + val connection = http1ServerConnection.tcpConnection + return connection.writeAndFlush(message).thenCompose { Result.DONE } + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerResponseHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerResponseHandler.kt new file mode 100644 index 000000000..4c9720657 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http1ServerResponseHandler.kt @@ -0,0 +1,235 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.exception.Http1GeneratingResultException +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v1.encoder.HttpGenerator +import com.fireflysource.net.http.common.v1.encoder.assert +import com.fireflysource.net.tcp.buffer.DelegatedOutputBufferArray +import com.fireflysource.net.tcp.buffer.OutputBufferArray +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +class Http1ServerResponseHandler(private val http1ServerConnection: Http1ServerConnection) { + + companion object { + private val log = SystemLogger.create(Http1ServerResponseHandler::class.java) + } + + private val generator = HttpGenerator() + private val headerBuffer = BufferUtils.allocateDirect(http1ServerConnection.getHeaderBufferSize()) + private val chunkBuffer: ByteBuffer by lazy(LazyThreadSafetyMode.NONE) { BufferUtils.allocateDirect(HttpGenerator.CHUNK_SIZE) } + private val responseChannel: Channel = Channel(Channel.UNLIMITED) + + fun sendResponseMessage(message: Http1ResponseMessage) { + responseChannel.trySend(message) + } + + fun generateResponseJob() = http1ServerConnection.coroutineScope.launch { + responseLoop@ while (true) { + when (val message = responseChannel.receive()) { + is Header -> generateHeader(message) + is Http1OutputBuffer -> generateContent(message) + is Http1OutputBufferList -> generateContent(message) + is Http1OutputBuffers -> generateContent(message) + is EndResponse -> completeContent(message) + is EndResponseHandler -> { + log.info { "Exit the server response handler. id: ${http1ServerConnection.id}" } + break@responseLoop + } + } + } + } + + fun endResponseHandler() { + responseChannel.trySend(EndResponseHandler) + } + + private suspend fun generateHeader(header: Header) { + val (response, future) = header + try { + generator.generateResponse(response, false, headerBuffer, null, null, false) + .assertFlush() + flushHeaderBuffer() + Result.done(future) + } catch (e: Exception) { + future.completeExceptionally(e) + } + } + + private suspend fun generateContent(http1OutputBuffers: Http1OutputBuffers) { + try { + val content = LinkedList() + val offset = http1OutputBuffers.getCurrentOffset() + val lastIndex = http1OutputBuffers.getLastIndex() + (offset..lastIndex).forEach { + val buffer = http1OutputBuffers.buffers[it] + if (generator.isChunking) { + val chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE) + generator.generateResponse(null, false, null, chunk, buffer, false) + .assertFlush() + content.add(chunk) + content.add(buffer) + } else { + generator.generateResponse(null, false, null, null, buffer, false) + .assertFlush() + content.add(buffer) + } + } + val length = http1ServerConnection.tcpConnection.write(content, 0, content.size).await() + http1OutputBuffers.result.accept(Result(true, length, null)) + } catch (e: Exception) { + http1OutputBuffers.result.accept(Result(false, -1, e)) + } + } + + private suspend fun generateContent(http1OutputBuffer: Http1OutputBuffer) { + val (buffer, result) = http1OutputBuffer + try { + val length = if (generator.isChunking) { + generator.generateResponse(null, false, null, chunkBuffer, buffer, false) + .assertFlush() + flushChunkedContentBuffer(buffer).toInt() + } else { + generator.generateResponse(null, false, null, null, buffer, false) + .assertFlush() + flushContentBuffer(buffer) + } + result.accept(Result(true, length, null)) + } catch (e: Exception) { + result.accept(Result(false, -1, e)) + } + } + + private fun HttpGenerator.Result.assertFlush() { + this.assert(HttpGenerator.Result.FLUSH) + assert(HttpGenerator.State.COMMITTED) + } + + private suspend fun completeContent(endResponse: EndResponse) { + try { + completing() + if (generator.isChunking) { + when (val generateResult = generator.generateResponse(null, false, null, chunkBuffer, null, true)) { + HttpGenerator.Result.FLUSH -> { + assert(HttpGenerator.State.COMPLETING) + flushChunkBuffer() + } + HttpGenerator.Result.NEED_CHUNK_TRAILER -> generateTrailer() + else -> throw Http1GeneratingResultException("The HTTP server generator result error. $generateResult") + } + } + end(endResponse) + Result.done(endResponse.future) + } catch (e: Exception) { + endResponse.future.completeExceptionally(e) + } + } + + private fun completing() { + generator.generateResponse(null, false, null, null, null, true) + .assert(HttpGenerator.Result.CONTINUE) + assert(HttpGenerator.State.COMPLETING) + } + + private suspend fun end(endResponse: EndResponse) { + http1ServerConnection.tcpConnection.flush().await() + val result = generator.generateResponse(null, false, null, null, null, true) + if (result == HttpGenerator.Result.SHUTDOWN_OUT || endResponse.closeConnection) { + http1ServerConnection.closeAsync() + log.debug { "HTTP1 server connection is closing. id: ${http1ServerConnection.id}" } + } + + assert(HttpGenerator.State.END) + generator.reset() + } + + private suspend fun generateTrailer() { + generator.generateResponse(null, false, null, headerBuffer, null, true) + .assert(HttpGenerator.Result.FLUSH) + assert(HttpGenerator.State.COMPLETING) + flushHeaderBuffer() + } + + private fun assert(expectState: HttpGenerator.State) { + if (!generator.isState(expectState)) { + throw Http1GeneratingResultException("The HTTP generator state error. ${generator.state}") + } + } + + private suspend fun flushHeaderBuffer() { + if (headerBuffer.hasRemaining()) { + val size = http1ServerConnection.tcpConnection.write(headerBuffer).await() + log.debug { "flush header bytes: $size" } + } + BufferUtils.clear(headerBuffer) + } + + private suspend fun flushContentBuffer(contentBuffer: ByteBuffer): Int { + return if (contentBuffer.hasRemaining()) { + val size = http1ServerConnection.tcpConnection.write(contentBuffer).await() + log.debug { "flush content bytes: $size" } + size + } else 0 + } + + private suspend fun flushChunkedContentBuffer(contentBuffer: ByteBuffer): Long { + val bufArray = arrayOf(chunkBuffer, contentBuffer) + val remaining = bufArray.sumOf { it.remaining().toLong() } + val length = if (remaining > 0) { + val len = http1ServerConnection.tcpConnection.write(bufArray, 0, bufArray.size).await() + log.debug { "flush chunked content bytes: $len" } + len + } else 0 + BufferUtils.clear(chunkBuffer) + return length + } + + private suspend fun flushChunkBuffer() { + if (chunkBuffer.hasRemaining()) { + val size = http1ServerConnection.tcpConnection.write(chunkBuffer).await() + log.debug { "flush chunked bytes: $size" } + } + BufferUtils.clear(chunkBuffer) + } + +} + +sealed class Http1ResponseMessage + +data class Header( + val response: MetaData.Response, + val future: CompletableFuture +) : Http1ResponseMessage() + +data class Http1OutputBuffer( + val buffer: ByteBuffer, val result: Consumer> +) : Http1ResponseMessage() + +open class Http1OutputBuffers( + val buffers: Array, + val offset: Int, + val length: Int, + val result: Consumer>, + private val outputBufferArray: DelegatedOutputBufferArray = DelegatedOutputBufferArray( + buffers, offset, length, result + ) +) : OutputBufferArray by outputBufferArray, Http1ResponseMessage() + +class Http1OutputBufferList( + bufferList: List, + offset: Int, + length: Int, + result: Consumer> +) : Http1OutputBuffers(bufferList.toTypedArray(), offset, length, result) + +data class EndResponse(val future: CompletableFuture, val closeConnection: Boolean) : Http1ResponseMessage() + +object EndResponseHandler : Http1ResponseMessage() \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerConnection.kt new file mode 100644 index 000000000..00536a819 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerConnection.kt @@ -0,0 +1,117 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.exception.HttpServerConnectionListenerNotSetException +import com.fireflysource.net.http.common.v2.decoder.ServerParser +import com.fireflysource.net.http.common.v2.frame.* +import com.fireflysource.net.http.common.v2.stream.* +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.tcp.TcpConnection +import java.util.function.UnaryOperator + +class Http2ServerConnection( + config: HttpConfig, + tcpConnection: TcpConnection, + flowControl: FlowControl = BufferedFlowControlStrategy(), + private val listener: Http2Connection.Listener = Http2ServerConnectionListener() +) : AsyncHttp2Connection(2, config, tcpConnection, flowControl, listener), ServerParser.Listener, HttpServerConnection { + + companion object { + private val log = SystemLogger.create(Http2ServerConnection::class.java) + } + + private val parser: ServerParser = ServerParser(this, config.maxDynamicTableSize, config.maxHeaderSize) + private var connectionListener: HttpServerConnection.Listener = HttpServerConnection.EMPTY_LISTENER + + init { + parser.init(UnaryOperator.identity()) + } + + fun upgradeHttp2(settingsFrame: SettingsFrame): Stream { + super.onSettings(settingsFrame) + val stream = createRemoteStream(1) + requireNotNull(stream) + return stream + } + + override fun begin() { + if (listener is Http2ServerConnectionListener) { + if (connectionListener === HttpServerConnection.EMPTY_LISTENER) { + throw HttpServerConnectionListenerNotSetException("Please set connection listener before begin parsing.") + } + listener.connectionListener = connectionListener + } + launchParserJob(parser) + } + + // preface frame + override fun onPreface() { + val settings = notifyPreface() + val settingsFrame = SettingsFrame(settings, false) + val windowDelta: Int = initialSessionRecvWindow - HttpConfig.DEFAULT_WINDOW_SIZE + + log.info { "HTTP2 server on preface. id: $id, window delta: $windowDelta, settings: $settingsFrame" } + if (windowDelta > 0) { + updateRecvWindow(windowDelta) + sendControlFrame(null, settingsFrame, WindowUpdateFrame(0, windowDelta)) + } else sendControlFrame(null, settingsFrame) + } + + // headers frame + override fun onHeaders(frame: HeadersFrame) { + log.debug { "Received $frame" } + + val streamId = frame.streamId + if (!isClientStream(streamId)) { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_stream_id") + return + } + + val stream = getStream(streamId) + val metaData = frame.metaData + when { + metaData.isRequest -> onHttpRequest(stream, streamId, frame) + else -> onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_request") + } + } + + private fun onHttpRequest(stream: Stream?, streamId: Int, frame: HeadersFrame) { + if (stream == null) { + if (isRemoteStreamClosed(streamId)) { + onConnectionFailure(ErrorCode.STREAM_CLOSED_ERROR.code, "unexpected_headers_frame") + } else { + val remoteStream = createRemoteStream(streamId) + if (remoteStream != null && remoteStream is AsyncHttp2Stream) { + onStreamOpened(remoteStream) + remoteStream.process(frame, discard()) + remoteStream.listener = notifyNewStream(remoteStream, frame) + } + } + } else { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream") + } + } + + // promise frame + override fun onPushPromise(frame: PushPromiseFrame) { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "push_promise") + } + + override fun onResetForUnknownStream(frame: ResetFrame) { + val streamId = frame.streamId + val closed = if (isClientStream(streamId)) isRemoteStreamClosed(streamId) else isLocalStreamClosed(streamId) + if (closed) { + notifyReset(this, frame) + } else { + onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_rst_stream_frame") + } + } + + override fun setListener(listener: HttpServerConnection.Listener): HttpServerConnection { + this.connectionListener = listener + return this + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerConnectionListener.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerConnectionListener.kt new file mode 100644 index 000000000..0628e77e6 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerConnectionListener.kt @@ -0,0 +1,175 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.concurrent.CompletableFutures +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v2.frame.* +import com.fireflysource.net.http.common.v2.stream.Http2Connection +import com.fireflysource.net.http.common.v2.stream.Stream +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.router.AsyncRoutingContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +class Http2ServerConnectionListener : Http2Connection.Listener.Adapter() { + + companion object { + private val log = SystemLogger.create(Http2ServerConnectionListener::class.java) + } + + var connectionListener: HttpServerConnection.Listener = HttpServerConnection.EMPTY_LISTENER + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + val http2Connection = stream.http2Connection as Http2ServerConnection + + val request = AsyncHttpServerRequest( + frame.metaData as MetaData.Request, + http2Connection.config, + CoroutineScope(CoroutineName("Firefly-HTTP-server-request") + http2Connection.coroutineDispatcher) + ) + val response = Http2ServerResponse(http2Connection, stream) + val context = AsyncRoutingContext( + request, + response, + http2Connection + ) + val trailer: HttpFields by lazy { HttpFields() } + var receivedData = false + var headerComplete: CompletableFuture? = null + + if (frame.isEndHeaders) { + headerComplete = notifyHeaderComplete(context) + } + if (frame.isEndStream) { + if (headerComplete != null) { + headerComplete.thenCompose { notifyRequestComplete(context) } + .exceptionallyAccept { notifyException(context, it) } + } else { + notifyException(context, IllegalStateException("The header complete future must not be null")) + } + } + + return object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + log.debug { "HTTP2 server received trailer frame. id: ${stream.id}" } + if (receivedData) { + trailer.addAll(frame.metaData.fields) + } else { + request.httpFields.addAll(frame.metaData.fields) + } + if (frame.isEndHeaders) { + headerComplete = notifyHeaderComplete(context) + } + if (frame.isEndStream) { + val future = headerComplete + if (future != null) { + future.thenCompose { notifyRequestComplete(context) } + .exceptionallyAccept { notifyException(context, it) } + } else { + notifyException(context, IllegalStateException("The header complete future must not be null")) + } + } + } + + override fun onData(stream: Stream, frame: DataFrame, result: Consumer>) { + receivedData = true + try { + context.request.contentHandler.accept(frame.data, context) + log.debug { "HTTP2 server accepts content success. id: ${stream.id}" } + + if (frame.isEndStream) { + val future = headerComplete + if (future != null) { + future.thenCompose { context.request.contentHandler.closeAsync() } + .thenCompose { notifyRequestComplete(context) } + .thenAccept { result.accept(Result.SUCCESS) } + .exceptionallyAccept { e -> + notifyException(context, e) + .thenAccept { result.accept(Result.createFailedResult(e)) } + .exceptionallyAccept { result.accept(Result.createFailedResult(e)) } + } + } else { + val e = IllegalStateException("The header complete future must not be null") + notifyException(context, e) + .thenAccept { result.accept(Result.createFailedResult(e)) } + .exceptionallyAccept { result.accept(Result.createFailedResult(e)) } + } + } else { + result.accept(Result.SUCCESS) + } + } catch (e: Exception) { + log.error { "HTTP2 server accepts content exception. id: ${stream.id} info: ${e.javaClass.name} ${e.message}" } + notifyException(context, e) + .thenAccept { result.accept(Result.createFailedResult(e)) } + .exceptionallyAccept { result.accept(Result.createFailedResult(e)) } + } + } + + override fun onClosed(stream: Stream) { + log.debug { "HTTP2 server stream closed. id: ${stream.id}" } + } + + override fun onReset(stream: Stream, frame: ResetFrame, result: Consumer>) { + val e = IllegalStateException(ErrorCode.toString(frame.error, "stream reset. id: ${stream.id}")) + notifyException(context, e) + .thenAccept { result.accept(Result.SUCCESS) } + .exceptionallyAccept { result.accept(Result.createFailedResult(it)) } + } + + override fun onFailure(stream: Stream, error: Int, reason: String, result: Consumer>) { + val defaultError = "stream failure. id: ${stream.id}, reason: $reason" + val e = IllegalStateException(ErrorCode.toString(error, defaultError)) + notifyException(context, e) + .thenAccept { result.accept(Result.SUCCESS) } + .exceptionallyAccept { result.accept(Result.createFailedResult(it)) } + } + + override fun onIdleTimeout(stream: Stream, e: Throwable): Boolean { + notifyException(context, e) + return true + } + + } + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + log.info { "HTTP2 server connection closed. id: ${http2Connection.id}, frame: $frame" } + } + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + notifyException(null, failure) + } + + override fun onReset(http2Connection: Http2Connection, frame: ResetFrame) { + val e = IllegalStateException(ErrorCode.toString(frame.error, "stream exception")) + notifyException(null, e) + } + + private fun notifyHeaderComplete(context: RoutingContext): CompletableFuture = try { + connectionListener.onHeaderComplete(context) + } catch (e: Exception) { + log.error(e) { "HTTP2 server handles header complete exception. id: ${context.connection.id}" } + notifyException(context, e) + } + + private fun notifyRequestComplete(context: RoutingContext): CompletableFuture = try { + context.request.isRequestComplete = true + connectionListener.onHttpRequestComplete(context) + } catch (e: Exception) { + log.error(e) { "HTTP2 server handles header complete exception. id: ${context.connection.id}" } + notifyException(context, e) + } + + private fun notifyException(context: RoutingContext?, e: Throwable): CompletableFuture = try { + connectionListener.onException(context, e) + } catch (t: Throwable) { + log.error(t) { "HTTP2 server handler exception. id: ${context?.connection?.id}" } + CompletableFutures.failedFuture(t) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerOutputChannel.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerOutputChannel.kt new file mode 100644 index 000000000..0f345fe44 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerOutputChannel.kt @@ -0,0 +1,172 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v2.frame.DataFrame +import com.fireflysource.net.http.common.v2.frame.Frame +import com.fireflysource.net.http.common.v2.frame.HeadersFrame +import com.fireflysource.net.http.common.v2.stream.Stream +import com.fireflysource.net.http.server.HttpServerOutputChannel +import com.fireflysource.net.tcp.buffer.DelegatedOutputBufferArray +import com.fireflysource.net.tcp.buffer.OutputBufferArray +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean + +/** + * @author Pengtao Qiu + */ +class Http2ServerOutputChannel( + private val response: MetaData.Response, + private val stream: Stream +) : HttpServerOutputChannel { + + companion object { + private val log = SystemLogger.create(Http2ServerOutputChannel::class.java) + private const val defaultMaxFrameSize = Frame.DEFAULT_MAX_LENGTH.toLong() + } + + private val committed = AtomicBoolean(false) + private val closed = AtomicBoolean(false) + private val messages = LinkedList() + + override fun commit(): CompletableFuture { + if (committed.compareAndSet(false, true)) { + messages.offer(HeadersOutputMessage) + } + return Result.DONE + } + + override fun isCommitted(): Boolean = committed.get() + + override fun write(byteBuffers: Array, offset: Int, length: Int): CompletableFuture { + val message = BuffersOutputMessage(byteBuffers, offset, length) + messages.offer(message) + writeOutputMessage() + + val future = CompletableFuture() + future.complete(message.remaining()) + return future + } + + override fun write(byteBufferList: List, offset: Int, length: Int): CompletableFuture { + return write(byteBufferList.toTypedArray(), offset, length) + } + + override fun write(string: String): CompletableFuture { + return write(string, StandardCharsets.UTF_8) + } + + override fun write(string: String, charset: Charset): CompletableFuture { + val byteBuffer = BufferUtils.toBuffer(string, charset) + return write(byteBuffer) + } + + override fun write(byteBuffer: ByteBuffer): CompletableFuture { + val message = BufferOutputMessage(byteBuffer) + messages.offer(message) + writeOutputMessage() + + val future = CompletableFuture() + future.complete(byteBuffer.remaining()) + return future + } + + override fun isOpen(): Boolean = closed.get() + + override fun closeAsync(): CompletableFuture { + if (closed.compareAndSet(false, true)) { + writeOutputMessage() + val trailers = response.trailerSupplier?.get() + if (trailers != null) { + val trailerMetaData = MetaData.Response(trailers) + trailerMetaData.isOnlyTrailer = true + val headersFrameTrailer = HeadersFrame(stream.id, trailerMetaData, null, true) + stream.headers(headersFrameTrailer, discard()) + } + } + return Result.DONE + } + + override fun close() { + closeAsync() + } + + private fun writeOutputMessage() { + val message = messages.poll() + if (message != null) { + val last = messages.isEmpty() && response.trailerSupplier == null + when (message) { + is HeadersOutputMessage -> writeHeaders(last) + is BufferOutputMessage -> writeBuffer(message, last) + is BuffersOutputMessage -> writeBuffers(message, last) + } + } + } + + private fun writeHeaders(last: Boolean) { + val headersFrame = HeadersFrame(stream.id, response, null, last) + stream.headers(headersFrame, discard()) + } + + private fun writeBuffer(message: BufferOutputMessage, last: Boolean) { + val dataFrame = DataFrame(stream.id, message.byteBuffer, last) + stream.data(dataFrame, discard()) + } + + private fun writeBuffers(message: BuffersOutputMessage, last: Boolean) { + val length = message.getCurrentLength() + when { + length == 1 -> { + val byteBuffer = message.byteBuffers[message.getCurrentOffset()] + val dataFrame = DataFrame(stream.id, byteBuffer, last) + stream.data(dataFrame, discard()) + } + length > 1 -> { + while (message.hasRemaining()) { + val remaining = message.remaining() + val size = remaining.coerceAtMost(defaultMaxFrameSize).toInt() + val buffer = BufferUtils.allocate(size) + val pos = buffer.flipToFill() + + log.debug { "HTTP2 outputs buffer array. before remaining: $remaining, size: $size" } + while (buffer.hasRemaining()) { + val offset = message.getCurrentOffset() + val src = message.byteBuffers[offset] + BufferUtils.put(src, buffer) + log.debug { "HTTP2 outputs buffer array. put offset: $offset" } + } + + buffer.flipToFlush(pos) + val end = last && !message.hasRemaining() + val dataFrame = DataFrame(stream.id, buffer, end) + stream.data(dataFrame, discard()) + log.debug { "HTTP2 outputs buffer array. after remaining: ${message.remaining()}, end: $end" } + } + } + } + } + +} + +sealed interface Http2OutputMessage + +object HeadersOutputMessage : Http2OutputMessage + +@JvmInline +value class BufferOutputMessage(val byteBuffer: ByteBuffer) : Http2OutputMessage + +class BuffersOutputMessage( + val byteBuffers: Array, val offset: Int, val length: Int, + private val delegatedBufferArray: DelegatedOutputBufferArray = DelegatedOutputBufferArray( + byteBuffers, offset, length, discard() + ) +) : OutputBufferArray by delegatedBufferArray, Http2OutputMessage \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerResponse.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerResponse.kt new file mode 100644 index 000000000..51bc2dc42 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/Http2ServerResponse.kt @@ -0,0 +1,39 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.HttpVersion +import com.fireflysource.net.http.common.model.MetaData +import com.fireflysource.net.http.common.v2.frame.HeadersFrame +import com.fireflysource.net.http.common.v2.stream.Stream +import com.fireflysource.net.http.server.HttpServerOutputChannel +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean + +/** + * @author Pengtao Qiu + */ +class Http2ServerResponse( + http2ServerConnection: Http2ServerConnection, + private val stream: Stream +) : AbstractHttpServerResponse(http2ServerConnection) { + + private val write100Continue = AtomicBoolean(false) + + override fun createHttpServerOutputChannel(response: MetaData.Response): HttpServerOutputChannel { + return Http2ServerOutputChannel(response, stream) + } + + override fun response100Continue(): CompletableFuture { + return if (write100Continue.compareAndSet(false, true)) { + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.CONTINUE_100, HttpFields()) + val headers = HeadersFrame(stream.id, response, null, false) + val future = CompletableFuture() + stream.headers(headers, Result.futureToConsumer(future)) + future + } else Result.DONE + } + + override fun response200ConnectionEstablished(): CompletableFuture = Result.DONE +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/AsyncMultiPart.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/AsyncMultiPart.kt new file mode 100644 index 000000000..ec7605133 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/AsyncMultiPart.kt @@ -0,0 +1,141 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.deleteIfExistsAsync +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.content.handler.AbstractByteBufferContentHandler +import com.fireflysource.net.http.common.content.handler.AbstractFileContentHandler +import com.fireflysource.net.http.common.content.provider.AbstractByteBufferContentProvider +import com.fireflysource.net.http.common.content.provider.AbstractFileContentProvider +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.MultiPart +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.future.await +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.util.concurrent.CompletableFuture + +class AsyncMultiPart( + private val maxFileSize: Long, + private val fileSizeThreshold: Int, + private val path: Path +) : MultiPart { + + companion object { + private val log = SystemLogger.create(AsyncMultiPart::class.java) + } + + private val byteBufferHandler = object : AbstractByteBufferContentHandler() {} + private var fileHandler: AbstractFileContentHandler? = null + private var fileHandlerFuture: CompletableFuture? = null + private val fileProvider: AbstractFileContentProvider by lazy { + object : AbstractFileContentProvider(path, StandardOpenOption.READ) {} + } + private val byteBufferProvider: AbstractByteBufferContentProvider by lazy { + val size = byteBufferHandler.getByteBuffers().sumOf { it.remaining() } + val buf = BufferUtils.allocate(size) + val pos = buf.flipToFill() + byteBufferHandler.getByteBuffers().forEach { BufferUtils.put(it, buf) } + buf.flipToFlush(pos) + object : AbstractByteBufferContentProvider(buf) {} + } + private val httpFields = HttpFields() + private var size: Long = 0 + private var name: String = "" + private var fileName: String = "" + private var exceededFileThreshold = false + + override fun getName(): String = name + + fun setName(name: String) { + this.name = name + } + + override fun getFileName(): String = fileName + + fun setFileName(fileName: String) { + this.fileName = fileName + } + + override fun getHttpFields(): HttpFields = httpFields + + override fun getSize(): Long = size + + fun accept(item: ByteBuffer, last: Boolean) { + size += item.remaining() + log.debug { "Multi part accepts data. name: $name size: ${item.remaining()}, last: $last" } + if (size > maxFileSize) { + throw BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413) + } + + if (size <= fileSizeThreshold) { + if (item.hasRemaining()) byteBufferHandler.accept(item, null) + } else { + val fileHandler = this.fileHandler + if (fileHandler != null) { + if (item.hasRemaining()) fileHandler.accept(item, null) + } else { + exceededFileThreshold = true + val newFileHandler = object : AbstractFileContentHandler( + path, + StandardOpenOption.WRITE, + StandardOpenOption.CREATE_NEW + ) {} + this.fileHandler = newFileHandler + byteBufferHandler.getByteBuffers().forEach { newFileHandler.accept(it, null) } + if (item.hasRemaining()) newFileHandler.accept(item, null) + } + + if (last) { + fileHandlerFuture = this.fileHandler?.closeAsync() + } + } + } + + suspend fun closeFileHandler() { + fileHandler?.closeAsync()?.await() + } + + override fun getContentType(): String = httpFields[HttpHeader.CONTENT_TYPE] + + override fun read(byteBuffer: ByteBuffer): CompletableFuture { + return getProvider().read(byteBuffer) + } + + override fun getStringBody(charset: Charset): String { + return if (exceededFileThreshold) "" + else { + val buffer = byteBufferProvider.toByteBuffer() + BufferUtils.toString(buffer, charset) + } + } + + override fun isOpen(): Boolean { + return getProvider().isOpen + } + + override fun close() { + closeAsync() + } + + override fun closeAsync(): CompletableFuture { + return getProvider().closeAsync() + .thenCompose { deleteIfExistsAsync(path).asCompletableFuture() } + .thenCompose { Result.DONE } + } + + private fun getProvider() = if (exceededFileThreshold) fileProvider else byteBufferProvider + + override fun toString(): String { + return "AsyncMultiPart(maxFileSize=$maxFileSize, fileSizeThreshold=$fileSizeThreshold, path=$path, httpFields=${httpFields.size()}, size=$size, name='$name', fileName='$fileName', exceededFileThreshold=$exceededFileThreshold)" + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/ByteBufferContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/ByteBufferContentHandler.kt new file mode 100644 index 000000000..2ddc88250 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/ByteBufferContentHandler.kt @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.net.http.common.content.handler.AbstractByteBufferContentHandler +import com.fireflysource.net.http.server.HttpServerContentHandler +import com.fireflysource.net.http.server.RoutingContext + +open class ByteBufferContentHandler(maxRequestBodySize: Long = 200 * 1024 * 1024) : + AbstractByteBufferContentHandler(maxRequestBodySize), HttpServerContentHandler \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/FileContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/FileContentHandler.kt new file mode 100644 index 000000000..4b5b07365 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/FileContentHandler.kt @@ -0,0 +1,10 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.net.http.common.content.handler.AbstractFileContentHandler +import com.fireflysource.net.http.server.HttpServerContentHandler +import com.fireflysource.net.http.server.RoutingContext +import java.nio.file.OpenOption +import java.nio.file.Path + +class FileContentHandler(path: Path, vararg options: OpenOption) : + AbstractFileContentHandler(path, *options), HttpServerContentHandler \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/FormInputsContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/FormInputsContentHandler.kt new file mode 100644 index 000000000..c2ecf3f9d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/FormInputsContentHandler.kt @@ -0,0 +1,32 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.net.http.common.codec.UrlEncoded +import com.fireflysource.net.http.common.model.ContentEncoding +import java.nio.charset.StandardCharsets +import java.util.* + +class FormInputsContentHandler(maxRequestBodySize: Long = 200 * 1024 * 1024) : + StringContentHandler(maxRequestBodySize) { + + private var urlEncoded: UrlEncoded? = null + + fun getFormInput(name: String, encoding: Optional = Optional.empty()): String { + return getUrlEncoded(encoding).getString(name) ?: "" + } + + fun getFormInputs(name: String, encoding: Optional = Optional.empty()): List { + return getUrlEncoded(encoding)[name] ?: listOf() + } + + fun getFormInputs(encoding: Optional = Optional.empty()): Map> = + getUrlEncoded(encoding) + + private fun getUrlEncoded(encoding: Optional): UrlEncoded { + val e = urlEncoded + return if (e == null) { + val encoded = UrlEncoded(this.toString(StandardCharsets.UTF_8, encoding)) + urlEncoded = encoded + encoded + } else e + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/MultiPartContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/MultiPartContentHandler.kt new file mode 100644 index 000000000..8b4dd0250 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/MultiPartContentHandler.kt @@ -0,0 +1,315 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.common.concurrent.CompletableFutures +import com.fireflysource.common.coroutine.clear +import com.fireflysource.common.string.QuotedStringTokenizer +import com.fireflysource.common.string.QuotedStringTokenizer.unquote +import com.fireflysource.common.string.QuotedStringTokenizer.unquoteOnly +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.http.common.codec.MultiPartParser +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.HttpServerContentHandler +import com.fireflysource.net.http.server.MultiPart +import com.fireflysource.net.http.server.RoutingContext +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.asCompletableFuture +import java.nio.ByteBuffer +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.CompletableFuture + +class MultiPartContentHandler( + private val maxUploadFileSize: Long = 200 * 1024 * 1024, + private val maxRequestBodySize: Long = 200 * 1024 * 1024, + private val uploadFileSizeThreshold: Int = 4 * 1024 * 1024, + private val scope: CoroutineScope = CoroutineScope(CoroutineName("Firefly-file-content-provider")), + private val path: Path = tempPath +) : HttpServerContentHandler { + + companion object { + private val log = SystemLogger.create(MultiPartContentHandler::class.java) + private val tempPath = Paths.get(System.getProperty("java.io.tmpdir")) + } + + init { + require(maxRequestBodySize >= maxUploadFileSize) { "The max request size must be greater than the max file size." } + require(maxUploadFileSize >= uploadFileSizeThreshold) { "The max file size must be greater than the file size threshold." } + } + + private val multiParts: LinkedList = LinkedList() + private val multiPartChannel: Channel = Channel(Channel.UNLIMITED) + private var firstMessage = true + private var requestSize: Long = 0 + private var parser: MultiPartParser? = null + private var byteBuffers: LinkedList = LinkedList() + private val multiPartHandler = MultiPartHandler() + private var job: Job? = null + private var part: AsyncMultiPart? = null + private var parsingException: Throwable? = null + + private inner class MultiPartHandler : MultiPartParser.Handler { + + override fun startPart() { + multiPartChannel.trySend(StartPart) + } + + override fun parsedField(name: String, value: String) { + multiPartChannel.trySend(PartField(name, value)) + } + + override fun headerComplete(): Boolean { + multiPartChannel.trySend(PartHeaderComplete) + return false + } + + override fun content(item: ByteBuffer, last: Boolean): Boolean { + multiPartChannel.trySend(PartContent(item, last)) + return false + } + + override fun messageComplete(): Boolean { + multiPartChannel.trySend(PartMessageComplete) + return true + } + + override fun earlyEOF() { + multiPartChannel.trySend(PartEarlyEOF) + } + + } + + private fun parsingJob() = scope.launch { + parseLoop@ while (true) { + try { + when (val message = multiPartChannel.receive()) { + is ParseMultiPartBoundary -> parseBoundaryAndCreateMultiPartParser(message) + is ParseMultiPartContent -> parseContent(message) + is EndMultiPartHandler -> endMultiPartHandler() + is StartPart -> createMultiPart() + is PartField -> addMultiPartField(message) + is PartHeaderComplete -> handleHeaderComplete() + is PartContent -> acceptContent(message) + is PartMessageComplete -> { + handleMessageComplete() + break@parseLoop + } + is PartEarlyEOF -> handleEarlyEOF() + } + } catch (e: Throwable) { + this@MultiPartContentHandler.parsingException = e + break@parseLoop + } + } + + multiPartChannel.clear() + val result = runCatching { closeAllFileHandlers() } + log.debug { "close file handlers. result: ${result.isSuccess}" } + } + + + private fun createMultiPart() { + val part = + AsyncMultiPart( + maxUploadFileSize, + uploadFileSizeThreshold, + Paths.get(path.toString(), UUID.randomUUID().toString()) + ) + multiParts.add(part) + this.part = part + log.debug { "Create multi-part. $part" } + } + + private fun addMultiPartField(message: PartField) { + part?.httpFields?.add(message.name, message.value) + } + + private fun handleHeaderComplete() { + val contentDisposition = part?.httpFields?.get("Content-Disposition") + requireNotNull(contentDisposition) { "Missing Content-Disposition header" } + + val token = QuotedStringTokenizer(contentDisposition, ";", false, true) + var formData = false + var name: String? = null + var fileName: String? = null + while (token.hasMoreTokens()) { + val tokenValue = token.nextToken().trim() + val lowerCaseValue = tokenValue.lowercase(Locale.getDefault()) + when { + lowerCaseValue.startsWith("form-data") -> formData = true + lowerCaseValue.startsWith("name=") -> name = value(tokenValue) + lowerCaseValue.startsWith("filename=") -> fileName = fileNameValue(tokenValue) + } + } + + require(formData) { "Part not form-data" } + requireNotNull(name) { "No name in part" } + part?.name = name + part?.fileName = fileName ?: "" + log.debug { "Multi-part header complete. name: $name, fileName: $fileName, fields: ${part?.httpFields?.size()}" } + } + + private fun acceptContent(message: PartContent) { + log.debug { "Accept multi-part content. size: ${message.byteBuffer.remaining()}, last: ${message.last}" } + part?.accept(message.byteBuffer, message.last) + } + + private suspend fun handleMessageComplete() { + val result = runCatching { closeAllFileHandlers() } + log.debug { "Multi-part complete. part: $part. result: ${result.isSuccess}" } + } + + private suspend fun handleEarlyEOF() { + val result = runCatching { closeAllFileHandlers() } + log.debug { "Early EOF. result: ${result.isSuccess}" } + throw BadMessageException(HttpStatus.BAD_REQUEST_400) + } + + private suspend fun closeAllFileHandlers() { + var ex: Exception? = null + multiParts.forEach { + try { + it.closeFileHandler() + } catch (e: Exception) { + ex = e + } + } + val e = ex + if (e != null) { + throw e + } + } + + private fun parseBoundary(contentType: String?): String { + if (contentType == null) return "" + if (!contentType.startsWith("multipart/form-data")) return "" + + var contentTypeBoundary = "" + val start: Int = contentType.indexOf("boundary=") + if (start >= 0) { + var end: Int = contentType.indexOf(";", start) + end = if (end < 0) contentType.length else end + contentTypeBoundary = unquote(value(contentType.substring(start, end)).trim()) + } + return contentTypeBoundary + } + + private fun parseBoundaryAndCreateMultiPartParser(message: ParseMultiPartBoundary) { + val boundary = parseBoundary(message.contentType) + log.debug { "Parsed multi-part boundary: $boundary" } + if (boundary.isNotBlank()) { + parser = MultiPartParser(multiPartHandler, boundary) + } else { + throw BadMessageException(HttpStatus.BAD_REQUEST_400) + } + } + + private fun parseContent(message: ParseMultiPartContent) { + byteBuffers.offer(message.byteBuffer) + log.debug { "Parse multi part content. state: ${parser?.state}, size: ${message.byteBuffer.remaining()}" } + if (byteBuffers.size > 1) { + parser?.parse(byteBuffers.poll(), false) + } + } + + private fun endMultiPartHandler() { + val buffer = byteBuffers.poll() + requireNotNull(buffer) + log.debug { "End multi-part handler. buffers: ${byteBuffers.size}, size: ${buffer.remaining()}" } + parser?.parse(buffer, true) + } + + private fun value(headerLine: String): String { + val idx = headerLine.indexOf('=') + val value = headerLine.substring(idx + 1).trim() + return unquoteOnly(value) + } + + private fun fileNameValue(headerLine: String): String { + val idx = headerLine.indexOf('=') + var value = headerLine.substring(idx + 1).trim() + + return if (value.matches(".??[a-z,A-Z]:\\\\[^\\\\].*".toRegex())) { + // incorrectly escaped IE filenames that have the whole path + // we just strip any leading & trailing quotes and leave it as is + val first = value[0] + if (first == '"' || first == '\'') value = value.substring(1) + val last = value[value.length - 1] + if (last == '"' || last == '\'') value = value.substring(0, value.length - 1) + value + } else unquoteOnly(value, true) + // unquote the string, but allow any backslashes that don't + // form a valid escape sequence to remain as many browsers + // even on *nix systems will not escape a filename containing + // backslashes + } + + override fun accept(byteBuffer: ByteBuffer, ctx: RoutingContext) { + requestSize += byteBuffer.remaining() + if (requestSize > maxRequestBodySize) { + throw BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413) + } + + if (firstMessage) { + job = parsingJob() + multiPartChannel.trySend(ParseMultiPartBoundary(ctx.httpFields[HttpHeader.CONTENT_TYPE])) + firstMessage = false + } + multiPartChannel.trySend(ParseMultiPartContent(byteBuffer)) + } + + override fun closeAsync(): CompletableFuture { + val parsingJob = job + return if (parsingJob != null) { + if (parsingJob.isCompleted) complete() + else scope.launch { closeAwait() }.asCompletableFuture().thenCompose { complete() } + .thenAccept { scope.cancel() } + } else Result.DONE + } + + private fun complete(): CompletableFuture { + val e = parsingException + return if (e != null) CompletableFutures.failedFuture(e) else Result.DONE + } + + private suspend fun closeAwait() { + multiPartChannel.trySend(EndMultiPartHandler) + job?.join() + } + + override fun close() { + closeAsync() + } + + fun getPart(name: String): MultiPart? { + return multiParts.find { it.name == name } + } + + fun getParts(): List = multiParts +} + +sealed interface MultiPartHandlerMessage + +@JvmInline +value class ParseMultiPartBoundary(val contentType: String?) : MultiPartHandlerMessage + +@JvmInline +value class ParseMultiPartContent(val byteBuffer: ByteBuffer) : MultiPartHandlerMessage + +object StartPart : MultiPartHandlerMessage + +data class PartField(val name: String, val value: String) : MultiPartHandlerMessage + +object PartHeaderComplete : MultiPartHandlerMessage + +class PartContent(val byteBuffer: ByteBuffer, val last: Boolean) : MultiPartHandlerMessage + +object PartMessageComplete : MultiPartHandlerMessage + +object PartEarlyEOF : MultiPartHandlerMessage + +object EndMultiPartHandler : MultiPartHandlerMessage \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/StringContentHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/StringContentHandler.kt new file mode 100644 index 000000000..f2ef8fbce --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/handler/StringContentHandler.kt @@ -0,0 +1,4 @@ +package com.fireflysource.net.http.server.impl.content.handler + +open class StringContentHandler(maxRequestBodySize: Long = 200 * 1024 * 1024) : + ByteBufferContentHandler(maxRequestBodySize) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/ByteBufferContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/ByteBufferContentProvider.kt new file mode 100644 index 000000000..96b43b6ef --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/ByteBufferContentProvider.kt @@ -0,0 +1,8 @@ +package com.fireflysource.net.http.server.impl.content.provider + +import com.fireflysource.net.http.common.content.provider.AbstractByteBufferContentProvider +import com.fireflysource.net.http.server.HttpServerContentProvider +import java.nio.ByteBuffer + +class ByteBufferContentProvider(content: ByteBuffer) : AbstractByteBufferContentProvider(content), + HttpServerContentProvider \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/DefaultContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/DefaultContentProvider.kt new file mode 100644 index 000000000..c51ef6b9f --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/DefaultContentProvider.kt @@ -0,0 +1,63 @@ +package com.fireflysource.net.http.server.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.ProjectVersion +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.HttpServerContentProvider +import com.fireflysource.net.http.server.RoutingContext +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.CompletableFuture + +class DefaultContentProvider( + private val status: Int, + private val exception: Throwable?, + private val ctx: RoutingContext +) : HttpServerContentProvider { + + private val html = """ + | + | + | + |${getTitle()} + | + | + |

${getTitle()}

+ |

${getContent()}

+ |
+ |
powered by Firefly ${ProjectVersion.getValue()}
+ | + | + """.trimMargin() + private val contentByteBuffer = BufferUtils.toBuffer(html, StandardCharsets.UTF_8) + private val provider: ByteBufferContentProvider = ByteBufferContentProvider(contentByteBuffer) + + private fun getTitle(): String { + return "$status ${getCode().message}" + } + + private fun getCode(): HttpStatus.Code { + return Optional.ofNullable(HttpStatus.getCode(status)).orElse(HttpStatus.Code.INTERNAL_SERVER_ERROR) + } + + private fun getContent(): String { + return when (getCode()) { + HttpStatus.Code.NOT_FOUND -> "The resource ${ctx.uri.path} is not found" + HttpStatus.Code.INTERNAL_SERVER_ERROR -> "The server internal error.
${exception?.message}" + else -> "${getTitle()}
${exception?.message}" + } + } + + override fun length(): Long = provider.length() + + override fun isOpen(): Boolean = provider.isOpen + + override fun toByteBuffer(): ByteBuffer = provider.toByteBuffer() + + override fun closeAsync(): CompletableFuture = provider.closeAsync() + + override fun close() = provider.close() + + override fun read(byteBuffer: ByteBuffer): CompletableFuture = provider.read(byteBuffer) +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/FileContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/FileContentProvider.kt new file mode 100644 index 000000000..e66399a1e --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/FileContentProvider.kt @@ -0,0 +1,35 @@ +package com.fireflysource.net.http.server.impl.content.provider + +import com.fireflysource.net.http.common.content.provider.AbstractFileContentProvider +import com.fireflysource.net.http.server.HttpServerContentProvider +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import java.nio.file.Files +import java.nio.file.OpenOption +import java.nio.file.Path + +class FileContentProvider( + path: Path, + options: Set, + position: Long, + length: Long, + scope: CoroutineScope = CoroutineScope(CoroutineName("Firefly-file-content-provider")) +) : AbstractFileContentProvider(path, options, position, length, scope), HttpServerContentProvider { + + constructor(path: Path, vararg options: OpenOption) : this(path, options.toSet(), 0, Files.size(path)) + + constructor(path: Path, scope: CoroutineScope, vararg options: OpenOption) : this( + path, + options.toSet(), + 0, + Files.size(path), + scope + ) + + constructor( + path: Path, + options: Set, + position: Long, + length: Long + ) : this(path, options, position, length, CoroutineScope(CoroutineName("Firefly-file-content-provider"))) +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/StringContentProvider.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/StringContentProvider.kt new file mode 100644 index 000000000..5da9b4ac1 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/content/provider/StringContentProvider.kt @@ -0,0 +1,9 @@ +package com.fireflysource.net.http.server.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.common.content.provider.AbstractByteBufferContentProvider +import com.fireflysource.net.http.server.HttpServerContentProvider +import java.nio.charset.Charset + +class StringContentProvider(val content: String, val charset: Charset) : + AbstractByteBufferContentProvider(BufferUtils.toBuffer(content, charset)), HttpServerContentProvider \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/ProxyAuthException.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/ProxyAuthException.kt new file mode 100644 index 000000000..2f27a6856 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/ProxyAuthException.kt @@ -0,0 +1,6 @@ +package com.fireflysource.net.http.server.impl.exception + +/** + * @author Pengtao Qiu + */ +class ProxyAuthException(message: String) : IllegalArgumentException(message) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/RouterNotCommitException.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/RouterNotCommitException.kt new file mode 100644 index 000000000..100734657 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/RouterNotCommitException.kt @@ -0,0 +1,6 @@ +package com.fireflysource.net.http.server.impl.exception + +/** + * @author Pengtao Qiu + */ +class RouterNotCommitException(message: String) : IllegalStateException(message) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/RouterUrlFormatException.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/RouterUrlFormatException.kt new file mode 100644 index 000000000..84a88249e --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/exception/RouterUrlFormatException.kt @@ -0,0 +1,3 @@ +package com.fireflysource.net.http.server.impl.exception + +class RouterUrlFormatException(message: String) : IllegalArgumentException(message) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractMatcher.kt new file mode 100644 index 000000000..0b6432c69 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractMatcher.kt @@ -0,0 +1,24 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Router +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import java.util.* + +abstract class AbstractMatcher(val routersMap: MutableMap> = HashMap()) { + + fun copyRouterMap(manager: AsyncRouterManager): MutableMap> { + val newRoutersMap: MutableMap> = HashMap() + routersMap.forEach { (key, routerSet) -> + val newRouterSet: SortedSet = TreeSet() + routerSet.forEach { router -> + if (router is AsyncRouter) { + val newRouter = router.copy(manager) + newRouterSet.add(newRouter) + } + } + newRoutersMap[key] = newRouterSet + } + return newRoutersMap + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractPatternMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractPatternMatcher.kt new file mode 100644 index 000000000..12fefc85f --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractPatternMatcher.kt @@ -0,0 +1,57 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.common.string.Pattern +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Router +import java.util.* + + +abstract class AbstractPatternMatcher : AbstractMatcher(), Matcher { + + companion object { + const val paramName = "param" + } + + class PatternRule(val rule: String) { + + val pattern: Pattern = Pattern.compile(rule, "*") + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PatternRule + return rule == other.rule + } + + override fun hashCode(): Int { + return rule.hashCode() + } + } + + override fun add(rule: String, router: Router) { + routersMap.computeIfAbsent(PatternRule(rule)) { TreeSet() }.add(router) + } + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val routers = TreeSet() + val parameters = HashMap>() + + routersMap.forEach { (rule, routerSet) -> + val strings: Array? = rule.pattern.match(value) + if (strings != null) { + routers.addAll(routerSet) + if (strings.isNotEmpty()) { + val param: MutableMap = HashMap() + for (i in strings.indices) { + param["$paramName$i"] = strings[i] + } + routerSet.forEach { router -> parameters[router] = param } + } + } + } + return if (routers.isEmpty()) null else Matcher.MatchResult(routers, parameters, matchType) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractPreciseMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractPreciseMatcher.kt new file mode 100644 index 000000000..0e199e7e4 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractPreciseMatcher.kt @@ -0,0 +1,19 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Router +import java.util.* + +abstract class AbstractPreciseMatcher : AbstractMatcher(), Matcher { + + override fun add(rule: String, router: Router) { + routersMap.computeIfAbsent(rule) { TreeSet() }.add(router) + } + + override fun match(value: String): Matcher.MatchResult? { + val routers = routersMap[value] + return if (!routers.isNullOrEmpty()) { + Matcher.MatchResult(routers, emptyMap(), matchType) + } else null + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractRegexMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractRegexMatcher.kt new file mode 100644 index 000000000..4414e9041 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AbstractRegexMatcher.kt @@ -0,0 +1,60 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Router +import java.util.* +import java.util.regex.Pattern + + +abstract class AbstractRegexMatcher :AbstractMatcher(), Matcher { + + companion object { + const val paramName = "group" + } + + class RegexRule(val rule: String) { + val pattern: Pattern = Pattern.compile(rule) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RegexRule + return rule == other.rule + } + + override fun hashCode(): Int { + return rule.hashCode() + } + } + + override fun add(rule: String, router: Router) { + routersMap.computeIfAbsent(RegexRule(rule)) { TreeSet() }.add(router) + } + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val routers = TreeSet() + val parameters = HashMap>() + + routersMap.forEach { (rule, routerSet) -> + var matcher = rule.pattern.matcher(value) + if (matcher.matches()) { + routers.addAll(routerSet) + matcher = rule.pattern.matcher(value) + val param: MutableMap = HashMap() + while (matcher.find()) { + for (i in 1..matcher.groupCount()) { + param["$paramName$i"] = matcher.group(i) + } + } + if (param.isNotEmpty()) { + routerSet.forEach { router -> parameters[router] = param } + } + } + } + + return if (routers.isEmpty()) null else Matcher.MatchResult(routers, parameters, matchType) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AcceptHeaderMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AcceptHeaderMatcher.kt new file mode 100644 index 000000000..7235156bb --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/AcceptHeaderMatcher.kt @@ -0,0 +1,47 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.common.string.StringUtils +import com.fireflysource.net.http.common.model.AcceptMIMEMatchType +import com.fireflysource.net.http.common.model.AcceptMIMEType +import com.fireflysource.net.http.common.model.MimeTypes.parseAcceptMIMETypes +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Matcher.MatchType +import com.fireflysource.net.http.server.Router +import java.util.* + + +class AcceptHeaderMatcher : AbstractPreciseMatcher() { + + override fun getMatchType(): MatchType { + return MatchType.ACCEPT + } + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val acceptMIMETypes = parseAcceptMIMETypes(value) + if (acceptMIMETypes.isNullOrEmpty()) return null + + return acceptMIMETypes + .map { findRouters(it) } + .filter { !it.isEmpty() } + .map { Matcher.MatchResult(it, emptyMap(), matchType) } + .firstOrNull() + } + + private fun findRouters(type: AcceptMIMEType) = + routersMap.entries.filter { matchMimeType(it, type) }.flatMap { it.value }.toSortedSet() + + private fun matchMimeType(e: MutableMap.MutableEntry>, type: AcceptMIMEType): Boolean { + val acceptType: Array = StringUtils.split(e.key, '/') + val parentType = acceptType[0].trim() + val childType = acceptType[1].trim() + return when (type.matchType) { + AcceptMIMEMatchType.EXACT -> parentType == type.parentType && childType == type.childType + AcceptMIMEMatchType.CHILD -> childType == type.childType + AcceptMIMEMatchType.PARENT -> parentType == type.parentType + AcceptMIMEMatchType.ALL -> true + else -> false + } + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/HttpMethodMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/HttpMethodMatcher.kt new file mode 100644 index 000000000..f4862b230 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/HttpMethodMatcher.kt @@ -0,0 +1,21 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Router +import java.util.* + +class HttpMethodMatcher : AbstractPreciseMatcher() { + + override fun add(rule: String, router: Router) { + super.add(rule.uppercase(Locale.getDefault()), router) + } + + override fun match(value: String): Matcher.MatchResult? { + return if (routersMap.isEmpty()) null else super.match(value.uppercase(Locale.getDefault())) + } + + override fun getMatchType(): Matcher.MatchType { + return Matcher.MatchType.METHOD + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/ParameterPathMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/ParameterPathMatcher.kt new file mode 100644 index 000000000..386bd5a9b --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/ParameterPathMatcher.kt @@ -0,0 +1,95 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Router +import java.util.* + + +class ParameterPathMatcher : AbstractMatcher(), Matcher { + + companion object { + fun isParameterPath(path: String): Boolean { + val paths = split(path) + return paths.any { it[0] == ':' } + } + + fun split(path: String): List { + val paths: MutableList = LinkedList() + var start = 1 + val last = path.lastIndex + + for (i in 1..last) { + if (path[i] == '/') { + paths.add(path.substring(start, i).trim()) + start = i + 1 + } + } + + if (path[last] != '/') { + paths.add(path.substring(start).trim()) + } + return paths + } + } + + inner class ParameterRule(val rule: String) { + + val paths = split(rule) + + fun match(list: List): Map { + if (paths.size != list.size) return emptyMap() + + val param: MutableMap = HashMap() + for (i in list.indices) { + val path = paths[i] + val value = list[i] + if (path[0] != ':') { + if (path != value) { + return emptyMap() + } + } else { + param[path.substring(1)] = value + } + } + return param + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ParameterRule + return rule == other.rule + } + + override fun hashCode(): Int { + return rule.hashCode() + } + } + + override fun getMatchType(): Matcher.MatchType { + return Matcher.MatchType.PATH + } + + override fun add(rule: String, router: Router) { + val parameterRule = ParameterRule(rule) + routersMap.computeIfAbsent(parameterRule) { TreeSet() }.add(router) + } + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val routers = TreeSet() + val parameters = HashMap>() + val paths = split(value) + + routersMap.forEach { (rule, routerSet) -> + val param = rule.match(paths) + if (param.isNotEmpty()) { + routers.addAll(routerSet) + routerSet.forEach { router -> parameters[router] = param } + } + } + return if (routers.isEmpty()) null else Matcher.MatchResult(routers, parameters, matchType) + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PatternedContentTypeMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PatternedContentTypeMatcher.kt new file mode 100644 index 000000000..49edad919 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PatternedContentTypeMatcher.kt @@ -0,0 +1,21 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.common.string.StringUtils +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Matcher.MatchType + + +class PatternedContentTypeMatcher : AbstractPatternMatcher() { + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val mimeType = MimeTypes.getContentTypeMIMEType(value) + return if (StringUtils.hasText(mimeType)) super.match(mimeType) else null + } + + override fun getMatchType(): MatchType { + return MatchType.CONTENT_TYPE + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PatternedPathMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PatternedPathMatcher.kt new file mode 100644 index 000000000..5f69f7675 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PatternedPathMatcher.kt @@ -0,0 +1,10 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher + +class PatternedPathMatcher : AbstractPatternMatcher() { + + override fun getMatchType(): Matcher.MatchType { + return Matcher.MatchType.PATH + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PreciseContentTypeMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PreciseContentTypeMatcher.kt new file mode 100644 index 000000000..be1bef2cd --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PreciseContentTypeMatcher.kt @@ -0,0 +1,20 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.common.string.StringUtils +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.Matcher + +class PreciseContentTypeMatcher : AbstractPreciseMatcher() { + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val mimeType = MimeTypes.getContentTypeMIMEType(value) + return if (StringUtils.hasText(mimeType)) super.match(mimeType) else null + } + + override fun getMatchType(): Matcher.MatchType { + return Matcher.MatchType.CONTENT_TYPE + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PrecisePathMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PrecisePathMatcher.kt new file mode 100644 index 000000000..c00112969 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/PrecisePathMatcher.kt @@ -0,0 +1,26 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Matcher.MatchType +import com.fireflysource.net.http.server.Router + +class PrecisePathMatcher : AbstractPreciseMatcher() { + + override fun add(rule: String, router: Router) { + val path = toPath(rule) + super.add(path, router) + } + + override fun match(value: String): Matcher.MatchResult? { + if (routersMap.isEmpty()) return null + + val path = toPath(value) + return super.match(path) + } + + override fun getMatchType(): MatchType { + return MatchType.PATH + } + + private fun toPath(value: String): String = if (value.last() != '/') "$value/" else value +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/RegexPathMatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/RegexPathMatcher.kt new file mode 100644 index 000000000..a9dd77e86 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/matcher/RegexPathMatcher.kt @@ -0,0 +1,10 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher + +class RegexPathMatcher : AbstractRegexMatcher() { + + override fun getMatchType(): Matcher.MatchType { + return Matcher.MatchType.PATH + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRouter.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRouter.kt new file mode 100644 index 000000000..1ad804ebf --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRouter.kt @@ -0,0 +1,148 @@ +package com.fireflysource.net.http.server.impl.router + +import com.fireflysource.common.coroutine.CoroutineDispatchers +import com.fireflysource.common.coroutine.CoroutineLocalContext +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.Router +import com.fireflysource.net.http.server.Router.EMPTY_HANDLER +import com.fireflysource.net.http.server.RoutingContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class AsyncRouter( + private val id: Int, + private val routerManager: AsyncRouterManager +) : Router { + + private val matchTypes = HashSet() + private var handler: Router.Handler = EMPTY_HANDLER + private var enabled = true + + override fun method(httpMethod: String): Router { + routerManager.method(httpMethod, this) + matchTypes.add(Matcher.MatchType.METHOD) + return this + } + + override fun method(httpMethod: HttpMethod): Router { + routerManager.method(httpMethod.value, this) + matchTypes.add(Matcher.MatchType.METHOD) + return this + } + + override fun path(url: String): Router { + routerManager.path(url, this) + matchTypes.add(Matcher.MatchType.PATH) + return this + } + + override fun paths(urlList: MutableList): Router { + routerManager.paths(urlList, this) + matchTypes.add(Matcher.MatchType.PATH) + return this + } + + override fun pathRegex(regex: String): Router { + routerManager.pathRegex(regex, this) + matchTypes.add(Matcher.MatchType.PATH) + return this + } + + override fun get(url: String): Router { + return method(HttpMethod.GET).path(url) + } + + override fun post(url: String): Router { + return method(HttpMethod.POST).path(url) + } + + override fun put(url: String): Router { + return method(HttpMethod.PUT).path(url) + } + + override fun delete(url: String): Router { + return method(HttpMethod.DELETE).path(url) + } + + override fun consumes(contentType: String): Router { + routerManager.consumes(contentType, this) + matchTypes.add(Matcher.MatchType.CONTENT_TYPE) + return this + } + + override fun produces(accept: String): Router { + routerManager.produces(accept, this) + matchTypes.add(Matcher.MatchType.ACCEPT) + return this + } + + override fun getId(): Int = id + + override fun compareTo(other: Router): Int = id.compareTo(other.id) + + override fun handler(handler: Router.Handler): HttpServer { + this.handler = handler + return routerManager.httpServer + } + + fun getHandler() = this.handler + + override fun getMatchTypes(): MutableSet = matchTypes + + override fun enable(): Router { + enabled = true + return this + } + + override fun isEnable(): Boolean = enabled + + override fun disable(): Router { + enabled = false + return this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AsyncRouter + return id == other.id + } + + override fun hashCode(): Int { + return id + } + + fun copy(manager: AsyncRouterManager): AsyncRouter { + val router = AsyncRouter(this.id, manager) + router.matchTypes.addAll(this.matchTypes) + router.enabled = this.enabled + router.handler = this.handler + return router + } +} + +private const val serverHandlerCoroutineContextKey = "_serverHandlerCoroutineContextKey" + +fun getCurrentRoutingContext(): RoutingContext? = CoroutineLocalContext.getAttr(serverHandlerCoroutineContextKey) + +fun Router.asyncHandler(block: suspend CoroutineScope.(RoutingContext) -> Unit): HttpServer { + return this.handler { ctx -> + ctx.connection.coroutineScope + .launch(CoroutineLocalContext.asElement(mutableMapOf(serverHandlerCoroutineContextKey to ctx))) { block(ctx) } + .asVoidFuture() + } +} + +fun Router.asyncBlockingHandler(block: suspend CoroutineScope.(RoutingContext) -> Unit): HttpServer { + return this.handler { ctx -> + ctx.connection.coroutineScope + .launch(CoroutineLocalContext.asElement(mutableMapOf(serverHandlerCoroutineContextKey to ctx)) + CoroutineDispatchers.ioBlocking) { + block(ctx) + } + .asVoidFuture() + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRouterManager.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRouterManager.kt new file mode 100644 index 000000000..7e6aad468 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRouterManager.kt @@ -0,0 +1,127 @@ +package com.fireflysource.net.http.server.impl.router + +import com.fireflysource.net.http.common.codec.URIUtils.canonicalPath +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.server.* +import com.fireflysource.net.http.server.impl.matcher.* +import java.util.* +import java.util.concurrent.atomic.AtomicInteger + +class AsyncRouterManager(val httpServer: HttpServer) : RouterManager { + + private val routerId = AtomicInteger() + + private val httpMethodMatcher: HttpMethodMatcher = HttpMethodMatcher() + private val precisePathMatcher: PrecisePathMatcher = PrecisePathMatcher() + private val parameterPathMatcher: ParameterPathMatcher = ParameterPathMatcher() + private val patternedPathMatcher: PatternedPathMatcher = PatternedPathMatcher() + private val regexPathMatcher: RegexPathMatcher = RegexPathMatcher() + private val preciseContentTypeMatcher: PreciseContentTypeMatcher = PreciseContentTypeMatcher() + private val patternedContentTypeMatcher: PatternedContentTypeMatcher = PatternedContentTypeMatcher() + private val acceptHeaderMatcher: AcceptHeaderMatcher = AcceptHeaderMatcher() + + override fun register(): Router = this.register(routerId.getAndIncrement()) + + override fun register(id: Int): Router = + AsyncRouter(id, this) + + override fun findRouters(ctx: RoutingContext): SortedSet { + val routerMatchTypeMap = TreeMap>() + val routerParameterMap = TreeMap>() + + val methodResult = httpMethodMatcher.match(ctx.method) + collectRouterResult(methodResult, routerMatchTypeMap, routerParameterMap) + + val path = canonicalPath(ctx.uri.decodedPath) + val precisePathResult = precisePathMatcher.match(path) + collectRouterResult(precisePathResult, routerMatchTypeMap, routerParameterMap) + + val parameterPathResult = parameterPathMatcher.match(path) + collectRouterResult(parameterPathResult, routerMatchTypeMap, routerParameterMap) + + val patternedPathResult = patternedPathMatcher.match(path) + collectRouterResult(patternedPathResult, routerMatchTypeMap, routerParameterMap) + + val regexPathResult = regexPathMatcher.match(path) + collectRouterResult(regexPathResult, routerMatchTypeMap, routerParameterMap) + + val contentType = ctx.contentType ?: "" + val preciseContentTypeResult = preciseContentTypeMatcher.match(contentType) + collectRouterResult(preciseContentTypeResult, routerMatchTypeMap, routerParameterMap) + + val patternedContentTypeResult = patternedContentTypeMatcher.match(contentType) + collectRouterResult(patternedContentTypeResult, routerMatchTypeMap, routerParameterMap) + + val accept = ctx.httpFields[HttpHeader.ACCEPT] ?: "" + val acceptHeaderResult = acceptHeaderMatcher.match(accept) + collectRouterResult(acceptHeaderResult, routerMatchTypeMap, routerParameterMap) + + return routerMatchTypeMap + .filter { it.key.isEnable } + .filter { it.key.matchTypes == it.value } + .map { RouterManager.RouterMatchResult(it.key, routerParameterMap[it.key] ?: emptyMap(), it.value) } + .toSortedSet() + } + + private fun collectRouterResult( + result: Matcher.MatchResult?, + routerMatchTypeMap: SortedMap>, + routerParameterMap: SortedMap> + ) { + result?.routers?.forEach { + routerMatchTypeMap.computeIfAbsent(it) { HashSet() }.add(result.matchType) + val params = result.parameters[it] + if (!params.isNullOrEmpty()) { + routerParameterMap.computeIfAbsent(it) { HashMap() }.putAll(params) + } + } + } + + fun method(httpMethod: String, router: AsyncRouter) { + httpMethodMatcher.add(httpMethod, router) + } + + fun path(url: String, router: AsyncRouter) { + when { + url == "/" -> precisePathMatcher.add(url, router) + url.contains("*") -> patternedPathMatcher.add(url, router) + ParameterPathMatcher.isParameterPath(url) -> parameterPathMatcher.add(url, router) + else -> precisePathMatcher.add(url, router) + } + } + + fun paths(urlList: MutableList, router: AsyncRouter) { + urlList.forEach { path(it, router) } + } + + fun pathRegex(regex: String, router: AsyncRouter) { + regexPathMatcher.add(regex, router) + } + + fun consumes(contentType: String, router: AsyncRouter) { + if (contentType.contains("*")) patternedContentTypeMatcher.add(contentType, router) + else preciseContentTypeMatcher.add(contentType, router) + } + + fun produces(accept: String, router: AsyncRouter) { + acceptHeaderMatcher.add(accept, router) + } + + fun copy(httpServer: HttpServer): AsyncRouterManager { + val newManager = AsyncRouterManager(httpServer) + newManager.routerId.set(this.routerId.get()) + newManager.httpMethodMatcher.routersMap.putAll(this.httpMethodMatcher.copyRouterMap(newManager)) + newManager.precisePathMatcher.routersMap.putAll(this.precisePathMatcher.copyRouterMap(newManager)) + newManager.parameterPathMatcher.routersMap.putAll(this.parameterPathMatcher.copyRouterMap(newManager)) + newManager.patternedPathMatcher.routersMap.putAll(this.patternedPathMatcher.copyRouterMap(newManager)) + newManager.regexPathMatcher.routersMap.putAll(this.regexPathMatcher.copyRouterMap(newManager)) + newManager.preciseContentTypeMatcher.routersMap.putAll(this.preciseContentTypeMatcher.copyRouterMap(newManager)) + newManager.patternedContentTypeMatcher.routersMap.putAll( + this.patternedContentTypeMatcher.copyRouterMap( + newManager + ) + ) + newManager.acceptHeaderMatcher.routersMap.putAll(this.acceptHeaderMatcher.copyRouterMap(newManager)) + return newManager + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRoutingContext.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRoutingContext.kt new file mode 100644 index 000000000..8b4133b5d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/AsyncRoutingContext.kt @@ -0,0 +1,84 @@ +package com.fireflysource.net.http.server.impl.router + +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.expectServerAcceptsContent +import com.fireflysource.net.http.server.* +import com.fireflysource.net.http.server.impl.content.provider.DefaultContentProvider +import com.fireflysource.net.http.server.impl.matcher.AbstractPatternMatcher +import com.fireflysource.net.http.server.impl.matcher.AbstractRegexMatcher +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +class AsyncRoutingContext( + private val request: HttpServerRequest, + private val response: HttpServerResponse, + private val connection: HttpServerConnection +) : RoutingContext { + + private val attributes: ConcurrentMap by lazy { ConcurrentHashMap() } + var routerMatchResult: RouterManager.RouterMatchResult? = null + var routerIterator: Iterator? = null + + override fun getAttribute(key: String): Any? = attributes[key] + + override fun setAttribute(key: String, value: Any): Any? = attributes.put(key, value) + + override fun getAttributes(): MutableMap = attributes + + override fun removeAttribute(key: String): Any? = attributes.remove(key) + + override fun getRequest(): HttpServerRequest = request + + override fun getResponse(): HttpServerResponse = response + + override fun getPathParameter(name: String): String { + val result = routerMatchResult + return if (result == null) "" + else result.parameters[name] ?: "" + } + + override fun getPathParameter(index: Int): String { + val result = routerMatchResult + return if (result == null) "" + else result.parameters[AbstractPatternMatcher.paramName + index] ?: "" + } + + override fun getPathParameterByRegexGroup(index: Int): String { + val result = routerMatchResult + return if (result == null) "" + else result.parameters[AbstractRegexMatcher.paramName + index] ?: "" + } + + override fun expect100Continue(): Boolean { + return request.httpFields.expectServerAcceptsContent() + } + + override fun redirect(url: String): CompletableFuture { + val status = HttpStatus.FOUND_302 + return setStatus(status) + .put(HttpHeader.LOCATION, url) + .contentProvider(DefaultContentProvider(status, null, this)) + .end() + } + + override fun next(): CompletableFuture { + if (!hasNext()) return Result.DONE + + val result = routerIterator?.next() + return if (result != null) { + routerMatchResult = result + val asyncRouter = result.router as AsyncRouter + asyncRouter.getHandler().apply(this) + } else Result.DONE + } + + override fun hasNext(): Boolean { + return routerIterator?.hasNext() ?: false + } + + override fun getConnection(): HttpServerConnection = connection + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/handler/CorsHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/handler/CorsHandler.kt new file mode 100644 index 000000000..fbb0d9661 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/handler/CorsHandler.kt @@ -0,0 +1,198 @@ +package com.fireflysource.net.http.server.impl.router.handler + +import com.fireflysource.common.annotation.NoArg +import com.fireflysource.common.string.Pattern +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.model.HttpHeader.* +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.Router +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.content.provider.DefaultContentProvider +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + + +class CorsHandler(val config: CorsConfig) : Router.Handler { + + companion object { + private val simpleRequestMethods = setOf( + HttpMethod.GET.value, + HttpMethod.HEAD.value, + HttpMethod.POST.value + ) + private val simpleRequestContentTypes = setOf( + MimeTypes.Type.FORM_ENCODED.value, + MimeTypes.Type.MULTIPART_FORM_DATA.value, + MimeTypes.Type.TEXT_PLAIN.value + ) + } + + private val allowOriginPattern = Pattern.compile(config.allowOriginPattern, "*") + + override fun apply(ctx: RoutingContext): CompletableFuture { + when { + isSimpleRequest(ctx) -> handleSimpleRequest(ctx) + isPreflightRequest(ctx) -> handlePreflightRequest(ctx) + else -> { + val origin: String? = ctx.httpFields[ORIGIN] + if (!origin.isNullOrBlank()) { + if (allowOrigin(ctx)) { + setAccessControlHeaders(ctx, origin) + } + } + } + } + return if (ctx.response.isCommitted) Result.DONE else ctx.next() + } + + private fun allowOrigin(ctx: RoutingContext): Boolean { + val origin: String? = ctx.httpFields[ORIGIN] + return !origin.isNullOrBlank() && allowOriginPattern.match(origin) != null + } + + private fun isSimpleRequest(ctx: RoutingContext): Boolean { + return if (ctx.httpFields.contains(ORIGIN) && simpleRequestMethods.contains(ctx.method)) { + if (ctx.method == HttpMethod.POST.value) + simpleRequestContentTypes.any { ctx.contentType.contains(it) } + else true + } else false + } + + private fun isPreflightRequest(ctx: RoutingContext): Boolean { + return ctx.httpFields.contains(ORIGIN) + && ctx.method == HttpMethod.OPTIONS.value + && ctx.httpFields.contains(ACCESS_CONTROL_REQUEST_METHOD) + } + + private fun handleSimpleRequest(ctx: RoutingContext) { + if (allowOrigin(ctx)) { + val origin: String? = ctx.httpFields[ORIGIN] + requireNotNull(origin) + setAccessControlHeaders(ctx, origin) + } else handleNotAllowOrigin(ctx) + } + + private fun setAccessControlHeaders(ctx: RoutingContext, origin: String) { + ctx.put(ACCESS_CONTROL_ALLOW_ORIGIN, origin) + .put(ACCESS_CONTROL_ALLOW_CREDENTIALS, config.allowCredentials.toString()) + if (config.exposeHeaders.isNotEmpty()) { + ctx.addCSV(ACCESS_CONTROL_EXPOSE_HEADERS, *config.exposeHeaders.toTypedArray()) + } + } + + private fun handlePreflightRequest(ctx: RoutingContext) { + if (!allowOrigin(ctx)) { + handleNotAllowOrigin(ctx) + return + } + + if (!allowMethod(ctx)) { + handleNotAllowMethod(ctx) + return + } + + if (!allowHeaders(ctx)) { + handleNotAllowHeader(ctx) + return + } + + val origin: String? = ctx.httpFields[ORIGIN] + requireNotNull(origin) + + ctx.setStatus(HttpStatus.NO_CONTENT_204) + .put(ACCESS_CONTROL_ALLOW_ORIGIN, origin) + .put(ACCESS_CONTROL_ALLOW_CREDENTIALS, config.allowCredentials.toString()) + .put(ACCESS_CONTROL_MAX_AGE, config.preflightMaxAge.toString()) + .addCSV(ACCESS_CONTROL_ALLOW_METHODS, *config.allowMethods.toTypedArray()) + .addCSV(ACCESS_CONTROL_ALLOW_HEADERS, *config.allowHeaders.toTypedArray()) + .end() + } + + private fun allowMethod(ctx: RoutingContext): Boolean { + val accessControlRequestMethods = ctx.httpFields.getCSV(ACCESS_CONTROL_REQUEST_METHOD, false) + return config.allowMethods.containsAll(accessControlRequestMethods) + } + + private fun allowHeaders(ctx: RoutingContext): Boolean { + val accessControlRequestHeaders = ctx.httpFields.getCSV(ACCESS_CONTROL_REQUEST_HEADERS, false) + return if (accessControlRequestHeaders.isNullOrEmpty()) true + else config.allowHeaders.map { it.lowercase(Locale.getDefault()) } + .containsAll(accessControlRequestHeaders.map { it.lowercase(Locale.getDefault()) }) + } + + + private fun handleNotAllowOrigin(ctx: RoutingContext) { + config.handleNotAllowOrigin.accept(ctx) + } + + private fun handleNotAllowMethod(ctx: RoutingContext) { + config.handleNotAllowMethod.accept(ctx) + } + + private fun handleNotAllowHeader(ctx: RoutingContext) { + config.handleNotAllowHeader.accept(ctx) + } +} + +@NoArg +data class CorsConfig @JvmOverloads constructor( + var allowOriginPattern: String, + var exposeHeaders: Set = setOf(), + var allowHeaders: Set = setOf("Content-Type"), + var preflightMaxAge: Int = 86400, + var allowCredentials: Boolean = true, + var allowMethods: Set = setOf( + HttpMethod.GET.value, + HttpMethod.POST.value, + HttpMethod.PUT.value, + HttpMethod.DELETE.value, + HttpMethod.OPTIONS.value, + HttpMethod.HEAD.value, + HttpMethod.PATCH.value + ), + var handleNotAllowOrigin: Consumer = Consumer { ctx -> + val origin: String? = ctx.httpFields[ORIGIN] + ctx.setStatus(HttpStatus.FORBIDDEN_403) + .contentProvider( + DefaultContentProvider( + HttpStatus.FORBIDDEN_403, + NotAllowOriginException("Not allow origin: $origin"), + ctx + ) + ) + .end() + }, + var handleNotAllowMethod: Consumer = Consumer { ctx -> + val accessControlRequestMethods = ctx.httpFields.getCSV(ACCESS_CONTROL_REQUEST_METHOD, false) + ctx.setStatus(HttpStatus.METHOD_NOT_ALLOWED_405) + .contentProvider( + DefaultContentProvider( + HttpStatus.METHOD_NOT_ALLOWED_405, + NotAllowMethodException("Not allow methods: $accessControlRequestMethods"), + ctx + ) + ) + .end() + }, + var handleNotAllowHeader: Consumer = Consumer { ctx -> + val accessControlRequestHeaders = ctx.httpFields.getCSV(ACCESS_CONTROL_REQUEST_HEADERS, false) + ctx.setStatus(HttpStatus.BAD_REQUEST_400) + .contentProvider( + DefaultContentProvider( + HttpStatus.BAD_REQUEST_400, + NotAllowHeaderException("Not allow headers: $accessControlRequestHeaders"), + ctx + ) + ) + .end() + } +) + +class NotAllowOriginException(message: String) : IllegalArgumentException(message) + +class NotAllowMethodException(message: String) : IllegalArgumentException(message) + +class NotAllowHeaderException(message: String) : IllegalArgumentException(message) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/handler/FileHandler.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/handler/FileHandler.kt new file mode 100644 index 000000000..73461c3f8 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/http/server/impl/router/handler/FileHandler.kt @@ -0,0 +1,156 @@ +package com.fireflysource.net.http.server.impl.router.handler + +import com.fireflysource.common.annotation.NoArg +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.common.io.existsAsync +import com.fireflysource.common.io.readAttributesAsync +import com.fireflysource.net.http.common.codec.InclusiveByteRange +import com.fireflysource.net.http.common.codec.URIUtils +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.Router +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.content.provider.DefaultContentProvider +import com.fireflysource.net.http.server.impl.content.provider.FileContentProvider +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.util.* +import java.util.concurrent.CompletableFuture + +class FileHandler(val config: FileConfig) : Router.Handler { + + companion object { + fun createFileHandlerByResourcePath(path: String): FileHandler { + val resourcePath = getResourcePath(path) + val fileConfig = FileConfig(resourcePath) + return FileHandler(fileConfig) + } + + fun getResourcePath(path: String): String { + return Optional.ofNullable(FileHandler::class.java.classLoader.getResource(path)) + .map { it.toURI() } + .map { Paths.get(it) } + .map { it.toString() } + .orElse("") + } + } + + override fun apply(ctx: RoutingContext): CompletableFuture = + ctx.connection.coroutineScope.launch { handleFile(ctx) }.asVoidFuture() + + private suspend fun handleFile(ctx: RoutingContext) { + val path = URIUtils.canonicalPath(ctx.uri.decodedPath) + val filePath = Paths.get(config.rootPath, path) + if (!existsAsync(filePath).await()) { + responseFileNotFound(ctx) + return + } + + val fileAttributes = readAttributesAsync(filePath).await() + if (fileAttributes.isDirectory) { + responseFileNotFound(ctx) + return + } + + val ranges = ctx.httpFields.getValuesList(HttpHeader.RANGE) + if (ranges.isNullOrEmpty()) { + responseFile(ctx, filePath) + } else { + val fileLength = fileAttributes.size() + val satisfiableRanges = InclusiveByteRange.satisfiableRanges(ranges, fileLength) + if (satisfiableRanges.isNullOrEmpty()) { + responseRangeNotSatisfiable(ctx, fileLength) + } else { + if (satisfiableRanges.size == 1) { + val inclusiveByteRange = satisfiableRanges[0] + responsePartialFile(ctx, filePath, inclusiveByteRange, fileLength) + } else { + responseRangeNotSatisfiable(ctx, fileLength) + } + } + } + } + + private suspend fun responseFileNotFound(ctx: RoutingContext) { + ctx.setStatus(HttpStatus.NOT_FOUND_404) + .setReason(HttpStatus.Code.NOT_FOUND.message) + .contentProvider(DefaultContentProvider(HttpStatus.NOT_FOUND_404, null, ctx)) + .end() + .await() + } + + private suspend fun responseFile(ctx: RoutingContext, filePath: Path) { + setContentType(ctx, filePath) + + ctx.setStatus(HttpStatus.OK_200) + .contentProvider( + FileContentProvider( + filePath, + CoroutineScope(CoroutineName("Firefly-server-file-content-provider") + ctx.connection.coroutineDispatcher), + StandardOpenOption.READ + ) + ) + .end() + .await() + } + + private suspend fun responseRangeNotSatisfiable(ctx: RoutingContext, fileLength: Long) { + ctx.setStatus(HttpStatus.RANGE_NOT_SATISFIABLE_416) + .put(HttpHeader.CONTENT_RANGE, InclusiveByteRange.to416HeaderRangeString(fileLength)) + .contentProvider( + DefaultContentProvider( + HttpStatus.RANGE_NOT_SATISFIABLE_416, + RangeNotSatisfiable("The range not satisfiable"), + ctx + ) + ) + .end() + .await() + } + + private suspend fun responsePartialFile( + ctx: RoutingContext, + filePath: Path, + inclusiveByteRange: InclusiveByteRange, + fileLength: Long + ) { + setContentType(ctx, filePath) + + val position = inclusiveByteRange.first + val length = inclusiveByteRange.size + ctx.setStatus(HttpStatus.PARTIAL_CONTENT_206) + .put(HttpHeader.CONTENT_RANGE, inclusiveByteRange.toHeaderRangeString(fileLength)) + .contentProvider( + FileContentProvider( + filePath, + setOf(StandardOpenOption.READ), + position, + length, + CoroutineScope(CoroutineName("Firefly-server-file-content-provider") + ctx.connection.coroutineDispatcher) + ) + ) + .end() + .await() + } + + private fun setContentType(ctx: RoutingContext, filePath: Path) { + val fileName = filePath.fileName.toString() + val mimeType: String? = MimeTypes.getDefaultMimeByExtension(fileName) + if (!mimeType.isNullOrBlank()) { + ctx.put(HttpHeader.CONTENT_TYPE, mimeType) + } + } +} + +class RangeNotSatisfiable(message: String) : IllegalArgumentException(message) + +@NoArg +data class FileConfig( + var rootPath: String +) \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/AsyncTcpConnectionExtension.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/AsyncTcpConnectionExtension.kt new file mode 100644 index 000000000..6218ec088 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/AsyncTcpConnectionExtension.kt @@ -0,0 +1,28 @@ +package com.fireflysource.net.tcp + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import java.nio.ByteBuffer + +fun TcpServer.onAcceptAsync(block: suspend CoroutineScope.(connection: TcpConnection) -> Unit): TcpServer { + this.onAccept { connection -> connection.coroutineScope.launch { block.invoke(this, connection) } } + return this +} + +suspend fun TcpConnection.read(timeout: Long): ByteBuffer = withTimeout(timeout) { + read().await() +} + +suspend fun TcpConnection.close(timeout: Long): Unit = withTimeout(timeout) { + closeAsync().await() +} + +suspend fun TcpConnection.write(byteBuffer: ByteBuffer, timeout: Long): Int = withTimeout(timeout) { + write(byteBuffer).await() +} + +suspend fun TcpConnection.flush(timeout: Long): Unit = withTimeout(timeout) { + flush().await() +} diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AbstractAioTcpConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AbstractAioTcpConnection.kt new file mode 100644 index 000000000..df8a01a25 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AbstractAioTcpConnection.kt @@ -0,0 +1,447 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.exception.UnknownTypeException +import com.fireflysource.common.func.Callback +import com.fireflysource.common.io.* +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.createFailedResult +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.AbstractConnection +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpCoroutineDispatcher +import com.fireflysource.net.tcp.buffer.* +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.channels.ChannelResult +import kotlinx.coroutines.launch +import java.io.IOException +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.AsynchronousSocketChannel +import java.nio.channels.ClosedChannelException +import java.nio.channels.InterruptedByTimeoutException +import java.util.concurrent.CancellationException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Consumer + +/** + * @author Pengtao Qiu + */ +abstract class AbstractAioTcpConnection( + id: Int, + maxIdleTime: Long, + private val socketChannel: AsynchronousSocketChannel, + dispatcher: CoroutineDispatcher, + inputBufferSize: Int, + private val aioTcpCoroutineDispatcher: TcpCoroutineDispatcher = AioTcpCoroutineDispatcher(id, dispatcher) +) : AbstractConnection(id, System.currentTimeMillis(), maxIdleTime), TcpConnection, + TcpCoroutineDispatcher by aioTcpCoroutineDispatcher { + + companion object { + private val log = SystemLogger.create(AbstractAioTcpConnection::class.java) + private val timeUnit = TimeUnit.SECONDS + } + + private val isInputShutdown: AtomicBoolean = AtomicBoolean(false) + private val isOutputShutdown: AtomicBoolean = AtomicBoolean(false) + private val socketChannelClosed: AtomicBoolean = AtomicBoolean(false) + private val closeRequest: AtomicBoolean = AtomicBoolean(false) + private val closeCallbacks: MutableList = mutableListOf() + private val outputMessageHandler = OutputMessageHandler() + private val inputMessageHandler = InputMessageHandler(inputBufferSize) + private val closeResultChannel: Channel>> = Channel(UNLIMITED) + + private inner class OutputMessageHandler { + private val outputMessageChannel: Channel = Channel(UNLIMITED) + private var writeTimeout = maxIdleTime + + init { + writeJob() + } + + fun sendOutputMessage(output: OutputMessage) = + outputMessageChannel.trySend(output) + + private fun writeJob() = coroutineScope.launch { + while (true) { + handleOutputMessage(outputMessageChannel.receive()) + } + }.invokeOnCompletion { cause -> + val e = cause ?: ClosedChannelException() + outputMessageChannel.consumeAll { message -> + when (message) { + is OutputBuffer -> message.result.accept(createFailedResult(-1, e)) + is OutputBufferList -> message.result.accept(createFailedResult(-1, e)) + is OutputBuffers -> message.result.accept(createFailedResult(-1, e)) + is ShutdownOutput -> message.result.accept(createFailedResult(e)) + else -> { + } + } + } + closeResultChannel.consumeAll { it.accept(Result.SUCCESS) } + } + + private suspend fun handleOutputMessage(output: OutputMessage) { + when (output) { + is OutputDataMessage -> writeBuffers(output) + is ShutdownOutput -> shutdownOutputAndClose(output) + is SetWriteTimeout -> if (output.timeout > 0) { + writeTimeout = output.timeout + log.info("Set write timeout: $writeTimeout, id: $id") + } + else -> throw UnknownTypeException("Unknown output message. $output") + } + } + + private suspend fun write(output: OutputDataMessage): Long = when (output) { + is OutputBuffer -> socketChannel.writeAwait(output.buffer, writeTimeout, timeUnit).toLong() + is OutputBufferList -> socketChannel.writeAwait( + output.buffers, output.getCurrentOffset(), output.getCurrentLength(), + writeTimeout, timeUnit + ) + is OutputBuffers -> socketChannel.writeAwait( + output.buffers, output.getCurrentOffset(), output.getCurrentLength(), + writeTimeout, timeUnit + ) + } + + private suspend fun writeBuffers(output: OutputDataMessage): Boolean { + lastWrittenTime = System.currentTimeMillis() + var totalLength = 0L + var success = true + var exception: Exception? = null + while (output.hasRemaining()) { + try { + val writtenLength = write(output) + if (writtenLength < 0) { + success = false + exception = ClosedChannelException() + break + } else { + writtenBytes += writtenLength + totalLength += writtenLength + } + } catch (e: InterruptedByTimeoutException) { + log.warn { "The TCP connection writing timeout. id: $id" } + success = false + exception = e + break + } catch (e: Exception) { + log.warn { "The TCP connection writing exception. ${e.message} id: $id" } + success = false + exception = e + break + } + } + + fun complete() { + when (output) { + is OutputBuffer -> output.result.accept(Result(true, totalLength.toInt(), null)) + is OutputBufferList -> output.result.accept(Result(true, totalLength, null)) + is OutputBuffers -> output.result.accept(Result(true, totalLength, null)) + } + } + + if (success) { + log.debug { "TCP connection writes buffers total length: $totalLength" } + complete() + } else { + shutdownOutputAndClose() + failed(output, exception) + } + return success + } + + private fun failed(outputBuffers: OutputDataMessage, exception: Exception?) { + when (outputBuffers) { + is OutputBuffer -> outputBuffers.result.accept(Result(false, -1, exception)) + is OutputBufferList -> outputBuffers.result.accept(Result(false, -1, exception)) + is OutputBuffers -> outputBuffers.result.accept(Result(false, -1, exception)) + } + } + + private fun shutdownOutputAndClose(output: ShutdownOutput) { + shutdownOutputAndClose() + output.result.accept(Result.SUCCESS) + } + + private fun shutdown() { + if (isOutputShutdown.compareAndSet(false, true)) { + try { + socketChannel.shutdownOutput() + } catch (e: ClosedChannelException) { + log.warn { "The channel closed. $id" } + } catch (e: IOException) { + log.warn { "Shutdown output exception. $id" } + } + } + } + + private fun shutdownOutputAndClose() { + if (isClosed) return + + this@OutputMessageHandler.shutdown() + log.debug { "TCP connection shutdown output. id $id, out: $isOutputShutdown, in: $isInputShutdown, socket: ${!socketChannel.isOpen}" } + if (isShutdownInput) { + closeNow() + } + } + } + + private inner class InputMessageHandler(inputBufferSize: Int) { + + private val inputMessageChannel: Channel = Channel(UNLIMITED) + private val inputBuffer = BufferUtils.allocateDirect(inputBufferSize) + private var readTimeout = maxIdleTime + + init { + readJob() + } + + fun sendInputMessage(input: InputMessage): ChannelResult { + if (input is ShutdownInput) { + this@InputMessageHandler.shutdown() + } + return inputMessageChannel.trySend(input) + } + + private fun readJob() = coroutineScope.launch { + while (true) { + handleInputMessage(inputMessageChannel.receive()) + } + }.invokeOnCompletion { cause -> + val e = cause ?: ClosedChannelException() + inputMessageChannel.consumeAll { message -> + if (message is InputBuffer) { + message.bufferFuture.completeExceptionally(e) + } + } + closeResultChannel.consumeAll { it.accept(Result.SUCCESS) } + } + + private suspend fun handleInputMessage(input: InputMessage) { + when (input) { + is InputBuffer -> readBuffers(input) + is ShutdownInput -> shutdownInputAndClose() + is SetReadTimeout -> if (input.timeout > 0) { + readTimeout = input.timeout + log.info("Set read timeout: $readTimeout, id: $id") + } + } + } + + private suspend fun readBuffers(input: InputBuffer): Boolean { + lastReadTime = System.currentTimeMillis() + var success = true + var exception: Exception? = null + var length = 0 + try { + val pos = inputBuffer.flipToFill() + length = socketChannel.readAwait(inputBuffer, readTimeout, timeUnit) + inputBuffer.flipToFlush(pos) + if (length < 0) { + success = false + exception = ClosedChannelException() + } else { + readBytes += length + } + } catch (e: InterruptedByTimeoutException) { + log.warn { "The TCP connection reading timeout. id: $id" } + success = false + exception = e + } catch (e: Exception) { + log.warn { "The TCP connection reading exception. ${e.message} id: $id" } + success = false + exception = e + } + + if (success) { + log.debug { "TCP connection reads buffers total length: $length" } + inputBuffer.copy().also { input.bufferFuture.complete(it) } + } else { + shutdownInputAndClose() + failed(input, exception) + } + BufferUtils.clear(inputBuffer) + return success + } + + private fun failed(input: InputMessage, e: Exception?) { + if (input is InputBuffer) { + input.bufferFuture.completeExceptionally(e) + } + } + + private fun shutdown() { + if (isInputShutdown.compareAndSet(false, true)) { + try { + socketChannel.shutdownInput() + } catch (e: ClosedChannelException) { + log.warn { "The channel closed. $id" } + } catch (e: IOException) { + log.warn { "Shutdown input exception. $id" } + } + } + } + + private fun shutdownInputAndClose() { + if (isClosed) { + return + } + + this@InputMessageHandler.shutdown() + log.debug { "TCP connection shutdown input. id $id, out: $isOutputShutdown, in: $isInputShutdown, socket: ${!socketChannel.isOpen}" } + if (isShutdownOutput) { + closeNow() + } else { + this@AbstractAioTcpConnection.shutdownOutput() + } + } + } + + override fun read(): CompletableFuture { + val future = CompletableFuture() + if (inputMessageHandler.sendInputMessage(InputBuffer(future)).isFailure) { + future.completeExceptionally(ClosedChannelException()) + } + return future + } + + override fun setReadTimeout(timeout: Long) { + inputMessageHandler.sendInputMessage(SetReadTimeout(timeout)) + } + + override fun write(byteBuffer: ByteBuffer, result: Consumer>): TcpConnection { + if (outputMessageHandler.sendOutputMessage(OutputBuffer(byteBuffer, result)).isFailure) { + result.accept(createFailedResult(-1, ClosedChannelException())) + } + return this + } + + override fun write( + byteBuffers: Array, offset: Int, length: Int, + result: Consumer> + ): TcpConnection { + if (outputMessageHandler.sendOutputMessage(OutputBuffers(byteBuffers, offset, length, result)).isFailure) { + result.accept(createFailedResult(-1L, ClosedChannelException())) + } + return this + } + + override fun write( + byteBufferList: List, offset: Int, length: Int, + result: Consumer> + ): TcpConnection { + if (outputMessageHandler.sendOutputMessage( + OutputBufferList( + byteBufferList, + offset, + length, + result + ) + ).isFailure + ) { + result.accept(createFailedResult(-1L, ClosedChannelException())) + } + return this + } + + override fun setWriteTimeout(timeout: Long) { + outputMessageHandler.sendOutputMessage(SetWriteTimeout(timeout)) + } + + override fun flush(result: Consumer>): TcpConnection { + result.accept(Result.SUCCESS) + return this + } + + override fun getBufferSize(): Int = 0 + + + override fun onClose(callback: Callback): TcpConnection { + closeCallbacks.add(callback) + return this + } + + override fun close(result: Consumer>): TcpConnection { + if (closeRequest.compareAndSet(false, true)) { + if (isClosed) { + result.accept(Result.SUCCESS) + } else { + closeResultChannel.trySend(result) + outputMessageHandler.sendOutputMessage(ShutdownOutput { + if (inputMessageHandler.sendInputMessage(ShutdownInput).isFailure) { + result.accept(createFailedResult(ClosedChannelException())) + } + }) + } + } else { + result.accept(Result.SUCCESS) + } + return this + } + + override fun close() { + close(discard()) + } + + override fun shutdownInput(): TcpConnection { + inputMessageHandler.sendInputMessage(ShutdownInput) + return this + } + + override fun shutdownOutput(): TcpConnection { + outputMessageHandler.sendOutputMessage(ShutdownOutput(discard())) + return this + } + + override fun isShutdownInput(): Boolean = isInputShutdown.get() + + override fun isShutdownOutput(): Boolean = isOutputShutdown.get() + + override fun closeNow(): TcpConnection { + if (socketChannelClosed.compareAndSet(false, true)) { + closeTime = System.currentTimeMillis() + try { + socketChannel.close() + } catch (e: Exception) { + log.warn { "Close socket channel exception. ${e.message} id: $id" } + } + + closeCallbacks.forEach { + try { + it.call() + } catch (e: Exception) { + log.warn { "The TCP connection close callback exception. ${e.message} id: $id" } + } + } + + try { + coroutineScope.cancel(CancellationException("Cancel TCP coroutine exception. id: $id")) + } catch (e: Throwable) { + log.warn { "Cancel TCP coroutine exception. ${e.message} id: $id" } + } + + closeResultChannel.consumeAll { it.accept(Result.SUCCESS) } + + log.info { "The TCP connection close success. id: $id, out: $isOutputShutdown, in: $isInputShutdown, socket: ${!socketChannel.isOpen}" } + } + return this + } + + + override fun isClosed(): Boolean = socketChannelClosed.get() + + override fun isInvalid(): Boolean = + closeRequest.get() || isShutdownInput || isShutdownOutput || socketChannelClosed.get() + + override fun getLocalAddress(): InetSocketAddress = socketChannel.localAddress as InetSocketAddress + + override fun getRemoteAddress(): InetSocketAddress = socketChannel.remoteAddress as InetSocketAddress +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AdaptiveBufferSize.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AdaptiveBufferSize.kt new file mode 100644 index 000000000..b9446360a --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AdaptiveBufferSize.kt @@ -0,0 +1,39 @@ +package com.fireflysource.net.tcp.aio + +/** + * @author Pengtao Qiu + */ +class AdaptiveBufferSize { + companion object { + private val sizeArray = arrayOf( + 128, + 256, + 512, + 1 * 1024, + 2 * 1024, + 4 * 1024, + 4 * 1024, + 8 * 1024, + 8 * 1024, + 8 * 1024, + 16 * 1024, + 32 * 1024, + 64 * 1024, + 128 * 1024, + 256 * 1024, + 512 * 1024 + ) + } + + private var index: Int = 0 + + fun getBufferSize() = sizeArray[index] + + fun update(size: Int) { + index = if (size >= getBufferSize()) { + (index + 1).coerceAtMost(sizeArray.lastIndex) + } else { + (index - 1).coerceAtLeast(0) + } + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioSecureTcpConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioSecureTcpConnection.kt new file mode 100644 index 000000000..bebd81758 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioSecureTcpConnection.kt @@ -0,0 +1,195 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.WrappedTcpConnection +import com.fireflysource.net.tcp.buffer.* +import com.fireflysource.net.tcp.secure.SecureEngine +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Consumer + +/** + * @author Pengtao Qiu + */ +class AioSecureTcpConnection( + private val tcpConnection: TcpConnection, + private val secureEngine: SecureEngine +) : TcpConnection by tcpConnection, WrappedTcpConnection { + + companion object { + private val log = SystemLogger.create(AioSecureTcpConnection::class.java) + } + + private val encryptedOutChannel: Channel = Channel(Channel.UNLIMITED) + private val stashedBuffers = LinkedList() + private val beginHandshake = AtomicBoolean(false) + + init { + secureEngine + .onHandshakeWrite { tcpConnection.write(it) } + .onHandshakeRead { tcpConnection.read() } + tcpConnection.onClose { secureEngine.close() } + } + + override fun getRawTcpConnection(): TcpConnection = tcpConnection + + override fun read(): CompletableFuture { + if (!beginHandshake.get()) { + val future = CompletableFuture() + future.completeExceptionally(IllegalStateException("The TLS handshake has not begun")) + return future + } + + val stashedBuf: ByteBuffer? = stashedBuffers.poll() + return if (stashedBuf != null) { + val future = CompletableFuture() + future.complete(stashedBuf) + future + } else { + tcpConnection.read().thenApply(secureEngine::decrypt) + } + } + + private fun launchEncryptingAndFlushJob() = tcpConnection.coroutineScope.launch { + while (true) { + when (val message = encryptedOutChannel.receive()) { + is OutputBuffer -> encryptAndFlushBuffer(message) + is OutputBufferList -> encryptAndFlushBuffers(message) + is OutputBuffers -> encryptAndFlushBuffers(message) + is ShutdownOutput -> { + shutdownOutput(message) + break + } + else -> {} + } + } + }.invokeOnCompletion { cause -> + val e = cause ?: ClosedChannelException() + encryptedOutChannel.consumeAll { message -> + when (message) { + is OutputBuffer -> message.result.accept(Result.createFailedResult(-1, e)) + is OutputBufferList -> message.result.accept(Result.createFailedResult(-1, e)) + is OutputBuffers -> message.result.accept(Result.createFailedResult(-1, e)) + is ShutdownOutput -> message.result.accept(Result.createFailedResult(e)) + else -> { + } + } + } + } + + private suspend fun encryptAndFlushBuffers(outputMessage: OutputBuffers) { + val result = outputMessage.result + try { + val remaining = outputMessage.remaining() + val buffers = outputMessage.buffers + val offset = outputMessage.getCurrentOffset() + val length = outputMessage.getCurrentLength() + val encryptedBuffer = secureEngine.encrypt(buffers, offset, length) + val size = encryptedBuffer.remaining() + log.debug { "Encrypt and flush buffer. id: $id, src: $remaining, desc: $size, offset: $offset, length: $length" } + + if (remaining == 0L || size == 0) { + result.accept(Result(true, 0, null)) + } else { + tcpConnection.write(encryptedBuffer).await() + result.accept(Result(true, remaining, null)) + } + } catch (e: Exception) { + result.accept(Result(false, -1, e)) + } + } + + private suspend fun encryptAndFlushBuffer(outputMessage: OutputBuffer) { + val (buffer, result) = outputMessage + try { + val remaining = buffer.remaining() + val encryptedBuffer = secureEngine.encrypt(buffer) + val size = encryptedBuffer.remaining() + log.debug { "Encrypt and flush buffer. id: $id, src: $remaining, desc: $size" } + + if (remaining == 0 || size == 0) { + result.accept(Result(true, 0, null)) + } else { + tcpConnection.write(encryptedBuffer).await() + result.accept(Result(true, remaining, null)) + } + } catch (e: Exception) { + result.accept(Result(false, -1, e)) + } + } + + private fun shutdownOutput(message: ShutdownOutput) { + tcpConnection.close(message.result) + } + + override fun write(byteBuffer: ByteBuffer, result: Consumer>): TcpConnection { + encryptedOutChannel.trySend(OutputBuffer(byteBuffer, result)) + return this + } + + override fun write( + byteBuffers: Array, + offset: Int, + length: Int, + result: Consumer> + ): TcpConnection { + encryptedOutChannel.trySend(OutputBuffers(byteBuffers, offset, length, result)) + return this + } + + override fun write( + byteBufferList: List, + offset: Int, + length: Int, + result: Consumer> + ): TcpConnection { + encryptedOutChannel.trySend(OutputBufferList(byteBufferList, offset, length, result)) + return this + } + + override fun close(result: Consumer>): TcpConnection { + encryptedOutChannel.trySend(ShutdownOutput(result)) + return this + } + + override fun isSecureConnection(): Boolean = true + + override fun isClientMode(): Boolean { + return secureEngine.isClientMode + } + + override fun isHandshakeComplete(): Boolean = secureEngine.isHandshakeComplete + + override fun beginHandshake(result: Consumer>): TcpConnection { + if (beginHandshake.compareAndSet(false, true)) { + secureEngine.beginHandshake() + .thenAccept { + result.accept(Result(true, it.applicationProtocol, null)) + it.stashedAppBuffers.forEach { b -> stashedBuffers.add(b) } + launchEncryptingAndFlushJob() + } + .exceptionallyAccept { result.accept(Result(false, "", it)) } + } else { + result.accept(Result(false, "", IllegalStateException("The handshake has begun"))) + } + return this + } + + override fun getSupportedApplicationProtocols(): List { + return secureEngine.supportedApplicationProtocols + } + + override fun getApplicationProtocol(): String { + return secureEngine.applicationProtocol + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpChannelGroup.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpChannelGroup.kt new file mode 100644 index 000000000..d701e4382 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpChannelGroup.kt @@ -0,0 +1,66 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.coroutine.CoroutineDispatchers.awaitTerminationTimeout +import com.fireflysource.common.coroutine.CoroutineDispatchers.defaultPoolSize +import com.fireflysource.common.coroutine.CoroutineDispatchers.newSingleThreadDispatcher +import com.fireflysource.common.coroutine.CoroutineDispatchers.newSingleThreadExecutor +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.tcp.TcpChannelGroup +import kotlinx.coroutines.CoroutineDispatcher +import java.nio.channels.AsynchronousChannelGroup +import java.nio.channels.AsynchronousChannelGroup.withThreadPool +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.abs + +class AioTcpChannelGroup(threadName: String) : AbstractLifeCycle(), TcpChannelGroup { + + companion object { + private val log = SystemLogger.create(AioTcpChannelGroup::class.java) + } + + private val id: AtomicInteger = AtomicInteger(0) + private val group: AsynchronousChannelGroup by lazy { + withThreadPool(newSingleThreadExecutor("firefly-aio-channel-group-thread")) + } + private val dispatchers: Array by lazy { + Array(defaultPoolSize) { i -> + newSingleThreadDispatcher("firefly-$threadName-$i") + } + } + + override fun getDispatcher(connectionId: Int): CoroutineDispatcher { + return dispatchers[abs(connectionId % defaultPoolSize)] + } + + override fun getAsynchronousChannelGroup(): AsynchronousChannelGroup = group + + override fun getNextId(): Int = id.getAndIncrement() + + override fun init() { + log.info { "Initialize TCP channel group. boss: 1, worker: $defaultPoolSize" } + } + + override fun destroy() { + try { + group.shutdown() + // Wait a while for existing tasks to terminate + if (!group.awaitTermination(awaitTerminationTimeout, TimeUnit.SECONDS)) { + group.shutdownNow() // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!group.awaitTermination(awaitTerminationTimeout, TimeUnit.SECONDS)) { + log.info("The TCP channel group did not terminate") + } + } + } catch (ie: InterruptedException) { + // (Re-)Cancel if current thread also interrupted + group.shutdownNow() + // Preserve interrupt status + Thread.currentThread().interrupt() + } catch (e: Exception) { + log.info { "shutdown channel group exception. ${e.message}" } + } + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpClient.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpClient.kt new file mode 100644 index 000000000..e5e29bf6a --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpClient.kt @@ -0,0 +1,151 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.tcp.TcpChannelGroup +import com.fireflysource.net.tcp.TcpClient +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.secure.DefaultSecureEngineFactorySelector +import com.fireflysource.net.tcp.secure.SecureEngineFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.net.SocketAddress +import java.net.StandardSocketOptions +import java.nio.channels.AsynchronousSocketChannel +import java.nio.channels.CompletionHandler +import java.util.concurrent.CompletableFuture + + +/** + * @author Pengtao Qiu + */ +class AioTcpClient(private val config: TcpConfig = TcpConfig()) : AbstractLifeCycle(), TcpClient { + + companion object { + private val log = SystemLogger.create(AioTcpClient::class.java) + } + + private var secureEngineFactory: SecureEngineFactory = + DefaultSecureEngineFactorySelector.createSecureEngineFactory(true) + private var group: TcpChannelGroup = AioTcpChannelGroup("aio-tcp-client") + private var stopGroup = true + + override fun init() { + group.start() + } + + override fun destroy() { + if (stopGroup) group.stop() + } + + override fun tcpChannelGroup(group: TcpChannelGroup): TcpClient { + this.group = group + return this + } + + override fun stopTcpChannelGroup(stop: Boolean): TcpClient { + this.stopGroup = stop + return this + } + + override fun secureEngineFactory(secureEngineFactory: SecureEngineFactory): TcpClient { + this.secureEngineFactory = secureEngineFactory + return this + } + + override fun enableSecureConnection(): TcpClient { + config.enableSecureConnection = true + return this + } + + override fun timeout(timeout: Long): TcpClient { + config.timeout = timeout + return this + } + + override fun bufferSize(bufferSize: Int): TcpClient { + config.outputBufferSize = bufferSize + return this + } + + override fun enableOutputBuffer(): TcpClient { + config.enableOutputBuffer = true + return this + } + + override fun connect(address: SocketAddress): CompletableFuture = + connect(address, defaultSupportedProtocols) + + override fun connect(address: SocketAddress, supportedProtocols: List): CompletableFuture = + connect(address, "", 0, supportedProtocols) + + override fun connect( + address: SocketAddress, + peerHost: String, + peerPort: Int, + supportedProtocols: List + ): CompletableFuture { + val future = CompletableFuture() + try { + connect(address, peerHost, peerPort, supportedProtocols, future) + } catch (e: Exception) { + log.warn(e) { "connecting exception. $address" } + future.completeExceptionally(e) + } + return future + } + + private fun connect( + address: SocketAddress, + peerHost: String, + peerPort: Int, + supportedProtocols: List, + future: CompletableFuture + ) { + start() + + try { + val socketChannel = AsynchronousSocketChannel.open(group.asynchronousChannelGroup) + socketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, config.reuseAddr) + socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, config.keepAlive) + socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, config.tcpNoDelay) + socketChannel.connect(address, group.nextId, object : CompletionHandler { + + override fun completed(result: Void?, connectionId: Int) { + try { + future.complete( + createTcpConnection( + connectionId, + socketChannel, + group, + config, + peerHost, + peerPort, + true, + supportedProtocols, + secureEngineFactory + ) + ) + } catch (e: Exception) { + log.warn(e) { "connecting exception. id: ${connectionId}, address: $address" } + future.completeExceptionally(e) + } + } + + override fun failed(t: Throwable?, connectionId: Int) { + log.warn(t) { "connecting exception. id: ${connectionId}, address: $address" } + future.completeExceptionally(t) + } + }) + } catch (e: Exception) { + log.error(e) { "TCP client connect exception" } + future.completeExceptionally(e) + } + } + +} + +fun TcpClient.connectAsync(host: String, port: Int, block: suspend CoroutineScope.(TcpConnection) -> Unit): TcpClient { + connect(host, port).thenAccept { connection -> connection.coroutineScope.launch { block(connection) } } + return this +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpConnection.kt new file mode 100644 index 000000000..e88fdf80d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpConnection.kt @@ -0,0 +1,35 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.exception.UnsupportedOperationException +import com.fireflysource.common.sys.Result +import com.fireflysource.net.tcp.TcpConnection +import kotlinx.coroutines.CoroutineDispatcher +import java.nio.channels.AsynchronousSocketChannel +import java.util.function.Consumer + +/** + * @author Pengtao Qiu + */ +class AioTcpConnection( + id: Int, + maxIdleTime: Long, + socketChannel: AsynchronousSocketChannel, + dispatcher: CoroutineDispatcher, + inputBufferSize: Int +) : AbstractAioTcpConnection(id, maxIdleTime, socketChannel, dispatcher, inputBufferSize) { + + override fun isSecureConnection(): Boolean = false + + override fun isClientMode(): Boolean { + throw UnsupportedOperationException() + } + + override fun isHandshakeComplete(): Boolean = true + + override fun getSupportedApplicationProtocols(): List = listOf() + + override fun beginHandshake(result: Consumer>): TcpConnection { + result.accept(Result(true, "", null)) + return this + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpConnectionFactory.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpConnectionFactory.kt new file mode 100644 index 000000000..072cc3ff8 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpConnectionFactory.kt @@ -0,0 +1,61 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.net.tcp.TcpChannelGroup +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.WrappedTcpConnection +import com.fireflysource.net.tcp.secure.SecureEngineFactory +import java.nio.channels.AsynchronousSocketChannel + +fun createSecureTcpConnection( + tcpConnection: TcpConnection, + peerHost: String, + peerPort: Int, + clientMode: Boolean, + supportedProtocols: List, + secureEngineFactory: SecureEngineFactory +): TcpConnection { + val rawTcpConnection = if (tcpConnection is WrappedTcpConnection) { + tcpConnection.rawTcpConnection + } else tcpConnection + val secureEngine = if (peerHost.isNotBlank() && peerPort != 0) { + secureEngineFactory.create(rawTcpConnection.coroutineScope, clientMode, peerHost, peerPort, supportedProtocols) + } else { + secureEngineFactory.create(rawTcpConnection.coroutineScope, clientMode, supportedProtocols) + } + return AioSecureTcpConnection(rawTcpConnection, secureEngine) +} + +fun createTcpConnection( + connectionId: Int, + socketChannel: AsynchronousSocketChannel, + group: TcpChannelGroup, + tcpConfig: TcpConfig, + peerHost: String, + peerPort: Int, + clientMode: Boolean, + supportedProtocols: List, + secureEngineFactory: SecureEngineFactory, +): TcpConnection { + val aioTcpConnection = AioTcpConnection( + connectionId, + tcpConfig.timeout, + socketChannel, + group.getDispatcher(connectionId), + tcpConfig.inputBufferSize + ) + + val tcpConnection = if (tcpConfig.enableSecureConnection) { + createSecureTcpConnection( + aioTcpConnection, + peerHost, + peerPort, + clientMode, + supportedProtocols, + secureEngineFactory + ) + } else aioTcpConnection + + return if (tcpConfig.enableOutputBuffer) { + BufferedOutputTcpConnection(tcpConnection, tcpConfig.outputBufferSize) + } else tcpConnection +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpCoroutineDispatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpCoroutineDispatcher.kt new file mode 100644 index 000000000..91d2a1256 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpCoroutineDispatcher.kt @@ -0,0 +1,22 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.net.tcp.TcpCoroutineDispatcher +import kotlinx.coroutines.* + +class AioTcpCoroutineDispatcher( + id: Int, + private val dispatcher: CoroutineDispatcher, + private val supervisor: CompletableJob = SupervisorJob(), + private val scope: CoroutineScope = CoroutineScope(dispatcher + supervisor + CoroutineName("TcpConnection#$id")) +) : TcpCoroutineDispatcher { + + override fun getCoroutineDispatcher(): CoroutineDispatcher = dispatcher + + override fun getSupervisorJob(): CompletableJob = supervisor + + override fun getCoroutineScope(): CoroutineScope = scope + + override fun execute(runnable: Runnable) { + scope.launch { runnable.run() } + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpServer.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpServer.kt new file mode 100644 index 000000000..658900c39 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/AioTcpServer.kt @@ -0,0 +1,203 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.tcp.TcpChannelGroup +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpServer +import com.fireflysource.net.tcp.secure.DefaultSecureEngineFactorySelector +import com.fireflysource.net.tcp.secure.SecureEngineFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.launch +import java.net.SocketAddress +import java.net.StandardSocketOptions +import java.nio.channels.* +import java.util.function.Consumer + +/** + * @author Pengtao Qiu + */ +class AioTcpServer(private val config: TcpConfig = TcpConfig()) : AbstractLifeCycle(), TcpServer { + + companion object { + private val log = SystemLogger.create(AioTcpServer::class.java) + } + + private var group: TcpChannelGroup = AioTcpChannelGroup("aio-tcp-server") + private var stopGroup = true + private val connectionChannel = Channel(UNLIMITED) + private var connectionConsumer: Consumer = Consumer { connectionChannel.trySend(it) } + private var secureEngineFactory: SecureEngineFactory = + DefaultSecureEngineFactorySelector.createSecureEngineFactory(false) + private var supportedProtocols: List = defaultSupportedProtocols + private var peerHost: String = "" + private var peerPort: Int = 0 + private var serverSocketChannel: AsynchronousServerSocketChannel? = null + private val acceptSocketConnectionCompletionHandler = + object : CompletionHandler { + override fun completed(socketChannel: AsynchronousSocketChannel, connectionId: Int) { + onAcceptCompleted(socketChannel, connectionId) + } + + override fun failed(e: Throwable, connectionId: Int) { + onAcceptFailed(e, connectionId) + } + } + + override fun init() { + group.start() + log.info("The Firefly HTTP server supported application protocols {}", supportedProtocols) + } + + override fun destroy() { + try { + serverSocketChannel?.close() + } catch (e: Exception) { + log.error(e) { "close server socket channel exception" } + } + if (stopGroup) group.stop() + } + + override fun tcpChannelGroup(group: TcpChannelGroup): TcpServer { + this.group = group + return this + } + + override fun stopTcpChannelGroup(stop: Boolean): TcpServer { + this.stopGroup = stop + return this + } + + override fun getTcpConnectionChannel(): Channel = connectionChannel + + override fun secureEngineFactory(secureEngineFactory: SecureEngineFactory): TcpServer { + this.secureEngineFactory = secureEngineFactory + return this + } + + override fun supportedProtocols(supportedProtocols: List): TcpServer { + this.supportedProtocols = supportedProtocols + return this + } + + override fun peerHost(peerHost: String): TcpServer { + this.peerHost = peerHost + return this + } + + override fun peerPort(peerPort: Int): TcpServer { + this.peerPort = peerPort + return this + } + + override fun enableSecureConnection(): TcpServer { + config.enableSecureConnection = true + return this + } + + override fun timeout(timeout: Long): TcpServer { + config.timeout = timeout + return this + } + + override fun bufferSize(bufferSize: Int): TcpServer { + config.outputBufferSize = bufferSize + return this + } + + override fun enableOutputBuffer(): TcpServer { + config.enableOutputBuffer = true + return this + } + + override fun onAccept(consumer: Consumer): TcpServer { + connectionConsumer = consumer + return this + } + + override fun listen(address: SocketAddress): TcpServer { + if (isStarted) { + return this + } + + start() + + try { + val socketChannel = AsynchronousServerSocketChannel.open(group.asynchronousChannelGroup) + socketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, config.reuseAddr) + socketChannel.bind(address, config.backlog) + this.serverSocketChannel = socketChannel + accept() + } catch (e: Exception) { + log.error(e) { "bind server address exception" } + } + return this + } + + private fun accept() { + try { + serverSocketChannel?.accept(group.nextId, acceptSocketConnectionCompletionHandler) + } catch (e: ShutdownChannelGroupException) { + log.info { "the channel group is shutdown." } + } catch (e: Exception) { + log.error(e) { "accept socket channel exception." } + } + } + + private fun onAcceptCompleted(socketChannel: AsynchronousSocketChannel, connectionId: Int) { + try { + socketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, config.reuseAddr) + socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, config.keepAlive) + socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, config.tcpNoDelay) + + connectionConsumer.accept( + createTcpConnection( + connectionId, + socketChannel, + group, + config, + peerHost, + peerPort, + false, + supportedProtocols, + secureEngineFactory + ) + ) + log.debug { "accept the client connection. $connectionId" } + } catch (e: Exception) { + log.warn(e) { "accept connection exception. $connectionId" } + } finally { + accept() + } + } + + private fun onAcceptFailed(e: Throwable, connectionId: Int) { + when (e) { + is ClosedChannelException -> { + log.info { "The server socket channel has been closed." } + } + is ShutdownChannelGroupException -> { + log.info { "the server is shutdown. stop to accept connection." } + } + else -> { + log.warn(e) { "accept connection failure. $connectionId" } + accept() + } + } + } + + public override fun clone(): AioTcpServer { + val config = this.config.copy() + val server = AioTcpServer(config) + server.group = this.group + server.stopGroup = this.stopGroup + return server + } +} + +fun TcpServer.onAcceptAsync(block: suspend CoroutineScope.(TcpConnection) -> Unit): TcpServer { + onAccept { connection -> connection.coroutineScope.launch { block(connection) } } + return this +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/BufferedOutputTcpConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/BufferedOutputTcpConnection.kt new file mode 100644 index 000000000..9d6864675 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/BufferedOutputTcpConnection.kt @@ -0,0 +1,159 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.WrappedTcpConnection +import com.fireflysource.net.tcp.buffer.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.util.function.Consumer + +class BufferedOutputTcpConnection( + private val tcpConnection: TcpConnection, + private val bufferSize: Int = 8192 +) : TcpConnection by tcpConnection, WrappedTcpConnection { + + companion object { + private val log = SystemLogger.create(BufferedOutputTcpConnection::class.java) + } + + private val buffer: ByteBuffer = BufferUtils.allocateDirect(bufferSize) + private var position = buffer.flipToFill() + private val outputMessageChannel: Channel = Channel(Channel.UNLIMITED) + + init { + flushJob() + } + + override fun getRawTcpConnection(): TcpConnection = tcpConnection + + private fun flushJob() = coroutineScope.launch { + while (true) { + when (val message = outputMessageChannel.receive()) { + is OutputBuffer -> appendOutputBuffer(message) + is OutputBufferList -> appendOutputBuffers(message) + is OutputBuffers -> appendOutputBuffers(message) + is FlushOutput -> flushBuffer(message) + is ShutdownOutput -> { + shutdownOutput(message) + break + } + else -> {} + } + } + }.invokeOnCompletion { cause -> + val e = cause ?: ClosedChannelException() + outputMessageChannel.consumeAll { message -> + when (message) { + is OutputBuffer -> message.result.accept(Result.createFailedResult(-1, e)) + is OutputBufferList -> message.result.accept(Result.createFailedResult(-1, e)) + is OutputBuffers -> message.result.accept(Result.createFailedResult(-1, e)) + is ShutdownOutput -> message.result.accept(Result.createFailedResult(e)) + is FlushOutput -> message.result.accept(Result.createFailedResult(e)) + else -> { + } + } + } + } + + private suspend fun appendOutputBuffer(message: OutputBuffer) { + try { + val remaining = message.buffer.remaining() + append(message.buffer) + message.result.accept(Result(true, remaining, null)) + } catch (e: Exception) { + message.result.accept(Result(false, -1, e)) + } + } + + private suspend fun appendOutputBuffers(message: OutputBuffers) { + try { + val remaining = message.remaining() + val offset = message.getCurrentOffset() + val lastIndex = message.getLastIndex() + (offset..lastIndex).map { message.buffers[it] }.forEach { append(it) } + message.result.accept(Result(true, remaining, null)) + } catch (e: Exception) { + message.result.accept(Result(false, -1, e)) + } + } + + private suspend fun append(src: ByteBuffer) { + while (src.hasRemaining()) { + val srcRemaining = src.remaining() + val consumed = BufferUtils.put(src, buffer) + log.debug { "Append buffer. id: $id, src: $srcRemaining, consumed: $consumed" } + if (!buffer.hasRemaining()) { + flushBuffer() + } + } + } + + private suspend fun flushBuffer(message: FlushOutput) { + try { + flushBuffer() + message.result.accept(Result.SUCCESS) + } catch (e: Exception) { + message.result.accept(Result.createFailedResult(e)) + } + } + + private suspend fun flushBuffer() { + buffer.flipToFlush(position) + val remaining = buffer.remaining() + val consumed = tcpConnection.write(buffer).await() + log.debug { "Flush buffer. id: $id, len: $remaining, consumed: $consumed" } + BufferUtils.clear(buffer) + position = buffer.flipToFill() + } + + private suspend fun shutdownOutput(message: ShutdownOutput) { + try { + flushBuffer() + tcpConnection.close(message.result) + } catch (e: Exception) { + message.result.accept(Result.createFailedResult(e)) + } + } + + override fun flush(result: Consumer>): TcpConnection { + outputMessageChannel.trySend(FlushOutput(result)) + return this + } + + override fun getBufferSize(): Int = bufferSize + + override fun write(byteBuffer: ByteBuffer, result: Consumer>): TcpConnection { + outputMessageChannel.trySend(OutputBuffer(byteBuffer, result)) + return this + } + + override fun write( + byteBuffers: Array, offset: Int, length: Int, result: Consumer> + ): TcpConnection { + val message = OutputBuffers(byteBuffers, offset, length, result) + outputMessageChannel.trySend(message) + return this + } + + override fun write( + byteBufferList: List, offset: Int, length: Int, result: Consumer> + ): TcpConnection { + val message = OutputBufferList(byteBufferList, offset, length, result) + outputMessageChannel.trySend(message) + return this + } + + override fun close(result: Consumer>): TcpConnection { + outputMessageChannel.trySend(ShutdownOutput(result)) + return this + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/TcpConfig.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/TcpConfig.kt new file mode 100644 index 000000000..4aa690385 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/aio/TcpConfig.kt @@ -0,0 +1,41 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.annotation.NoArg +import com.fireflysource.net.tcp.exception.UnknownProtocolException + +/** + * @author Pengtao Qiu + */ +@NoArg +data class TcpConfig @JvmOverloads constructor( + var timeout: Long = 30, + var enableSecureConnection: Boolean = false, + var backlog: Int = 16 * 1024, + var reuseAddr: Boolean = true, + var keepAlive: Boolean = true, + var tcpNoDelay: Boolean = false, + var inputBufferSize: Int = 16 * 1024, + var outputBufferSize: Int = 16 * 1024, + var enableOutputBuffer: Boolean = false +) + +enum class ApplicationProtocol(val value: String) { + HTTP2("h2"), HTTP1("http/1.1") +} + +val defaultSupportedProtocols: List = ApplicationProtocol.values().map { it.value } + +val schemaDefaultPort = mapOf( + "http" to 80, + "https" to 443, + "ws" to 80, + "wss" to 443 +) + +fun isSecureProtocol(scheme: String): Boolean { + return when (scheme) { + "wss", "https" -> true + "ws", "http" -> false + else -> throw UnknownProtocolException("Unknown protocol $scheme") + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/buffer/InputOutputMessages.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/buffer/InputOutputMessages.kt new file mode 100644 index 000000000..3aefd2a0f --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/buffer/InputOutputMessages.kt @@ -0,0 +1,107 @@ +package com.fireflysource.net.tcp.buffer + +import com.fireflysource.common.sys.Result +import java.nio.ByteBuffer +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +sealed interface InputMessage + +@JvmInline +value class InputBuffer(val bufferFuture: CompletableFuture) : InputMessage + +object ShutdownInput : InputMessage + +@JvmInline +value class SetReadTimeout(val timeout: Long) : InputMessage + +sealed interface OutputMessage + +sealed interface OutputDataMessage { + fun hasRemaining(): Boolean = false +} + +data class OutputBuffer(val buffer: ByteBuffer, val result: Consumer>) : OutputMessage, OutputDataMessage { + override fun hasRemaining(): Boolean = buffer.hasRemaining() +} + +open class OutputBuffers( + val buffers: Array, + val offset: Int, + val length: Int, + val result: Consumer>, + private val outputBufferArray: DelegatedOutputBufferArray = DelegatedOutputBufferArray( + buffers, offset, length, result + ) +) : OutputMessage, OutputBufferArray by outputBufferArray, OutputDataMessage { + override fun hasRemaining(): Boolean { + return outputBufferArray.hasRemaining() + } +} + +class OutputBufferList( + bufferList: List, + offset: Int, + length: Int, + result: Consumer> +) : OutputBuffers(bufferList.toTypedArray(), offset, length, result) + +@JvmInline +value class ShutdownOutput(val result: Consumer>) : OutputMessage + +@JvmInline +value class FlushOutput(val result: Consumer>) : OutputMessage + +@JvmInline +value class SetWriteTimeout(val timeout: Long) : OutputMessage + +interface OutputBufferArray { + fun getCurrentOffset(): Int + fun getCurrentLength(): Int + fun getLastIndex(): Int + fun remaining(): Long + fun hasRemaining(): Boolean +} + +class DelegatedOutputBufferArray( + val buffers: Array, + val offset: Int, + val length: Int, + val result: Consumer> +) : OutputBufferArray { + init { + require(offset >= 0) { "The offset must be greater than or equal the 0" } + require(length > 0) { "The length must be greater than 0" } + require(offset < buffers.size) { "The offset must be less than the buffer size" } + require((offset + length) <= buffers.size) { "The length must be less than or equal the buffer size" } + } + + private val maxSize = offset + length + private val lastIndex = maxSize - 1 + private var currentOffset = offset + + override fun getCurrentOffset(): Int { + for (i in currentOffset..lastIndex) { + if (buffers[i].hasRemaining()) { + currentOffset = i + return i + } + } + return maxSize + } + + override fun getCurrentLength(): Int { + return maxSize - getCurrentOffset() + } + + override fun getLastIndex(): Int = lastIndex + + override fun remaining(): Long { + val offset = getCurrentOffset() + return (offset..lastIndex).sumOf { buffers[it].remaining().toLong() } + } + + override fun hasRemaining(): Boolean { + return getCurrentOffset() < maxSize + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/secure/AbstractAsyncSecureEngine.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/secure/AbstractAsyncSecureEngine.kt new file mode 100644 index 000000000..4d78c795d --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/tcp/secure/AbstractAsyncSecureEngine.kt @@ -0,0 +1,305 @@ +package com.fireflysource.net.tcp.secure + +import com.fireflysource.common.coroutine.blocking +import com.fireflysource.common.exception.UnknownTypeException +import com.fireflysource.common.io.* +import com.fireflysource.common.io.BufferUtils.EMPTY_BUFFER +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.tcp.buffer.OutputBuffer +import com.fireflysource.net.tcp.buffer.OutputBufferList +import com.fireflysource.net.tcp.buffer.OutputBuffers +import com.fireflysource.net.tcp.buffer.OutputDataMessage +import com.fireflysource.net.tcp.secure.exception.SecureNetException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Supplier +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLEngineResult +import javax.net.ssl.SSLEngineResult.HandshakeStatus.* +import javax.net.ssl.SSLEngineResult.Status.* + +abstract class AbstractAsyncSecureEngine( + private val coroutineScope: CoroutineScope, + private val sslEngine: SSLEngine, + private val applicationProtocolSelector: ApplicationProtocolSelector +) : SecureEngine { + + companion object { + private val log = SystemLogger.create(AbstractAsyncSecureEngine::class.java) + } + + private var readSupplier: Supplier>? = null + private var writeFunction: Function>? = null + + private var inPacketBuffer = EMPTY_BUFFER + private val inAppBuffer = BufferUtils.allocateDirect(sslEngine.session.applicationBufferSize) + private val outPacketBuffer = BufferUtils.allocateDirect(sslEngine.session.packetBufferSize) + + private val closed = AtomicBoolean(false) + private var handshakeStatus = sslEngine.handshakeStatus + private val handshakeFinished = AtomicBoolean(false) + private val beginHandshake = AtomicBoolean(false) + private var unwrapResultStatus: SSLEngineResult.Status = OK + + override fun onHandshakeRead(supplier: Supplier>): SecureEngine { + this.readSupplier = supplier + return this + } + + override fun onHandshakeWrite(function: Function>): SecureEngine { + this.writeFunction = function + return this + } + + override fun beginHandshake(result: Consumer>) { + if (beginHandshake.compareAndSet(false, true)) { + launchHandshakeJob(result) + } else { + result.accept( + Result( + false, null, + SecureNetException("The handshake has begun, do not invoke it method repeatedly.") + ) + ) + } + } + + private fun launchHandshakeJob(result: Consumer>) = coroutineScope.launch { + try { + val stashedAppBuffers = LinkedList() + doHandshake(stashedAppBuffers) + result.accept(Result(true, HandshakeData(stashedAppBuffers, applicationProtocol), null)) + } catch (e: Exception) { + result.accept(Result(false, null, e)) + } + } + + private suspend fun doHandshake(stashedAppBuffers: MutableList) { + begin() + handshakeLoop@ while (true) { + when (handshakeStatus) { + NEED_WRAP -> doHandshakeWrap() + NEED_UNWRAP -> doHandshakeUnwrap(stashedAppBuffers) + NEED_TASK -> runDelegatedTasks() + NOT_HANDSHAKING, FINISHED -> { + handshakeComplete() + break@handshakeLoop + } + else -> throw UnknownTypeException("Unknown handshake status. $handshakeStatus") + } + } + } + + private fun begin() { + sslEngine.beginHandshake() + handshakeStatus = sslEngine.handshakeStatus + val packetBufferSize = sslEngine.session.packetBufferSize + val applicationBufferSize = sslEngine.session.applicationBufferSize + val status = handshakeStatus + log.info { + "Begin TLS handshake. mode: ${getMode()}, status: ${status}, " + + "packetSize: ${packetBufferSize}, appSize: $applicationBufferSize" + } + } + + private suspend fun doHandshakeWrap() { + val bufferList = encrypt(EMPTY_BUFFER) + if (bufferList.hasRemaining()) { + val length = writeFunction?.apply(bufferList)?.await() + log.debug { "Wrap TLS handshake data. status: $handshakeStatus, mode: ${getMode()}, length: $length" } + } + } + + private suspend fun doHandshakeUnwrap(stashedAppBuffers: MutableList) { + val receivedBuffer = when { + unwrapResultStatus == BUFFER_UNDERFLOW -> readSupplier?.get()?.await() + inPacketBuffer.hasRemaining() -> EMPTY_BUFFER + else -> readSupplier?.get()?.await() + } + if (receivedBuffer != null) { + val length = inPacketBuffer.remaining() + receivedBuffer.remaining() + val inAppBuffer = decrypt(receivedBuffer) + val remaining = inAppBuffer.remaining() + if (remaining > 0) { + stashedAppBuffers.add(inAppBuffer) + } + log.debug { "Unwrap TLS handshake data. status: $handshakeStatus, mode: ${getMode()}, length: ${length}, stashedBuffer: $remaining" } + } + } + + private suspend fun runDelegatedTasks() { + while (true) { + val runnable: Runnable? = sslEngine.delegatedTask + if (runnable != null) { + log.debug { "Run TLS handshake delegated tasks. status: ${sslEngine.handshakeStatus}" } + blocking { runnable.run() }.join() + } else break + } + handshakeStatus = sslEngine.handshakeStatus + log.debug { "After run TLS handshake delegated tasks. no tasks: ${sslEngine.delegatedTask == null}, status: $handshakeStatus" } + } + + private fun handshakeComplete() { + if (handshakeFinished.compareAndSet(false, true)) { + val tlsProtocol = sslEngine.session.protocol + val cipherSuite = sslEngine.session.cipherSuite + val inPacketBufferRemaining = inPacketBuffer.remaining() + log.info( + "TLS handshake success. mode: ${getMode()}, protocol: {} {}, cipher: {}, status: {}, inPacketRemaining: {}", + applicationProtocol, tlsProtocol, cipherSuite, handshakeStatus, inPacketBufferRemaining + ) + } + } + + override fun encrypt(outAppBuffer: ByteBuffer): ByteBuffer = + encryptBuffers(OutputBuffer(outAppBuffer, discard())) + + override fun encrypt(byteBuffers: Array, offset: Int, length: Int): ByteBuffer = + encryptBuffers(OutputBuffers(byteBuffers, offset, length, discard())) + + + override fun encrypt(byteBuffers: MutableList, offset: Int, length: Int): ByteBuffer = + encryptBuffers(OutputBufferList(byteBuffers, offset, length, discard())) + + private fun encryptBuffers(outAppBuffer: OutputDataMessage): ByteBuffer { + var packetBuffer = this.outPacketBuffer + val pos = packetBuffer.flipToFill() + + fun wrap() = when (outAppBuffer) { + is OutputBuffer -> sslEngine.wrap(outAppBuffer.buffer, packetBuffer) + is OutputBufferList -> sslEngine.wrap(outAppBuffer.buffers, packetBuffer) + is OutputBuffers -> sslEngine.wrap(outAppBuffer.buffers, packetBuffer) + } + + wrap@ while (true) { + val result = wrap() + handshakeStatus = result.handshakeStatus + + when (result.status) { + BUFFER_OVERFLOW -> { + packetBuffer = packetBuffer.addCapacity(sslEngine.session.packetBufferSize) + val capacity = packetBuffer.capacity() + val remaining = packetBuffer.remaining() + log.debug { "Resize out packet buffer. capacity: $capacity, remaining: $remaining" } + } + OK -> { + if (handshakeStatus != NEED_WRAP && result.bytesProduced() == 0 && result.bytesConsumed() == 0) { + break@wrap + } + if (!outAppBuffer.hasRemaining()) { + break@wrap + } + } + CLOSED -> { + sslEngine.closeOutbound() + break@wrap + } + else -> throw SecureNetException("Wrap data result status error. ${result.status}") + } + + } + + return packetBuffer.flipToFlush(pos).copy().also { BufferUtils.clear(this.outPacketBuffer) } + } + + override fun decrypt(receivedBuffer: ByteBuffer): ByteBuffer { + merge(receivedBuffer) + + if (!inPacketBuffer.hasRemaining()) { + return EMPTY_BUFFER + } + + var appBuffer = this.inAppBuffer + val pos = appBuffer.flipToFill() + + unwrap@ while (true) { + val result = sslEngine.unwrap(inPacketBuffer, appBuffer) + handshakeStatus = result.handshakeStatus + unwrapResultStatus = result.status + + when (result.status) { + BUFFER_UNDERFLOW -> { + if (inPacketBuffer.remaining() < sslEngine.session.packetBufferSize) { + break@unwrap + } + } + BUFFER_OVERFLOW -> { + appBuffer = appBuffer.addCapacity(sslEngine.session.applicationBufferSize) + val capacity = appBuffer.capacity() + val remaining = appBuffer.remaining() + log.debug { "Resize in app buffer. capacity: $capacity, remaining: $remaining" } + } + OK -> { + if (handshakeStatus != NEED_UNWRAP && result.bytesProduced() == 0 && result.bytesConsumed() == 0) { + break@unwrap + } + if (!inPacketBuffer.hasRemaining()) { + inPacketBuffer = EMPTY_BUFFER + break@unwrap + } + } + CLOSED -> { + sslEngine.closeInbound() + break@unwrap + } + else -> throw SecureNetException("Unwrap packets state exception. ${result.status}") + } + } + + return appBuffer.flipToFlush(pos).copy().also { BufferUtils.clear(this.inAppBuffer) } + } + + private fun merge(receivedBuffer: ByteBuffer) { + if (!receivedBuffer.hasRemaining()) { + return + } + + inPacketBuffer = if (inPacketBuffer.hasRemaining()) { + log.debug { + "Merge received packet buffer. mode: ${getMode()}, " + + "in packet buffer: ${inPacketBuffer.remaining()}, " + + "received buffer: ${receivedBuffer.remaining()}" + } + + val capacity = inPacketBuffer.remaining() + receivedBuffer.remaining() + BufferUtils.allocate(capacity).append(inPacketBuffer).append(receivedBuffer) + } else { + receivedBuffer + } + } + + private fun getMode(): String = if (isClientMode) "Client" else "Server" + + override fun isClientMode(): Boolean = sslEngine.useClientMode + + override fun getSupportedApplicationProtocols(): MutableList = + applicationProtocolSelector.supportedApplicationProtocols + + override fun getApplicationProtocol(): String = applicationProtocolSelector.applicationProtocol + + override fun close() { + if (closed.compareAndSet(false, true)) { + sslEngine.closeOutbound() + } + } + + override fun isHandshakeComplete(): Boolean = handshakeFinished.get() + + private class HandshakeData( + private val stashedAppBuffers: MutableList, + private val applicationProtocol: String + ) : HandshakeResult { + override fun getApplicationProtocol(): String = applicationProtocol + override fun getStashedAppBuffers(): MutableList = stashedAppBuffers + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/udp/buffer/InputOutputMessages.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/buffer/InputOutputMessages.kt new file mode 100644 index 000000000..e65acf046 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/buffer/InputOutputMessages.kt @@ -0,0 +1,26 @@ +package com.fireflysource.net.udp.buffer + +import com.fireflysource.net.udp.UdpConnection +import java.nio.ByteBuffer +import java.nio.channels.SelectionKey +import java.util.concurrent.CompletableFuture + +sealed interface InputMessage + +@JvmInline +value class InputBuffer(val bufferFuture: CompletableFuture) : InputMessage + +object CancelSelectionKey : InputMessage + +object InvalidSelectionKey : InputMessage + +object UnregisterRead : InputMessage +@JvmInline +value class ReadComplete(val buffer: ByteBuffer): InputMessage + +sealed interface NioWorkerMessage + +data class RegisterRead( + val udpConnection: UdpConnection, + val future: CompletableFuture +) : NioWorkerMessage \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/AbstractNioUdpConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/AbstractNioUdpConnection.kt new file mode 100644 index 000000000..f1d98a133 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/AbstractNioUdpConnection.kt @@ -0,0 +1,143 @@ +package com.fireflysource.net.udp.nio + +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.AbstractConnection +import com.fireflysource.net.udp.UdpConnection +import com.fireflysource.net.udp.UdpCoroutineDispatcher +import com.fireflysource.net.udp.buffer.* +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.nio.channels.DatagramChannel +import java.nio.channels.SelectionKey +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +abstract class AbstractNioUdpConnection( + id: Int, + maxIdleTime: Long, + dispatcher: CoroutineDispatcher, + inputBufferSize: Int, + private val nioUdpCoroutineDispatcher: UdpCoroutineDispatcher = NioUdpCoroutineDispatcher(id, dispatcher), + val datagramChannel: DatagramChannel, + private val nioUdpWorker: NioUdpWorker, +) : AbstractConnection(id, System.currentTimeMillis(), maxIdleTime), UdpConnection, + UdpCoroutineDispatcher by nioUdpCoroutineDispatcher { + + companion object { + private val log = SystemLogger.create(AbstractNioUdpConnection::class.java) + private val timeUnit = TimeUnit.SECONDS + } + + private val closeResultChannel: Channel>> = Channel(Channel.UNLIMITED) + private val inputMessageHandler = InputMessageHandler(inputBufferSize) + + private inner class InputMessageHandler(inputBufferSize: Int) { + private val inputMessageChannel: Channel = Channel(Channel.UNLIMITED) + private val readRequestQueue = LinkedList() + private val readCompleteQueue = LinkedList() + private var readTimeout = maxIdleTime + private var readWaterline = 0 + private var selectionKey: SelectionKey? = null + + init { + readJob() + } + + + private fun readJob() = coroutineScope.launch { + selectionKey = nioUdpWorker.registerRead(this@AbstractNioUdpConnection).await() + while (true) { + when (val msg = inputMessageChannel.receive()) { + is InputBuffer -> offerReadRequest(msg) + is ReadComplete -> offerReadComplete(msg.buffer) + is CancelSelectionKey -> { + selectionKey = null + } + is InvalidSelectionKey -> { + selectionKey = null + } + is UnregisterRead -> { + + } + } + } + }.invokeOnCompletion { cause -> + val e = cause ?: ClosedChannelException() + inputMessageChannel.consumeAll { message -> + if (message is InputBuffer) { + message.bufferFuture.completeExceptionally(e) + } + } + closeResultChannel.consumeAll { it.accept(Result.SUCCESS) } + } + + private fun offerReadComplete(buffer: ByteBuffer) { + if (readCompleteQueue.offer(buffer)) { + readWaterline += buffer.remaining() + updateReadQueue() + } + } + + private fun pollReadComplete(): ByteBuffer? { + val buffer = readCompleteQueue.poll() + if (buffer != null) { + readWaterline -= buffer.remaining() + } + return buffer + } + + private fun offerReadRequest(request: InputBuffer) { + if (readRequestQueue.offer(request)) { + updateReadQueue() + } + } + + private fun updateReadQueue() { + while (!readRequestQueue.isEmpty() && !readCompleteQueue.isEmpty()) { + val readRequest = readRequestQueue.poll()!! + val buffer: ByteBuffer = pollReadComplete()!! + readRequest.bufferFuture.complete(buffer) + } + } + + fun sendMessage(message: InputMessage) { + if (inputMessageChannel.trySend(message).isFailure) { + log.error("send input message failure. $message") + } + } + + } + + fun sendInvalidSelectionKeyMessage() { + inputMessageHandler.sendMessage(InvalidSelectionKey) + } + + fun sendCancelSelectionKeyMessage() { + inputMessageHandler.sendMessage(CancelSelectionKey) + } + + fun sendUnregisterReadMessage() { + inputMessageHandler.sendMessage(UnregisterRead) + } + + fun sendReadCompleteMessage(buffer: ByteBuffer) { + inputMessageHandler.sendMessage(ReadComplete(buffer)) + } + + +} + +enum class ReadResult { + REMOTE_CLOSE, SUSPEND_READ, CONTINUE_READ, READ_EXCEPTION +} + +enum class WriteResult { + REMOTE_CLOSE, SUSPEND_WRITE, CONTINUE_WRITE, WRITE_EXCEPTION +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/NioUdpCoroutineDispatcher.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/NioUdpCoroutineDispatcher.kt new file mode 100644 index 000000000..3d830476f --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/NioUdpCoroutineDispatcher.kt @@ -0,0 +1,21 @@ +package com.fireflysource.net.udp.nio + +import com.fireflysource.net.udp.UdpCoroutineDispatcher +import kotlinx.coroutines.* + +class NioUdpCoroutineDispatcher( + id: Int, + private val dispatcher: CoroutineDispatcher, + private val supervisor: CompletableJob = SupervisorJob(), + private val scope: CoroutineScope = CoroutineScope(dispatcher + supervisor + CoroutineName("UdpConnection#$id")) +) : UdpCoroutineDispatcher { + override fun execute(runnable: Runnable) { + scope.launch { runnable.run() } + } + + override fun getCoroutineDispatcher(): CoroutineDispatcher = dispatcher + + override fun getCoroutineScope(): CoroutineScope = scope + + override fun getSupervisorJob(): CompletableJob = supervisor +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/NioUdpWorker.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/NioUdpWorker.kt new file mode 100644 index 000000000..4a6211ad2 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/udp/nio/NioUdpWorker.kt @@ -0,0 +1,178 @@ +package com.fireflysource.net.udp.nio + +import com.fireflysource.common.concurrent.ExecutorServiceUtils.shutdownAndAwaitTermination +import com.fireflysource.common.coroutine.CoroutineDispatchers.newSingleThreadExecutor +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.copy +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.udp.UdpConnection +import com.fireflysource.net.udp.buffer.NioWorkerMessage +import com.fireflysource.net.udp.buffer.RegisterRead +import com.fireflysource.net.udp.exception.UdpAttachmentTypeException +import org.jctools.queues.MpscLinkedQueue +import java.nio.channels.SelectionKey +import java.nio.channels.Selector +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +class NioUdpWorker( + id: Int +) : AbstractLifeCycle(), Runnable { + + companion object { + private val log = SystemLogger.create(NioUdpWorker::class.java) + private val seconds = TimeUnit.SECONDS + } + + private val executor = newSingleThreadExecutor("firefly-nio-udp-worker-thread-$id") + private val selector = Selector.open() + private val workerMessageQueue = MpscLinkedQueue() + private val inputBuffer = BufferUtils.allocateDirect(8 * 1024) + + override fun init() { + executor.execute(this) + } + + override fun destroy() { + val closeResult = runCatching { selector.close() } + log.info { "Nio UDP worker selector close result: $closeResult" } + shutdownAndAwaitTermination(executor, 5, seconds) + } + + override fun run() { + while (true) { + val count = selectKeys() + handleNioUdpWorkerMessages() + if (count == 0) + continue + + val iterator = selector.selectedKeys().iterator() + while (iterator.hasNext()) { + val selectedKey = iterator.next() + iterator.remove() + + val result = runCatching { + val udpConnection = selectedKey.attachment() + if (udpConnection !is AbstractNioUdpConnection) { + throw UdpAttachmentTypeException("attachment type exception. ${udpConnection::class.java.name}") + } + val datagramChannel = udpConnection.datagramChannel + + fun readComplete(): ReadResult { + val pos = inputBuffer.flipToFill() + val result = runCatching { datagramChannel.read(inputBuffer) } + inputBuffer.flipToFlush(pos) + + return if (result.isSuccess) { + val length = result.getOrDefault(0) + when { + length > 0 -> { + udpConnection.sendReadCompleteMessage(inputBuffer.copy()) + ReadResult.CONTINUE_READ + } + + length == 0 -> { + ReadResult.CONTINUE_READ + } + + else -> { + ReadResult.REMOTE_CLOSE + } + } + } else { + ReadResult.REMOTE_CLOSE + } + } + + fun writeComplete(): WriteResult { + return WriteResult.CONTINUE_WRITE + } + + fun cancelSelectedKey() { + selectedKey.cancel() + selector.selectNow() + udpConnection.sendCancelSelectionKeyMessage() + } + if (selectedKey.isValid) { + if (selectedKey.isReadable) { + when (readComplete()) { + ReadResult.CONTINUE_READ -> TODO() + ReadResult.REMOTE_CLOSE -> { + val unregisterReadResult = runCatching { + selectedKey.interestOps(selectedKey.interestOps() and SelectionKey.OP_READ.inv()) + } + if (unregisterReadResult.isSuccess) { + udpConnection.sendUnregisterReadMessage() + } else { + val e = unregisterReadResult.exceptionOrNull() + if (e != null && e is IllegalArgumentException) { + cancelSelectedKey() + } + } + } + + ReadResult.SUSPEND_READ -> TODO() + ReadResult.READ_EXCEPTION -> TODO() + } + } + if (selectedKey.isWritable) { + when (writeComplete()) { + WriteResult.REMOTE_CLOSE -> { + } + + WriteResult.SUSPEND_WRITE -> TODO() + WriteResult.CONTINUE_WRITE -> TODO() + WriteResult.WRITE_EXCEPTION -> TODO() + } + } + } else { + udpConnection.sendInvalidSelectionKeyMessage() + } + } + if (result.isFailure) { + log.error { "handle nio selected key failure. $result" } + } + } + } + } + + fun registerRead(udpConnection: UdpConnection): CompletableFuture { + val future = CompletableFuture() + sendMessage(RegisterRead(udpConnection, future)) + return future + } + + private fun selectKeys(): Int { + return selector.select() + } + + private fun sendMessage(message: NioWorkerMessage) { + if (workerMessageQueue.offer(message)) { + selector.wakeup() + } + } + + private fun handleNioUdpWorkerMessages() { + while (true) { + val message = workerMessageQueue.poll() ?: break + when (message) { + is RegisterRead -> { + if (message.udpConnection is AbstractNioUdpConnection) { + val udpConnection = message.udpConnection + val datagramChannel = message.udpConnection.datagramChannel + val key = datagramChannel.register( + selector, + SelectionKey.OP_READ, + udpConnection + ) + message.future.complete(key) + } + } + } + } + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/client/impl/AsyncWebSocketClientConnectionBuilder.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/client/impl/AsyncWebSocketClientConnectionBuilder.kt new file mode 100644 index 000000000..6f9167fe3 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/client/impl/AsyncWebSocketClientConnectionBuilder.kt @@ -0,0 +1,72 @@ +package com.fireflysource.net.websocket.client.impl + +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.net.websocket.client.WebSocketClientConnectionBuilder +import com.fireflysource.net.websocket.client.WebSocketClientConnectionManager +import com.fireflysource.net.websocket.client.WebSocketClientRequest +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.WebSocketMessageHandler +import com.fireflysource.net.websocket.common.frame.Frame +import com.fireflysource.net.websocket.common.model.WebSocketBehavior +import com.fireflysource.net.websocket.common.model.WebSocketPolicy +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.concurrent.CompletableFuture + +/** + * @author Pengtao Qiu + */ +class AsyncWebSocketClientConnectionBuilder( + private val connectionManager: WebSocketClientConnectionManager +) : WebSocketClientConnectionBuilder { + + private val request = WebSocketClientRequest() + + override fun url(url: String): WebSocketClientConnectionBuilder { + request.url = url + return this + } + + override fun policy(policy: WebSocketPolicy): WebSocketClientConnectionBuilder { + request.policy = policy + return this + } + + override fun extensions(extensions: List): WebSocketClientConnectionBuilder { + request.extensions = extensions + return this + } + + override fun subProtocols(subProtocols: List): WebSocketClientConnectionBuilder { + request.subProtocols = subProtocols + return this + } + + override fun onMessage(handler: WebSocketMessageHandler): WebSocketClientConnectionBuilder { + request.handler = handler + return this + } + + override fun connect(): CompletableFuture { + if (request.policy == null) { + request.policy = WebSocketPolicy(WebSocketBehavior.CLIENT) + } + if (request.subProtocols == null) { + request.subProtocols = listOf() + } + if (request.extensions == null) { + request.extensions = listOf() + } + return connectionManager.connect(request) + } + +} + +fun WebSocketClientConnectionBuilder.connectAsync(block: suspend CoroutineScope.(WebSocketConnection) -> Unit) { + this.connect().thenAccept { connection -> connection.coroutineScope.launch { block(connection) } } +} + +fun WebSocketClientConnectionBuilder.onClientMessageAsync(block: suspend CoroutineScope.(Frame, WebSocketConnection) -> Unit): WebSocketClientConnectionBuilder { + this.onMessage { frame, connection -> connection.coroutineScope.launch { block(frame, connection) }.asVoidFuture() } + return this +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/client/impl/AsyncWebSocketClientConnectionManager.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/client/impl/AsyncWebSocketClientConnectionManager.kt new file mode 100644 index 000000000..f1761e8a5 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/client/impl/AsyncWebSocketClientConnectionManager.kt @@ -0,0 +1,58 @@ +package com.fireflysource.net.websocket.client.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.common.lifecycle.AbstractLifeCycle +import com.fireflysource.net.http.client.impl.Http1ClientConnection +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.tcp.TcpClientConnectionFactory +import com.fireflysource.net.tcp.aio.ApplicationProtocol.HTTP1 +import com.fireflysource.net.websocket.client.WebSocketClientConnectionManager +import com.fireflysource.net.websocket.client.WebSocketClientRequest +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.exception.WebSocketException +import com.fireflysource.net.websocket.common.model.WebSocketBehavior +import java.net.InetSocketAddress +import java.util.concurrent.CompletableFuture + +/** + * @author Pengtao Qiu + */ +class AsyncWebSocketClientConnectionManager( + private val config: HttpConfig, + private val connectionFactory: TcpClientConnectionFactory +) : WebSocketClientConnectionManager, AbstractLifeCycle() { + + init { + start() + } + + override fun connect(request: WebSocketClientRequest): CompletableFuture { + Assert.hasText(request.url, "The websocket url must be not blank") + Assert.notNull(request.policy, "The websocket policy must be not null") + Assert.notNull(request.handler, "The websocket message handler must be not null") + Assert.isTrue(request.policy.behavior == WebSocketBehavior.CLIENT, "The websocket behavior must be client") + + val uri = HttpURI(request.url) + val inetSocketAddress = InetSocketAddress(uri.host, uri.port) + val tcpConnection = when (uri.scheme) { + "ws" -> connectionFactory.connect(inetSocketAddress, false) + "wss" -> connectionFactory.connect(inetSocketAddress, true, listOf(HTTP1.value)) + else -> throw WebSocketException("The websocket scheme error. scheme: ${uri.scheme}") + } + + return tcpConnection + .thenCompose { connection -> connection.beginHandshake().thenApply { connection } } + .thenApply { Http1ClientConnection(config, it) } + .thenCompose { it.upgradeWebSocket(request) } + } + + override fun init() { + connectionFactory.start() + } + + override fun destroy() { + connectionFactory.stop() + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/common/impl/AsyncWebSocketConnection.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/common/impl/AsyncWebSocketConnection.kt new file mode 100644 index 000000000..92b9c122a --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/common/impl/AsyncWebSocketConnection.kt @@ -0,0 +1,255 @@ +package com.fireflysource.net.websocket.common.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.coroutine.consumeAll +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.common.sys.Result.futureToConsumer +import com.fireflysource.common.sys.SystemLogger +import com.fireflysource.net.Connection +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpCoroutineDispatcher +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.WebSocketConnectionState +import com.fireflysource.net.websocket.common.WebSocketMessageHandler +import com.fireflysource.net.websocket.common.decoder.Parser +import com.fireflysource.net.websocket.common.encoder.Generator +import com.fireflysource.net.websocket.common.exception.NextIncomingFramesNotSetException +import com.fireflysource.net.websocket.common.extension.ExtensionFactory +import com.fireflysource.net.websocket.common.extension.WebSocketExtensionFactory +import com.fireflysource.net.websocket.common.frame.* +import com.fireflysource.net.websocket.common.model.* +import com.fireflysource.net.websocket.common.stream.ConnectionState +import com.fireflysource.net.websocket.common.stream.ExtensionNegotiator +import com.fireflysource.net.websocket.common.stream.IOState +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.util.concurrent.CancellationException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ThreadLocalRandom +import java.util.function.Consumer + + +/** + * @author Pengtao Qiu + */ +class AsyncWebSocketConnection( + private val tcpConnection: TcpConnection, + private val webSocketPolicy: WebSocketPolicy, + private val url: String, + private val extensions: List = listOf(), + private val extensionFactory: ExtensionFactory = defaultExtensionFactory, + private val subProtocols: List = listOf(), + private val ioState: IOState = IOState(), + private val remainingData: ByteBuffer? = null +) : Connection by tcpConnection, TcpCoroutineDispatcher by tcpConnection, + WebSocketConnectionState by ioState, WebSocketConnection, + IncomingFrames, OutgoingFrames { + + companion object { + private val log = SystemLogger.create(AsyncWebSocketConnection::class.java) + val defaultExtensionFactory = WebSocketExtensionFactory() + } + + private val extensionNegotiator = ExtensionNegotiator(extensionFactory) + private val parser = Parser(webSocketPolicy) + private val generator = Generator(webSocketPolicy) + private val messageChannel = Channel(Channel.UNLIMITED) + private var messageHandler: WebSocketMessageHandler? = null + + override fun getUrl(): String = url + + override fun getExtensions(): List = extensions + + override fun getSubProtocols(): List = subProtocols + + override fun getPolicy(): WebSocketPolicy = webSocketPolicy + + override fun getExtensionFactory(): ExtensionFactory = extensionFactory + + override fun generateMask(): ByteArray { + val mask = ByteArray(4) + ThreadLocalRandom.current().nextBytes(mask) + return mask + } + + override fun sendData(data: ByteBuffer): CompletableFuture { + val binaryFrame = BinaryFrame() + binaryFrame.payload = data + return sendFrame(binaryFrame) + } + + override fun sendText(text: String): CompletableFuture { + val textFrame = TextFrame() + textFrame.setPayload(text) + return sendFrame(textFrame) + } + + override fun sendFrame(frame: Frame): CompletableFuture { + val future = CompletableFuture() + outgoingFrame(frame, futureToConsumer(future)) + return future + } + + override fun incomingFrame(frame: Frame) { + when (frame.type) { + Frame.Type.PING -> { + val pong = PongFrame() + outgoingFrame(pong, discard()) + } + Frame.Type.PONG -> log.info { "The websocket connection received pong frame. id: ${this.id}" } + Frame.Type.CLOSE -> { + val closeFrame = frame as CloseFrame + val closeInfo = CloseInfo(closeFrame.payload, false) + ioState.onCloseRemote(closeInfo) + } + else -> { + } + } + extensionNegotiator.incomingFrames.incomingFrame(frame) + } + + override fun setWebSocketMessageHandler(handler: WebSocketMessageHandler) { + extensionNegotiator.setNextIncomingFrames { messageChannel.trySend(it) } + messageHandler = handler + } + + override fun outgoingFrame(frame: Frame, result: Consumer>) { + extensionNegotiator.outgoingFrames.outgoingFrame(frame, result) + } + + private fun close(code: Int, reason: String?): CompletableFuture { + val closeInfo = CloseInfo(code, reason) + return close(closeInfo) + } + + private fun close(closeInfo: CloseInfo): CompletableFuture { + val closeFrame = closeInfo.asFrame() + return sendFrame(closeFrame) + } + + override fun closeAsync(): CompletableFuture { + return close(StatusCode.NORMAL, null) + } + + override fun close() { + closeAsync() + } + + override fun begin() { + if (extensionNegotiator.nextIncomingFrames == null) { + throw NextIncomingFramesNotSetException("Please set the next incoming frames listener before start websocket connection.") + } + + parser.incomingFramesHandler = this + setNextOutgoingFrames() + configureExtensions() + ioState.onConnected() + + receiveMessageJob() + parseFrameJob() + ioState.addListener { state -> + when (state) { + ConnectionState.CLOSED -> tcpConnection.closeAsync() + ConnectionState.CLOSING -> { + if (ioState.isOutputAvailable && ioState.isRemoteCloseInitiated) { + close(StatusCode.NORMAL, null) + } + } + else -> { + } + } + } + ioState.onOpen() + } + + private fun parseFrameJob() { + tcpConnection.coroutineScope.launch { + try { + if (remainingData != null && remainingData.hasRemaining()) { + parser.parse(remainingData) + } + } catch (e: Exception) { + log.error(e) { "Parse websocket frame error. id: ${this@AsyncWebSocketConnection.id}" } + ioState.onReadFailure(e) + } + + while (true) { + try { + val buffer = tcpConnection.read().await() + parser.parse(buffer) + } catch (e: CancellationException) { + log.info { "The websocket parsing job canceled. id: ${this@AsyncWebSocketConnection.id}" } + break + } catch (e: ClosedChannelException) { + log.warn("The remote endpoint closed connection. message: ${e.message} id: ${this@AsyncWebSocketConnection.id}") + } catch (e: Exception) { + log.error(e) { "Parse websocket frame error. id: ${this@AsyncWebSocketConnection.id}" } + ioState.onReadFailure(e) + break + } + } + + } + } + + private fun receiveMessageJob() { + tcpConnection.coroutineScope.launch { + while (true) { + val frame = messageChannel.receive() + try { + messageHandler?.handle(frame, this@AsyncWebSocketConnection)?.await() + } catch (e: CancellationException) { + log.info { "The websocket receiving message job canceled. id: ${this@AsyncWebSocketConnection.id}" } + } catch (e: Exception) { + log.error(e) { "Handle websocket frame exception. id: ${this@AsyncWebSocketConnection.id}" } + } + } + }.invokeOnCompletion { cause -> + log.info { "The websocket connection closed, handle the remaining message. id: ${this@AsyncWebSocketConnection.id}, cause: ${cause?.message}" } + messageChannel.consumeAll { frame -> + try { + messageHandler?.handle(frame, this@AsyncWebSocketConnection) + } catch (e: Exception) { + log.error(e) { "Handle websocket frame exception. id: ${this@AsyncWebSocketConnection.id}" } + } + } + } + } + + private fun configureExtensions() { + extensionNegotiator.configureExtensions(extensions, parser, generator, policy) + } + + private fun setNextOutgoingFrames() { + extensionNegotiator.setNextOutgoingFrames { frame, result -> + if (policy.behavior == WebSocketBehavior.CLIENT && frame is WebSocketFrame && !frame.isMasked) { + frame.mask = generateMask() + } + + val buf = BufferUtils.allocate(Generator.MAX_HEADER_LENGTH + frame.payloadLength) + val pos = buf.flipToFill() + generator.generateWholeFrame(frame, buf) + buf.flipToFlush(pos) + tcpConnection.writeAndFlush(buf) + .thenAccept { + if (frame.type == Frame.Type.CLOSE && frame is CloseFrame) { + val closeInfo = CloseInfo(frame.getPayload(), false) + ioState.onCloseLocal(closeInfo) + } + result.accept(Result.SUCCESS) + } + .exceptionallyAccept { + result.accept(Result.createFailedResult(it)) + ioState.onWriteFailure(it) + } + } + } + +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/server/impl/AsyncWebSocketManager.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/server/impl/AsyncWebSocketManager.kt new file mode 100644 index 000000000..0279f1bcc --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/server/impl/AsyncWebSocketManager.kt @@ -0,0 +1,39 @@ +package com.fireflysource.net.websocket.server.impl + +import com.fireflysource.common.`object`.Assert +import com.fireflysource.net.websocket.common.model.WebSocketBehavior +import com.fireflysource.net.websocket.server.WebSocketManager +import com.fireflysource.net.websocket.server.WebSocketServerConnectionHandler + +/** + * @author Pengtao Qiu + */ +class AsyncWebSocketManager : WebSocketManager { + + private val webSocketHandlers: MutableMap = HashMap() + + override fun register(connectionHandler: WebSocketServerConnectionHandler) { + Assert.notNull(connectionHandler.url, "The websocket url must be not null") + Assert.notNull(connectionHandler.extensionSelector, "The websocket extension selector must be not null") + Assert.notNull(connectionHandler.subProtocolSelector, "The websocket sub protocol selector must be not null") + Assert.notNull(connectionHandler.policy, "The websocket policy must be not null") + Assert.notNull(connectionHandler.connectionListener, "The websocket connection listener must be not null") + Assert.notNull(connectionHandler.messageHandler, "The websocket message handler must be not null") + Assert.isTrue( + connectionHandler.policy.behavior == WebSocketBehavior.SERVER, + "The websocket behavior must be server" + ) + + webSocketHandlers[connectionHandler.url] = connectionHandler + } + + override fun findWebSocketHandler(path: String): WebSocketServerConnectionHandler? { + return webSocketHandlers[path] + } + + public override fun clone(): AsyncWebSocketManager { + val newWebSocketManager = AsyncWebSocketManager() + newWebSocketManager.webSocketHandlers.putAll(this.webSocketHandlers) + return newWebSocketManager + } +} \ No newline at end of file diff --git a/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/server/impl/AsyncWebSocketServerConnectionBuilder.kt b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/server/impl/AsyncWebSocketServerConnectionBuilder.kt new file mode 100644 index 000000000..826606b69 --- /dev/null +++ b/firefly-net/src/main/kotlin/com/fireflysource/net/websocket/server/impl/AsyncWebSocketServerConnectionBuilder.kt @@ -0,0 +1,87 @@ +package com.fireflysource.net.websocket.server.impl + +import com.fireflysource.common.coroutine.asVoidFuture +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.WebSocketMessageHandler +import com.fireflysource.net.websocket.common.frame.Frame +import com.fireflysource.net.websocket.common.impl.AsyncWebSocketConnection.Companion.defaultExtensionFactory +import com.fireflysource.net.websocket.common.model.ExtensionConfig +import com.fireflysource.net.websocket.common.model.WebSocketBehavior +import com.fireflysource.net.websocket.common.model.WebSocketPolicy +import com.fireflysource.net.websocket.server.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * @author Pengtao Qiu + */ +class AsyncWebSocketServerConnectionBuilder( + private val httpServer: HttpServer, + private val webSocketManager: WebSocketManager +) : WebSocketServerConnectionBuilder { + + private val connectionHandler = WebSocketServerConnectionHandler() + + override fun url(url: String): WebSocketServerConnectionBuilder { + connectionHandler.url = url + return this + } + + override fun policy(policy: WebSocketPolicy): WebSocketServerConnectionBuilder { + connectionHandler.policy = policy + return this + } + + override fun onExtensionSelect(selector: ExtensionSelector): WebSocketServerConnectionBuilder { + connectionHandler.extensionSelector = selector + return this + } + + override fun onSubProtocolSelect(selector: SubProtocolSelector): WebSocketServerConnectionBuilder { + connectionHandler.subProtocolSelector = selector + return this + } + + override fun onMessage(handler: WebSocketMessageHandler): WebSocketServerConnectionBuilder { + connectionHandler.messageHandler = handler + return this + } + + override fun onAccept(listener: WebSocketServerConnectionListener): HttpServer { + connectionHandler.connectionListener = listener + if (connectionHandler.policy == null) { + connectionHandler.policy = WebSocketPolicy(WebSocketBehavior.SERVER) + } + if (connectionHandler.subProtocolSelector == null) { + connectionHandler.setSubProtocolSelector { listOf() } + } + if (connectionHandler.extensionSelector == null) { + connectionHandler.setExtensionSelector { clientExtensions -> + if (clientExtensions.isNullOrEmpty()) { + listOf() + } else { + ExtensionConfig + .parseList(clientExtensions) + .filter { c -> defaultExtensionFactory.isAvailable(c.name) } + .map { c -> c.name } + } + } + } + webSocketManager.register(connectionHandler) + return httpServer + } +} + +fun WebSocketServerConnectionBuilder.onAcceptAsync(block: suspend CoroutineScope.(WebSocketConnection) -> Unit): HttpServer { + return this.onAccept { connection -> + connection.coroutineScope.launch { block(connection) } + Result.DONE + } +} + +fun WebSocketServerConnectionBuilder.onServerMessageAsync(block: suspend CoroutineScope.(Frame, WebSocketConnection) -> Unit): WebSocketServerConnectionBuilder { + this.onMessage { frame, connection -> connection.coroutineScope.launch { block(frame, connection) }.asVoidFuture() } + return this +} \ No newline at end of file diff --git a/firefly-net/src/main/resources/com/fireflysource/net/http/common/model/encoding.properties b/firefly-net/src/main/resources/com/fireflysource/net/http/common/model/encoding.properties new file mode 100644 index 000000000..327ef71ae --- /dev/null +++ b/firefly-net/src/main/resources/com/fireflysource/net/http/common/model/encoding.properties @@ -0,0 +1,6 @@ +text/html=utf-8 +text/plain=iso-8859-1 +text/xml=utf-8 +application/xhtml+xml=utf-8 +text/json=-utf-8 +application/vnd.api+json=-utf-8 \ No newline at end of file diff --git a/firefly-net/src/main/resources/com/fireflysource/net/http/common/model/mime.properties b/firefly-net/src/main/resources/com/fireflysource/net/http/common/model/mime.properties new file mode 100644 index 000000000..ed4e4540e --- /dev/null +++ b/firefly-net/src/main/resources/com/fireflysource/net/http/common/model/mime.properties @@ -0,0 +1,191 @@ +ai=application/postscript +aif=audio/x-aiff +aifc=audio/x-aiff +aiff=audio/x-aiff +apk=application/vnd.android.package-archive +asc=text/plain +asf=video/x.ms.asf +asx=video/x.ms.asx +au=audio/basic +avi=video/x-msvideo +bcpio=application/x-bcpio +bin=application/octet-stream +bmp=image/bmp +br=application/brotli +cab=application/x-cabinet +cdf=application/x-netcdf +chm=application/vnd.ms-htmlhelp +class=application/java-vm +cpio=application/x-cpio +cpt=application/mac-compactpro +crt=application/x-x509-ca-cert +csh=application/x-csh +css=text/css +csv=text/csv +dcr=application/x-director +dir=application/x-director +dll=application/x-msdownload +dms=application/octet-stream +doc=application/msword +dtd=application/xml-dtd +dvi=application/x-dvi +dxr=application/x-director +eps=application/postscript +etx=text/x-setext +exe=application/octet-stream +ez=application/andrew-inset +gif=image/gif +gtar=application/x-gtar +gz=application/gzip +gzip=application/gzip +hdf=application/x-hdf +hqx=application/mac-binhex40 +htc=text/x-component +htm=text/html +html=text/html +ice=x-conference/x-cooltalk +ico=image/x-icon +ief=image/ief +iges=model/iges +igs=model/iges +jad=text/vnd.sun.j2me.app-descriptor +jar=application/java-archive +java=text/plain +jnlp=application/x-java-jnlp-file +jpe=image/jpeg +jp2=image/jpeg2000 +jpeg=image/jpeg +jpg=image/jpeg +js=application/javascript +json=application/json +jsp=text/html +kar=audio/midi +latex=application/x-latex +lha=application/octet-stream +lzh=application/octet-stream +man=application/x-troff-man +mathml=application/mathml+xml +me=application/x-troff-me +mesh=model/mesh +mid=audio/midi +midi=audio/midi +mif=application/vnd.mif +mol=chemical/x-mdl-molfile +mov=video/quicktime +movie=video/x-sgi-movie +mp2=audio/mpeg +mp3=audio/mpeg +mpe=video/mpeg +mpeg=video/mpeg +mpg=video/mpeg +mpga=audio/mpeg +ms=application/x-troff-ms +msh=model/mesh +msi=application/octet-stream +nc=application/x-netcdf +oda=application/oda +odb=application/vnd.oasis.opendocument.database +odc=application/vnd.oasis.opendocument.chart +odf=application/vnd.oasis.opendocument.formula +odg=application/vnd.oasis.opendocument.graphics +odi=application/vnd.oasis.opendocument.image +odm=application/vnd.oasis.opendocument.text-master +odp=application/vnd.oasis.opendocument.presentation +ods=application/vnd.oasis.opendocument.spreadsheet +odt=application/vnd.oasis.opendocument.text +ogg=application/ogg +otc=application/vnd.oasis.opendocument.chart-template +otf=application/vnd.oasis.opendocument.formula-template +otg=application/vnd.oasis.opendocument.graphics-template +oth=application/vnd.oasis.opendocument.text-web +oti=application/vnd.oasis.opendocument.image-template +otp=application/vnd.oasis.opendocument.presentation-template +ots=application/vnd.oasis.opendocument.spreadsheet-template +ott=application/vnd.oasis.opendocument.text-template +pbm=image/x-portable-bitmap +pdb=chemical/x-pdb +pdf=application/pdf +pgm=image/x-portable-graymap +pgn=application/x-chess-pgn +png=image/png +pnm=image/x-portable-anymap +ppm=image/x-portable-pixmap +pps=application/vnd.ms-powerpoint +ppt=application/vnd.ms-powerpoint +ps=application/postscript +qml=text/x-qml +qt=video/quicktime +ra=audio/x-pn-realaudio +rar=application/x-rar-compressed +ram=audio/x-pn-realaudio +ras=image/x-cmu-raster +rdf=application/rdf+xml +rgb=image/x-rgb +rm=audio/x-pn-realaudio +roff=application/x-troff +rpm=application/x-rpm +rtf=application/rtf +rtx=text/richtext +rv=video/vnd.rn-realvideo +ser=application/java-serialized-object +sgm=text/sgml +sgml=text/sgml +sh=application/x-sh +shar=application/x-shar +silo=model/mesh +sit=application/x-stuffit +skd=application/x-koan +skm=application/x-koan +skp=application/x-koan +skt=application/x-koan +smi=application/smil +smil=application/smil +snd=audio/basic +spl=application/x-futuresplash +src=application/x-wais-source +sv4cpio=application/x-sv4cpio +sv4crc=application/x-sv4crc +svg=image/svg+xml +svgz=image/svg+xml +swf=application/x-shockwave-flash +t=application/x-troff +tar=application/x-tar +tar.gz=application/x-gtar +tcl=application/x-tcl +tex=application/x-tex +texi=application/x-texinfo +texinfo=application/x-texinfo +tgz=application/x-gtar +tif=image/tiff +tiff=image/tiff +tr=application/x-troff +tsv=text/tab-separated-values +txt=text/plain +ustar=application/x-ustar +vcd=application/x-cdlink +vrml=model/vrml +vxml=application/voicexml+xml +wav=audio/x-wav +wbmp=image/vnd.wap.wbmp +wml=text/vnd.wap.wml +wmlc=application/vnd.wap.wmlc +wmls=text/vnd.wap.wmlscript +wmlsc=application/vnd.wap.wmlscriptc +wrl=model/vrml +wtls-ca-certificate=application/vnd.wap.wtls-ca-certificate +xbm=image/x-xbitmap +xcf=image/xcf +xht=application/xhtml+xml +xhtml=application/xhtml+xml +xls=application/vnd.ms-excel +xml=application/xml +xpm=image/x-xpixmap +xsd=application/xml +xsl=application/xml +xslt=application/xslt+xml +xul=application/vnd.mozilla.xul+xml +xwd=image/x-xwindowdump +xyz=chemical/x-xyz +xz=application/x-xz +z=application/compress +zip=application/zip diff --git a/firefly-net/src/main/resources/fireflyKeystore.jks b/firefly-net/src/main/resources/fireflyKeystore.jks new file mode 100644 index 000000000..9694f10c8 Binary files /dev/null and b/firefly-net/src/main/resources/fireflyKeystore.jks differ diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/ContentEncodedTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/ContentEncodedTest.java new file mode 100644 index 000000000..9d4c2e59f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/ContentEncodedTest.java @@ -0,0 +1,35 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.model.ContentEncoding; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class ContentEncodedTest { + + static Stream testParametersProvider() { + return Stream.of( + arguments(ContentEncoding.GZIP), + arguments(ContentEncoding.DEFLATE) + ); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should encode and decode content successfully.") + void test(ContentEncoding encoding) throws Exception { + ByteBuffer buffer = BufferUtils.toBuffer("测试hello", StandardCharsets.UTF_8); + byte[] encodedBytes = ContentEncoded.encode(BufferUtils.toArray(buffer), encoding); + byte[] decodedBytes = ContentEncoded.decode(encodedBytes, encoding); + assertEquals("测试hello", new String(decodedBytes, StandardCharsets.UTF_8)); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/CookieTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/CookieTest.java new file mode 100644 index 000000000..a65bc923d --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/CookieTest.java @@ -0,0 +1,64 @@ +package com.fireflysource.net.http.common.codec; + + +import com.fireflysource.net.http.common.model.Cookie; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CookieTest { + + @Test + void setCookieTest() { + Cookie cookie = new Cookie("test31", "hello"); + cookie.setDomain("www.fireflysource.com"); + cookie.setPath("/test/hello"); + cookie.setMaxAge(10); + cookie.setSecure(true); + cookie.setComment("commenttest"); + cookie.setVersion(20); + + String setCookieString = CookieGenerator.generateSetCookie(cookie); + + Cookie setCookie = CookieParser.parseSetCookie(setCookieString); + assertEquals("test31", setCookie.getName()); + assertEquals("hello", setCookie.getValue()); + assertEquals("www.fireflysource.com", setCookie.getDomain()); + assertEquals("/test/hello", setCookie.getPath()); + assertTrue(setCookie.getSecure()); + assertEquals("commenttest", setCookie.getComment()); + assertEquals(20, setCookie.getVersion()); + } + + @Test + void cookieTest() { + Cookie cookie = new Cookie("test21", "hello"); + String cookieString = CookieGenerator.generateCookie(cookie); + + List list = CookieParser.parseCookie(cookieString); + assertEquals(1, list.size()); + assertEquals("test21", list.get(0).getName()); + assertEquals("hello", list.get(0).getValue()); + } + + @Test + void cookieListTest() { + List list = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + list.add(new Cookie("test" + i, "hello" + i)); + } + String cookieString = CookieGenerator.generateCookies(list); + System.out.println(cookieString); + + List ret = CookieParser.parseCookie(cookieString); + assertEquals(10, ret.size()); + for (int i = 0; i < 10; i++) { + assertEquals("test" + i, ret.get(i).getName()); + assertEquals("hello" + i, ret.get(i).getValue()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/HttpURIParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/HttpURIParseTest.java new file mode 100644 index 000000000..1bd0b2c67 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/HttpURIParseTest.java @@ -0,0 +1,232 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.net.http.common.model.HttpURI; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class HttpURIParseTest { + + public static Stream testParametersProvider() { + return data().stream().map(arr -> arguments(Arrays.asList(arr).toArray())); + } + + public static List data() { + String[][] tests = { + // Nothing but path + {"path", null, null, "-1", "path", null, null, null}, + {"path/path", null, null, "-1", "path/path", null, null, null}, + {"%65ncoded/path", null, null, "-1", "%65ncoded/path", null, null, null}, + + // Basic path reference + {"/path/to/context", null, null, "-1", "/path/to/context", null, null, null}, + + // Basic with encoded query + {"http://example.com/path/to/context;param?query=%22value%22#fragment", "http", "example.com", "-1", + "/path/to/context;param", "param", "query=%22value%22", "fragment"}, + {"http://[::1]/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "-1", + "/path/to/context;param", "param", "query=%22value%22", "fragment"}, + + // Basic with parameters and query + {"http://example.com:8080/path/to/context;param?query=%22value%22#fragment", "http", "example.com", + "8080", "/path/to/context;param", "param", "query=%22value%22", "fragment"}, + {"http://[::1]:8080/path/to/context;param?query=%22value%22#fragment", "http", "[::1]", "8080", + "/path/to/context;param", "param", "query=%22value%22", "fragment"}, + + // Path References + {"/path/info", null, null, null, "/path/info", null, null, null}, + {"/path/info#fragment", null, null, null, "/path/info", null, null, "fragment"}, + {"/path/info?query", null, null, null, "/path/info", null, "query", null}, + {"/path/info?query#fragment", null, null, null, "/path/info", null, "query", "fragment"}, + {"/path/info;param", null, null, null, "/path/info;param", "param", null, null}, + {"/path/info;param#fragment", null, null, null, "/path/info;param", "param", null, "fragment"}, + {"/path/info;param?query", null, null, null, "/path/info;param", "param", "query", null}, + {"/path/info;param?query#fragment", null, null, null, "/path/info;param", "param", "query", + "fragment"}, + + // Protocol Less (aka scheme-less) URIs + {"//host/path/info", null, "host", null, "/path/info", null, null, null}, + {"//user@host/path/info", null, "host", null, "/path/info", null, null, null}, + {"//user@host:8080/path/info", null, "host", "8080", "/path/info", null, null, null}, + {"//host:8080/path/info", null, "host", "8080", "/path/info", null, null, null}, + + // Host Less + {"http:/path/info", "http", null, null, "/path/info", null, null, null}, + {"http:/path/info#fragment", "http", null, null, "/path/info", null, null, "fragment"}, + {"http:/path/info?query", "http", null, null, "/path/info", null, "query", null}, + {"http:/path/info?query#fragment", "http", null, null, "/path/info", null, "query", "fragment"}, + {"http:/path/info;param", "http", null, null, "/path/info;param", "param", null, null}, + {"http:/path/info;param#fragment", "http", null, null, "/path/info;param", "param", null, "fragment"}, + {"http:/path/info;param?query", "http", null, null, "/path/info;param", "param", "query", null}, + {"http:/path/info;param?query#fragment", "http", null, null, "/path/info;param", "param", "query", + "fragment"}, + + // Everything and the kitchen sink + {"http://user@host:8080/path/info;param?query#fragment", "http", "host", "8080", "/path/info;param", + "param", "query", "fragment"}, + {"xxxxx://user@host:8080/path/info;param?query#fragment", "xxxxx", "host", "8080", "/path/info;param", + "param", "query", "fragment"}, + + // No host, parameter with no content + {"http:///;?#", "http", null, null, "/;", "", "", ""}, + + // Path with query that has no value + {"/path/info?a=?query", null, null, null, "/path/info", null, "a=?query", null}, + + // Path with query alt syntax + {"/path/info?a=;query", null, null, null, "/path/info", null, "a=;query", null}, + + // URI with host character + {"/@path/info", null, null, null, "/@path/info", null, null, null}, + {"/user@path/info", null, null, null, "/user@path/info", null, null, null}, + {"//user@host/info", null, "host", null, "/info", null, null, null}, + {"//@host/info", null, "host", null, "/info", null, null, null}, + {"@host/info", null, null, null, "@host/info", null, null, null}, + + // Scheme-less, with host and port (overlapping with path) + {"//host:8080//", null, "host", "8080", "//", null, null, null}, + + // File reference + {"file:///path/info", "file", null, null, "/path/info", null, null, null}, + {"file:/path/info", "file", null, null, "/path/info", null, null, null}, + + // Bad URI (no scheme, no host, no path) + {"//", null, null, null, null, null, null, null}, + + // Simple localhost references + {"http://localhost/", "http", "localhost", null, "/", null, null, null}, + {"http://localhost:8080/", "http", "localhost", "8080", "/", null, null, null}, + {"http://localhost/?x=y", "http", "localhost", null, "/", null, "x=y", null}, + + // Simple path with parameter + {"/;param", null, null, null, "/;param", "param", null, null}, + {";param", null, null, null, ";param", "param", null, null}, + + // Simple path with query + {"/?x=y", null, null, null, "/", null, "x=y", null}, + {"/?abc=test", null, null, null, "/", null, "abc=test", null}, + + // Simple path with fragment + {"/#fragment", null, null, null, "/", null, null, "fragment"}, + + // Simple IPv4 host with port (default path) + {"http://192.0.0.1:8080/", "http", "192.0.0.1", "8080", "/", null, null, null}, + + // Simple IPv6 host with port (default path) + + {"http://[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null}, + // IPv6 authenticated host with port (default path) + + {"http://user@[2001:db8::1]:8080/", "http", "[2001:db8::1]", "8080", "/", null, null, null}, + + // Simple IPv6 host no port (default path) + {"http://[2001:db8::1]/", "http", "[2001:db8::1]", null, "/", null, null, null}, + + // Scheme-less IPv6, host with port (default path) + {"//[2001:db8::1]:8080/", null, "[2001:db8::1]", "8080", "/", null, null, null}, + + // Interpreted as relative path of "*" (no + // host/port/scheme/query/fragment) + {"*", null, null, null, "*", null, null, null}, + + // Path detection Tests (seen from JSP/JSTL and use + {"http://host:8080/path/info?q1=v1&q2=v2", "http", "host", "8080", "/path/info", null, "q1=v1&q2=v2", + null}, + {"/path/info?q1=v1&q2=v2", null, null, null, "/path/info", null, "q1=v1&q2=v2", null}, + {"/info?q1=v1&q2=v2", null, null, null, "/info", null, "q1=v1&q2=v2", null}, + {"info?q1=v1&q2=v2", null, null, null, "info", null, "q1=v1&q2=v2", null}, + {"info;q1=v1?q2=v2", null, null, null, "info;q1=v1", "q1=v1", "q2=v2", null}, + + // Path-less, query only (seen from JSP/JSTL and use + {"?q1=v1&q2=v2", null, null, null, "", null, "q1=v1&q2=v2", null} + }; + + return Arrays.asList(tests); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void testParseString(String input, String scheme, String host, String port, String path, String param, String query, String fragment) { + HttpURI httpUri = new HttpURI(input); + + try { + new URI(input); + // URI is valid (per java.net.URI parsing) + + // Test case sanity check + assertNotNull(path); + + // Assert expectations + assertEquals(host, httpUri.getHost()); + assertEquals(port == null ? -1 : Integer.parseInt(port), httpUri.getPort()); + assertEquals(path, httpUri.getPath()); + assertEquals(param, httpUri.getParam()); + assertEquals(query, httpUri.getQuery()); + assertEquals(fragment, httpUri.getFragment()); + assertEquals(input, httpUri.toString()); + assertEquals(scheme, httpUri.getScheme()); + } catch (URISyntaxException e) { + // Assert HttpURI values for invalid URI (such as "//") + assertNull(httpUri.getScheme()); + assertNull(httpUri.getHost()); + assertEquals(-1, httpUri.getPort()); + assertNull(httpUri.getPath()); + assertNull(httpUri.getParam()); + assertNull(httpUri.getQuery()); + assertNull(httpUri.getFragment()); + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void testParseURI(String input, String scheme, String host, String port, String path, String param, String query, String fragment) { + URI javaUri = null; + try { + javaUri = new URI(input); + } catch (URISyntaxException ignore) { + } + + assumeTrue(javaUri != null); + HttpURI httpUri = new HttpURI(javaUri); + + assertEquals(scheme, httpUri.getScheme()); + assertEquals(host, httpUri.getHost()); + assertEquals(port == null ? -1 : Integer.parseInt(port), httpUri.getPort()); + assertEquals(path, httpUri.getPath()); + assertEquals(param, httpUri.getParam()); + assertEquals(query, httpUri.getQuery()); + assertEquals(fragment, httpUri.getFragment()); + assertEquals(input, httpUri.toString()); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void testCompareToJavaNetURI(String input, String scheme, String host, String port, String path, String param, String query, String fragment) { + URI javaUri = null; + try { + javaUri = new URI(input); + } catch (URISyntaxException ignore) { + } + + assumeTrue(javaUri != null); + HttpURI httpUri = new HttpURI(javaUri); + + assertEquals(javaUri.getScheme(), httpUri.getScheme()); + assertEquals(javaUri.getHost(), httpUri.getHost()); + assertEquals(javaUri.getPort(), httpUri.getPort()); + assertEquals(javaUri.getRawPath(), httpUri.getPath()); + assertEquals(javaUri.getRawQuery(), httpUri.getQuery()); + assertEquals(javaUri.getFragment(), httpUri.getFragment()); + assertEquals(javaUri.toASCIIString(), httpUri.toString()); + } +} \ No newline at end of file diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/InclusiveByteRangeTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/InclusiveByteRangeTest.java new file mode 100644 index 000000000..7c99d8e25 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/InclusiveByteRangeTest.java @@ -0,0 +1,297 @@ +package com.fireflysource.net.http.common.codec; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import static org.junit.jupiter.api.Assertions.*; + +public class InclusiveByteRangeTest { + private void assertInvalidRange(String rangeString) { + Vector strings = new Vector<>(); + strings.add(rangeString); + + List ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), 200); + assertNull(ranges, "Invalid Range [" + rangeString + "] should result in no satisfiable ranges"); + } + + private void assertRange(String msg, int expectedFirst, int expectedLast, int size, InclusiveByteRange actualRange) { + assertEquals(expectedFirst, actualRange.getFirst(), msg + " - first"); + assertEquals(expectedLast, actualRange.getLast(), msg + " - last"); + String expectedHeader = String.format("bytes %d-%d/%d", expectedFirst, expectedLast, size); + assertEquals(expectedHeader, actualRange.toHeaderRangeString(size)); + } + + private void assertSimpleRange(int expectedFirst, int expectedLast, String rangeId, int size) { + InclusiveByteRange range = parseRange(rangeId, size); + + assertEquals(expectedFirst, range.getFirst(), "Range [" + rangeId + "] - first"); + assertEquals(expectedLast, range.getLast(), "Range [" + rangeId + "] - last"); + String expectedHeader = String.format("bytes %d-%d/%d", expectedFirst, expectedLast, size); + assertEquals(expectedHeader, range.toHeaderRangeString(size), "Range [" + rangeId + "] - header range string"); + } + + private InclusiveByteRange parseRange(String rangeString, int size) { + Vector strings = new Vector<>(); + strings.add(rangeString); + + List ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), size); + assertNotNull(ranges, "Satisfiable Ranges should not be null"); + assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + return ranges.iterator().next(); + } + + private List parseRanges(int size, String... rangeString) { + Vector strings = new Vector<>(Arrays.asList(rangeString)); + + List ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), size); + assertNotNull(ranges, "Satisfiable Ranges should not be null"); + return ranges; + } + + @Test + public void testHeader416RangeString() { + assertEquals("bytes */100", InclusiveByteRange.to416HeaderRangeString(100), "416 Header on size 100"); + assertEquals("bytes */123456789", InclusiveByteRange.to416HeaderRangeString(123456789), "416 Header on size 123456789"); + } + + @Test + public void testInvalidRanges() { + // Invalid if parsing "Range" header + assertInvalidRange("bytes=a-b"); // letters invalid + assertInvalidRange("byte=10-3"); // key is bad + assertInvalidRange("onceuponatime=5-10"); // key is bad + assertInvalidRange("bytes=300-310"); // outside of size (200) + } + + /** + * Ranges have a multiple ranges, all absolutely defined. + */ + @Test + public void testMultipleAbsoluteRanges() { + int size = 50; + String rangeString; + + rangeString = "bytes=5-20,35-65"; + + List ranges = parseRanges(size, rangeString); + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 5, 20, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 35, 49, size, inclusiveByteRangeIterator.next()); + } + + /** + * Ranges have a multiple ranges, all absolutely defined. + */ + @Test + public void testMultipleAbsoluteRangesSplit() { + int size = 50; + + List ranges = parseRanges(size, "bytes=5-20", "bytes=35-65"); + assertEquals(2, ranges.size()); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("testMultipleAbsoluteRangesSplit[0]", 5, 20, size, inclusiveByteRangeIterator.next()); + assertRange("testMultipleAbsoluteRangesSplit[1]", 35, 49, size, inclusiveByteRangeIterator.next()); + } + + /** + * Range definition has a range that is clipped due to the size. + */ + @Test + public void testMultipleRangesClipped() { + int size = 50; + String rangeString; + + rangeString = "bytes=5-20,35-65,-5"; + + List ranges = parseRanges(size, rangeString); + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 5, 20, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 35, 49, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testMultipleRangesOverlapping() { + int size = 200; + String rangeString; + + rangeString = "bytes=5-20,15-25"; + + List ranges = parseRanges(size, rangeString); + assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 5, 25, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testMultipleRangesSplit() { + int size = 200; + String rangeString; + rangeString = "bytes=5-10,15-20"; + + List ranges = parseRanges(size, rangeString); + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 5, 10, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 15, 20, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testMultipleSameRangesSplit() { + int size = 200; + String rangeString; + rangeString = "bytes=5-10,15-20,5-10,15-20,5-10,5-10,5-10,5-10,5-10,5-10"; + + List ranges = parseRanges(size, rangeString); + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 5, 10, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 15, 20, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testMultipleOverlappingRanges() { + int size = 200; + String rangeString; + rangeString = "bytes=5-15,20-30,10-25"; + + List ranges = parseRanges(size, rangeString); + assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 5, 30, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testMultipleOverlappingRangesOrdered() { + int size = 200; + String rangeString; + rangeString = "bytes=20-30,5-15,0-5,25-35"; + + List ranges = parseRanges(size, rangeString); + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 20, 35, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 0, 15, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testMultipleOverlappingRangesOrderedSplit() { + int size = 200; + String rangeString; + rangeString = "bytes=20-30,5-15,0-5,25-35"; + List ranges = parseRanges(size, "bytes=20-30", "bytes=5-15", "bytes=0-5,25-35"); + + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 20, 35, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 0, 15, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testNasty() { + int size = 200; + String rangeString; + + rangeString = "bytes=90-100, 10-20, 30-40, -161"; + List ranges = parseRanges(size, rangeString); + + assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count"); + Iterator inclusiveByteRangeIterator = ranges.iterator(); + assertRange("Range [" + rangeString + "]", 30, 199, size, inclusiveByteRangeIterator.next()); + assertRange("Range [" + rangeString + "]", 10, 20, size, inclusiveByteRangeIterator.next()); + } + + @Test + public void testRangeOpenEnded() { + assertSimpleRange(50, 499, "bytes=50-", 500); + } + + @Test + public void testSimpleRange() { + assertSimpleRange(5, 10, "bytes=5-10", 200); + assertSimpleRange(195, 199, "bytes=-5", 200); + assertSimpleRange(50, 119, "bytes=50-150", 120); + assertSimpleRange(50, 119, "bytes=50-", 120); + + assertSimpleRange(1, 50, "bytes= 1 - 50", 120); + } + + // evaluate this vs assertInvalidRange() above, which behavior is correct? null? or empty list? + private void assertBadRangeList(int size, String badRange) { + Vector strings = new Vector<>(); + strings.add(badRange); + + List ranges = InclusiveByteRange.satisfiableRanges(strings.elements(), size); + // if one part is bad, the entire set of ranges should be treated as bad, per RFC7233 + assertNull(ranges); + } + + @Test + @Disabled + public void testBadRangeSetPartiallyBad() { + assertBadRangeList(500, "bytes=1-50,1-b,a-50"); + } + + @Test + public void testBadRangeNoNumbers() { + assertBadRangeList(500, "bytes=a-b"); + } + + @Test + public void testBadRangeEmpty() { + assertBadRangeList(500, "bytes="); + } + + @Test + @Disabled + public void testBadRangeZeroPrefixed() { + assertBadRangeList(500, "bytes=01-050"); + } + + @Test + public void testBadRangeHex() { + assertBadRangeList(500, "bytes=0F-FF"); + } + + @Test + @Disabled + public void testBadRangeTabWhitespace() { + assertBadRangeList(500, "bytes=\t1\t-\t50"); + } + + @Test + public void testBadRangeTabDelim() { + assertBadRangeList(500, "bytes=1-50\t90-101\t200-250"); + } + + @Test + public void testBadRangeSemiColonDelim() { + assertBadRangeList(500, "bytes=1-50;90-101;200-250"); + } + + @Test + public void testBadRangeNegativeSize() { + assertBadRangeList(500, "bytes=50-1"); + } + + @Test + public void testBadRangeDoubleDash() { + assertBadRangeList(500, "bytes=1--20"); + } + + @Test + public void testBadRangeTrippleDash() { + assertBadRangeList(500, "bytes=1---"); + } + + @Test + public void testBadRangeZeroedNegativeSize() { + assertBadRangeList(500, "bytes=050-001"); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/MultiPartParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/MultiPartParserTest.java new file mode 100644 index 000000000..402ffeb06 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/MultiPartParserTest.java @@ -0,0 +1,665 @@ +package com.fireflysource.net.http.common.codec; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.exception.BadMessageException; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static com.fireflysource.net.http.common.codec.MultiPartParser.State; +import static org.junit.jupiter.api.Assertions.*; + +public class MultiPartParserTest { + + @Test + public void testEmptyPreamble() { + MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler() { + }, "BOUNDARY"); + ByteBuffer data = BufferUtils.toBuffer(""); + + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + } + + @Test + public void testNoPreamble() { + MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler() { + }, "BOUNDARY"); + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY \r\n"); + + parser.parse(data, false); + assertTrue(parser.isState(State.BODY_PART)); + assertEquals(0, data.remaining()); + } + + @Test + public void testPreamble() { + MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler() { + }, "BOUNDARY"); + ByteBuffer data; + + data = BufferUtils.toBuffer("This is not part of a part\r\n"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + + data = BufferUtils.toBuffer("More data that almost includes \n--BOUNDARY but no CR before."); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + + data = BufferUtils.toBuffer("Could be a boundary \r\n--BOUNDAR"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + + data = BufferUtils.toBuffer("but not it isn't \r\n--BOUN"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + + data = BufferUtils.toBuffer("DARX nor is this"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + } + + @Test + public void testPreambleCompleteBoundary() { + MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler() { + }, "BOUNDARY"); + ByteBuffer data = BufferUtils.toBuffer("This is not part of a part\r\n--BOUNDARY \r\n"); + + parser.parse(data, false); + assertEquals(MultiPartParser.State.BODY_PART, parser.getState()); + assertEquals(0, data.remaining()); + } + + @Test + public void testPreambleSplitBoundary() { + MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler() { + }, "BOUNDARY"); + ByteBuffer data = BufferUtils.toBuffer("This is not part of a part\r\n"); + + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + data = BufferUtils.toBuffer("-"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + data = BufferUtils.toBuffer("-"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + data = BufferUtils.toBuffer("B"); + parser.parse(data, false); + assertEquals(State.PREAMBLE, parser.getState()); + assertEquals(0, data.remaining()); + data = BufferUtils.toBuffer("OUNDARY-"); + parser.parse(data, false); + assertEquals(State.DELIMITER_CLOSE, parser.getState()); + assertEquals(0, data.remaining()); + data = BufferUtils.toBuffer("ignore\r"); + parser.parse(data, false); + assertEquals(State.DELIMITER_PADDING, parser.getState()); + assertEquals(0, data.remaining()); + data = BufferUtils.toBuffer("\n"); + parser.parse(data, false); + assertEquals(State.BODY_PART, parser.getState()); + assertEquals(0, data.remaining()); + } + + @Test + public void testFirstPartNoFields() { + MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler() { + }, "BOUNDARY"); + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n\r\n"); + + parser.parse(data, false); + assertEquals(State.FIRST_OCTETS, parser.getState()); + assertEquals(0, data.remaining()); + } + + @Test + public void testFirstPartFields() { + TestHandler handler = new TestHandler() { + @Override + public boolean headerComplete() { + super.headerComplete(); + return true; + } + }; + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name0: value0\r\n" + + "name1 :value1 \r\n" + + "name2:value\r\n" + + " 2\r\n" + + "\r\n" + + "Content"); + + parser.parse(data, false); + assertEquals(State.FIRST_OCTETS, parser.getState()); + assertEquals(7, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name0: value0", "name1: value1", "name2: value 2", "<>"))); + } + + @Test + public void testFirstPartNoContent() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name: value\r\n" + + "\r\n" + + "\r\n" + + "--BOUNDARY"); + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.contains("<>")); + } + + @Test + public void testFirstPartNoContentNoCRLF() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name: value\r\n" + + "\r\n" + + "--BOUNDARY"); + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.contains("<>")); + } + + @Test + public void testFirstPartContentLookingLikeNoCRLF() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name: value\r\n" + + "\r\n" + + "-"); + parser.parse(data, false); + data = BufferUtils.toBuffer("Content!"); + parser.parse(data, false); + + assertEquals(State.OCTETS, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("-", "Content!"))); + } + + @Test + public void testFirstPartPartialContent() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name: value\n" + + "\r\n" + + "Hello\r\n"); + parser.parse(data, false); + assertEquals(State.OCTETS, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.contains("Hello")); + + data = BufferUtils.toBuffer( + "Now is the time for all good ment to come to the aid of the party.\r\n" + + "How now brown cow.\r\n" + + "The quick brown fox jumped over the lazy dog.\r\n" + + "this is not a --BOUNDARY\r\n"); + parser.parse(data, false); + assertEquals(State.OCTETS, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Hello", "\r\n", "Now is the time for all good ment to come to the aid of the party.\r\n" + + "How now brown cow.\r\n" + + "The quick brown fox jumped over the lazy dog.\r\n" + + "this is not a --BOUNDARY"))); + } + + @Test + public void testFirstPartShortContent() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name: value\n" + + "\r\n" + + "Hello\r\n" + + "--BOUNDARY"); + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Hello", "<>"))); + } + + @Test + public void testFirstPartLongContent() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\r\n" + + "name: value\n" + + "\r\n" + + "Now is the time for all good ment to come to the aid of the party.\r\n" + + "How now brown cow.\r\n" + + "The quick brown fox jumped over the lazy dog.\r\n" + + "\r\n" + + "--BOUNDARY"); + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Now is the time for all good ment to come to the aid of the party.\r\n" + + "How now brown cow.\r\n" + + "The quick brown fox jumped over the lazy dog.\r\n", "<>"))); + } + + @Test + public void testFirstPartLongContentNoCarriageReturn() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + //boundary still requires carriage return + ByteBuffer data = BufferUtils.toBuffer("--BOUNDARY\n" + + "name: value\n" + + "\n" + + "Now is the time for all good men to come to the aid of the party.\n" + + "How now brown cow.\n" + + "The quick brown fox jumped over the lazy dog.\n" + + "\r\n" + + "--BOUNDARY"); + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertEquals(0, data.remaining()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Now is the time for all good men to come to the aid of the party.\n" + + "How now brown cow.\n" + + "The quick brown fox jumped over the lazy dog.\n", "<>"))); + } + + @Test + public void testBinaryPart() { + byte[] random = new byte[8192]; + final ByteBuffer bytes = BufferUtils.allocate(random.length); + ThreadLocalRandom.current().nextBytes(random); + // Arrays.fill(random,(byte)'X'); + + TestHandler handler = new TestHandler() { + @Override + public boolean content(ByteBuffer buffer, boolean last) { + BufferUtils.append(bytes, buffer); + return last; + } + }; + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + String preamble = "Blah blah blah\r\n--BOUNDARY\r\n\r\n"; + String epilogue = "\r\n--BOUNDARY\r\nBlah blah blah!\r\n"; + + ByteBuffer data = BufferUtils.allocate(preamble.length() + random.length + epilogue.length()); + BufferUtils.append(data, BufferUtils.toBuffer(preamble)); + BufferUtils.append(data, ByteBuffer.wrap(random)); + BufferUtils.append(data, BufferUtils.toBuffer(epilogue)); + + parser.parse(data, true); + assertEquals(State.DELIMITER, parser.getState()); + assertEquals(19, data.remaining()); + assertArrayEquals(random, bytes.array()); + } + + @Test + public void testEpilogue() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("" + + "--BOUNDARY\r\n" + + "name: value\n" + + "\r\n" + + "Hello\r\n" + + "--BOUNDARY--" + + "epilogue here:" + + "\r\n" + + "--BOUNDARY--" + + "\r\n" + + "--BOUNDARY"); + + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Hello", "<>"))); + System.out.println(data.remaining()); + System.out.println(BufferUtils.toString(data)); + + parser.parse(data, true); + assertEquals(State.END, parser.getState()); + } + + @Test + public void testMultipleContent() { + TestHandler handler = new TestHandler(); + MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY"); + + ByteBuffer data = BufferUtils.toBuffer("" + + "--BOUNDARY\r\n" + + "name: value\n" + + "\r\n" + + "Hello" + + "\r\n" + + "--BOUNDARY\r\n" + + "powerLevel: 9001\n" + + "\r\n" + + "secondary" + + "\r\n" + + "content" + + "\r\n--BOUNDARY--" + + "epilogue here"); + + /* Test First Content Section */ + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Hello", "<>"))); + + /* Test Second Content Section */ + parser.parse(data, false); + assertEquals(State.DELIMITER, parser.getState()); + assertTrue(handler.fields.containsAll(Arrays.asList("name: value", "<>", "powerLevel: 9001", "<>"))); + assertTrue(handler.content.containsAll(Arrays.asList("Hello", "<>", "secondary\r\ncontent", "<>"))); + System.out.println(data.remaining()); + System.out.println(BufferUtils.toString(data)); + + /* Test Progression to END State */ + parser.parse(data, true); + assertEquals(State.END, parser.getState()); + assertEquals(0, data.remaining()); + } + + @Test + public void testCrAsLineTermination() { + TestHandler handler = new TestHandler() { + @Override + public boolean messageComplete() { + return true; + } + + @Override + public boolean content(ByteBuffer buffer, boolean last) { + super.content(buffer, last); + return false; + } + }; + MultiPartParser parser = new MultiPartParser(handler, "AaB03x"); + + ByteBuffer data = BufferUtils.toBuffer( + "--AaB03x\r\n" + + "content-disposition: form-data; name=\"field1\"\r\n" + + "\r" + + "Joe Blow\r\n" + + "--AaB03x--\r\n"); + + BadMessageException x = assertThrows(BadMessageException.class, + () -> parser.parse(data, true), + "Invalid EOL"); + assertTrue(x.getMessage().contains("Bad EOL")); + } + + @Test + public void testBadHeaderNames() { + String[] bad = new String[] + { + "Foo\\Bar: value\r\n", + "Foo@Bar: value\r\n", + "Foo,Bar: value\r\n", + "Foo}Bar: value\r\n", + "Foo{Bar: value\r\n", + "Foo=Bar: value\r\n", + "Foo>Bar: value\r\n", + "FooContent of a.html.\n" + + "\r\n" + + "-----------------------------9051914041544843365972754266\n" + + "Field1: value1\n" + + "Field2: value2\n" + + "Field3: value3\n" + + "Field4: value4\n" + + "Field5: value5\n" + + "Field6: value6\n" + + "Field7: value7\n" + + "Field8: value8\n" + + "Field9: value\n" + + " 9\n" + + "\r\n" + + "-----------------------------9051914041544843365972754266\n" + + "Field1: value1\n" + + "\r\n" + + "But the amount of denudation which the strata have\n" + + "in many places suffered, independently of the rate\n" + + "of accumulation of the degraded matter, probably\n" + + "offers the best evidence of the lapse of time. I remember\n" + + "having been much struck with the evidence of\n" + + "denudation, when viewing volcanic islands, which\n" + + "have been worn by the waves and pared all round\n" + + "into perpendicular cliffs of one or two thousand feet\n" + + "in height; for the gentle slope of the lava-streams,\n" + + "due to their formerly liquid state, showed at a glance\n" + + "how far the hard, rocky beds had once extended into\n" + + "the open ocean.\n" + + "\r\n" + + "-----------------------------9051914041544843365972754266--" + + "===== ajlkfja;lkdj;lakjd;lkjf ==== epilogue here ==== kajflajdfl;kjafl;kjl;dkfja ====\n\r\n\r\r\r\n\n\n"); + + int length = data.remaining(); + for (int i = 0; i < length - 1; i++) { + //partition 0 to i + ByteBuffer dataSeg = data.slice(); + dataSeg.position(0); + dataSeg.limit(i); + assertFalse(parser.parse(dataSeg, false)); + + //partition i + dataSeg = data.slice(); + dataSeg.position(i); + dataSeg.limit(i + 1); + assertFalse(parser.parse(dataSeg, false)); + + //partition i to length + dataSeg = data.slice(); + dataSeg.position(i + 1); + dataSeg.limit(length); + assertTrue(parser.parse(dataSeg, true)); + + assertTrue(handler.fields.containsAll(Arrays.asList("Content-Disposition: form-data; name=\"text\"", "<>", + "Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"", + "Content-Type: text/plain", "<>", + "Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"", + "Content-Type: text/html", "<>", + "Field1: value1", "Field2: value2", "Field3: value3", + "Field4: value4", "Field5: value5", "Field6: value6", + "Field7: value7", "Field8: value8", "Field9: value 9", "<>", + "Field1: value1", "<>"))); + + assertEquals("text default" + "<>" + + "Content of a.txt.\n" + "<>" + + "Content of a.html.\n" + "<>" + + "<>" + + "But the amount of denudation which the strata have\n" + + "in many places suffered, independently of the rate\n" + + "of accumulation of the degraded matter, probably\n" + + "offers the best evidence of the lapse of time. I remember\n" + + "having been much struck with the evidence of\n" + + "denudation, when viewing volcanic islands, which\n" + + "have been worn by the waves and pared all round\n" + + "into perpendicular cliffs of one or two thousand feet\n" + + "in height; for the gentle slope of the lava-streams,\n" + + "due to their formerly liquid state, showed at a glance\n" + + "how far the hard, rocky beds had once extended into\n" + + "the open ocean.\n" + "<>", handler.contentString()); + + handler.clear(); + parser.reset(); + } + } + + @Test + public void testGeneratedForm() { + TestHandler handler = new TestHandler() { + @Override + public boolean messageComplete() { + return true; + } + + @Override + public boolean content(ByteBuffer buffer, boolean last) { + super.content(buffer, last); + return false; + } + + @Override + public boolean headerComplete() { + return false; + } + }; + + MultiPartParser parser = new MultiPartParser(handler, "WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW"); + ByteBuffer data = BufferUtils.toBuffer("" + + "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" + + "\r\n" + + "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" + + "Content-Disposition: form-data; name=\"part1\"\r\n" + + "\n" + + "wNfミxVam﾿t\r\n" + + "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" + + "Content-Disposition: form-data; name=\"part2\"\r\n" + + "\r\n" + + "&ᄈᄎ￙ᅱᅢO\r\n" + + "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--"); + + parser.parse(data, true); + assertEquals(State.END, parser.getState()); + assertEquals(2, handler.fields.size()); + } + + static class TestHandler implements MultiPartParser.Handler { + List fields = new ArrayList<>(); + List content = new ArrayList<>(); + + @Override + public void parsedField(String name, String value) { + fields.add(name + ": " + value); + } + + public String contentString() { + StringBuilder sb = new StringBuilder(); + for (String s : content) { + sb.append(s); + } + return sb.toString(); + } + + @Override + public boolean headerComplete() { + fields.add("<>"); + return false; + } + + @Override + public boolean content(ByteBuffer buffer, boolean last) { + if (BufferUtils.hasContent(buffer)) + content.add(BufferUtils.toString(buffer)); + if (last) + content.add("<>"); + return last; + } + + public void clear() { + fields.clear(); + content.clear(); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URIUtilsCanonicalPathTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URIUtilsCanonicalPathTest.java new file mode 100644 index 000000000..dcc2c85c5 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URIUtilsCanonicalPathTest.java @@ -0,0 +1,112 @@ +package com.fireflysource.net.http.common.codec; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class URIUtilsCanonicalPathTest { + + public static Stream testParametersProvider() { + return data().stream().map(arr -> arguments(Arrays.asList(arr).toArray())); + } + + public static List data() { + String[][] canonical = { + // Basic examples (no changes expected) + {"/hello.html", "/hello.html"}, + {"/css/main.css", "/css/main.css"}, + {"/", "/"}, + {"", ""}, + {"/aaa/bbb/", "/aaa/bbb/"}, + {"/aaa/bbb", "/aaa/bbb"}, + {"aaa/bbb", "aaa/bbb"}, + {"aaa/", "aaa/"}, + {"aaa", "aaa"}, + {"a", "a"}, + {"a/", "a/"}, + + // Extra slashes + {"/aaa//bbb/", "/aaa//bbb/"}, + {"/aaa//bbb", "/aaa//bbb"}, + {"/aaa///bbb/", "/aaa///bbb/"}, + + // Path traversal with current references "./" + {"/aaa/./bbb/", "/aaa/bbb/"}, + {"/aaa/./bbb", "/aaa/bbb"}, + {"./bbb/", "bbb/"}, + {"./aaa/../bbb/", "bbb/"}, + {"/foo/.", "/foo/"}, + {"./", ""}, + {".", ""}, + {".//", "/"}, + {".///", "//"}, + {"/.", "/"}, + {"//.", "//"}, + {"///.", "///"}, + + // Path traversal directory (but not past root) + {"/aaa/../bbb/", "/bbb/"}, + {"/aaa/../bbb", "/bbb"}, + {"/aaa..bbb/", "/aaa..bbb/"}, + {"/aaa..bbb", "/aaa..bbb"}, + {"/aaa/..bbb/", "/aaa/..bbb/"}, + {"/aaa/..bbb", "/aaa/..bbb"}, + {"/aaa/./../bbb/", "/bbb/"}, + {"/aaa/./../bbb", "/bbb"}, + {"/aaa/bbb/ccc/../../ddd/", "/aaa/ddd/"}, + {"/aaa/bbb/ccc/../../ddd", "/aaa/ddd"}, + {"/foo/../bar//", "/bar//"}, + {"/ctx/../bar/../ctx/all/index.txt", "/ctx/all/index.txt"}, + {"/down/.././index.html", "/index.html"}, + + // Path traversal up past root + {"..", null}, + {"./..", null}, + {"aaa/../..", null}, + {"/foo/bar/../../..", null}, + {"/../foo", null}, + {"a/.", "a/"}, + {"a/..", ""}, + {"a/../..", null}, + {"/foo/../../bar", null}, + + // Query parameter specifics + {"/ctx/dir?/../index.html", "/ctx/index.html"}, + {"/get-files?file=/etc/passwd", "/get-files?file=/etc/passwd"}, + {"/get-files?file=../../../../../passwd", null}, + + // Known windows shell quirks + {"file.txt ", "file.txt "}, // with spaces + {"file.txt...", "file.txt..."}, // extra dots ignored by windows + // BREAKS Jenkins: {"file.txt\u0000", "file.txt\u0000"}, // null terminated is ignored by windows + {"file.txt\r", "file.txt\r"}, // CR terminated is ignored by windows + {"file.txt\n", "file.txt\n"}, // LF terminated is ignored by windows + {"file.txt\"\"\"\"", "file.txt\"\"\"\""}, // extra quotes ignored by windows + {"file.txt<<<>>><", "file.txt<<<>>><"}, // angle brackets at end of path ignored by windows + {"././././././file.txt", "file.txt"}, + + // Oddball requests that look like path traversal, but are not + {"/....", "/...."}, + {"/..../ctx/..../blah/logo.jpg", "/..../ctx/..../blah/logo.jpg"}, + + // paths with encoded segments should remain encoded + // canonicalPath() is not responsible for decoding characters + {"%2e%2e/", "%2e%2e/"}, + }; + return Arrays.asList(canonical); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void test(String input, String expect) { + assertEquals(expect, URIUtils.canonicalPath(input)); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URIUtilsTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URIUtilsTest.java new file mode 100644 index 000000000..212dfe694 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URIUtilsTest.java @@ -0,0 +1,501 @@ +package com.fireflysource.net.http.common.codec; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + + +/** + * Util meta Tests. + */ +@SuppressWarnings("SpellCheckingInspection") +class URIUtilsTest { + + static Stream encodePathSource() { + return Stream.of( + Arguments.of("/foo%23+;,:=/b a r/?info ", "/foo%2523+%3B,:=/b%20a%20r/%3Finfo%20"), + Arguments.of("/context/'list'/\"me\"/;", + "/context/%27list%27/%22me%22/%3B%3Cscript%3Ewindow.alert(%27xss%27)%3B%3C/script%3E"), + Arguments.of("test\u00f6?\u00f6:\u00df", "test%C3%B6%3F%C3%B6:%C3%9F"), + Arguments.of("test?\u00f6?\u00f6:\u00df", "test%3F%C3%B6%3F%C3%B6:%C3%9F") + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("encodePathSource") + void testEncodePath(String rawPath, String expectedEncoded) { + // test basic encode/decode + StringBuilder buf = new StringBuilder(); + buf.setLength(0); + URIUtils.encodePath(buf, rawPath); + assertEquals(expectedEncoded, buf.toString()); + } + + @Test + void testEncodeString() { + StringBuilder buf = new StringBuilder(); + buf.setLength(0); + URIUtils.encodeString(buf, "foo%23;,:=b a r", ";,= "); + assertEquals("foo%2523%3b%2c:%3db%20a%20r", buf.toString()); + } + + static Stream decodePathSource() { + List arguments = new ArrayList<>(); + arguments.add(Arguments.of("/foo/bar", "/foo/bar")); + + arguments.add(Arguments.of("/f%20o/b%20r", "/f o/b r")); + arguments.add(Arguments.of("fää%2523%3b%2c:%3db%20a%20r%3D", "f\u00e4\u00e4%23;,:=b a r=")); + arguments.add(Arguments.of("f%d8%a9%d8%a9%2523%3b%2c:%3db%20a%20r", "f\u0629\u0629%23;,:=b a r")); + + // path parameters should be ignored + arguments.add(Arguments.of("/foo;ignore/bar;ignore", "/foo/bar")); + arguments.add(Arguments.of("/f\u00e4\u00e4;ignore/bar;ignore", "/fää/bar")); + arguments.add(Arguments.of("/f%d8%a9%d8%a9%2523;ignore/bar;ignore", "/f\u0629\u0629%23/bar")); + arguments.add(Arguments.of("foo%2523%3b%2c:%3db%20a%20r;rubbish", "foo%23;,:=b a r")); + + // Test for null character (real world ugly test case) + byte[] oddBytes = {'/', 0x00, '/'}; + String odd = new String(oddBytes, StandardCharsets.ISO_8859_1); + arguments.add(Arguments.of("/%00/", odd)); + + // Deprecated Microsoft Percent-U encoding + arguments.add(Arguments.of("abc%u3040", "abc\u3040")); + + // Lenient decode + arguments.add(Arguments.of("abc%xyz", "abc%xyz")); // not a "%##" + arguments.add(Arguments.of("abc%", "abc%")); // percent at end of string + arguments.add(Arguments.of("abc%A", "abc%A")); // incomplete "%##" at end of string + arguments.add(Arguments.of("abc%uvwxyz", "abc%uvwxyz")); // not a valid "%u####" + arguments.add(Arguments.of("abc%uEFGHIJ", "abc%uEFGHIJ")); // not a valid "%u####" + arguments.add(Arguments.of("abc%uABC", "abc%uABC")); // incomplete "%u####" + arguments.add(Arguments.of("abc%uAB", "abc%uAB")); // incomplete "%u####" + arguments.add(Arguments.of("abc%uA", "abc%uA")); // incomplete "%u####" + arguments.add(Arguments.of("abc%u", "abc%u")); // incomplete "%u####" + + return arguments.stream(); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("decodePathSource") + void testDecodePath(String encodedPath, String expectedPath) { + String path = URIUtils.decodePath(encodedPath); + assertEquals(expectedPath, path); + } + + @Test + void testDecodePathSubstring() { + String path = URIUtils.decodePath("xx/foo/barxx", 2, 8); + assertEquals("/foo/bar", path); + + path = URIUtils.decodePath("xxx/foo/bar%2523%3b%2c:%3db%20a%20r%3Dxxx;rubbish", 3, 35); + assertEquals("/foo/bar%23;,:=b a r=", path); + } + + static Stream addEncodedPathsSource() { + return Stream.of( + Arguments.of(null, null, null), + Arguments.of(null, "", ""), + Arguments.of(null, "bbb", "bbb"), + Arguments.of(null, "/", "/"), + Arguments.of(null, "/bbb", "/bbb"), + + Arguments.of("", null, ""), + Arguments.of("", "", ""), + Arguments.of("", "bbb", "bbb"), + Arguments.of("", "/", "/"), + Arguments.of("", "/bbb", "/bbb"), + + Arguments.of("aaa", null, "aaa"), + Arguments.of("aaa", "", "aaa"), + Arguments.of("aaa", "bbb", "aaa/bbb"), + Arguments.of("aaa", "/", "aaa/"), + Arguments.of("aaa", "/bbb", "aaa/bbb"), + + Arguments.of("/", null, "/"), + Arguments.of("/", "", "/"), + Arguments.of("/", "bbb", "/bbb"), + Arguments.of("/", "/", "/"), + Arguments.of("/", "/bbb", "/bbb"), + + Arguments.of("aaa/", null, "aaa/"), + Arguments.of("aaa/", "", "aaa/"), + Arguments.of("aaa/", "bbb", "aaa/bbb"), + Arguments.of("aaa/", "/", "aaa/"), + Arguments.of("aaa/", "/bbb", "aaa/bbb"), + + Arguments.of(";JS", null, ";JS"), + Arguments.of(";JS", "", ";JS"), + Arguments.of(";JS", "bbb", "bbb;JS"), + Arguments.of(";JS", "/", "/;JS"), + Arguments.of(";JS", "/bbb", "/bbb;JS"), + + Arguments.of("aaa;JS", null, "aaa;JS"), + Arguments.of("aaa;JS", "", "aaa;JS"), + Arguments.of("aaa;JS", "bbb", "aaa/bbb;JS"), + Arguments.of("aaa;JS", "/", "aaa/;JS"), + Arguments.of("aaa;JS", "/bbb", "aaa/bbb;JS"), + + Arguments.of("aaa/;JS", null, "aaa/;JS"), + Arguments.of("aaa/;JS", "", "aaa/;JS"), + Arguments.of("aaa/;JS", "bbb", "aaa/bbb;JS"), + Arguments.of("aaa/;JS", "/", "aaa/;JS"), + Arguments.of("aaa/;JS", "/bbb", "aaa/bbb;JS"), + + Arguments.of("?A=1", null, "?A=1"), + Arguments.of("?A=1", "", "?A=1"), + Arguments.of("?A=1", "bbb", "bbb?A=1"), + Arguments.of("?A=1", "/", "/?A=1"), + Arguments.of("?A=1", "/bbb", "/bbb?A=1"), + + Arguments.of("aaa?A=1", null, "aaa?A=1"), + Arguments.of("aaa?A=1", "", "aaa?A=1"), + Arguments.of("aaa?A=1", "bbb", "aaa/bbb?A=1"), + Arguments.of("aaa?A=1", "/", "aaa/?A=1"), + Arguments.of("aaa?A=1", "/bbb", "aaa/bbb?A=1"), + + Arguments.of("aaa/?A=1", null, "aaa/?A=1"), + Arguments.of("aaa/?A=1", "", "aaa/?A=1"), + Arguments.of("aaa/?A=1", "bbb", "aaa/bbb?A=1"), + Arguments.of("aaa/?A=1", "/", "aaa/?A=1"), + Arguments.of("aaa/?A=1", "/bbb", "aaa/bbb?A=1"), + + Arguments.of(";JS?A=1", null, ";JS?A=1"), + Arguments.of(";JS?A=1", "", ";JS?A=1"), + Arguments.of(";JS?A=1", "bbb", "bbb;JS?A=1"), + Arguments.of(";JS?A=1", "/", "/;JS?A=1"), + Arguments.of(";JS?A=1", "/bbb", "/bbb;JS?A=1"), + + Arguments.of("aaa;JS?A=1", null, "aaa;JS?A=1"), + Arguments.of("aaa;JS?A=1", "", "aaa;JS?A=1"), + Arguments.of("aaa;JS?A=1", "bbb", "aaa/bbb;JS?A=1"), + Arguments.of("aaa;JS?A=1", "/", "aaa/;JS?A=1"), + Arguments.of("aaa;JS?A=1", "/bbb", "aaa/bbb;JS?A=1"), + + Arguments.of("aaa/;JS?A=1", null, "aaa/;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "", "aaa/;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "bbb", "aaa/bbb;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "/", "aaa/;JS?A=1"), + Arguments.of("aaa/;JS?A=1", "/bbb", "aaa/bbb;JS?A=1") + ); + } + + @ParameterizedTest(name = "[{index}] {0}+{1}") + @MethodSource("addEncodedPathsSource") + void testAddEncodedPaths(String path1, String path2, String expected) { + String actual = URIUtils.addEncodedPaths(path1, path2); + assertEquals(expected, actual, String.format("%s+%s", path1, path2)); + } + + static Stream addDecodedPathsSource() { + return Stream.of( + Arguments.of(null, null, null), + Arguments.of(null, "", ""), + Arguments.of(null, "bbb", "bbb"), + Arguments.of(null, "/", "/"), + Arguments.of(null, "/bbb", "/bbb"), + + Arguments.of("", null, ""), + Arguments.of("", "", ""), + Arguments.of("", "bbb", "bbb"), + Arguments.of("", "/", "/"), + Arguments.of("", "/bbb", "/bbb"), + + Arguments.of("aaa", null, "aaa"), + Arguments.of("aaa", "", "aaa"), + Arguments.of("aaa", "bbb", "aaa/bbb"), + Arguments.of("aaa", "/", "aaa/"), + Arguments.of("aaa", "/bbb", "aaa/bbb"), + + Arguments.of("/", null, "/"), + Arguments.of("/", "", "/"), + Arguments.of("/", "bbb", "/bbb"), + Arguments.of("/", "/", "/"), + Arguments.of("/", "/bbb", "/bbb"), + + Arguments.of("aaa/", null, "aaa/"), + Arguments.of("aaa/", "", "aaa/"), + Arguments.of("aaa/", "bbb", "aaa/bbb"), + Arguments.of("aaa/", "/", "aaa/"), + Arguments.of("aaa/", "/bbb", "aaa/bbb"), + + Arguments.of(";JS", null, ";JS"), + Arguments.of(";JS", "", ";JS"), + Arguments.of(";JS", "bbb", ";JS/bbb"), + Arguments.of(";JS", "/", ";JS/"), + Arguments.of(";JS", "/bbb", ";JS/bbb"), + + Arguments.of("aaa;JS", null, "aaa;JS"), + Arguments.of("aaa;JS", "", "aaa;JS"), + Arguments.of("aaa;JS", "bbb", "aaa;JS/bbb"), + Arguments.of("aaa;JS", "/", "aaa;JS/"), + Arguments.of("aaa;JS", "/bbb", "aaa;JS/bbb"), + + Arguments.of("aaa/;JS", null, "aaa/;JS"), + Arguments.of("aaa/;JS", "", "aaa/;JS"), + Arguments.of("aaa/;JS", "bbb", "aaa/;JS/bbb"), + Arguments.of("aaa/;JS", "/", "aaa/;JS/"), + Arguments.of("aaa/;JS", "/bbb", "aaa/;JS/bbb"), + + Arguments.of("?A=1", null, "?A=1"), + Arguments.of("?A=1", "", "?A=1"), + Arguments.of("?A=1", "bbb", "?A=1/bbb"), + Arguments.of("?A=1", "/", "?A=1/"), + Arguments.of("?A=1", "/bbb", "?A=1/bbb"), + + Arguments.of("aaa?A=1", null, "aaa?A=1"), + Arguments.of("aaa?A=1", "", "aaa?A=1"), + Arguments.of("aaa?A=1", "bbb", "aaa?A=1/bbb"), + Arguments.of("aaa?A=1", "/", "aaa?A=1/"), + Arguments.of("aaa?A=1", "/bbb", "aaa?A=1/bbb"), + + Arguments.of("aaa/?A=1", null, "aaa/?A=1"), + Arguments.of("aaa/?A=1", "", "aaa/?A=1"), + Arguments.of("aaa/?A=1", "bbb", "aaa/?A=1/bbb"), + Arguments.of("aaa/?A=1", "/", "aaa/?A=1/"), + Arguments.of("aaa/?A=1", "/bbb", "aaa/?A=1/bbb") + ); + } + + @ParameterizedTest(name = "[{index}] {0}+{1}") + @MethodSource("addDecodedPathsSource") + void testAddDecodedPaths(String path1, String path2, String expected) { + String actual = URIUtils.addPaths(path1, path2); + assertEquals(expected, actual, String.format("%s+%s", path1, path2)); + } + + static Stream compactPathSource() { + return Stream.of( + Arguments.of("/foo/bar", "/foo/bar"), + Arguments.of("/foo/bar?a=b//c", "/foo/bar?a=b//c"), + + Arguments.of("//foo//bar", "/foo/bar"), + Arguments.of("//foo//bar?a=b//c", "/foo/bar?a=b//c"), + + Arguments.of("/foo///bar", "/foo/bar"), + Arguments.of("/foo///bar?a=b//c", "/foo/bar?a=b//c") + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("compactPathSource") + void testCompactPath(String path, String expected) { + String actual = URIUtils.compactPath(path); + assertEquals(expected, actual); + } + + static Stream parentPathSource() { + return Stream.of( + Arguments.of("/aaa/bbb/", "/aaa/"), + Arguments.of("/aaa/bbb", "/aaa/"), + Arguments.of("/aaa/", "/"), + Arguments.of("/aaa", "/"), + Arguments.of("/", null), + Arguments.of(null, null) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("parentPathSource") + void testParentPath(String path, String expectedPath) { + String actual = URIUtils.parentPath(path); + assertEquals(expectedPath, actual, String.format("parent %s", path)); + } + + static Stream equalsIgnoreEncodingStringTrueSource() { + return Stream.of( + Arguments.of("http://example.com/foo/bar", "http://example.com/foo/bar"), + Arguments.of("/barry's", "/barry%27s"), + Arguments.of("/barry%27s", "/barry's"), + Arguments.of("/barry%27s", "/barry%27s"), + Arguments.of("/b rry's", "/b%20rry%27s"), + Arguments.of("/b rry%27s", "/b%20rry's"), + Arguments.of("/b rry%27s", "/b%20rry%27s"), + + Arguments.of("/foo%2fbar", "/foo%2fbar"), + Arguments.of("/foo%2fbar", "/foo%2Fbar"), + + // encoded vs not-encode ("%" symbol is encoded as "%25") + Arguments.of("/abc%25xyz", "/abc%xyz"), + Arguments.of("/abc%25xy", "/abc%xy"), + Arguments.of("/abc%25x", "/abc%x"), + Arguments.of("/zzz%25", "/zzz%") + ); + } + + @ParameterizedTest + @MethodSource("equalsIgnoreEncodingStringTrueSource") + void testEqualsIgnoreEncodingStringTrue(String uriA, String uriB) { + assertTrue(URIUtils.equalsIgnoreEncodings(uriA, uriB)); + } + + static Stream equalsIgnoreEncodingStringFalseSource() { + return Stream.of( + // case difference + Arguments.of("ABC", "abc"), + // Encoding difference ("'" is "%27") + Arguments.of("/barry's", "/barry%26s"), + // Never match on "%2f" differences - only intested in filename / directory name differences + // This could be a directory called "foo" with a file called "bar" on the left, and just a file "foo%2fbar" on the right + Arguments.of("/foo/bar", "/foo%2fbar"), + // not actually encoded + Arguments.of("/foo2fbar", "/foo/bar"), + // encoded vs not-encode ("%" symbol is encoded as "%25") + Arguments.of("/yyy%25zzz", "/aaa%xxx"), + Arguments.of("/zzz%25", "/aaa%") + ); + } + + @ParameterizedTest + @MethodSource("equalsIgnoreEncodingStringFalseSource") + void testEqualsIgnoreEncodingStringFalse(String uriA, String uriB) { + assertFalse(URIUtils.equalsIgnoreEncodings(uriA, uriB)); + } + + static Stream equalsIgnoreEncodingURITrueSource() { + return Stream.of( + Arguments.of( + URI.create("jar:file:/path/to/main.jar!/META-INF/versions/"), + URI.create("jar:file:/path/to/main.jar!/META-INF/%76ersions/") + ), + Arguments.of( + URI.create("JAR:FILE:/path/to/main.jar!/META-INF/versions/"), + URI.create("jar:file:/path/to/main.jar!/META-INF/versions/") + ) + ); + } + + @ParameterizedTest + @MethodSource("equalsIgnoreEncodingURITrueSource") + void testEqualsIgnoreEncodingURITrue(URI uriA, URI uriB) { + assertTrue(URIUtils.equalsIgnoreEncodings(uriA, uriB)); + } + + static Stream getJarSourceStringSource() { + return Stream.of( + Arguments.of("file:///tmp/", "file:///tmp/"), + Arguments.of("jar:file:///tmp/foo.jar", "file:///tmp/foo.jar"), + Arguments.of("jar:file:///tmp/foo.jar!/some/path", "file:///tmp/foo.jar") + ); + } + + @ParameterizedTest + @MethodSource("getJarSourceStringSource") + void testJarSourceString(String uri, String expectedJarUri) { + assertEquals(expectedJarUri, URIUtils.getJarSource(uri)); + } + + static Stream getJarSourceURISource() { + return Stream.of( + Arguments.of(URI.create("file:///tmp/"), URI.create("file:///tmp/")), + Arguments.of(URI.create("jar:file:///tmp/foo.jar"), URI.create("file:///tmp/foo.jar")), + Arguments.of(URI.create("jar:file:///tmp/foo.jar!/some/path"), URI.create("file:///tmp/foo.jar")) + ); + } + + @ParameterizedTest + @MethodSource("getJarSourceURISource") + void testJarSourceURI(URI uri, URI expectedJarUri) { + assertEquals(expectedJarUri, URIUtils.getJarSource(uri)); + } + + static Stream encodeSpacesSource() { + return Stream.of( + // null + Arguments.of(null, null), + + // no spaces + Arguments.of("abc", "abc"), + + // match + Arguments.of("a c", "a%20c"), + Arguments.of(" ", "%20%20%20"), + Arguments.of("a%20space", "a%20space") + ); + } + + @ParameterizedTest + @MethodSource("encodeSpacesSource") + void testEncodeSpaces(String raw, String expected) { + assertEquals(expected, URIUtils.encodeSpaces(raw)); + } + + static Stream encodeSpecific() { + return Stream.of( + // [raw, chars, expected] + + // null input + Arguments.of(null, null, null), + + // null chars + Arguments.of("abc", null, "abc"), + + // empty chars + Arguments.of("abc", "", "abc"), + + // no matches + Arguments.of("abc", ".;", "abc"), + Arguments.of("xyz", ".;", "xyz"), + Arguments.of(":::", ".;", ":::"), + + // matches + Arguments.of("a c", " ", "a%20c"), + Arguments.of("name=value", "=", "name%3Dvalue"), + Arguments.of("This has fewer then 10% hits.", ".%", "This has fewer then 10%25 hits%2E"), + + // partially encoded already + Arguments.of("a%20name=value%20pair", "=", "a%20name%3Dvalue%20pair"), + Arguments.of("a%20name=value%20pair", "=%", "a%2520name%3Dvalue%2520pair") + ); + } + + @ParameterizedTest + @MethodSource(value = "encodeSpecific") + void testEncodeSpecific(String raw, String chars, String expected) { + assertEquals(expected, URIUtils.encodeSpecific(raw, chars)); + } + + static Stream decodeSpecific() { + return Stream.of( + // [raw, chars, expected] + + // null input + Arguments.of(null, null, null), + + // null chars + Arguments.of("abc", null, "abc"), + + // empty chars + Arguments.of("abc", "", "abc"), + + // no matches + Arguments.of("abc", ".;", "abc"), + Arguments.of("xyz", ".;", "xyz"), + Arguments.of(":::", ".;", ":::"), + + // matches + Arguments.of("a%20c", " ", "a c"), + Arguments.of("name%3Dvalue", "=", "name=value"), + Arguments.of("This has fewer then 10%25 hits%2E", ".%", "This has fewer then 10% hits."), + + // partially decode + Arguments.of("a%20name%3Dvalue%20pair", "=", "a%20name=value%20pair"), + Arguments.of("a%2520name%3Dvalue%2520pair", "=%", "a%20name=value%20pair") + ); + } + + @ParameterizedTest + @MethodSource(value = "decodeSpecific") + void testDecodeSpecific(String raw, String chars, String expected) { + assertEquals(expected, URIUtils.decodeSpecific(raw, chars)); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URLEncodedTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URLEncodedTest.java new file mode 100644 index 000000000..e26c5e2cb --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/URLEncodedTest.java @@ -0,0 +1,157 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.collection.map.MultiMap; +import com.fireflysource.common.object.TypeUtils; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Util meta Tests. + */ +class URLEncodedTest { + + private static final String __ISO_8859_1 = "iso-8859-1"; + private static final String __UTF8 = "utf-8"; + private static final String __UTF16 = "utf-16"; + + @Test + void testUrlEncoded() { + + UrlEncoded url_encoded = new UrlEncoded(); + assertEquals(0, url_encoded.size()); + + url_encoded.clear(); + url_encoded.decode(""); + assertEquals(0, url_encoded.size()); + + url_encoded.clear(); + url_encoded.decode("Name1=Value1"); + assertEquals(1, url_encoded.size()); + assertEquals("Name1=Value1", url_encoded.encode()); + assertEquals("Value1", url_encoded.getString("Name1")); + + url_encoded.clear(); + url_encoded.decode("Name2="); + assertEquals(1, url_encoded.size()); + assertEquals("Name2", url_encoded.encode()); + assertEquals("", url_encoded.getString("Name2")); + + url_encoded.clear(); + url_encoded.decode("Name3"); + assertEquals(1, url_encoded.size()); + assertEquals("Name3", url_encoded.encode()); + assertEquals("", url_encoded.getString("Name3")); + + url_encoded.clear(); + url_encoded.decode("Name4=V\u0629lue+4%21"); + assertEquals(1, url_encoded.size()); + assertEquals("Name4=V%D8%A9lue+4%21", url_encoded.encode()); + assertEquals("V\u0629lue 4!", url_encoded.getString("Name4")); + + url_encoded.clear(); + url_encoded.decode("Name4=Value%2B4%21"); + assertEquals(1, url_encoded.size()); + assertEquals("Name4=Value%2B4%21", url_encoded.encode()); + assertEquals("Value+4!", url_encoded.getString("Name4")); + + url_encoded.clear(); + url_encoded.decode("Name4=Value+4%21%20%214"); + assertEquals(1, url_encoded.size()); + assertEquals("Name4=Value+4%21+%214", url_encoded.encode()); + assertEquals("Value 4! !4", url_encoded.getString("Name4")); + + + url_encoded.clear(); + url_encoded.decode("Name5=aaa&Name6=bbb"); + assertEquals(2, url_encoded.size()); + assertTrue(url_encoded.encode().equals("Name5=aaa&Name6=bbb") || + url_encoded.encode().equals("Name6=bbb&Name5=aaa") + ); + assertEquals("aaa", url_encoded.getString("Name5")); + assertEquals("bbb", url_encoded.getString("Name6")); + + url_encoded.clear(); + url_encoded.decode("Name7=aaa&Name7=b%2Cb&Name7=ccc"); + assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", url_encoded.encode()); + assertEquals("aaa,b,b,ccc", url_encoded.getString("Name7")); + assertEquals("aaa", url_encoded.getValues("Name7").get(0)); + assertEquals("b,b", url_encoded.getValues("Name7").get(1)); + assertEquals("ccc", url_encoded.getValues("Name7").get(2)); + + url_encoded.clear(); + url_encoded.decode("Name8=xx%2C++yy++%2Czz"); + assertEquals(1, url_encoded.size()); + assertEquals("Name8=xx%2C++yy++%2Czz", url_encoded.encode()); + assertEquals("xx, yy ,zz", url_encoded.getString("Name8")); + } + + @Test + void testUrlEncodedStream() + throws Exception { + @SuppressWarnings("InjectedReferences") String[][] charsets = new String[][] + { + {__UTF8, null, "%30"}, + {__ISO_8859_1, __ISO_8859_1, "%30"}, + {__UTF8, __UTF8, "%30"}, + {__UTF16, __UTF16, "%00%30"}, + }; + + // Note: "%30" -> decode -> "0" + + for (int i = 0; i < charsets.length; i++) { + ByteArrayInputStream in = new ByteArrayInputStream(("name\n=value+" + charsets[i][2] + "&name1=&name2&n\u00e3me3=value+3").getBytes(charsets[i][0])); + MultiMap m = new MultiMap<>(); + UrlEncoded.decodeTo(in, m, charsets[i][1] == null ? null : Charset.forName(charsets[i][1]), -1, -1); + assertEquals(4, m.size()); + assertEquals("value 0", m.getString("name\n")); + assertEquals("", m.getString("name1")); + assertEquals("", m.getString("name2")); + assertEquals("value 3", m.getString("n\u00e3me3")); + } + + + if (Charset.isSupported("Shift_JIS")) { + ByteArrayInputStream in2 = new ByteArrayInputStream("name=%83e%83X%83g".getBytes(StandardCharsets.ISO_8859_1)); + MultiMap m2 = new MultiMap<>(); + UrlEncoded.decodeTo(in2, m2, Charset.forName("Shift_JIS"), -1, -1); + assertEquals(1, m2.size()); + assertEquals("\u30c6\u30b9\u30c8", m2.getString("name")); + } else + assertTrue(true); + + } + + @Test + void testUtf8() + throws Exception { + UrlEncoded url_encoded = new UrlEncoded(); + assertEquals(0, url_encoded.size()); + + url_encoded.clear(); + url_encoded.decode("text=%E0%B8%9F%E0%B8%AB%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%81%E0%B8%9F%E0%B8%A7%E0%B8%AB%E0%B8%AA%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%AB%E0%B8%9F%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%AA%E0%B8%B2%E0%B8%9F%E0%B8%81%E0%B8%AB%E0%B8%A3%E0%B8%94%E0%B9%89%E0%B8%9F%E0%B8%AB%E0%B8%99%E0%B8%81%E0%B8%A3%E0%B8%94%E0%B8%B5&Action=Submit"); + + String hex = "E0B89FE0B8ABE0B881E0B8A7E0B894E0B8B2E0B988E0B881E0B89FE0B8A7E0B8ABE0B8AAE0B894E0B8B2E0B988E0B8ABE0B89FE0B881E0B8A7E0B894E0B8AAE0B8B2E0B89FE0B881E0B8ABE0B8A3E0B894E0B989E0B89FE0B8ABE0B899E0B881E0B8A3E0B894E0B8B5"; + String expected = new String(TypeUtils.fromHexString(hex), StandardCharsets.UTF_8); + assertEquals(expected, url_encoded.getString("text")); + } + + @Test + void testUtf8_MultiByteCodePoint() { + String input = "text=test%C3%A4"; + UrlEncoded url_encoded = new UrlEncoded(); + url_encoded.decode(input); + + // http://www.ltg.ed.ac.uk/~richard/utf-8.cgi?input=00e4&mode=hex + // Should be "testä" + // "test" followed by a LATIN SMALL LETTER A WITH DIAERESIS + + String expected = "test\u00e4"; + assertEquals(expected, url_encoded.getString("text")); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/UrlEncodedUtf8Test.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/UrlEncodedUtf8Test.java new file mode 100644 index 000000000..50696e1b1 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/codec/UrlEncodedUtf8Test.java @@ -0,0 +1,86 @@ +package com.fireflysource.net.http.common.codec; + +import com.fireflysource.common.collection.map.MultiMap; +import com.fireflysource.common.string.Utf8Appendable; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class UrlEncodedUtf8Test { + + static void fromString(String test, String s, String field, String expected, boolean thrown) { + MultiMap values = new MultiMap<>(); + try { + UrlEncoded.decodeUtf8To(s, 0, s.length(), values); + if (thrown) + fail(); + assertEquals(expected, values.getString(field), test); + } catch (Exception e) { + if (!thrown) + throw e; + } + } + + static void fromInputStream(String test, byte[] b, String field, String expected, boolean thrown) throws Exception { + InputStream is = new ByteArrayInputStream(b); + MultiMap values = new MultiMap<>(); + try { + UrlEncoded.decodeUtf8To(is, values, 1000000, -1); + if (thrown) + fail(); + assertEquals(expected, values.getString(field), test); + } catch (Exception e) { + if (!thrown) + throw e; + } + } + + @Test + void testIncompleteSequestAtTheEnd() throws Exception { + byte[] bytes = {97, 98, 61, 99, -50}; + String test = new String(bytes, StandardCharsets.UTF_8); + String expected = "c" + Utf8Appendable.REPLACEMENT; + + fromString(test, test, "ab", expected, false); + fromInputStream(test, bytes, "ab", expected, false); + } + + @Test + void testIncompleteSequestAtTheEnd2() throws Exception { + byte[] bytes = {97, 98, 61, -50}; + String test = new String(bytes, StandardCharsets.UTF_8); + String expected = "" + Utf8Appendable.REPLACEMENT; + + fromString(test, test, "ab", expected, false); + fromInputStream(test, bytes, "ab", expected, false); + + } + + @Test + void testIncompleteSequestInName() throws Exception { + byte[] bytes = {101, -50, 61, 102, 103, 38, 97, 98, 61, 99, 100}; + String test = new String(bytes, StandardCharsets.UTF_8); + String name = "e" + Utf8Appendable.REPLACEMENT; + String value = "fg"; + + fromString(test, test, name, value, false); + fromInputStream(test, bytes, name, value, false); + } + + @Test + void testIncompleteSequestInValue() throws Exception { + byte[] bytes = {101, 102, 61, 103, -50, 38, 97, 98, 61, 99, 100}; + String test = new String(bytes, StandardCharsets.UTF_8); + String name = "ef"; + String value = "g" + Utf8Appendable.REPLACEMENT; + + fromString(test, test, name, value, false); + fromInputStream(test, bytes, name, value, false); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HostPortTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HostPortTest.java new file mode 100644 index 000000000..a76460320 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HostPortTest.java @@ -0,0 +1,66 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Pengtao Qiu + */ +class HostPortTest { + private static Stream validAuthorityProvider() { + return Stream.of( + Arguments.of("", "", null), + Arguments.of(":80", "", "80"), + Arguments.of("host", "host", null), + Arguments.of("host:80", "host", "80"), + Arguments.of("10.10.10.1", "10.10.10.1", null), + Arguments.of("10.10.10.1:80", "10.10.10.1", "80"), + Arguments.of("[0::0::0::1]", "[0::0::0::1]", null), + Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80") + ); + } + + private static Stream invalidAuthorityProvider() { + return Stream.of( + null, + "host:", + "127.0.0.1:", + "[0::0::0::0::1]:", + "host:xxx", + "127.0.0.1:xxx", + "[0::0::0::0::1]:xxx", + "host:-80", + "127.0.0.1:-80", + "[0::0::0::0::1]:-80") + .map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("validAuthorityProvider") + void testValidAuthority(String authority, String expectedHost, Integer expectedPort) { + try { + HostPort hostPort = new HostPort(authority); + assertEquals(expectedHost, hostPort.getHost(), authority); + + if (expectedPort == null) + assertEquals(0, hostPort.getPort(), authority); + else + assertEquals(expectedPort, Integer.valueOf(hostPort.getPort()), authority); + } catch (Exception e) { + if (expectedHost != null) + e.printStackTrace(); + assertNull(authority, expectedHost); + } + } + + @ParameterizedTest + @MethodSource("invalidAuthorityProvider") + void testInvalidAuthority(String authority) { + assertThrows(IllegalArgumentException.class, () -> new HostPort(authority)); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpFieldsTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpFieldsTest.java new file mode 100644 index 000000000..48f303010 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpFieldsTest.java @@ -0,0 +1,485 @@ +package com.fireflysource.net.http.common.model; + + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.*; + + +class HttpFieldsTest { + + @Test + void testPut() { + HttpFields header = new HttpFields(); + + header.put("name0", "value:0"); + header.put("name1", "value1"); + + assertEquals(2, header.size()); + assertEquals("value:0", header.get("name0")); + assertEquals("value1", header.get("name1")); + assertNull(header.get("name2")); + + int matches = 0; + Enumeration e = header.getFieldNames(); + while (e.hasMoreElements()) { + Object o = e.nextElement(); + if ("name0".equals(o)) + matches++; + if ("name1".equals(o)) + matches++; + } + assertEquals(2, matches); + + e = header.getValues("name0"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value:0"); + assertFalse(e.hasMoreElements()); + } + + @Test + void testGet() { + HttpFields header = new HttpFields(); + + header.put("name0", "value0"); + header.put("name1", "value1"); + + assertEquals("value0", header.get("name0")); + assertEquals("value0", header.get("Name0")); + assertEquals("value1", header.get("name1")); + assertEquals("value1", header.get("Name1")); + assertNull(header.get("Name2")); + + assertEquals("value0", header.getField("name0").getValue()); + assertEquals("value0", header.getField("Name0").getValue()); + assertEquals("value1", header.getField("name1").getValue()); + assertEquals("value1", header.getField("Name1").getValue()); + assertNull(header.getField("Name2")); + + assertEquals("value0", header.getField(0).getValue()); + assertEquals("value1", header.getField(1).getValue()); + try { + header.getField(2); + fail(); + } catch (NoSuchElementException ignored) { + } + } + + @Test + void testGetKnown() { + HttpFields header = new HttpFields(); + + header.put("Connection", "value0"); + header.put(HttpHeader.ACCEPT, "value1"); + + assertEquals("value0", header.get(HttpHeader.CONNECTION)); + assertEquals("value1", header.get(HttpHeader.ACCEPT)); + + assertEquals("value0", header.getField(HttpHeader.CONNECTION).getValue()); + assertEquals("value1", header.getField(HttpHeader.ACCEPT).getValue()); + + assertNull(header.getField(HttpHeader.AGE)); + assertNull(header.get(HttpHeader.AGE)); + } + + @Test + void testRePut() { + HttpFields header = new HttpFields(); + + header.put("name0", "value0"); + header.put("name1", "xxxxxx"); + header.put("name2", "value2"); + + assertEquals("value0", header.get("name0")); + assertEquals("xxxxxx", header.get("name1")); + assertEquals("value2", header.get("name2")); + + header.put("name1", "value1"); + + assertEquals("value0", header.get("name0")); + assertEquals("value1", header.get("name1")); + assertEquals("value2", header.get("name2")); + assertNull(header.get("name3")); + + int matches = 0; + Enumeration e = header.getFieldNames(); + while (e.hasMoreElements()) { + String o = e.nextElement(); + if ("name0".equals(o)) + matches++; + if ("name1".equals(o)) + matches++; + if ("name2".equals(o)) + matches++; + } + assertEquals(3, matches); + + e = header.getValues("name1"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value1"); + assertFalse(e.hasMoreElements()); + } + + @Test + void testRemovePut() { + HttpFields header = new HttpFields(1); + + header.put("name0", "value0"); + header.put("name1", "value1"); + header.put("name2", "value2"); + + assertEquals("value0", header.get("name0")); + assertEquals("value1", header.get("name1")); + assertEquals("value2", header.get("name2")); + + header.remove("name1"); + + assertEquals("value0", header.get("name0")); + assertNull(header.get("name1")); + assertEquals("value2", header.get("name2")); + assertNull(header.get("name3")); + + int matches = 0; + Enumeration e = header.getFieldNames(); + while (e.hasMoreElements()) { + Object o = e.nextElement(); + if ("name0".equals(o)) + matches++; + if ("name1".equals(o)) + matches++; + if ("name2".equals(o)) + matches++; + } + assertEquals(2, matches); + + e = header.getValues("name1"); + assertFalse(e.hasMoreElements()); + } + + @Test + void testAdd() { + HttpFields fields = new HttpFields(); + + fields.add("name0", "value0"); + fields.add("name1", "valueA"); + fields.add("name2", "value2"); + + assertEquals("value0", fields.get("name0")); + assertEquals("valueA", fields.get("name1")); + assertEquals("value2", fields.get("name2")); + + fields.add("name1", "valueB"); + + assertEquals("value0", fields.get("name0")); + assertEquals("valueA", fields.get("name1")); + assertEquals("value2", fields.get("name2")); + assertNull(fields.get("name3")); + + int matches = 0; + Enumeration e = fields.getFieldNames(); + while (e.hasMoreElements()) { + Object o = e.nextElement(); + if ("name0".equals(o)) + matches++; + if ("name1".equals(o)) + matches++; + if ("name2".equals(o)) + matches++; + } + assertEquals(3, matches); + + e = fields.getValues("name1"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "valueA"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "valueB"); + assertFalse(e.hasMoreElements()); + } + + @Test + void testGetValues() { + HttpFields fields = new HttpFields(); + + fields.put("name0", "value0A,value0B"); + fields.add("name0", "value0C,value0D"); + fields.put("name1", "value1A, \"value\t, 1B\" "); + fields.add("name1", "\"value1C\",\tvalue1D"); + + Enumeration e = fields.getValues("name0"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0A,value0B"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0C,value0D"); + assertFalse(e.hasMoreElements()); + +// e = fields.getValues("name0",","); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value0A"); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value0B"); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value0C"); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value0D"); +// assertEquals(false, e.hasMoreElements()); +// +// e = fields.getValues("name1",","); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value1A"); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value\t, 1B"); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value1C"); +// assertEquals(true, e.hasMoreElements()); +// assertEquals(e.nextElement(), "value1D"); +// assertEquals(false, e.hasMoreElements()); + } + + @Test + void testGetCSV() { + HttpFields fields = new HttpFields(); + + fields.put("name0", "value0A,value0B"); + fields.add("name0", "value0C,value0D"); + fields.put("name1", "value1A, \"value\t, 1B\" "); + fields.add("name1", "\"value1C\",\tvalue1D"); + + Enumeration e = fields.getValues("name0"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0A,value0B"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0C,value0D"); + assertFalse(e.hasMoreElements()); + + e = Collections.enumeration(fields.getCSV("name0", false)); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0A"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0B"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0C"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value0D"); + assertFalse(e.hasMoreElements()); + + e = Collections.enumeration(fields.getCSV("name1", false)); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value1A"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value\t, 1B"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value1C"); + assertTrue(e.hasMoreElements()); + assertEquals(e.nextElement(), "value1D"); + assertFalse(e.hasMoreElements()); + } + + @Test + void testAddQuotedCSV() { + HttpFields fields = new HttpFields(); + + fields.put("some", "value"); + fields.add("name", "\"zero\""); + fields.add("name", "one, \"1 + 1\""); + fields.put("other", "value"); + fields.add("name", "three"); + fields.add("name", "four, I V"); + + List list = fields.getCSV("name", false); + assertEquals("zero", HttpFields.valueParameters(list.get(0), null)); + assertEquals("one", HttpFields.valueParameters(list.get(1), null)); + assertEquals("1 + 1", HttpFields.valueParameters(list.get(2), null)); + assertEquals("three", HttpFields.valueParameters(list.get(3), null)); + assertEquals("four", HttpFields.valueParameters(list.get(4), null)); + assertEquals("I V", HttpFields.valueParameters(list.get(5), null)); + + fields.addCSV("name", "six"); + list = fields.getCSV("name", false); + assertEquals("zero", HttpFields.valueParameters(list.get(0), null)); + assertEquals("one", HttpFields.valueParameters(list.get(1), null)); + assertEquals("1 + 1", HttpFields.valueParameters(list.get(2), null)); + assertEquals("three", HttpFields.valueParameters(list.get(3), null)); + assertEquals("four", HttpFields.valueParameters(list.get(4), null)); + assertEquals("I V", HttpFields.valueParameters(list.get(5), null)); + assertEquals("six", HttpFields.valueParameters(list.get(6), null)); + + fields.addCSV("name", "1 + 1", "7", "zero"); + list = fields.getCSV("name", false); + assertEquals("zero", HttpFields.valueParameters(list.get(0), null)); + assertEquals("one", HttpFields.valueParameters(list.get(1), null)); + assertEquals("1 + 1", HttpFields.valueParameters(list.get(2), null)); + assertEquals("three", HttpFields.valueParameters(list.get(3), null)); + assertEquals("four", HttpFields.valueParameters(list.get(4), null)); + assertEquals("I V", HttpFields.valueParameters(list.get(5), null)); + assertEquals("six", HttpFields.valueParameters(list.get(6), null)); + assertEquals("7", HttpFields.valueParameters(list.get(7), null)); + } + + @Test + void testGetQualityCSV() { + HttpFields fields = new HttpFields(); + + fields.put("some", "value"); + fields.add("name", "zero;q=0.9,four;q=0.1"); + fields.put("other", "value"); + fields.add("name", "nothing;q=0"); + fields.add("name", "one;q=0.4"); + fields.add("name", "three;x=y;q=0.2;a=b,two;q=0.3"); + + List list = fields.getQualityCSV("name"); + assertEquals("zero", HttpFields.valueParameters(list.get(0), null)); + assertEquals("one", HttpFields.valueParameters(list.get(1), null)); + assertEquals("two", HttpFields.valueParameters(list.get(2), null)); + assertEquals("three", HttpFields.valueParameters(list.get(3), null)); + assertEquals("four", HttpFields.valueParameters(list.get(4), null)); + } + + @Test + void testDateFields() { + HttpFields fields = new HttpFields(); + + fields.put("D0", "Wed, 31 Dec 1969 23:59:59 GMT"); + fields.put("D1", "Fri, 31 Dec 1999 23:59:59 GMT"); + fields.put("D2", "Friday, 31-Dec-99 23:59:59 GMT"); + fields.put("D3", "Fri Dec 31 23:59:59 1999"); + fields.put("D4", "Mon Jan 1 2000 00:00:01"); + fields.put("D5", "Tue Feb 29 2000 12:00:00"); + + long d1 = fields.getDateField("D1"); + long d0 = fields.getDateField("D0"); + long d2 = fields.getDateField("D2"); + long d3 = fields.getDateField("D3"); + long d4 = fields.getDateField("D4"); + long d5 = fields.getDateField("D5"); + assertTrue(d0 != -1); + assertTrue(d1 > 0); + assertTrue(d2 > 0); + assertEquals(d1, d2); + assertEquals(d2, d3); + assertEquals(d3 + 2000, d4); + assertEquals(951825600000L, d5); + + d1 = fields.getDateField("D1"); + d2 = fields.getDateField("D2"); + d3 = fields.getDateField("D3"); + d4 = fields.getDateField("D4"); + d5 = fields.getDateField("D5"); + assertTrue(d1 > 0); + assertTrue(d2 > 0); + assertEquals(d1, d2); + assertEquals(d2, d3); + assertEquals(d3 + 2000, d4); + assertEquals(951825600000L, d5); + + fields.putDateField("D2", d1); + assertEquals("Fri, 31 Dec 1999 23:59:59 GMT", fields.get("D2")); + } + + @Test + void testNegDateFields() { + HttpFields fields = new HttpFields(); + + fields.putDateField("Dzero", 0); + assertEquals("Thu, 01 Jan 1970 00:00:00 GMT", fields.get("Dzero")); + + fields.putDateField("Dminus", -1); + assertEquals("Wed, 31 Dec 1969 23:59:59 GMT", fields.get("Dminus")); + + fields.putDateField("Dminus", -1000); + assertEquals("Wed, 31 Dec 1969 23:59:59 GMT", fields.get("Dminus")); + + fields.putDateField("Dancient", Long.MIN_VALUE); + assertEquals("Sun, 02 Dec 55 16:47:04 GMT", fields.get("Dancient")); + } + + @Test + void testLongFields() { + HttpFields header = new HttpFields(); + + header.put("I1", "42"); + header.put("I2", " 43 99"); + header.put("I3", "-44"); + header.put("I4", " - 45abc"); + header.put("N1", " - "); + header.put("N2", "xx"); + + long i1 = header.getLongField("I1"); + try { + header.getLongField("I2"); + fail(); + } catch (NumberFormatException e) { + assertTrue(true); + } + + long i3 = header.getLongField("I3"); + + try { + header.getLongField("I4"); + fail(); + } catch (NumberFormatException e) { + assertTrue(true); + } + + try { + header.getLongField("N1"); + fail(); + } catch (NumberFormatException e) { + assertTrue(true); + } + + try { + header.getLongField("N2"); + fail(); + } catch (NumberFormatException e) { + assertTrue(true); + } + + assertEquals(42, i1); + assertEquals(-44, i3); + + header.putLongField("I5", 46); + header.putLongField("I6", -47); + assertEquals("46", header.get("I5")); + assertEquals("-47", header.get("I6")); + } + + @Test + void testContains() { + HttpFields header = new HttpFields(); + + header.add("n0", ""); + header.add("n1", ","); + header.add("n2", ",,"); + header.add("N3", "abc"); + header.add("N4", "def"); + header.add("n5", "abc,def,hig"); + header.add("N6", "abc"); + header.add("n6", "def"); + header.add("N6", "hig"); + header.add("n7", "abc , def;q=0.9 , hig"); + header.add("n8", "abc , def;q=0 , hig"); + header.add(HttpHeader.ACCEPT, "abc , def;q=0 , hig"); + + for (int i = 0; i < 8; i++) { + assertTrue(header.containsKey("n" + i)); + assertTrue(header.containsKey("N" + i)); + assertFalse(header.contains("n" + i, "xyz")); + assertEquals(i >= 4, header.contains("n" + i, "def")); + } + + assertTrue(header.contains(new HttpField("N5", "def"))); + assertTrue(header.contains(new HttpField("accept", "abc"))); + assertTrue(header.contains(HttpHeader.ACCEPT, "abc")); + assertFalse(header.contains(new HttpField("N5", "xyz"))); + assertFalse(header.contains(new HttpField("N8", "def"))); + assertFalse(header.contains(HttpHeader.ACCEPT, "def")); + assertFalse(header.contains(HttpHeader.AGE, "abc")); + + assertFalse(header.containsKey("n11")); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpStatusCodeTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpStatusCodeTest.java new file mode 100644 index 000000000..71429076e --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpStatusCodeTest.java @@ -0,0 +1,23 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class HttpStatusCodeTest { + + @Test + void testInvalidGetCode() { + assertNull(HttpStatus.getCode(800), "Invalid code: 800"); + assertNull(HttpStatus.getCode(190), "Invalid code: 190"); + } + + + @Test + void testImATeapot() { + assertEquals("I'm a Teapot", HttpStatus.getMessage(418)); + assertEquals("Expectation Failed", HttpStatus.getMessage(417)); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpURITest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpURITest.java new file mode 100644 index 000000000..476902840 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/HttpURITest.java @@ -0,0 +1,187 @@ +package com.fireflysource.net.http.common.model; + +import com.fireflysource.common.collection.map.MultiMap; +import org.junit.jupiter.api.Test; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class HttpURITest { + + @Test + void testInvalidAddress() { + assertInvalidURI("http://[ffff::1:8080/", "Invalid URL; no closing ']' -- should throw exception"); + assertInvalidURI("**", "only '*', not '**'"); + assertInvalidURI("*/", "only '*', not '*/'"); + } + + private void assertInvalidURI(String invalidURI, String message) { + HttpURI uri = new HttpURI(); + assertThrows(IllegalArgumentException.class, () -> uri.parse(invalidURI), message); + } + + @Test + void testParse() { + HttpURI uri = new HttpURI(); + + uri.parse("*"); + assertNull(uri.getHost()); + assertEquals("*", uri.getPath()); + + uri.parse("/foo/bar"); + assertNull(uri.getHost()); + assertEquals("/foo/bar", uri.getPath()); + + uri.parse("//foo/bar"); + assertEquals("foo", uri.getHost()); + assertEquals("/bar", uri.getPath()); + + uri.parse("http://foo/bar"); + assertEquals("foo", uri.getHost()); + assertEquals("/bar", uri.getPath()); + } + + @Test + void testParseRequestTarget() { + HttpURI uri = new HttpURI(); + + uri.parseRequestTarget("GET", "*"); + assertNull(uri.getHost()); + assertEquals("*", uri.getPath()); + + uri.parseRequestTarget("GET", "/foo/bar"); + assertNull(uri.getHost()); + assertEquals("/foo/bar", uri.getPath()); + + uri.parseRequestTarget("GET", "//foo/bar"); + assertNull(uri.getHost()); + assertEquals("//foo/bar", uri.getPath()); + + uri.parseRequestTarget("GET", "http://foo/bar"); + assertEquals("foo", uri.getHost()); + assertEquals("/bar", uri.getPath()); + } + + @Test + void testExtB() throws Exception { + for (String value : new String[]{"a", "abcdABCD", "\u00C0", "\u697C", "\uD869\uDED5", "\uD840\uDC08"}) { + HttpURI uri = new HttpURI("/path?value=" + URLEncoder.encode(value, "UTF-8")); + + MultiMap parameters = new MultiMap<>(); + uri.decodeQueryTo(parameters, StandardCharsets.UTF_8); + assertEquals(value, parameters.getString("value")); + } + } + + @Test + void testAt() { + HttpURI uri = new HttpURI("/@foo/bar"); + assertEquals("/@foo/bar", uri.getPath()); + } + + @Test + void testParams() { + HttpURI uri = new HttpURI("/foo/bar"); + assertEquals("/foo/bar", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + assertNull(uri.getParam()); + + uri = new HttpURI("/foo/bar;jsessionid=12345"); + assertEquals("/foo/bar;jsessionid=12345", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + assertEquals("jsessionid=12345", uri.getParam()); + + uri = new HttpURI("/foo;abc=123/bar;jsessionid=12345"); + assertEquals("/foo;abc=123/bar;jsessionid=12345", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + assertEquals("jsessionid=12345", uri.getParam()); + + uri = new HttpURI("/foo;abc=123/bar;jsessionid=12345?name=value"); + assertEquals("/foo;abc=123/bar;jsessionid=12345", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + assertEquals("jsessionid=12345", uri.getParam()); + + uri = new HttpURI("/foo;abc=123/bar;jsessionid=12345#target"); + assertEquals("/foo;abc=123/bar;jsessionid=12345", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + assertEquals("jsessionid=12345", uri.getParam()); + + uri = new HttpURI("/trainingCamp/poster.jpeg;jsessionid=12345?id=420"); + assertEquals("/trainingCamp/poster.jpeg;jsessionid=12345", uri.getPath()); + assertEquals("jsessionid=12345", uri.getParam()); + assertEquals("/trainingCamp/poster.jpeg", uri.getDecodedPath()); + } + + @Test + void testMutableURI() { + HttpURI uri = new HttpURI("/foo/bar"); + assertEquals("/foo/bar", uri.toString()); + assertEquals("/foo/bar", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + + uri.setScheme("http"); + assertEquals("http:/foo/bar", uri.toString()); + assertEquals("/foo/bar", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + + uri.setAuthority("host", 0); + assertEquals("http://host/foo/bar", uri.toString()); + assertEquals("/foo/bar", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + + uri.setAuthority("host", 8888); + assertEquals("http://host:8888/foo/bar", uri.toString()); + assertEquals("/foo/bar", uri.getPath()); + assertEquals("/foo/bar", uri.getDecodedPath()); + + uri.setPathQuery("/f%30%30;p0/bar;p1;p2"); + assertEquals("http://host:8888/f%30%30;p0/bar;p1;p2", uri.toString()); + assertEquals("/f%30%30;p0/bar;p1;p2", uri.getPath()); + assertEquals("/f00/bar", uri.getDecodedPath()); + assertEquals("p2", uri.getParam()); + assertEquals(null, uri.getQuery()); + + uri.setPathQuery("/f%30%30;p0/bar;p1;p2?name=value"); + assertEquals("http://host:8888/f%30%30;p0/bar;p1;p2?name=value", uri.toString()); + assertEquals("/f%30%30;p0/bar;p1;p2", uri.getPath()); + assertEquals("/f00/bar", uri.getDecodedPath()); + assertEquals("p2", uri.getParam()); + assertEquals("name=value", uri.getQuery()); + + uri.setQuery("other=123456"); + assertEquals("http://host:8888/f%30%30;p0/bar;p1;p2?other=123456", uri.toString()); + assertEquals("/f%30%30;p0/bar;p1;p2", uri.getPath()); + assertEquals("/f00/bar", uri.getDecodedPath()); + assertEquals("p2", uri.getParam()); + assertEquals("other=123456", uri.getQuery()); + } + + @Test + void testSchemeAndOrAuthority() { + HttpURI uri = new HttpURI("/path/info"); + assertEquals("/path/info", uri.toString()); + + uri.setAuthority("host", 0); + assertEquals("//host/path/info", uri.toString()); + + uri.setAuthority("host", 8888); + assertEquals("//host:8888/path/info", uri.toString()); + + uri.setScheme("http"); + assertEquals("http://host:8888/path/info", uri.toString()); + + uri.setAuthority(null, 0); + assertEquals("http:/path/info", uri.toString()); + + } + + @Test + void testBasicAuthCredentials() { + HttpURI uri = new HttpURI("http://user:password@example.com:8888/blah"); + assertEquals("http://user:password@example.com:8888/blah", uri.toString()); + assertEquals("example.com:8888", uri.getAuthority()); + assertEquals("user:password", uri.getUser()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/MimeTypesTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/MimeTypesTest.java new file mode 100644 index 000000000..8bbb0f1c1 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/MimeTypesTest.java @@ -0,0 +1,155 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MimeTypesTest { + + @Test + void testGetMimeByExtension_Gzip() { + assertMimeTypeByExtension("application/gzip", "test.gz"); + } + + @Test + void testGetMimeByExtension_Png() { + assertMimeTypeByExtension("image/png", "test.png"); + assertMimeTypeByExtension("image/png", "TEST.PNG"); + assertMimeTypeByExtension("image/png", "Test.Png"); + } + + @Test + void testGetMimeByExtension_Png_MultiDot() { + assertMimeTypeByExtension("image/png", "com.fireflysource.Logo.png"); + } + + @Test + void testGetMimeByExtension_Png_DeepPath() { + assertMimeTypeByExtension("image/png", "/com/fireflysource/Logo.png"); + } + + @Test + void testGetMimeByExtension_Text() { + assertMimeTypeByExtension("text/plain", "test.txt"); + assertMimeTypeByExtension("text/plain", "TEST.TXT"); + } + + @Test + void testGetMimeByExtension_NoExtension() { + MimeTypes mimetypes = new MimeTypes(); + String contentType = mimetypes.getMimeByExtension("README"); + assertNull(contentType); + } + + private void assertMimeTypeByExtension(String expectedMimeType, String filename) { + MimeTypes mimetypes = new MimeTypes(); + String contentType = mimetypes.getMimeByExtension(filename); + assertNotNull(contentType); + assertEquals(expectedMimeType, contentType); + } + + private void assertCharsetFromContentType(String contentType, String expectedCharset) { + assertEquals(expectedCharset, MimeTypes.getCharsetFromContentType(contentType)); + } + + @Test + void testCharsetFromContentType() { + assertCharsetFromContentType("foo/bar;charset=abc;some=else", "abc"); + assertCharsetFromContentType("foo/bar;charset=abc", "abc"); + assertCharsetFromContentType("foo/bar ; charset = abc", "abc"); + assertCharsetFromContentType("foo/bar ; charset = abc ; some=else", "abc"); + assertCharsetFromContentType("foo/bar;other=param;charset=abc;some=else", "abc"); + assertCharsetFromContentType("foo/bar;other=param;charset=abc", "abc"); + assertCharsetFromContentType("foo/bar other = param ; charset = abc", "abc"); + assertCharsetFromContentType("foo/bar other = param ; charset = abc ; some=else", "abc"); + assertCharsetFromContentType("foo/bar other = param ; charset = abc", "abc"); + assertCharsetFromContentType("foo/bar other = param ; charset = \"abc\" ; some=else", "abc"); + assertCharsetFromContentType("foo/bar", null); + assertCharsetFromContentType("foo/bar;charset=uTf8", "utf-8"); + assertCharsetFromContentType("foo/bar;other=\"charset=abc\";charset=uTf8", "utf-8"); + assertCharsetFromContentType("application/pdf; charset=UTF-8", "utf-8"); + assertCharsetFromContentType("application/pdf;; charset=UTF-8", "utf-8"); + assertCharsetFromContentType("application/pdf;;; charset=UTF-8", "utf-8"); + assertCharsetFromContentType("application/pdf;;;; charset=UTF-8", "utf-8"); + assertCharsetFromContentType("text/html;charset=utf-8", "utf-8"); + } + + @Test + void testContentTypeWithoutCharset() { + assertEquals("foo/bar;some=else", MimeTypes.getContentTypeWithoutCharset("foo/bar;charset=abc;some=else")); + assertEquals("foo/bar", MimeTypes.getContentTypeWithoutCharset("foo/bar;charset=abc")); + assertEquals("foo/bar", MimeTypes.getContentTypeWithoutCharset("foo/bar ; charset = abc")); + assertEquals("foo/bar;some=else", MimeTypes.getContentTypeWithoutCharset("foo/bar ; charset = abc ; some=else")); + assertEquals("foo/bar;other=param;some=else", MimeTypes.getContentTypeWithoutCharset("foo/bar;other=param;charset=abc;some=else")); + assertEquals("foo/bar;other=param", MimeTypes.getContentTypeWithoutCharset("foo/bar;other=param;charset=abc")); + assertEquals("foo/bar ; other = param", MimeTypes.getContentTypeWithoutCharset("foo/bar ; other = param ; charset = abc")); + assertEquals("foo/bar ; other = param;some=else", MimeTypes.getContentTypeWithoutCharset("foo/bar ; other = param ; charset = abc ; some=else")); + assertEquals("foo/bar ; other = param", MimeTypes.getContentTypeWithoutCharset("foo/bar ; other = param ; charset = abc")); + assertEquals("foo/bar ; other = param;some=else", MimeTypes.getContentTypeWithoutCharset("foo/bar ; other = param ; charset = \"abc\" ; some=else")); + assertEquals("foo/bar", MimeTypes.getContentTypeWithoutCharset("foo/bar")); + assertEquals("foo/bar", MimeTypes.getContentTypeWithoutCharset("foo/bar;charset=uTf8")); + assertEquals("foo/bar;other=\"charset=abc\"", MimeTypes.getContentTypeWithoutCharset("foo/bar;other=\"charset=abc\";charset=uTf8")); + assertEquals("text/html", MimeTypes.getContentTypeWithoutCharset("text/html;charset=utf-8")); + } + + @Test + void testAcceptMimeTypes() { + List list = MimeTypes.parseAcceptMIMETypes("text/plain; q=0.9, text/html"); + assertEquals(2, list.size()); + assertEquals("text", list.get(0).getParentType()); + assertEquals("html", list.get(0).getChildType()); + assertEquals(1.0F, list.get(0).getQuality()); + assertEquals("text", list.get(1).getParentType()); + assertEquals("plain", list.get(1).getChildType()); + assertEquals(0.9F, list.get(1).getQuality()); + + list = MimeTypes.parseAcceptMIMETypes("text/plain, text/html"); + assertEquals(2, list.size()); + assertEquals("text", list.get(0).getParentType()); + assertEquals("plain", list.get(0).getChildType()); + assertEquals("text", list.get(1).getParentType()); + assertEquals("html", list.get(1).getChildType()); + + list = MimeTypes.parseAcceptMIMETypes("text/plain"); + assertEquals(1, list.size()); + assertEquals("text", list.get(0).getParentType()); + assertEquals("plain", list.get(0).getChildType()); + + list = MimeTypes.parseAcceptMIMETypes("*/*; q=0.8, text/plain; q=0.9, text/html, */json"); + assertEquals(4, list.size()); + + assertEquals("text", list.get(0).getParentType()); + assertEquals("html", list.get(0).getChildType()); + assertEquals(1.0F, list.get(0).getQuality()); + assertEquals(AcceptMIMEMatchType.EXACT, list.get(0).getMatchType()); + + assertEquals("*", list.get(1).getParentType()); + assertEquals("json", list.get(1).getChildType()); + assertEquals(1.0F, list.get(1).getQuality()); + assertEquals(AcceptMIMEMatchType.CHILD, list.get(1).getMatchType()); + + assertEquals("text", list.get(2).getParentType()); + assertEquals("plain", list.get(2).getChildType()); + assertEquals(0.9F, list.get(2).getQuality()); + assertEquals(AcceptMIMEMatchType.EXACT, list.get(2).getMatchType()); + + assertEquals("*", list.get(3).getParentType()); + assertEquals("*", list.get(3).getChildType()); + assertEquals(0.8F, list.get(3).getQuality()); + assertEquals(AcceptMIMEMatchType.ALL, list.get(3).getMatchType()); + + + } + + @Test + @DisplayName("should skip the non quality field") + void testSkipNonQualityField() { + List list = MimeTypes.parseAcceptMIMETypes("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); + assertEquals("application", list.get(5).getParentType()); + assertEquals("signed-exchange", list.get(5).getChildType()); + assertEquals(0.9F, list.get(5).getQuality()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/QuotedCSVTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/QuotedCSVTest.java new file mode 100644 index 000000000..7f65dabeb --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/QuotedCSVTest.java @@ -0,0 +1,114 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class QuotedCSVTest { + + @SafeVarargs + static void assertContains(List src, T... dest) { + Arrays.stream(dest).forEach(e -> assertTrue(src.contains(e))); + } + + @Test + void testOWS() { + QuotedCSV values = new QuotedCSV(); + values.addValue(" value 0.5 ; pqy = vwz ; q =0.5 , value 1.0 , other ; param "); + assertContains(values.getValues(), "value 0.5;pqy=vwz;q=0.5", "value 1.0", "other;param"); + } + + @Test + void testEmpty() { + QuotedCSV values = new QuotedCSV(); + values.addValue(",aaaa, , bbbb ,,cccc,"); + assertContains(values.getValues(), "aaaa", "bbbb", "cccc"); + } + + @Test + void testQuoted() { + QuotedCSV values = new QuotedCSV(); + values.addValue("A;p=\"v\",B,\"C, D\""); + assertContains(values.getValues(), "A;p=\"v\"", "B", "\"C, D\""); + } + + @Test + void testOpenQuote() { + QuotedCSV values = new QuotedCSV(); + values.addValue("value;p=\"v"); + assertContains(values.getValues(), "value;p=\"v"); + } + + @Test + void testQuotedNoQuotes() { + QuotedCSV values = new QuotedCSV(false); + values.addValue("A;p=\"v\",B,\"C, D\""); + assertContains(values.getValues(), "A;p=v", "B", "C, D"); + } + + @Test + void testOpenQuoteNoQuotes() { + QuotedCSV values = new QuotedCSV(false); + values.addValue("value;p=\"v"); + assertContains(values.getValues(), "value;p=v"); + } + + @Test + void testParamsOnly() { + QuotedCSV values = new QuotedCSV(false); + values.addValue("for=192.0.2.43, for=\"[2001:db8:cafe::17]\", for=unknown"); + assertContains(values.getValues(), "for=192.0.2.43", "for=[2001:db8:cafe::17]", "for=unknown"); + } + + @Test + void testMutation() { + QuotedCSV values = new QuotedCSV(false) { + + @Override + protected void parsedValue(StringBuilder buffer) { + if (buffer.toString().contains("DELETE")) { + String s = buffer.toString().replace("DELETE", ""); + buffer.setLength(0); + buffer.append(s); + } + if (buffer.toString().contains("APPEND")) { + String s = buffer.toString().replace("APPEND", "Append") + "!"; + buffer.setLength(0); + buffer.append(s); + } + } + + @Override + protected void parsedParam(StringBuilder buffer, int valueLength, int paramName, int paramValue) { + String name = paramValue > 0 ? buffer.substring(paramName, paramValue - 1) : buffer.substring(paramName); + if ("IGNORE".equals(name)) + buffer.setLength(paramName - 1); + } + + }; + + values.addValue("normal;param=val, testAPPENDandDELETEvalue ; n=v; IGNORE = this; x=y "); + + assertContains(values.getValues(), + "normal;param=val", + "testAppendandvalue!;n=v;x=y"); + } + + + @Test + void testUnQuote() { + assertEquals("", QuotedCSV.unquote("")); + assertEquals("", QuotedCSV.unquote("\"\"")); + assertEquals("foo", QuotedCSV.unquote("foo")); + assertEquals("foo", QuotedCSV.unquote("\"foo\"")); + assertEquals("foo", QuotedCSV.unquote("f\"o\"o")); + assertEquals("\"foo", QuotedCSV.unquote("\"\\\"foo\"")); + assertEquals("\\foo", QuotedCSV.unquote("\\foo")); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/QuotedQualityCSVTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/QuotedQualityCSVTest.java new file mode 100644 index 000000000..ab3fcd253 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/QuotedQualityCSVTest.java @@ -0,0 +1,316 @@ +package com.fireflysource.net.http.common.model; + + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class QuotedQualityCSVTest { + + private static final String[] preferBrotli = {"br", "gzip"}; + private static final String[] preferGzip = {"gzip", "br"}; + private static final String[] noFormats = {}; + + @SafeVarargs + static void assertContains(List src, T... dest) { + Arrays.stream(dest).forEach(e -> assertTrue(src.contains(e))); + } + + @Test + void test7231_5_3_2_example1() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" audio/*; q=0.2, audio/basic"); + assertContains(values.getValues(), "audio/basic", "audio/*"); + } + + @Test + void test7231_5_3_2_example2() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("text/plain; q=0.5, text/html,"); + values.addValue("text/x-dvi; q=0.8, text/x-c"); + assertContains(values.getValues(), "text/html", "text/x-c", "text/x-dvi", "text/plain"); + } + + @Test + void test7231_5_3_2_example3() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("text/*, text/plain, text/plain;format=flowed, */*"); + + // Note this sort is only on quality and not the most specific type as per 5.3.2 + assertContains(values.getValues(), "text/*", "text/plain", "text/plain;format=flowed", "*/*"); + } + + @Test + void test7231_5_3_2_example3_most_specific() { + QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC); + values.addValue("text/*, text/plain, text/plain;format=flowed, */*"); + + assertContains(values.getValues(), "text/plain;format=flowed", "text/plain", "text/*", "*/*"); + } + + @Test + void test7231_5_3_2_example4() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("text/*;q=0.3, text/html;q=0.7, text/html;level=1,"); + values.addValue("text/html;level=2;q=0.4, */*;q=0.5"); + assertContains(values.getValues(), + "text/html;level=1", + "text/html", + "*/*", + "text/html;level=2", + "text/*" + ); + } + + @Test + void test7231_5_3_4_example1() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("compress, gzip"); + values.addValue(""); + values.addValue("*"); + values.addValue("compress;q=0.5, gzip;q=1.0"); + values.addValue("gzip;q=1.0, identity; q=0.5, *;q=0"); + + assertContains(values.getValues(), + "compress", + "gzip", + "*", + "gzip", + "gzip", + "compress", + "identity" + ); + } + + @Test + void testOWS() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" value 0.5 ; p = v ; q =0.5 , value 1.0 "); + assertContains(values.getValues(), + "value 1.0", + "value 0.5;p=v"); + } + + @Test + void testEmpty() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(",aaaa, , bbbb ,,cccc,"); + assertContains(values.getValues(), + "aaaa", + "bbbb", + "cccc"); + } + + @Test + void testQuoted() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" value 0.5 ; p = \"v ; q = \\\"0.5\\\" , value 1.0 \" "); + assertContains(values.getValues(), + "value 0.5;p=\"v ; q = \\\"0.5\\\" , value 1.0 \""); + } + + @Test + void testOpenQuote() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("value;p=\"v"); + assertContains(values.getValues(), + "value;p=\"v"); + } + + /* ------------------------------------------------------------ */ + + @Test + void testQuotedQuality() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue(" value 0.5 ; p = v ; q = \"0.5\" , value 1.0 "); + assertContains(values.getValues(), + "value 1.0", + "value 0.5;p=v"); + } + + @Test + void testBadQuality() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("value0.5;p=v;q=0.5,value1.0,valueBad;q=X"); + assertContains(values.getValues(), + "value1.0", + "value0.5;p=v"); + } + + @Test + void testBad() { + QuotedQualityCSV values = new QuotedQualityCSV(); + + + // None of these should throw exceptions + values.addValue(null); + values.addValue(""); + + values.addValue(";"); + values.addValue("="); + values.addValue(","); + + values.addValue(";;"); + values.addValue(";="); + values.addValue(";,"); + values.addValue("=;"); + values.addValue("=="); + values.addValue("=,"); + values.addValue(",;"); + values.addValue(",="); + values.addValue(",,"); + + values.addValue(";;;"); + values.addValue(";;="); + values.addValue(";;,"); + values.addValue(";=;"); + values.addValue(";=="); + values.addValue(";=,"); + values.addValue(";,;"); + values.addValue(";,="); + values.addValue(";,,"); + + values.addValue("=;;"); + values.addValue("=;="); + values.addValue("=;,"); + values.addValue("==;"); + values.addValue("==="); + values.addValue("==,"); + values.addValue("=,;"); + values.addValue("=,="); + values.addValue("=,,"); + + values.addValue(",;;"); + values.addValue(",;="); + values.addValue(",;,"); + values.addValue(",=;"); + values.addValue(",=="); + values.addValue(",=,"); + values.addValue(",,;"); + values.addValue(",,="); + values.addValue(",,,"); + + values.addValue("x;=1"); + values.addValue("=1"); + values.addValue("q=x"); + values.addValue("q=0"); + values.addValue("q="); + values.addValue("q=,"); + values.addValue("q=;"); + + } + + @Test + void testFirefoxContentEncodingWithBrotliPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli); + values.addValue("gzip, deflate, br"); + assertContains(values.getValues(), "br", "gzip", "deflate"); + } + + @Test + void testFirefoxContentEncodingWithGzipPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferGzip); + values.addValue("gzip, deflate, br"); + assertContains(values.getValues(), "gzip", "br", "deflate"); + } + + @Test + void testFirefoxContentEncodingWithNoPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(noFormats); + values.addValue("gzip, deflate, br"); + assertContains(values.getValues(), "gzip", "deflate", "br"); + } + + @Test + void testChromeContentEncodingWithBrotliPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli); + values.addValue("gzip, deflate, sdch, br"); + assertContains(values.getValues(), "br", "gzip", "deflate", "sdch"); + } + + @Test + void testComplexEncodingWithGzipPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferGzip); + values.addValue("gzip;q=0.9, identity;q=0.1, *;q=0.01, deflate;q=0.9, sdch;q=0.7, br;q=0.9"); + assertContains(values.getValues(), "gzip", "br", "deflate", "sdch", "identity", "*"); + } + + @Test + void testComplexEncodingWithBrotliPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli); + values.addValue("gzip;q=0.9, identity;q=0.1, *;q=0, deflate;q=0.9, sdch;q=0.7, br;q=0.99"); + assertContains(values.getValues(), "br", "gzip", "deflate", "sdch", "identity"); + } + + @Test + void testStarEncodingWithGzipPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferGzip); + values.addValue("br, *"); + assertContains(values.getValues(), "*", "br"); + } + + @Test + void testStarEncodingWithBrotliPreference() { + QuotedQualityCSV values = new QuotedQualityCSV(preferBrotli); + values.addValue("gzip, *"); + assertContains(values.getValues(), "*", "gzip"); + } + + + @Test + void testSameQuality() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("one;q=0.5,two;q=0.5,three;q=0.5"); + assertContains(values.getValues(), "one", "two", "three"); + } + + @Test + void testNoQuality() { + QuotedQualityCSV values = new QuotedQualityCSV(); + values.addValue("one,two;,three;x=y"); + assertContains(values.getValues(), "one", "two", "three;x=y"); + } + + + @Test + void testQuality() { + List results = new ArrayList<>(); + + QuotedQualityCSV values = new QuotedQualityCSV() { + @Override + protected void parsedValue(StringBuilder buffer) { + results.add("parsedValue: " + buffer.toString()); + + super.parsedValue(buffer); + } + + @Override + protected void parsedParam(StringBuilder buffer, int valueLength, int paramName, int paramValue) { + String param = buffer.substring(paramName, buffer.length()); + results.add("parsedParam: " + param); + + super.parsedParam(buffer, valueLength, paramName, paramValue); + } + }; + + + // The provided string is not legal according to some RFCs ( not a token because of = and not a parameter because not preceded by ; ) + // The string is legal according to RFC7239 which allows for just parameters (called forwarded-pairs) + values.addValue("p=0.5,q=0.5"); + + + // The QuotedCSV implementation is lenient and adopts the later interpretation and thus sees q=0.5 and p=0.5 both as parameters + assertContains(results, "parsedValue: ", "parsedParam: p=0.5", + "parsedValue: ", "parsedParam: q=0.5"); + + + // However the QuotedQualityCSV only handles the q parameter and that is consumed from the parameter string. + assertContains(values.getValues(), "p=0.5", ""); + + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpField.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpField.java new file mode 100644 index 000000000..834bd0c2a --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpField.java @@ -0,0 +1,143 @@ +package com.fireflysource.net.http.common.model; + + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + + +class TestHttpField { + + @Test + void testContainsSimple() { + HttpField field = new HttpField("name", "SomeValue"); + assertTrue(field.contains("somevalue")); + assertTrue(field.contains("sOmEvAlUe")); + assertTrue(field.contains("SomeValue")); + assertFalse(field.contains("other")); + assertFalse(field.contains("some")); + assertFalse(field.contains("Some")); + assertFalse(field.contains("value")); + assertFalse(field.contains("v")); + assertFalse(field.contains("")); + assertFalse(field.contains(null)); + + field = new HttpField(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings"); + assertTrue(field.contains("Upgrade")); + assertTrue(field.contains("HTTP2-Settings")); + } + + @Test + void testCaseInsensitiveHashcode_KnownField() { + HttpField fieldFoo1 = new HttpField("Cookie", "foo"); + HttpField fieldFoo2 = new HttpField("cookie", "foo"); + + assertEquals(fieldFoo2.hashCode(), fieldFoo1.hashCode()); + } + + @Test + void testCaseInsensitiveHashcode_UnknownField() { + HttpField fieldFoo1 = new HttpField("X-Foo", "bar"); + HttpField fieldFoo2 = new HttpField("x-foo", "bar"); + + assertEquals(fieldFoo2.hashCode(), fieldFoo1.hashCode()); + } + + @Test + void testContainsList() { + HttpField field = new HttpField("name", ",aaa,Bbb,CCC, ddd , e e, \"\\\"f,f\\\"\", "); + assertTrue(field.contains("aaa")); + assertTrue(field.contains("bbb")); + assertTrue(field.contains("ccc")); + assertTrue(field.contains("Aaa")); + assertTrue(field.contains("Bbb")); + assertTrue(field.contains("Ccc")); + assertTrue(field.contains("AAA")); + assertTrue(field.contains("BBB")); + assertTrue(field.contains("CCC")); + assertTrue(field.contains("ddd")); + assertTrue(field.contains("e e")); + assertTrue(field.contains("\"f,f\"")); + assertFalse(field.contains("")); + assertFalse(field.contains("aa")); + assertFalse(field.contains("bb")); + assertFalse(field.contains("cc")); + assertFalse(field.contains(null)); + } + + @Test + void testQualityContainsList() { + HttpField field; + + field = new HttpField("name", "yes"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", ",yes,"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "other,yes,other"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "other, yes ,other"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "other, y s ,other"); + assertTrue(field.contains("y s")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "other, \"yes\" ,other"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "other, \"\\\"yes\\\"\" ,other"); + assertTrue(field.contains("\"yes\"")); + assertFalse(field.contains("no")); + + field = new HttpField("name", ";no,yes,;no"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "no;q=0,yes;q=1,no; q = 0"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "no;q=0.0000,yes;q=0.0001,no; q = 0.00000"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + field = new HttpField("name", "no;q=0.0000,Yes;Q=0.0001,no; Q = 0.00000"); + assertTrue(field.contains("yes")); + assertFalse(field.contains("no")); + + } + + @Test + void testValues() { + String[] values = new HttpField("name", "value").getValues(); + assertEquals(1, values.length); + assertEquals("value", values[0]); + + values = new HttpField("name", "a,b,c").getValues(); + assertEquals(3, values.length); + assertEquals("a", values[0]); + assertEquals("b", values[1]); + assertEquals("c", values[2]); + + values = new HttpField("name", "a,\"x,y,z\",c").getValues(); + assertEquals(3, values.length); + assertEquals("a", values[0]); + assertEquals("x,y,z", values[1]); + assertEquals("c", values[2]); + + values = new HttpField("name", "a,\"x,\\\"p,q\\\",z\",c").getValues(); + assertEquals(3, values.length); + assertEquals("a", values[0]); + assertEquals("x,\"p,q\",z", values[1]); + assertEquals("c", values[2]); + + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpMethod.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpMethod.java new file mode 100644 index 000000000..e054659d3 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpMethod.java @@ -0,0 +1,30 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author Pengtao Qiu + */ +class TestHttpMethod { + + static Stream testParametersProvider() { + return Arrays.stream(HttpMethod.values()).map(m -> arguments(m, m.getValue())); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void test(HttpMethod method, String name) { + assertEquals(method, HttpMethod.from(name)); + assertTrue(method.is(name.toLowerCase())); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpScheme.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpScheme.java new file mode 100644 index 000000000..3dd7d2599 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpScheme.java @@ -0,0 +1,30 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author Pengtao Qiu + */ +class TestHttpScheme { + + static Stream testParametersProvider() { + return Arrays.stream(HttpScheme.values()).map(m -> arguments(m, m.getValue())); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void test(HttpScheme scheme, String name) { + assertEquals(scheme, HttpScheme.from(name)); + assertTrue(scheme.is(name.toLowerCase())); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpVersion.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpVersion.java new file mode 100644 index 000000000..ea4c7504a --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/model/TestHttpVersion.java @@ -0,0 +1,29 @@ +package com.fireflysource.net.http.common.model; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author Pengtao Qiu + */ +class TestHttpVersion { + + static Stream testParametersProvider() { + return Arrays.stream(HttpVersion.values()).map(m -> arguments(m, m.getValue())); + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + void test(HttpVersion version, String name) { + assertEquals(version, HttpVersion.from(name)); + assertTrue(version.is(name.toLowerCase())); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/decoder/HttpParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/decoder/HttpParserTest.java new file mode 100644 index 000000000..41b9a05d9 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/decoder/HttpParserTest.java @@ -0,0 +1,2314 @@ +package com.fireflysource.net.http.common.v1.decoder; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.exception.BadMessageException; +import com.fireflysource.net.http.common.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.fireflysource.net.http.common.model.HttpComplianceSection.NO_FIELD_FOLDING; +import static com.fireflysource.net.http.common.model.HttpHeader.EXPECT; +import static com.fireflysource.net.http.common.model.HttpHeaderValue.CONTINUE; +import static com.fireflysource.net.http.common.v1.decoder.HttpParser.State; +import static org.junit.jupiter.api.Assertions.*; + +@SuppressWarnings("deprecation") +class HttpParserTest { + static { + HttpCompliance.CUSTOM0.sections().remove(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME); + } + + private final List complianceViolation = new ArrayList<>(); + private String host; + private int port; + private String bad; + private String content; + private String methodOrVersion; + private String uriOrStatus; + private String versionOrReason; + private List fields = new ArrayList<>(); + private List trailers = new ArrayList<>(); + private String[] hdr; + private String[] val; + private int headers; + private boolean early; + private boolean headerCompleted; + private boolean messageCompleted; + + /** + * Parse until {@link State#END} state. + * If the parser is already in the END state, then it is {@link HttpParser#reset()} and re-parsed. + * + * @param parser The parser to test + * @param buffer the buffer to parse + * @throws IllegalStateException If the buffers have already been partially parsed. + */ + static void parseAll(HttpParser parser, ByteBuffer buffer) { + if (parser.isState(State.END)) + parser.reset(); + if (!parser.isState(State.START)) + throw new IllegalStateException("!START"); + + // continue parsing + int remaining = buffer.remaining(); + while (!parser.isState(State.END) && remaining > 0) { + int was_remaining = remaining; + parser.parseNext(buffer); + remaining = buffer.remaining(); + if (remaining == was_remaining) + break; + } + } + + @Test + void HttpMethodTest() { + assertNull(HttpMethod.lookAheadGet(BufferUtils.toBuffer("Wibble "))); + assertNull(HttpMethod.lookAheadGet(BufferUtils.toBuffer("GET"))); + assertNull(HttpMethod.lookAheadGet(BufferUtils.toBuffer("MO"))); + + assertEquals(HttpMethod.GET, HttpMethod.lookAheadGet(BufferUtils.toBuffer("GET "))); + assertEquals(HttpMethod.MOVE, HttpMethod.lookAheadGet(BufferUtils.toBuffer("MOVE "))); + + ByteBuffer b = BufferUtils.allocateDirect(128); + BufferUtils.append(b, BufferUtils.toBuffer("GET")); + assertNull(HttpMethod.lookAheadGet(b)); + + BufferUtils.append(b, BufferUtils.toBuffer(" ")); + assertEquals(HttpMethod.GET, HttpMethod.lookAheadGet(b)); + } + + @Test + void testLineParse_Mock_IP() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /mock/127.0.0.1 HTTP/1.1\r\n" + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("POST", methodOrVersion); + assertEquals("/mock/127.0.0.1", uriOrStatus); + assertEquals("HTTP/1.1", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testLineParse0() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /foo HTTP/1.0\r\n" + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("POST", methodOrVersion); + assertEquals("/foo", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testLineParse1_RFC2616() { + ByteBuffer buffer = BufferUtils.toBuffer("GET /999\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + parseAll(parser, buffer); + + assertNull(bad); + assertEquals("GET", methodOrVersion); + assertEquals("/999", uriOrStatus); + assertEquals("HTTP/0.9", versionOrReason); + assertEquals(-1, headers); + assertTrue(complianceViolation.contains(HttpComplianceSection.NO_HTTP_0_9)); + } + + @Test + void testLineParse1() { + ByteBuffer buffer = BufferUtils.toBuffer("GET /999\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("HTTP/0.9 not supported", bad); + assertTrue(complianceViolation.isEmpty()); + } + + @Test + void testLineParse2_RFC2616() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /222 \r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + parseAll(parser, buffer); + + assertNull(bad); + assertEquals("POST", methodOrVersion); + assertEquals("/222", uriOrStatus); + assertEquals("HTTP/0.9", versionOrReason); + assertEquals(-1, headers); + assertTrue(complianceViolation.contains(HttpComplianceSection.NO_HTTP_0_9)); + } + + @Test + void testLineParse2() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /222 \r\n"); + + versionOrReason = null; + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("HTTP/0.9 not supported", bad); + assertTrue(complianceViolation.isEmpty()); + } + + @Test + void testLineParse3() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /fo\u0690 HTTP/1.0\r\n" + "\r\n", StandardCharsets.UTF_8); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("POST", methodOrVersion); + assertEquals("/fo\u0690", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testLineParse4() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /foo?param=\u0690 HTTP/1.0\r\n" + "\r\n", StandardCharsets.UTF_8); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("POST", methodOrVersion); + assertEquals("/foo?param=\u0690", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testLongURLParse() { + ByteBuffer buffer = BufferUtils.toBuffer("POST /123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/ HTTP/1.0\r\n" + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("POST", methodOrVersion); + assertEquals("/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testAllowedLinePreamble() { + ByteBuffer buffer = BufferUtils.toBuffer("\r\n\r\nGET / HTTP/1.0\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testDisallowedLinePreamble() { + ByteBuffer buffer = BufferUtils.toBuffer("\r\n \r\nGET / HTTP/1.0\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("Illegal character SPACE=' '", bad); + } + + @Test + void testConnect() { + ByteBuffer buffer = BufferUtils.toBuffer("CONNECT 192.168.1.2:80 HTTP/1.1\r\n" + "\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("CONNECT", methodOrVersion); + assertEquals("192.168.1.2:80", uriOrStatus); + assertEquals("HTTP/1.1", versionOrReason); + assertEquals(-1, headers); + } + + @Test + void testSimple() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Connection", hdr[1]); + assertEquals("close", val[1]); + assertEquals(1, headers); + } + + @Test + void testFoldedField2616() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name: value\r\n" + + " extra\r\n" + + "Name2: \r\n" + + "\tvalue2\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + parseAll(parser, buffer); + + assertNull(bad); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals(2, headers); + assertEquals("Name", hdr[1]); + assertEquals("value extra", val[1]); + assertEquals("Name2", hdr[2]); + assertEquals("value2", val[2]); + Arrays.asList(NO_FIELD_FOLDING, NO_FIELD_FOLDING) + .forEach(e -> assertTrue(complianceViolation.contains(e))); + } + + @Test + void testFoldedField7230() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name: value\r\n" + + " extra\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY); + parseAll(parser, buffer); + + assertNotNull(bad); + assertTrue(bad.contains("Header Folding")); + assertTrue(complianceViolation.isEmpty()); + } + + @Test + void testWhiteSpaceInName() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "N ame: value\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY); + parseAll(parser, buffer); + + assertNotNull(bad); + assertTrue(bad.contains("Illegal character")); + } + + @Test + void testWhiteSpaceAfterName() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name : value\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, 4096, HttpCompliance.RFC7230_LEGACY); + parseAll(parser, buffer); + + assertNotNull(bad); + assertTrue(bad.contains("Illegal character")); + } + + @Test + // TODO: Parameterize Test + void testWhiteSpaceBeforeRequest() { + HttpCompliance[] complianceArray = { + HttpCompliance.RFC7230, HttpCompliance.RFC2616 + }; + + String[][] whitespaces = { + {" ", "Illegal character SPACE"}, + {"\t", "Illegal character HTAB"}, + {"\n", null}, + {"\r", "Bad EOL"}, + {"\r\n", null}, + {"\r\n\r\n", null}, + {"\r\n \r\n", "Illegal character SPACE"}, + {"\r\n\t\r\n", "Illegal character HTAB"}, + {"\r\t\n", "Bad EOL"}, + {"\r\r\n", "Bad EOL"}, + {"\t\r\t\r\n", "Illegal character HTAB"}, + {" \t \r \t \n\n", "Illegal character SPACE"}, + {" \r \t \r\n\r\n\r\n", "Illegal character SPACE"} + }; + + + for (HttpCompliance compliance : complianceArray) { + for (int j = 0; j < whitespaces.length; j++) { + String request = + whitespaces[j][0] + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Name: value" + j + "\r\n" + + "Connection: close\r\n" + + "\r\n"; + + ByteBuffer buffer = BufferUtils.toBuffer(request); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, 4096, compliance); + bad = null; + parseAll(parser, buffer); + + String test = "whitespace.[" + compliance + "].[" + j + "]"; + String expected = whitespaces[j][1]; + if (expected == null) + assertNull(bad, test); + else + assertTrue(bad.contains(expected), test); + } + } + } + + @Test + void testNoValue() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name0: \r\n" + + "Name1:\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Name0", hdr[1]); + assertEquals("", val[1]); + assertEquals("Name1", hdr[2]); + assertEquals("", val[2]); + assertEquals(2, headers); + } + + @Test + void testSpaceInNameCustom0() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name with space: value\r\n" + + "Other: value\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.CUSTOM0); + parseAll(parser, buffer); + + assertTrue(bad.contains("Illegal character")); + assertTrue(complianceViolation.contains(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME)); + } + + @Test + void testNoColonCustom0() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name \r\n" + + "Other: value\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.CUSTOM0); + parseAll(parser, buffer); + + assertTrue(bad.contains("Illegal character")); + assertTrue(complianceViolation.contains(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME)); + } + + @Test + void testTrailingSpacesInHeaderNameInCustom0Mode() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 204 No Content\r\n" + + "Access-Control-Allow-Headers : Origin\r\n" + + "Other\t : value\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, -1, HttpCompliance.CUSTOM0); + parseAll(parser, buffer); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("204", uriOrStatus); + assertEquals("No Content", versionOrReason); + assertNull(content); + + assertEquals(1, headers); + System.out.println(Arrays.asList(hdr)); + System.out.println(Arrays.asList(val)); + assertEquals("Access-Control-Allow-Headers", hdr[0]); + assertEquals("Origin", val[0]); + assertEquals("Other", hdr[1]); + assertEquals("value", val[1]); + + Arrays.asList(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME, HttpComplianceSection.NO_WS_AFTER_FIELD_NAME) + .forEach(e -> assertTrue(complianceViolation.contains(e))); + } + + @Test + void testTrailingSpacesInHeaderNameNoCustom0() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 204 No Content\r\n" + + "Access-Control-Allow-Headers : Origin\r\n" + + "Other: value\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("204", uriOrStatus); + assertEquals("No Content", versionOrReason); + assertTrue(bad.contains("Illegal character ")); + } + + @Test + void testNoColon7230() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Name\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC7230_LEGACY); + parseAll(parser, buffer); + assertTrue(bad.contains("Illegal character")); + assertTrue(complianceViolation.isEmpty()); + } + + @Test + void testHeaderParseDirect() { + ByteBuffer b0 = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Header1: value1\r\n" + + "Header2: value 2a \r\n" + + "Header3: 3\r\n" + + "Header4:value4\r\n" + + "Server5: notServer\r\n" + + "HostHeader: notHost\r\n" + + "Connection: close\r\n" + + "Accept-Encoding: gzip, deflated\r\n" + + "Accept: unknown\r\n" + + "\r\n"); + ByteBuffer buffer = BufferUtils.allocateDirect(b0.capacity()); + int pos = BufferUtils.flipToFill(buffer); + BufferUtils.put(b0, buffer); + BufferUtils.flipToFlush(buffer, pos); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Header1", hdr[1]); + assertEquals("value1", val[1]); + assertEquals("Header2", hdr[2]); + assertEquals("value 2a", val[2]); + assertEquals("Header3", hdr[3]); + assertEquals("3", val[3]); + assertEquals("Header4", hdr[4]); + assertEquals("value4", val[4]); + assertEquals("Server5", hdr[5]); + assertEquals("notServer", val[5]); + assertEquals("HostHeader", hdr[6]); + assertEquals("notHost", val[6]); + assertEquals("Connection", hdr[7]); + assertEquals("close", val[7]); + assertEquals("Accept-Encoding", hdr[8]); + assertEquals("gzip, deflated", val[8]); + assertEquals("Accept", hdr[9]); + assertEquals("unknown", val[9]); + assertEquals(9, headers); + } + + @Test + void testHeaderParseCRLF() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Header1: value1\r\n" + + "Header2: value 2a \r\n" + + "Header3: 3\r\n" + + "Header4:value4\r\n" + + "Server5: notServer\r\n" + + "HostHeader: notHost\r\n" + + "Connection: close\r\n" + + "Accept-Encoding: gzip, deflated\r\n" + + "Accept: unknown\r\n" + + "\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Header1", hdr[1]); + assertEquals("value1", val[1]); + assertEquals("Header2", hdr[2]); + assertEquals("value 2a", val[2]); + assertEquals("Header3", hdr[3]); + assertEquals("3", val[3]); + assertEquals("Header4", hdr[4]); + assertEquals("value4", val[4]); + assertEquals("Server5", hdr[5]); + assertEquals("notServer", val[5]); + assertEquals("HostHeader", hdr[6]); + assertEquals("notHost", val[6]); + assertEquals("Connection", hdr[7]); + assertEquals("close", val[7]); + assertEquals("Accept-Encoding", hdr[8]); + assertEquals("gzip, deflated", val[8]); + assertEquals("Accept", hdr[9]); + assertEquals("unknown", val[9]); + assertEquals(9, headers); + } + + @Test + void testHeaderParseLF() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\n" + + "Host: localhost\n" + + "Header1: value1\n" + + "Header2: value 2a value 2b \n" + + "Header3: 3\n" + + "Header4:value4\n" + + "Server5: notServer\n" + + "HostHeader: notHost\n" + + "Connection: close\n" + + "Accept-Encoding: gzip, deflated\n" + + "Accept: unknown\n" + + "\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Header1", hdr[1]); + assertEquals("value1", val[1]); + assertEquals("Header2", hdr[2]); + assertEquals("value 2a value 2b", val[2]); + assertEquals("Header3", hdr[3]); + assertEquals("3", val[3]); + assertEquals("Header4", hdr[4]); + assertEquals("value4", val[4]); + assertEquals("Server5", hdr[5]); + assertEquals("notServer", val[5]); + assertEquals("HostHeader", hdr[6]); + assertEquals("notHost", val[6]); + assertEquals("Connection", hdr[7]); + assertEquals("close", val[7]); + assertEquals("Accept-Encoding", hdr[8]); + assertEquals("gzip, deflated", val[8]); + assertEquals("Accept", hdr[9]); + assertEquals("unknown", val[9]); + assertEquals(9, headers); + } + + @Test + void testQuoted() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\n" + + "Name0: \"value0\"\t\n" + + "Name1: \"value\t1\"\n" + + "Name2: \"value\t2A\",\"value,2B\"\t\n" + + "\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Name0", hdr[0]); + assertEquals("\"value0\"", val[0]); + assertEquals("Name1", hdr[1]); + assertEquals("\"value\t1\"", val[1]); + assertEquals("Name2", hdr[2]); + assertEquals("\"value\t2A\",\"value,2B\"", val[2]); + assertEquals(2, headers); + } + + @Test + void testEncodedHeader() { + ByteBuffer buffer = BufferUtils.allocate(4096); + BufferUtils.flipToFill(buffer); + BufferUtils.put(BufferUtils.toBuffer("GET "), buffer); + buffer.put("/foo/\u0690/".getBytes(StandardCharsets.UTF_8)); + BufferUtils.put(BufferUtils.toBuffer(" HTTP/1.0\r\n"), buffer); + BufferUtils.put(BufferUtils.toBuffer("Header1: "), buffer); + buffer.put("\u00e6 \u00e6".getBytes(StandardCharsets.ISO_8859_1)); + BufferUtils.put(BufferUtils.toBuffer(" \r\nHeader2: "), buffer); + buffer.put((byte) -1); + BufferUtils.put(BufferUtils.toBuffer("\r\n\r\n"), buffer); + BufferUtils.flipToFlush(buffer, 0); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/foo/\u0690/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Header1", hdr[0]); + assertEquals("\u00e6 \u00e6", val[0]); + assertEquals("Header2", hdr[1]); + assertEquals("" + (char) 255, val[1]); + assertEquals(1, headers); + assertNull(bad); + } + + @Test + void testResponseBufferUpgradeFrom() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 101 Upgrade\r\n" + + "Connection: upgrade\r\n" + + "Content-Length: 0\r\n" + + "Sec-WebSocket-Accept: 4GnyoUP4Sc1JD+2pCbNYAhFYVVA\r\n" + + "\r\n" + + "FOOGRADE"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + System.out.println(buffer.remaining()); + + while (!parser.isState(State.END)) { + parser.parseNext(buffer); + } + System.out.println(buffer.remaining()); + + assertEquals("FOOGRADE", BufferUtils.toUTF8String(buffer)); + } + + @Test + void testBadMethodEncoding() { + ByteBuffer buffer = BufferUtils.toBuffer( + "G\u00e6T / HTTP/1.0\r\nHeader0: value0\r\n\n\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertNotNull(bad); + } + + @Test + void testBadVersionEncoding() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / H\u00e6P/1.0\r\nHeader0: value0\r\n\n\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertNotNull(bad); + } + + @Test + void testBadHeaderEncoding() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "H\u00e6der0: value0\r\n" + + "\n\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertNotNull(bad); + } + + @Test + // TODO: Parameterize Test + void testBadHeaderNames() { + String[] bad = new String[] + { + "Foo\\Bar: value\r\n", + "Foo@Bar: value\r\n", + "Foo,Bar: value\r\n", + "Foo}Bar: value\r\n", + "Foo{Bar: value\r\n", + "Foo=Bar: value\r\n", + "Foo>Bar: value\r\n", + "Foo assertTrue(complianceViolation.contains(e))); + + } + + @Test + void testSplitHeaderParse() { + ByteBuffer buffer = BufferUtils.toBuffer( + "XXXXSPLIT / HTTP/1.0\r\n" + + "Host: localhost\r\n" + + "Header1: value1\r\n" + + "Header2: value 2a \r\n" + + "Header3: 3\r\n" + + "Header4:value4\r\n" + + "Server5: notServer\r\n" + + "\r\nZZZZ"); + buffer.position(2); + buffer.limit(buffer.capacity() - 2); + buffer = buffer.slice(); + + for (int i = 0; i < buffer.capacity() - 4; i++) { + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + buffer.position(2); + buffer.limit(2 + i); + + if (!parser.parseNext(buffer)) { + // consumed all + assertEquals(0, buffer.remaining()); + + // parse the rest + buffer.limit(buffer.capacity() - 2); + parser.parseNext(buffer); + } + + assertEquals("SPLIT", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Header1", hdr[1]); + assertEquals("value1", val[1]); + assertEquals("Header2", hdr[2]); + assertEquals("value 2a", val[2]); + assertEquals("Header3", hdr[3]); + assertEquals("3", val[3]); + assertEquals("Header4", hdr[4]); + assertEquals("value4", val[4]); + assertEquals("Server5", hdr[5]); + assertEquals("notServer", val[5]); + assertEquals(5, headers); + } + } + + @Test + void testChunkParse() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + "\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(1, headers); + assertEquals("Header1", hdr[0]); + assertEquals("value1", val[0]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testBadChunkParse() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked, identity\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + "\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertTrue(bad.contains("Bad chunking")); + } + + @Test + void testChunkParseTrailer() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + "Trailer: value\r\n" + + "\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(1, headers); + assertEquals("Header1", hdr[0]); + assertEquals("value1", val[0]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + assertEquals(1, trailers.size()); + HttpField trailer1 = trailers.get(0); + assertEquals("Trailer", trailer1.getName()); + assertEquals("value", trailer1.getValue()); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testChunkParseTrailers() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + "Trailer: value\r\n" + + "Foo: bar\r\n" + + "\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(0, headers); + assertEquals("Transfer-Encoding", hdr[0]); + assertEquals("chunked", val[0]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + assertEquals(2, trailers.size()); + HttpField trailer1 = trailers.get(0); + assertEquals("Trailer", trailer1.getName()); + assertEquals("value", trailer1.getValue()); + HttpField trailer2 = trailers.get(1); + assertEquals("Foo", trailer2.getName()); + assertEquals("bar", trailer2.getValue()); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testChunkParseBadTrailer() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + "Trailer: value"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(1, headers); + assertEquals("Header1", hdr[0]); + assertEquals("value1", val[0]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + + assertTrue(headerCompleted); + assertTrue(early); + assertFalse(messageCompleted); + } + + @Test + void testChunkParseNoTrailer() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(1, headers); + assertEquals("Header1", hdr[0]); + assertEquals("value1", val[0]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testStartEOF() { + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + + assertTrue(early); + assertNull(bad); + } + + @Test + void testEarlyEOF() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /uri HTTP/1.0\r\n" + + "Content-Length: 20\r\n" + + "\r\n" + + "0123456789"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.atEOF(); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/uri", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals("0123456789", content); + + assertTrue(early); + } + + @Test + void testChunkEarlyEOF() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /chunk HTTP/1.0\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n"); + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.atEOF(); + parseAll(parser, buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(1, headers); + assertEquals("Header1", hdr[0]); + assertEquals("value1", val[0]); + assertEquals("0123456789", content); + + assertTrue(early); + } + + @Test + void testMultiParse() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET /mp HTTP/1.0\r\n" + + "Connection: Keep-Alive\r\n" + + "Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + + "\r\n" + + + "POST /foo HTTP/1.0\r\n" + + "Connection: Keep-Alive\r\n" + + "Header2: value2\r\n" + + "Content-Length: 0\r\n" + + "\r\n" + + + "PUT /doodle HTTP/1.0\r\n" + + "Connection: close\r\n" + + "Header3: value3\r\n" + + "Content-Length: 10\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("GET", methodOrVersion); + assertEquals("/mp", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(2, headers); + assertEquals("Header1", hdr[1]); + assertEquals("value1", val[1]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + + parser.reset(); + init(); + parser.parseNext(buffer); + assertEquals("POST", methodOrVersion); + assertEquals("/foo", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(2, headers); + assertEquals("Header2", hdr[1]); + assertEquals("value2", val[1]); + assertNull(content); + + parser.reset(); + init(); + parser.parseNext(buffer); + parser.atEOF(); + assertEquals("PUT", methodOrVersion); + assertEquals("/doodle", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(2, headers); + assertEquals("Header3", hdr[1]); + assertEquals("value3", val[1]); + assertEquals("0123456789", content); + } + + @Test + void testMultiParseEarlyEOF() { + ByteBuffer buffer0 = BufferUtils.toBuffer( + "GET /mp HTTP/1.0\r\n" + + "Connection: Keep-Alive\r\n"); + + ByteBuffer buffer1 = BufferUtils.toBuffer("Header1: value1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "a;\r\n" + + "0123456789\r\n" + + "1a\r\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + + "0\r\n" + + + "\r\n" + + + "POST /foo HTTP/1.0\r\n" + + "Connection: Keep-Alive\r\n" + + "Header2: value2\r\n" + + "Content-Length: 0\r\n" + + "\r\n" + + + "PUT /doodle HTTP/1.0\r\n" + + "Connection: close\r\n" + + "Header3: value3\r\n" + + "Content-Length: 10\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer0); + parser.atEOF(); + parser.parseNext(buffer1); + assertEquals("GET", methodOrVersion); + assertEquals("/mp", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(2, headers); + assertEquals("Header1", hdr[1]); + assertEquals("value1", val[1]); + assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", content); + + parser.reset(); + init(); + parser.parseNext(buffer1); + assertEquals("POST", methodOrVersion); + assertEquals("/foo", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(2, headers); + assertEquals("Header2", hdr[1]); + assertEquals("value2", val[1]); + assertNull(content); + + parser.reset(); + init(); + parser.parseNext(buffer1); + assertEquals("PUT", methodOrVersion); + assertEquals("/doodle", uriOrStatus); + assertEquals("HTTP/1.0", versionOrReason); + assertEquals(2, headers); + assertEquals("Header3", hdr[1]); + assertEquals("value3", val[1]); + assertEquals("0123456789", content); + } + + @Test + void testResponseParse0() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 200 Correct\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertEquals("Correct", versionOrReason); + assertEquals(10, content.length()); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponseParse1() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 304 Not-Modified\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("304", uriOrStatus); + assertEquals("Not-Modified", versionOrReason); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponseParse2() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 204 No-Content\r\n" + + "Header: value\r\n" + + "\r\n" + + + "HTTP/1.1 200 Correct\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("204", uriOrStatus); + assertEquals("No-Content", versionOrReason); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + + parser.reset(); + init(); + + parser.parseNext(buffer); + parser.atEOF(); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertEquals("Correct", versionOrReason); + assertEquals(content.length(), 10); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponseParse3() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 200\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertNull(versionOrReason); + assertEquals(content.length(), 10); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponseParse4() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 200 \r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertNull(versionOrReason); + assertEquals(content.length(), 10); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponseEOFContent() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 200 \r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "0123456789\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.atEOF(); + parser.parseNext(buffer); + + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertNull(versionOrReason); + assertEquals(12, content.length()); + assertEquals("0123456789\r\n", content); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponse304WithContentLength() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 304 found\r\n" + + "Content-Length: 10\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("304", uriOrStatus); + assertEquals("found", versionOrReason); + assertNull(content); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponse101WithTransferEncoding() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 101 switching protocols\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("101", uriOrStatus); + assertEquals("switching protocols", versionOrReason); + assertNull(content); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @Test + void testResponseReasonIso8859_1() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 302 déplacé temporairement\r\n" + + "Content-Length: 0\r\n" + + "\r\n", StandardCharsets.ISO_8859_1); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("302", uriOrStatus); + assertEquals("déplacé temporairement", versionOrReason); + } + + @Test + void testSeekEOF() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n" + + "\r\n" // extra CRLF ignored + + "HTTP/1.1 400 OK\r\n"); // extra data causes close ?? + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertEquals("OK", versionOrReason); + assertNull(content); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + + parser.close(); + parser.reset(); + parser.parseNext(buffer); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testNoURI() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("No URI", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testNoURI2() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET \r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("No URI", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testUnknownResponseVersion() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HPPT/7.7 200 OK\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("Unknown Version", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + + } + + @Test + void testNoStatus() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("No Status", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testNoStatus2() { + ByteBuffer buffer = BufferUtils.toBuffer( + "HTTP/1.1 \r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("No Status", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testBadRequestVersion() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HPPT/7.7\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("Unknown Version", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + + buffer = BufferUtils.toBuffer( + "GET / HTTP/1.01\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"); + + handler = new Handler(); + parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertNull(methodOrVersion); + assertEquals("Unknown Version", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testBadCR() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Content-Length: 0\r" + + "Connection: close\r" + + "\r"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("Bad EOL", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + + buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r" + + "Content-Length: 0\r" + + "Connection: close\r" + + "\r"); + + handler = new Handler(); + parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("Bad EOL", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testBadContentLength0() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Content-Length: abc\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("GET", methodOrVersion); + assertEquals("Invalid Content-Length Value", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testBadContentLength1() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Content-Length: 9999999999999999999999999999999999999999999999\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("GET", methodOrVersion); + assertEquals("Invalid Content-Length Value", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testBadContentLength2() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.0\r\n" + + "Content-Length: 1.5\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("GET", methodOrVersion); + assertEquals("Invalid Content-Length Value", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testMultipleContentLengthWithLargerThenCorrectValue() { + ByteBuffer buffer = BufferUtils.toBuffer( + "POST / HTTP/1.1\r\n" + + "Content-Length: 2\r\n" + + "Content-Length: 1\r\n" + + "Connection: close\r\n" + + "\r\n" + + "X"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("POST", methodOrVersion); + assertEquals("Multiple Content-Lengths", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testMultipleContentLengthWithCorrectThenLargerValue() { + ByteBuffer buffer = BufferUtils.toBuffer( + "POST / HTTP/1.1\r\n" + + "Content-Length: 1\r\n" + + "Content-Length: 2\r\n" + + "Connection: close\r\n" + + "\r\n" + + "X"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + + parser.parseNext(buffer); + assertEquals("POST", methodOrVersion); + assertEquals("Multiple Content-Lengths", bad); + assertFalse(buffer.hasRemaining()); + assertEquals(HttpParser.State.CLOSE, parser.getState()); + parser.atEOF(); + parser.parseNext(BufferUtils.EMPTY_BUFFER); + assertEquals(HttpParser.State.CLOSED, parser.getState()); + } + + @Test + void testTransferEncodingChunkedThenContentLength() { + ByteBuffer buffer = BufferUtils.toBuffer( + "POST /chunk HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Content-Length: 1\r\n" + + "\r\n" + + "1\r\n" + + "X\r\n" + + "0\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + parseAll(parser, buffer); + + assertEquals("POST", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.1", versionOrReason); + assertEquals("X", content); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + + assertTrue(complianceViolation.contains(HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); + } + + @Test + void testContentLengthThenTransferEncodingChunked() { + ByteBuffer buffer = BufferUtils.toBuffer( + "POST /chunk HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Length: 1\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "1\r\n" + + "X\r\n" + + "0\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY); + parseAll(parser, buffer); + + assertEquals("POST", methodOrVersion); + assertEquals("/chunk", uriOrStatus); + assertEquals("HTTP/1.1", versionOrReason); + assertEquals("X", content); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + + assertTrue(complianceViolation.contains(HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); + } + + @Test + void testHost() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: host\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("host", host); + assertEquals(0, port); + } + + @Test + void testUriHost11() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET http://host/ HTTP/1.1\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("No Host", bad); + assertEquals("http://host/", uriOrStatus); + assertEquals(0, port); + } + + @Test + void testUriHost10() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET http://host/ HTTP/1.0\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertNull(bad); + assertEquals("http://host/", uriOrStatus); + assertEquals(0, port); + } + + @Test + void testNoHost() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("No Host", bad); + } + + @Test + void testIPHost() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: 192.168.0.1\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("192.168.0.1", host); + assertEquals(0, port); + } + + @Test + void testIPv6Host() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: [::1]\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("[::1]", host); + assertEquals(0, port); + } + + @Test + void testBadIPv6Host() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: [::1\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertTrue(bad.contains("Bad")); + } + + @Test + void testHostPort() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: myhost:8888\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("myhost", host); + assertEquals(8888, port); + } + + @Test + void testHostBadPort() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: myhost:testBadPort\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertTrue(bad.contains("Bad Host")); + assertFalse(messageCompleted); + } + + @Test + void testIPHostPort() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: 192.168.0.1:8888\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("192.168.0.1", host); + assertEquals(8888, port); + } + + @Test + void testIPv6HostPort() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: [::1]:8888\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertEquals("[::1]", host); + assertEquals(8888, port); + } + + @Test + void testEmptyHostPort() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host:\r\n" + + "Connection: close\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + assertNull(host); + assertNull(bad); + } + + @Test + void testCachedField() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: www.smh.com.au\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + assertEquals("www.smh.com.au", parser.getFieldCache().get("Host: www.smh.com.au").getValue()); + HttpField field = fields.get(0); + + buffer.position(0); + parseAll(parser, buffer); + assertSame(field, fields.get(0)); + } + + @Test + void testParseRequest() { + ByteBuffer buffer = BufferUtils.toBuffer( + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Header1: value1\r\n" + + "Connection: close\r\n" + + "Accept-Encoding: gzip, deflated\r\n" + + "Accept: unknown\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer); + + assertEquals("GET", methodOrVersion); + assertEquals("/", uriOrStatus); + assertEquals("HTTP/1.1", versionOrReason); + assertEquals("Host", hdr[0]); + assertEquals("localhost", val[0]); + assertEquals("Connection", hdr[2]); + assertEquals("close", val[2]); + assertEquals("Accept-Encoding", hdr[3]); + assertEquals("gzip, deflated", val[3]); + assertEquals("Accept", hdr[4]); + assertEquals("unknown", val[4]); + } + + @Test + void testHTTP2Preface() { + ByteBuffer buffer = BufferUtils.toBuffer( + "PRI * HTTP/2.0\r\n" + + "\r\n" + + "SM\r\n" + + "\r\n"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + assertTrue(headerCompleted); + assertTrue(messageCompleted); + assertEquals("PRI", methodOrVersion); + assertEquals("*", uriOrStatus); + assertEquals("HTTP/2.0", versionOrReason); + assertEquals(-1, headers); + assertNull(bad); + } + + @Test + @DisplayName("should parse 100 continue response successfully.") + void testExpect100Continue() { + ByteBuffer buffer1 = BufferUtils.toBuffer("HTTP/1.1 100 Continue\r\n"); + ByteBuffer buffer2 = BufferUtils.toBuffer("HTTP/1.1 200 OK\r\n"); + ByteBuffer buffer3 = BufferUtils.toBuffer("Content-Length: 4\r\n" + + "\r\n" + + "test"); + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + boolean exit = parser.parseNext(buffer1); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("100", uriOrStatus); + assertEquals("Continue", versionOrReason); + assertEquals(State.HEADER, parser.getState()); + assertTrue(exit); + parser.reset(); + + assertEquals(State.START, parser.getState()); + System.out.println(parser.getState()); + + parser.parseNext(buffer2); + assertEquals("HTTP/1.1", methodOrVersion); + assertEquals("200", uriOrStatus); + assertEquals("OK", versionOrReason); + System.out.println(parser.getState()); + + parser.parseNext(buffer3); + assertEquals("Content-Length", hdr[0]); + assertEquals("4", val[0]); + assertEquals("test", content); + System.out.println(parser.getState()); + } + + @Test + @DisplayName("should parse expect 100-continue request successfully.") + void testServerAcceptExpect100Header() { + ByteBuffer buffer = BufferUtils.toBuffer( + "POST /test/data HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Expect: 100-continue\r\n" + + "Content-Length: 4\r\n" + + "Accept-Encoding: gzip, deflated\r\n" + + "Accept: unknown\r\n" + "\r\n"); + + ByteBuffer content = BufferUtils.toBuffer("test"); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + System.out.println(buffer.remaining()); + boolean exit = parser.parseNext(buffer); + System.out.println(buffer.remaining()); + assertTrue(exit); + assertEquals(State.CONTENT, parser.getState()); + + exit = parser.parseNext(content); + assertTrue(exit); + assertEquals(State.END, parser.getState()); + } + + @Test + @DisplayName("should parse 101 successfully.") + void test101SwitchingProtocols() { + ByteBuffer buffer1 = BufferUtils.toBuffer("HTTP/1.1 101 Switching Protocols\r\n\r\n"); + HttpParser.ResponseHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parser.parseNext(buffer1); + assertEquals(-1, headers); + assertTrue(headerCompleted); + assertTrue(messageCompleted); + } + + @BeforeEach + void init() { + bad = null; + content = null; + methodOrVersion = null; + uriOrStatus = null; + versionOrReason = null; + hdr = null; + val = null; + headers = 0; + headerCompleted = false; + messageCompleted = false; + complianceViolation.clear(); + } + + private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler, HttpParser.ComplianceHandler { + + @Override + public boolean content(ByteBuffer ref) { + if (content == null) + content = ""; + String c = BufferUtils.toString(ref, StandardCharsets.UTF_8); + content = content + c; + ref.position(ref.limit()); + return false; + } + + @Override + public boolean startRequest(String method, String uri, HttpVersion version) { + fields.clear(); + trailers.clear(); + headers = -1; + hdr = new String[10]; + val = new String[10]; + methodOrVersion = method; + uriOrStatus = uri; + versionOrReason = version == null ? null : version.getValue(); + messageCompleted = false; + headerCompleted = false; + early = false; + return false; + } + + @Override + public void parsedHeader(HttpField field) { + fields.add(field); + hdr[++headers] = field.getName(); + val[headers] = field.getValue(); + + if (field instanceof HostPortHttpField) { + HostPortHttpField hpfield = (HostPortHttpField) field; + host = hpfield.getHost(); + port = hpfield.getPort(); + } + } + + @Override + public boolean headerComplete() { + content = null; + headerCompleted = true; + return fields.stream().anyMatch(f -> f.getHeader() == EXPECT && f.getValue().equals(CONTINUE.getValue())); + } + + @Override + public void parsedTrailer(HttpField field) { + trailers.add(field); + } + + @Override + public boolean contentComplete() { + return false; + } + + @Override + public boolean messageComplete() { + messageCompleted = true; + return true; + } + + @Override + public void badMessage(BadMessageException failure) { + String reason = failure.getReason(); + bad = reason == null ? String.valueOf(failure.getCode()) : reason; + } + + @Override + public boolean startResponse(HttpVersion version, int status, String reason) { + fields.clear(); + trailers.clear(); + methodOrVersion = version.getValue(); + uriOrStatus = Integer.toString(status); + versionOrReason = reason; + headers = -1; + hdr = new String[10]; + val = new String[10]; + messageCompleted = false; + headerCompleted = false; + return status == HttpStatus.CONTINUE_100; + } + + @Override + public void earlyEOF() { + early = true; + } + + @Override + public int getHeaderCacheSize() { + return 4096; + } + + @Override + public void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String reason) { + complianceViolation.add(violation); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorClientTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorClientTest.java new file mode 100644 index 000000000..992587c2f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorClientTest.java @@ -0,0 +1,358 @@ +package com.fireflysource.net.http.common.v1.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.model.*; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class HttpGeneratorClientTest { + + @Test + void testGETRequestNoContent() { + ByteBuffer header = BufferUtils.allocate(2048); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("GET", "/index.html"); + info.getFields().add("Host", "something"); + info.getFields().add("User-Agent", "test"); + assertFalse(gen.isChunking()); + + result = gen.generateRequest(info, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertFalse(gen.isChunking()); + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateRequest(null, null, null, null, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + assertFalse(gen.isChunking()); + + assertEquals(0, gen.getContentPrepared()); + assertTrue(out.contains("GET /index.html HTTP/1.1")); + assertFalse(out.contains("Content-Length")); + } + + @Test + void testEmptyHeaders() { + ByteBuffer header = BufferUtils.allocate(2048); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("GET", "/index.html"); + info.getFields().add("Host", "something"); + info.getFields().add("Null", null); + info.getFields().add("Empty", ""); + assertFalse(gen.isChunking()); + + result = gen.generateRequest(info, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertFalse(gen.isChunking()); + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + assertFalse(gen.isChunking()); + + assertEquals(0, gen.getContentPrepared()); + assertTrue(out.contains("GET /index.html HTTP/1.1")); + assertFalse(out.contains("Content-Length")); + assertTrue(out.contains("Empty:")); + assertFalse(out.contains("Null:")); + } + + @Test + void testPOSTRequestNoContent() { + ByteBuffer header = BufferUtils.allocate(2048); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("POST", "/index.html"); + info.getFields().add("Host", "something"); + info.getFields().add("User-Agent", "test"); + assertFalse(gen.isChunking()); + + result = gen.generateRequest(info, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertFalse(gen.isChunking()); + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateRequest(null, null, null, null, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + assertFalse(gen.isChunking()); + + assertEquals(0, gen.getContentPrepared()); + assertTrue(out.contains("POST /index.html HTTP/1.1")); + assertTrue(out.contains("Content-Length: 0")); + } + + @Test + void testRequestWithContent() { + String out; + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World. The quick brown fox jumped over the lazy dog."); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, content0, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("POST", "/index.html"); + info.getFields().add("Host", "something"); + info.getFields().add("User-Agent", "test"); + + result = gen.generateRequest(info, null, null, content0, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, content0, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertFalse(gen.isChunking()); + + out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + assertFalse(gen.isChunking()); + + + assertTrue(out.contains("POST /index.html HTTP/1.1")); + assertTrue(out.contains("Host: something")); + assertTrue(out.contains("Content-Length: 58")); + assertTrue(out.contains("Hello World. The quick brown fox jumped over the lazy dog.")); + + assertEquals(58, gen.getContentPrepared()); + } + + @Test + void testRequestWithChunkedContent() { + String out; + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World. "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog."); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, null, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("POST", "/index.html"); + info.getFields().add("Host", "something"); + info.getFields().add("User-Agent", "test"); + + result = gen.generateRequest(info, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, null, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + assertTrue(gen.isChunking()); + out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateRequest(info, header, chunk, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + assertTrue(gen.isChunking()); + + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateRequest(null, null, null, content1, false); + assertEquals(HttpGenerator.Result.NEED_CHUNK, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + result = gen.generateRequest(null, null, chunk, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + assertTrue(gen.isChunking()); + + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateRequest(null, null, chunk, content1, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertTrue(gen.isChunking()); + + + result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_CHUNK, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertTrue(gen.isChunking()); + + result = gen.generateRequest(null, null, chunk, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + assertFalse(gen.isChunking()); + + result = gen.generateRequest(null, null, chunk, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("POST /index.html HTTP/1.1")); + assertTrue(out.contains("Host: something")); + assertTrue(out.contains("Transfer-Encoding: chunked")); + assertTrue(out.contains("\r\nD\r\nHello World. \r\n")); + assertTrue(out.contains("\r\n2D\r\nThe quick brown fox jumped over the lazy dog.\r\n")); + assertTrue(out.contains("\r\n0\r\n\r\n")); + + assertEquals(58, gen.getContentPrepared()); + } + + @Test + void testRequestWithKnownContent() { + String out; + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World. "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog."); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("POST", "/index.html", 58); + info.getFields().add("Host", "something"); + info.getFields().add("User-Agent", "test"); + + result = gen.generateRequest(info, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + assertFalse(gen.isChunking()); + out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateRequest(null, null, null, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + assertFalse(gen.isChunking()); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertFalse(gen.isChunking()); + + result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + + assertTrue(out.contains("POST /index.html HTTP/1.1")); + assertTrue(out.contains("Host: something")); + assertTrue(out.contains("Content-Length: 58")); + assertTrue(out.contains("\r\n\r\nHello World. The quick brown fox jumped over the lazy dog.")); + + assertEquals(58, gen.getContentPrepared()); + } + + @Test + void testAddFields() { + ByteBuffer header = BufferUtils.allocate(2048); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + Info info = new Info("GET", "/index.html"); + info.getFields().add("Host", "something1"); + info.getFields().add("User-Agent", "test1"); + info.getFields().add("Connection", HttpHeaderValue.KEEP_ALIVE.getValue()); + List values = info.getFields().getValuesList("Connection"); + if (values != null && !values.isEmpty()) { + info.getFields().remove("Connection"); + List newValues = new LinkedList<>(values); + newValues.add("Upgrade"); + newValues.add("HTTP2-Settings"); + info.getFields().addCSV("Connection", newValues.toArray(new String[0])); + } + assertFalse(gen.isChunking()); + + result = gen.generateRequest(info, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateRequest(info, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + assertFalse(gen.isChunking()); + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateRequest(null, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + assertFalse(gen.isChunking()); + + System.out.println(out); + assertTrue(out.contains("keep-alive, Upgrade, HTTP2-Settings")); + } + + class Info extends MetaData.Request { + Info(String method, String uri) { + super(method, new HttpURI(uri), HttpVersion.HTTP_1_1, new HttpFields(), -1); + } + + Info(String method, String uri, int contentLength) { + super(method, new HttpURI(uri), HttpVersion.HTTP_1_1, new HttpFields(), contentLength); + } + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorServerHTTPTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorServerHTTPTest.java new file mode 100644 index 000000000..6c2c545f4 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorServerHTTPTest.java @@ -0,0 +1,307 @@ +package com.fireflysource.net.http.common.v1.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.exception.BadMessageException; +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpVersion; +import com.fireflysource.net.http.common.model.MetaData; +import com.fireflysource.net.http.common.v1.decoder.HttpParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class HttpGeneratorServerHTTPTest { + public final static String CONTENT = "The quick brown fox jumped over the lazy dog.\nNow is the time for all good men to come to the aid of the party\nThe moon is blue to a fish in love.\n"; + private String content; + private String reason; + + public static Stream data() { + Result[] results = { + new Result(200, null, -1, null, false), + new Result(200, null, -1, CONTENT, false), + new Result(200, null, CONTENT.length(), null, true), + new Result(200, null, CONTENT.length(), CONTENT, false), + new Result(200, "text/html", -1, null, true), + new Result(200, "text/html", -1, CONTENT, false), + new Result(200, "text/html", CONTENT.length(), null, true), + new Result(200, "text/html", CONTENT.length(), CONTENT, false) + }; + + ArrayList data = new ArrayList<>(); + + // For each test result + for (Result result : results) { + // Loop over HTTP versions + for (int v = 10; v <= 11; v++) { + // Loop over chunks + for (int chunks = 1; chunks <= 6; chunks++) { + // Loop over Connection values + for (ConnectionType connection : ConnectionType.values()) { + if (connection.isSupportedByHttp(v)) { + data.add(Arguments.of(new Run(result, v, chunks, connection))); + } + } + } + } + } + + return data.stream(); + } + + @ParameterizedTest + @MethodSource("data") + void testHTTP(Run run) throws Exception { + Handler handler = new Handler(); + + HttpGenerator gen = new HttpGenerator(); + + String msg = run.toString(); + + run.result.getHttpFields().clear(); + + String response = run.result.build(run.httpVersion, gen, "OK\r\nTest", run.connection.val, null, run.chunks); + + HttpParser parser = new HttpParser(handler); + parser.setHeadResponse(run.result.head); + + parser.parseNext(BufferUtils.toBuffer(response)); + + if (run.result.body != null) + assertEquals(run.result.body, this.content, msg); + + // TODO: Break down rationale more clearly, these should be separate checks and/or assertions + if (run.httpVersion == 10) + assertTrue(gen.isPersistent() || run.result.contentLength >= 0 || EnumSet.of(ConnectionType.CLOSE, ConnectionType.KEEP_ALIVE, ConnectionType.NONE).contains(run.connection), msg); + else + assertTrue(gen.isPersistent() || EnumSet.of(ConnectionType.CLOSE, ConnectionType.TE_CLOSE).contains(run.connection), msg); + + assertEquals("OK??Test", reason); + + if (content == null) + assertNull(run.result.body, msg); + else + assertTrue(run.result.contentLength == content.length() || run.result.contentLength == -1, msg); + } + + private enum ConnectionType { + NONE(null, 9, 10, 11), + KEEP_ALIVE("keep-alive", 9, 10, 11), + CLOSE("close", 9, 10, 11), + TE_CLOSE("TE, close", 11); + + private String val; + private int[] supportedHttpVersions; + + ConnectionType(String val, int... supportedHttpVersions) { + this.val = val; + this.supportedHttpVersions = supportedHttpVersions; + } + + public boolean isSupportedByHttp(int version) { + for (int supported : supportedHttpVersions) { + if (supported == version) { + return true; + } + } + return false; + } + } + + private static class Result { + private final String body; + private final int code; + private final boolean head; + private HttpFields fields = new HttpFields(); + private String connection; + private int contentLength; + private String contentType; + private String other; + private String te; + + private Result(int code, String contentType, int contentLength, String content, boolean head) { + this.code = code; + this.contentType = contentType; + this.contentLength = contentLength; + other = "value"; + body = content; + this.head = head; + } + + private String build(int version, HttpGenerator gen, String reason, String connection, String te, int nchunks) throws Exception { + String response = ""; + this.connection = connection; + this.te = te; + + if (contentType != null) + fields.put("Content-Type", contentType); + if (contentLength >= 0) + fields.put("Content-Length", "" + contentLength); + if (this.connection != null) + fields.put("Connection", this.connection); + if (this.te != null) + fields.put("Transfer-Encoding", this.te); + if (other != null) + fields.put("Other", other); + + ByteBuffer source = body == null ? null : BufferUtils.toBuffer(body); + ByteBuffer[] chunks = new ByteBuffer[nchunks]; + ByteBuffer content = null; + int c = 0; + if (source != null) { + for (int i = 0; i < nchunks; i++) { + chunks[i] = source.duplicate(); + chunks[i].position(i * (source.capacity() / nchunks)); + if (i > 0) + chunks[i - 1].limit(chunks[i].position()); + } + content = chunks[c++]; + } + ByteBuffer header = null; + ByteBuffer chunk = null; + MetaData.Response info = null; + + loop: + while (true) { + // if we have unwritten content + if (source != null && content != null && content.remaining() == 0 && c < nchunks) + content = chunks[c++]; + + // Generate + boolean last = !BufferUtils.hasContent(content); + + HttpGenerator.Result result = gen.generateResponse(info, head, header, chunk, content, last); + + switch (result) { + case NEED_INFO: + info = new MetaData.Response(HttpVersion.fromVersion(version), code, reason, fields, contentLength); + continue; + + case NEED_HEADER: + header = BufferUtils.allocate(2048); + continue; + + case NEED_CHUNK: + chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + continue; + + case NEED_CHUNK_TRAILER: + chunk = BufferUtils.allocate(2048); + continue; + + + case FLUSH: + if (BufferUtils.hasContent(header)) { + response += BufferUtils.toString(header); + header.position(header.limit()); + } + if (BufferUtils.hasContent(chunk)) { + response += BufferUtils.toString(chunk); + chunk.position(chunk.limit()); + } + if (BufferUtils.hasContent(content)) { + response += BufferUtils.toString(content); + content.position(content.limit()); + } + break; + + case CONTINUE: + continue; + + case SHUTDOWN_OUT: + break; + + case DONE: + break loop; + } + } + return response; + } + + @Override + public String toString() { + return "[" + code + "," + contentType + "," + contentLength + "," + (body == null ? "null" : "content") + "]"; + } + + public HttpFields getHttpFields() { + return fields; + } + } + + private static class Run { + private Result result; + private ConnectionType connection; + private int httpVersion; + private int chunks; + + public Run(Result result, int ver, int chunks, ConnectionType connection) { + this.result = result; + this.httpVersion = ver; + this.chunks = chunks; + this.connection = connection; + } + + @Override + public String toString() { + return String.format("result=%s,version=%d,chunks=%d,connection=%s", result, httpVersion, chunks, connection.name()); + } + } + + private class Handler implements HttpParser.ResponseHandler { + @Override + public boolean content(ByteBuffer ref) { + if (content == null) + content = ""; + content += BufferUtils.toString(ref); + ref.position(ref.limit()); + return false; + } + + @Override + public void earlyEOF() { + } + + @Override + public boolean headerComplete() { + content = null; + return false; + } + + @Override + public boolean contentComplete() { + return false; + } + + @Override + public boolean messageComplete() { + return true; + } + + @Override + public void parsedHeader(HttpField field) { + } + + @Override + public boolean startResponse(HttpVersion version, int status, String reason) { + HttpGeneratorServerHTTPTest.this.reason = reason; + return false; + } + + @Override + public void badMessage(BadMessageException failure) { + throw failure; + } + + @Override + public int getHeaderCacheSize() { + return 4096; + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorServerTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorServerTest.java new file mode 100644 index 000000000..0bed59c4b --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v1/encoder/HttpGeneratorServerTest.java @@ -0,0 +1,861 @@ +package com.fireflysource.net.http.common.v1.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.sys.ProjectVersion; +import com.fireflysource.net.http.common.codec.DateGenerator; +import com.fireflysource.net.http.common.exception.BadMessageException; +import com.fireflysource.net.http.common.model.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +class HttpGeneratorServerTest { + + @Test + void test_0_9() { + ByteBuffer header = BufferUtils.allocate(8096); + ByteBuffer content = BufferUtils.toBuffer("0123456789"); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_0_9, 200, null, new HttpFields(), 10); + info.getFields().add("Content-Type", "test/data"); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + + result = gen.generateResponse(info, false, null, null, content, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String response = BufferUtils.toString(header); + BufferUtils.clear(header); + response += BufferUtils.toString(content); + BufferUtils.clear(content); + + result = gen.generateResponse(null, false, null, null, content, false); + assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertEquals(10, gen.getContentPrepared()); + + assertFalse(response.contains("200 OK")); + assertFalse(response.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(response.contains("Content-Length: 10")); + assertTrue(response.contains("0123456789")); + } + + @Test + void testSimple() { + ByteBuffer header = BufferUtils.allocate(8096); + ByteBuffer content = BufferUtils.toBuffer("0123456789"); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 10); + info.getFields().add("Content-Type", "test/data"); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + + result = gen.generateResponse(info, false, null, null, content, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + + result = gen.generateResponse(info, false, header, null, content, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String response = BufferUtils.toString(header); + BufferUtils.clear(header); + response += BufferUtils.toString(content); + BufferUtils.clear(content); + + result = gen.generateResponse(null, false, null, null, null, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertEquals(10, gen.getContentPrepared()); + + assertTrue(response.contains("HTTP/1.1 200 OK")); + assertTrue(response.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertTrue(response.contains("Content-Length: 10")); + assertTrue(response.contains("\r\n0123456789")); + } + + @Test + @DisplayName("should generate response with content length successfully") + void testGenerateHeaderNoLast() { + ByteBuffer header = BufferUtils.allocate(8096); + ByteBuffer content = BufferUtils.toBuffer("0123456789"); + + HttpGenerator gen = new HttpGenerator(); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 10); + info.getFields().add("Content-Type", "test/data"); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + + HttpGenerator.Result result = gen.generateResponse(info, false, header, null, null, false); + System.out.println(result + ", " + gen.getState()); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + String response = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(null, false, null, null, content, false); + System.out.println(result + ", " + gen.getState()); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + response += BufferUtils.toString(content); + BufferUtils.clear(content); + + result = gen.generateResponse(null, false, null, null, null, true); + System.out.println(result + ", " + gen.getState()); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, null, null, true); + System.out.println(result + ", " + gen.getState()); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + System.out.println(response); + assertTrue(response.contains("HTTP/1.1 200 OK")); + assertTrue(response.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertTrue(response.contains("Content-Length: 10")); + assertTrue(response.contains("\r\n0123456789")); + } + + @Test + void test204() { + ByteBuffer header = BufferUtils.allocate(8096); + ByteBuffer content = BufferUtils.toBuffer("0123456789"); + + HttpGenerator gen = new HttpGenerator(); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 204, "Foo", new HttpFields(), 10); + info.getFields().add("Content-Type", "test/data"); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + + HttpGenerator.Result result = gen.generateResponse(info, false, header, null, content, true); + + assertTrue(gen.isNoContent()); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String responseHeaders = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(null, false, null, null, content, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(responseHeaders.contains("HTTP/1.1 204 Foo")); + assertTrue(responseHeaders.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(responseHeaders.contains("Content-Length: 10")); + + //Note: the HttpConnection.process() method is responsible for actually + //excluding the content from the response based on generator.isNoContent()==true + } + + + @Test + void testComplexChars() { + ByteBuffer header = BufferUtils.allocate(8096); + ByteBuffer content = BufferUtils.toBuffer("0123456789"); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, "ØÆ", new HttpFields(), 10); + info.getFields().add("Content-Type", "test/data;\r\nextra=value"); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + + result = gen.generateResponse(info, false, null, null, content, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + + result = gen.generateResponse(info, false, header, null, content, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String response = BufferUtils.toString(header); + BufferUtils.clear(header); + response += BufferUtils.toString(content); + BufferUtils.clear(content); + + result = gen.generateResponse(null, false, null, null, content, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertEquals(10, gen.getContentPrepared()); + + assertTrue(response.contains("HTTP/1.1 200 ØÆ")); + assertTrue(response.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertTrue(response.contains("Content-Type: test/data; extra=value")); + assertTrue(response.contains("Content-Length: 10")); + assertTrue(response.contains("\r\n0123456789")); + } + + @Test + void testSendServerXPoweredBy() { + ByteBuffer header = BufferUtils.allocate(8096); + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1); + HttpFields fields = new HttpFields(); + fields.add(HttpHeader.SERVER, "SomeServer"); + fields.add(HttpHeader.X_POWERED_BY, "SomePower"); + MetaData.Response infoF = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, fields, -1); + String head; + + HttpGenerator gen = new HttpGenerator(true, true); + gen.generateResponse(info, false, header, null, null, true); + head = BufferUtils.toString(header); + BufferUtils.clear(header); + assertTrue(head.contains("HTTP/1.1 200 OK")); + assertTrue(head.contains("Server: Firefly(" + ProjectVersion.getValue() + ")")); + assertTrue(head.contains("X-Powered-By: Firefly(" + ProjectVersion.getValue() + ")")); + gen.reset(); + gen.generateResponse(infoF, false, header, null, null, true); + head = BufferUtils.toString(header); + BufferUtils.clear(header); + assertTrue(head.contains("HTTP/1.1 200 OK")); + assertFalse(head.contains("Server: Firefly(" + ProjectVersion.getValue() + ")")); + assertTrue(head.contains("Server: SomeServer")); + assertTrue(head.contains("X-Powered-By: Firefly(" + ProjectVersion.getValue() + ")")); + assertTrue(head.contains("X-Powered-By: SomePower")); + gen.reset(); + + gen = new HttpGenerator(false, false); + gen.generateResponse(info, false, header, null, null, true); + head = BufferUtils.toString(header); + BufferUtils.clear(header); + assertTrue(head.contains("HTTP/1.1 200 OK")); + assertFalse(head.contains("Server: Firefly(" + ProjectVersion.getValue() + ")")); + assertFalse(head.contains("X-Powered-By: Firefly(" + ProjectVersion.getValue() + ")")); + gen.reset(); + gen.generateResponse(infoF, false, header, null, null, true); + head = BufferUtils.toString(header); + BufferUtils.clear(header); + assertTrue(head.contains("HTTP/1.1 200 OK")); + assertFalse(head.contains("Server: Firefly(" + ProjectVersion.getValue() + ")")); + assertTrue(head.contains("Server: SomeServer")); + assertFalse(head.contains("X-Powered-By: Firefly(" + ProjectVersion.getValue() + ")")); + assertTrue(head.contains("X-Powered-By: SomePower")); + gen.reset(); + } + + @Test + void testResponseIncorrectContentLength() { + ByteBuffer header = BufferUtils.allocate(8096); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 10); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + info.getFields().add("Content-Length", "11"); + + result = gen.generateResponse(info, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + + BadMessageException e = assertThrows(BadMessageException.class, + () -> gen.generateResponse(info, false, header, null, null, true)); + assertEquals(500, e.getCode()); + } + + @Test + void testResponseNoContentPersistent() { + ByteBuffer header = BufferUtils.allocate(8096); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 0); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + + result = gen.generateResponse(info, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + + result = gen.generateResponse(info, false, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String head = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(null, false, null, null, null, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertEquals(0, gen.getContentPrepared()); + assertTrue(head.contains("HTTP/1.1 200 OK")); + assertTrue(head.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertTrue(head.contains("Content-Length: 0")); + } + + @Test + void testResponseKnownNoContentNotPersistent() { + ByteBuffer header = BufferUtils.allocate(8096); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 0); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + info.getFields().add("Connection", "close"); + + result = gen.generateResponse(info, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + + result = gen.generateResponse(info, false, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String head = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(null, false, null, null, null, false); + assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertEquals(0, gen.getContentPrepared()); + assertTrue(head.contains("HTTP/1.1 200 OK")); + assertTrue(head.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertTrue(head.contains("Connection: close")); + } + + @Test + void testResponseUpgrade() { + ByteBuffer header = BufferUtils.allocate(8096); + + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 101, null, new HttpFields(), -1); + info.getFields().add("Upgrade", "WebSocket"); + info.getFields().add("Connection", "Upgrade"); + info.getFields().add("Sec-WebSocket-Accept", "123456789=="); + + result = gen.generateResponse(info, false, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + String head = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(info, false, null, null, null, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertEquals(0, gen.getContentPrepared()); + + assertTrue(head.startsWith("HTTP/1.1 101 Switching Protocols")); + assertTrue(head.contains("Upgrade: WebSocket\r\n")); + assertTrue(head.contains("Connection: Upgrade\r\n")); + } + + @Test + void testResponseWithChunkedContent() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World! "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog. "); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + result = gen.generateResponse(info, false, null, null, null, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, null, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(null, false, null, chunk, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateResponse(null, false, null, chunk, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, chunk, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + System.out.println(out); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("Content-Length")); + assertTrue(out.contains("Transfer-Encoding: chunked")); + + assertTrue(out.endsWith( + "\r\n\r\nD\r\n" + + "Hello World! \r\n" + + "2E\r\n" + + "The quick brown fox jumped over the lazy dog. \r\n" + + "0\r\n" + + "\r\n")); + } + + @Test + void testResponseWithHintedChunkedContent() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World! "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog. "); + HttpGenerator gen = new HttpGenerator(); + gen.setPersistent(false); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + info.getFields().add(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED); + result = gen.generateResponse(info, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateResponse(null, false, null, null, content1, false); + assertEquals(HttpGenerator.Result.NEED_CHUNK, result); + + result = gen.generateResponse(null, false, null, chunk, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateResponse(null, false, null, chunk, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, chunk, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + + result = gen.generateResponse(null, false, null, chunk, null, true); + assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("Content-Length")); + assertTrue(out.contains("Transfer-Encoding: chunked")); + + assertTrue(out.endsWith( + "\r\n\r\nD\r\n" + + "Hello World! \r\n" + + "2E\r\n" + + "The quick brown fox jumped over the lazy dog. \r\n" + + "0\r\n" + + "\r\n")); + } + + @Test + void testResponseWithContentAndTrailer() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + ByteBuffer trailer = BufferUtils.allocate(4096); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World! "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog. "); + HttpGenerator gen = new HttpGenerator(); + gen.setPersistent(false); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + info.getFields().add(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED); + info.setTrailerSupplier(() -> { + HttpFields trailer1 = new HttpFields(); + trailer1.add("T-Name0", "T-ValueA"); + trailer1.add("T-Name0", "T-ValueB"); + trailer1.add("T-Name1", "T-ValueC"); + return trailer1; + }); + + result = gen.generateResponse(info, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateResponse(null, false, null, null, content1, false); + assertEquals(HttpGenerator.Result.NEED_CHUNK, result); + + result = gen.generateResponse(null, false, null, chunk, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(chunk); + BufferUtils.clear(chunk); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, null, null, true); + + assertEquals(HttpGenerator.Result.NEED_CHUNK_TRAILER, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, trailer, null, true); + + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + out += BufferUtils.toString(trailer); + BufferUtils.clear(trailer); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("Content-Length")); + assertTrue(out.contains("Transfer-Encoding: chunked")); + + assertTrue(out.endsWith( + "\r\n\r\nD\r\n" + + "Hello World! \r\n" + + "2E\r\n" + + "The quick brown fox jumped over the lazy dog. \r\n" + + "0\r\n" + + "T-Name0: T-ValueA\r\n" + + "T-Name0: T-ValueB\r\n" + + "T-Name1: T-ValueC\r\n" + + "\r\n")); + } + + @Test + void testResponseWithTrailer() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer chunk = BufferUtils.allocate(HttpGenerator.CHUNK_SIZE); + ByteBuffer trailer = BufferUtils.allocate(4096); + HttpGenerator gen = new HttpGenerator(); + gen.setPersistent(false); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + info.getFields().add(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED); + info.setTrailerSupplier(() -> { + HttpFields trailer1 = new HttpFields(); + trailer1.add("T-Name0", "T-ValueA"); + trailer1.add("T-Name0", "T-ValueB"); + trailer1.add("T-Name1", "T-ValueC"); + return trailer1; + }); + + result = gen.generateResponse(info, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.NEED_CHUNK_TRAILER, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, chunk, null, true); + assertEquals(HttpGenerator.Result.NEED_CHUNK_TRAILER, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, trailer, null, true); + + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + out += BufferUtils.toString(trailer); + BufferUtils.clear(trailer); + + result = gen.generateResponse(null, false, null, trailer, null, true); + assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("Content-Length")); + assertTrue(out.contains("Transfer-Encoding: chunked")); + + assertTrue(out.endsWith( + "\r\n\r\n" + + "0\r\n" + + "T-Name0: T-ValueA\r\n" + + "T-Name0: T-ValueB\r\n" + + "T-Name1: T-ValueC\r\n" + + "\r\n")); + } + + @Test + void testResponseWithKnownContentLengthFromMetaData() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World! "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog. "); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 59); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + result = gen.generateResponse(info, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateResponse(null, false, null, null, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("chunked")); + assertTrue(out.contains("Content-Length: 59")); + assertTrue(out.contains("\r\n\r\nHello World! The quick brown fox jumped over the lazy dog. ")); + } + + @Test + void testResponseWithKnownContentLengthFromHeader() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World! "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog. "); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(null, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), -1); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + info.getFields().add("Content-Length", "" + (content0.remaining() + content1.remaining())); + result = gen.generateResponse(info, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + + String out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateResponse(null, false, null, null, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("chunked")); + assertTrue(out.contains("Content-Length: 59")); + assertTrue(out.contains("\r\n\r\nHello World! The quick brown fox jumped over the lazy dog. ")); + } + + + @Test + void test100ThenResponseWithContent() { + ByteBuffer header = BufferUtils.allocate(4096); + ByteBuffer content0 = BufferUtils.toBuffer("Hello World! "); + ByteBuffer content1 = BufferUtils.toBuffer("The quick brown fox jumped over the lazy dog. "); + HttpGenerator gen = new HttpGenerator(); + + HttpGenerator.Result result = gen.generateResponse(HttpGenerator.CONTINUE_100_INFO, false, null, null, null, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(HttpGenerator.CONTINUE_100_INFO, false, header, null, null, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMPLETING_1XX, gen.getState()); + String out = BufferUtils.toString(header); + + result = gen.generateResponse(null, false, null, null, null, false); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 100 Continue\r\n")); + + result = gen.generateResponse(null, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_INFO, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), BufferUtils.length(content0) + BufferUtils.length(content1)); + info.getFields().add("Last-Modified", DateGenerator.JAN_01_1970); + result = gen.generateResponse(info, false, null, null, content0, false); + assertEquals(HttpGenerator.Result.NEED_HEADER, result); + assertEquals(HttpGenerator.State.START, gen.getState()); + + result = gen.generateResponse(info, false, header, null, content0, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + + out = BufferUtils.toString(header); + BufferUtils.clear(header); + out += BufferUtils.toString(content0); + BufferUtils.clear(content0); + + result = gen.generateResponse(null, false, null, null, content1, false); + assertEquals(HttpGenerator.Result.FLUSH, result); + assertEquals(HttpGenerator.State.COMMITTED, gen.getState()); + out += BufferUtils.toString(content1); + BufferUtils.clear(content1); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.CONTINUE, result); + assertEquals(HttpGenerator.State.COMPLETING, gen.getState()); + + result = gen.generateResponse(null, false, null, null, null, true); + assertEquals(HttpGenerator.Result.DONE, result); + assertEquals(HttpGenerator.State.END, gen.getState()); + + assertTrue(out.contains("HTTP/1.1 200 OK")); + assertTrue(out.contains("Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT")); + assertFalse(out.contains("chunked")); + assertTrue(out.contains("Content-Length: 59")); + assertTrue(out.contains("\r\n\r\nHello World! The quick brown fox jumped over the lazy dog. ")); + } + + @Test + void testConnectionKeepAliveWithAdditionalCustomValue() { + HttpGenerator generator = new HttpGenerator(); + + HttpFields fields = new HttpFields(); + fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE); + String customValue = "test"; + fields.add(HttpHeader.CONNECTION, customValue); + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_0, 200, "OK", fields, -1); + ByteBuffer header = BufferUtils.allocate(4096); + HttpGenerator.Result result = generator.generateResponse(info, false, header, null, null, true); + assertSame(HttpGenerator.Result.FLUSH, result); + String headers = BufferUtils.toString(header); + assertTrue(headers.contains(HttpHeaderValue.KEEP_ALIVE.getValue())); + assertTrue(headers.contains(customValue)); + } + + @Test + void testResponseLine() { + HttpGenerator generator = new HttpGenerator(); + HttpFields fields = new HttpFields(); + MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_0, 200, "Connection Established", fields, -1); + ByteBuffer header = BufferUtils.allocate(4096); + HttpGenerator.Result result = generator.generateResponse(info, false, header, null, null, true); + assertEquals(HttpGenerator.Result.FLUSH, result); + + String headers = BufferUtils.toString(header); + System.out.println(headers); + assertTrue(headers.contains("HTTP/1.1 200 Connection Established\r\n\r\n")); + + HttpGenerator.Result endResult = generator.generateResponse(null, false, null, null, null, true); + System.out.println(generator.isChunking()); + System.out.println(endResult); + assertEquals(HttpGenerator.Result.SHUTDOWN_OUT, endResult); + + HttpGenerator.Result done = generator.generateResponse(null, false, null, null, null, true); + System.out.println(done); + assertEquals(HttpGenerator.Result.DONE, done); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/ContinuationParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/ContinuationParseTest.java new file mode 100644 index 000000000..76a405df5 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/ContinuationParseTest.java @@ -0,0 +1,127 @@ +package com.fireflysource.net.http.common.v2.frame; + + +import com.fireflysource.net.http.common.model.*; +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.HeadersGenerator; +import com.fireflysource.net.http.common.v2.hpack.HpackEncoder; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.*; + +class ContinuationParseTest { + + @Test + void testParseOneByteAtATime() { + HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onHeaders(HeadersFrame frame) { + frames.add(frame); + } + + @Override + public void onConnectionFailure(int error, String reason) { + frames.add(new HeadersFrame(null, null, false)); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure the parser is properly reset. + for (int i = 0; i < 2; ++i) { + int streamId = 13; + HttpFields fields = new HttpFields(); + fields.put("Accept", "text/html"); + fields.put("User-Agent", "Firefly"); + MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields); + + FrameBytes frameBytes = generator.generateHeaders(streamId, metaData, null, true); + + List byteBuffers = frameBytes.getByteBuffers(); + assertEquals(2, byteBuffers.size()); + + ByteBuffer headersBody = byteBuffers.remove(1); + int start = headersBody.position(); + int length = headersBody.remaining(); + int oneThird = length / 3; + int lastThird = length - 2 * oneThird; + + // Adjust the length of the HEADERS frame. + ByteBuffer headersHeader = byteBuffers.get(0); + headersHeader.put(0, (byte) ((oneThird >>> 16) & 0xFF)); + headersHeader.put(1, (byte) ((oneThird >>> 8) & 0xFF)); + headersHeader.put(2, (byte) (oneThird & 0xFF)); + + // Remove the END_HEADERS flag from the HEADERS header. + headersHeader.put(4, (byte) (headersHeader.get(4) & ~Flags.END_HEADERS)); + + // New HEADERS body. + headersBody.position(start); + headersBody.limit(start + oneThird); + byteBuffers.add(headersBody.slice()); + + // Split the rest of the HEADERS body into CONTINUATION frames. + // First CONTINUATION header. + byte[] continuationHeader1 = new byte[9]; + continuationHeader1[0] = (byte) ((oneThird >>> 16) & 0xFF); + continuationHeader1[1] = (byte) ((oneThird >>> 8) & 0xFF); + continuationHeader1[2] = (byte) (oneThird & 0xFF); + continuationHeader1[3] = (byte) FrameType.CONTINUATION.getType(); + continuationHeader1[4] = Flags.NONE; + continuationHeader1[5] = 0x00; + continuationHeader1[6] = 0x00; + continuationHeader1[7] = 0x00; + continuationHeader1[8] = (byte) streamId; + byteBuffers.add(ByteBuffer.wrap(continuationHeader1)); + // First CONTINUATION body. + headersBody.position(start + oneThird); + headersBody.limit(start + 2 * oneThird); + byteBuffers.add(headersBody.slice()); + // Second CONTINUATION header. + byte[] continuationHeader2 = new byte[9]; + continuationHeader2[0] = (byte) ((lastThird >>> 16) & 0xFF); + continuationHeader2[1] = (byte) ((lastThird >>> 8) & 0xFF); + continuationHeader2[2] = (byte) (lastThird & 0xFF); + continuationHeader2[3] = (byte) FrameType.CONTINUATION.getType(); + continuationHeader2[4] = Flags.END_HEADERS; + continuationHeader2[5] = 0x00; + continuationHeader2[6] = 0x00; + continuationHeader2[7] = 0x00; + continuationHeader2[8] = (byte) streamId; + byteBuffers.add(ByteBuffer.wrap(continuationHeader2)); + headersBody.position(start + 2 * oneThird); + headersBody.limit(start + length); + byteBuffers.add(headersBody.slice()); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + HeadersFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertTrue(frame.isEndStream()); + MetaData.Request request = (MetaData.Request) frame.getMetaData(); + assertEquals(metaData.getMethod(), request.getMethod()); + assertEquals(metaData.getURI(), request.getURI()); + for (int j = 0; j < fields.size(); ++j) { + HttpField field = fields.getField(j); + assertTrue(request.getFields().contains(field)); + } + PriorityFrame priority = frame.getPriority(); + assertNull(priority); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/DataGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/DataGenerateParseTest.java new file mode 100644 index 000000000..04a4e432f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/DataGenerateParseTest.java @@ -0,0 +1,139 @@ +package com.fireflysource.net.http.common.v2.frame; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.DataGenerator; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DataGenerateParseTest { + private final byte[] smallContent = new byte[128]; + private final byte[] largeContent = new byte[128 * 1024]; + + DataGenerateParseTest() { + Random random = new Random(); + random.nextBytes(smallContent); + random.nextBytes(largeContent); + } + + @Test + void testGenerateParseNoContentNoPadding() { + testGenerateParseContent(BufferUtils.EMPTY_BUFFER); + } + + @Test + void testGenerateParseSmallContentNoPadding() { + testGenerateParseContent(ByteBuffer.wrap(smallContent)); + } + + private void testGenerateParseContent(ByteBuffer content) { + List frames = testGenerateParse(content); + assertEquals(1, frames.size()); + DataFrame frame = frames.get(0); + assertTrue(frame.getStreamId() != 0); + assertTrue(frame.isEndStream()); + assertEquals(content, frame.getData()); + } + + @Test + void testGenerateParseLargeContent() { + ByteBuffer content = ByteBuffer.wrap(largeContent); + List frames = testGenerateParse(content); + assertEquals(8, frames.size()); + ByteBuffer aggregate = ByteBuffer.allocate(content.remaining()); + for (int i = 1; i <= frames.size(); ++i) { + DataFrame frame = frames.get(i - 1); + assertTrue(frame.getStreamId() != 0); + assertEquals(i == frames.size(), frame.isEndStream()); + aggregate.put(frame.getData()); + } + aggregate.flip(); + assertEquals(content, aggregate); + } + + private List testGenerateParse(ByteBuffer data) { + DataGenerator generator = new DataGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onData(DataFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + ByteBuffer slice = data.slice(); + int generated = 0; + List list = new LinkedList<>(); + while (true) { + FrameBytes frameBytes = generator.generateData(13, slice, true, slice.remaining()); + generated += frameBytes.getLength(); + generated -= Frame.HEADER_LENGTH; + list.addAll(frameBytes.getByteBuffers()); + if (generated == data.remaining()) + break; + } + + frames.clear(); + for (ByteBuffer buffer : list) { + parser.parse(buffer); + } + } + + return frames; + } + + @Test + void testGenerateParseOneByteAtATime() { + DataGenerator generator = new DataGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onData(DataFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + ByteBuffer data = ByteBuffer.wrap(largeContent); + ByteBuffer slice = data.slice(); + int generated = 0; + List list = new LinkedList<>(); + while (true) { + FrameBytes frameBytes = generator.generateData(13, slice, true, slice.remaining()); + generated += frameBytes.getLength(); + generated -= Frame.HEADER_LENGTH; + list.addAll(frameBytes.getByteBuffers()); + if (generated == data.remaining()) + break; + } + + frames.clear(); + for (ByteBuffer buffer : list) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(largeContent.length, frames.size()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/GoAwayGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/GoAwayGenerateParseTest.java new file mode 100644 index 000000000..7c9aea8cf --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/GoAwayGenerateParseTest.java @@ -0,0 +1,90 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.GoAwayGenerator; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.*; + +class GoAwayGenerateParseTest { + + @Test + void testGenerateParse() { + GoAwayGenerator generator = new GoAwayGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onGoAway(GoAwayFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int lastStreamId = 13; + int error = 17; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateGoAway(lastStreamId, error, null); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + } + + assertEquals(1, frames.size()); + GoAwayFrame frame = frames.get(0); + assertEquals(lastStreamId, frame.getLastStreamId()); + assertEquals(error, frame.getError()); + assertNull(frame.getPayload()); + } + + @Test + void testGenerateParseOneByteAtATime() { + GoAwayGenerator generator = new GoAwayGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onGoAway(GoAwayFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int lastStreamId = 13; + int error = 17; + byte[] payload = new byte[16]; + new Random().nextBytes(payload); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateGoAway(lastStreamId, error, payload); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + GoAwayFrame frame = frames.get(0); + assertEquals(lastStreamId, frame.getLastStreamId()); + assertEquals(error, frame.getError()); + assertArrayEquals(payload, frame.getPayload()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/HeadersGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/HeadersGenerateParseTest.java new file mode 100644 index 000000000..838b3aff3 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/HeadersGenerateParseTest.java @@ -0,0 +1,159 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.model.*; +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.HeadersGenerator; +import com.fireflysource.net.http.common.v2.hpack.HpackEncoder; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.*; + +class HeadersGenerateParseTest { + + @Test + void testGenerateTrailer() { + HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder()); + + int streamId = 13; + HttpFields fields = new HttpFields(); + fields.put("trailer1", "foo"); + fields.put("trailer2", "bar"); + MetaData.Request metaData = new MetaData.Request(fields); + metaData.setOnlyTrailer(true); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onHeaders(HeadersFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + FrameBytes frameBytes = generator.generateHeaders(streamId, metaData, null, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + + assertEquals(1, frames.size()); + HeadersFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertTrue(frame.isEndStream()); + assertEquals("foo", frame.getMetaData().getFields().get("trailer1")); + assertEquals("bar", frame.getMetaData().getFields().get("trailer2")); + } + + @Test + void testGenerateParse() { + HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder()); + + int streamId = 13; + HttpFields fields = new HttpFields(); + fields.put("Accept", "text/html"); + fields.put("User-Agent", "Firefly"); + MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onHeaders(HeadersFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + PriorityFrame priorityFrame = new PriorityFrame(streamId, 3 * streamId, 200, true); + FrameBytes frameBytes = generator.generateHeaders(streamId, metaData, priorityFrame, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + + assertEquals(1, frames.size()); + HeadersFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertTrue(frame.isEndStream()); + MetaData.Request request = (MetaData.Request) frame.getMetaData(); + assertEquals(metaData.getMethod(), request.getMethod()); + assertEquals(metaData.getURI(), request.getURI()); + for (int j = 0; j < fields.size(); ++j) { + HttpField field = fields.getField(j); + assertTrue(request.getFields().contains(field)); + } + PriorityFrame priority = frame.getPriority(); + assertNotNull(priority); + assertEquals(priorityFrame.getStreamId(), priority.getStreamId()); + assertEquals(priorityFrame.getParentStreamId(), priority.getParentStreamId()); + assertEquals(priorityFrame.getWeight(), priority.getWeight()); + assertEquals(priorityFrame.isExclusive(), priority.isExclusive()); + } + } + + @Test + void testGenerateParseOneByteAtATime() { + HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onHeaders(HeadersFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + int streamId = 13; + HttpFields fields = new HttpFields(); + fields.put("Accept", "text/html"); + fields.put("User-Agent", "Firefly"); + MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields); + + PriorityFrame priorityFrame = new PriorityFrame(streamId, 3 * streamId, 200, true); + FrameBytes frameBytes = generator.generateHeaders(streamId, metaData, priorityFrame, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + buffer = buffer.slice(); + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + HeadersFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertTrue(frame.isEndStream()); + MetaData.Request request = (MetaData.Request) frame.getMetaData(); + assertEquals(metaData.getMethod(), request.getMethod()); + assertEquals(metaData.getURI(), request.getURI()); + for (int j = 0; j < fields.size(); ++j) { + HttpField field = fields.getField(j); + assertTrue(request.getFields().contains(field)); + } + PriorityFrame priority = frame.getPriority(); + assertNotNull(priority); + assertEquals(priorityFrame.getStreamId(), priority.getStreamId()); + assertEquals(priorityFrame.getParentStreamId(), priority.getParentStreamId()); + assertEquals(priorityFrame.getWeight(), priority.getWeight()); + assertEquals(priorityFrame.isExclusive(), priority.isExclusive()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/MaxFrameSizeParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/MaxFrameSizeParseTest.java new file mode 100644 index 000000000..ca6184eb1 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/MaxFrameSizeParseTest.java @@ -0,0 +1,40 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MaxFrameSizeParseTest { + + @Test + void testMaxFrameSize() { + int maxFrameLength = Frame.DEFAULT_MAX_LENGTH + 16; + + AtomicInteger failure = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onConnectionFailure(int error, String reason) { + failure.set(error); + } + }, 4096, 8192); + parser.setMaxFrameLength(maxFrameLength); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure the parser is properly reset. + for (int i = 0; i < 2; ++i) { + byte[] bytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.putInt(0, maxFrameLength + 1); + buffer.position(1); + while (buffer.hasRemaining()) + parser.parse(buffer); + } + + assertEquals(ErrorCode.FRAME_SIZE_ERROR.code, failure.get()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PingGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PingGenerateParseTest.java new file mode 100644 index 000000000..3ba077f7f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PingGenerateParseTest.java @@ -0,0 +1,114 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.PingGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.*; + +class PingGenerateParseTest { + + @Test + void testGenerateParse() { + PingGenerator generator = new PingGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPing(PingFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + byte[] payload = new byte[8]; + new Random().nextBytes(payload); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generatePing(payload, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + } + + assertEquals(1, frames.size()); + PingFrame frame = frames.get(0); + assertArrayEquals(payload, frame.getPayload()); + assertTrue(frame.isReply()); + } + + @Test + void testGenerateParseOneByteAtATime() { + PingGenerator generator = new PingGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPing(PingFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + byte[] payload = new byte[8]; + new Random().nextBytes(payload); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generatePing(payload, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + PingFrame frame = frames.get(0); + assertArrayEquals(payload, frame.getPayload()); + assertTrue(frame.isReply()); + } + } + + @Test + void testPayloadAsLong() { + PingGenerator generator = new PingGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPing(PingFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + PingFrame ping = new PingFrame(System.nanoTime(), true); + FrameBytes frameBytes = generator.generate(ping); + + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + + assertEquals(1, frames.size()); + PingFrame pong = frames.get(0); + assertEquals(ping.getPayloadAsLong(), pong.getPayloadAsLong()); + assertTrue(pong.isReply()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PriorityGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PriorityGenerateParseTest.java new file mode 100644 index 000000000..0acef9507 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PriorityGenerateParseTest.java @@ -0,0 +1,93 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.PriorityGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PriorityGenerateParseTest { + + @Test + void testGenerateParse() { + PriorityGenerator generator = new PriorityGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPriority(PriorityFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int parentStreamId = 17; + int weight = 256; + boolean exclusive = true; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generatePriority(streamId, parentStreamId, weight, exclusive); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + } + + assertEquals(1, frames.size()); + PriorityFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(parentStreamId, frame.getParentStreamId()); + assertEquals(weight, frame.getWeight()); + assertEquals(exclusive, frame.isExclusive()); + } + + @Test + void testGenerateParseOneByteAtATime() { + PriorityGenerator generator = new PriorityGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPriority(PriorityFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int parentStreamId = 17; + int weight = 3; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generatePriority(streamId, parentStreamId, weight, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + PriorityFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(parentStreamId, frame.getParentStreamId()); + assertEquals(weight, frame.getWeight()); + assertTrue(frame.isExclusive()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PushPromiseGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PushPromiseGenerateParseTest.java new file mode 100644 index 000000000..48c1adbf2 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/PushPromiseGenerateParseTest.java @@ -0,0 +1,110 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.model.*; +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.PushPromiseGenerator; +import com.fireflysource.net.http.common.v2.hpack.HpackEncoder; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PushPromiseGenerateParseTest { + + @Test + void testGenerateParse() { + PushPromiseGenerator generator = new PushPromiseGenerator(new HeaderGenerator(), new HpackEncoder()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPushPromise(PushPromiseFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int promisedStreamId = 17; + HttpFields fields = new HttpFields(); + fields.put("Accept", "text/html"); + fields.put("User-Agent", "Firefly"); + MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generatePushPromise(streamId, promisedStreamId, metaData); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + + assertEquals(1, frames.size()); + PushPromiseFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(promisedStreamId, frame.getPromisedStreamId()); + MetaData.Request request = (MetaData.Request) frame.getMetaData(); + assertEquals(metaData.getMethod(), request.getMethod()); + assertEquals(metaData.getURI(), request.getURI()); + for (int j = 0; j < fields.size(); ++j) { + HttpField field = fields.getField(j); + assertTrue(request.getFields().contains(field)); + } + } + } + + @Test + void testGenerateParseOneByteAtATime() { + PushPromiseGenerator generator = new PushPromiseGenerator(new HeaderGenerator(), new HpackEncoder()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onPushPromise(PushPromiseFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int promisedStreamId = 17; + HttpFields fields = new HttpFields(); + fields.put("Accept", "text/html"); + fields.put("User-Agent", "Firefly"); + MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generatePushPromise(streamId, promisedStreamId, metaData); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + PushPromiseFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(promisedStreamId, frame.getPromisedStreamId()); + MetaData.Request request = (MetaData.Request) frame.getMetaData(); + assertEquals(metaData.getMethod(), request.getMethod()); + assertEquals(metaData.getURI(), request.getURI()); + for (int j = 0; j < fields.size(); ++j) { + HttpField field = fields.getField(j); + assertTrue(request.getFields().contains(field)); + } + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/ResetGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/ResetGenerateParseTest.java new file mode 100644 index 000000000..80127f127 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/ResetGenerateParseTest.java @@ -0,0 +1,85 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.ResetGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResetGenerateParseTest { + + @Test + void testGenerateParse() { + ResetGenerator generator = new ResetGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onReset(ResetFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int error = 17; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateReset(streamId, error); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + } + + assertEquals(1, frames.size()); + ResetFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(error, frame.getError()); + } + + @Test + void testGenerateParseOneByteAtATime() { + ResetGenerator generator = new ResetGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onReset(ResetFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int error = 17; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateReset(streamId, error); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + ResetFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(error, frame.getError()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/SettingsGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/SettingsGenerateParseTest.java new file mode 100644 index 000000000..82dd640e6 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/SettingsGenerateParseTest.java @@ -0,0 +1,237 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.SettingsGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SettingsGenerateParseTest { + + @Test + void testGenerateParseNoSettings() { + List frames = testGenerateParse(Collections.emptyMap()); + assertEquals(1, frames.size()); + SettingsFrame frame = frames.get(0); + assertEquals(0, frame.getSettings().size()); + assertTrue(frame.isReply()); + } + + @Test + void testGenerateParseSettings() { + Map settings1 = new HashMap<>(); + int key1 = 13; + Integer value1 = 17; + settings1.put(key1, value1); + int key2 = 19; + Integer value2 = 23; + settings1.put(key2, value2); + List frames = testGenerateParse(settings1); + assertEquals(1, frames.size()); + SettingsFrame frame = frames.get(0); + Map settings2 = frame.getSettings(); + assertEquals(2, settings2.size()); + assertEquals(value1, settings2.get(key1)); + assertEquals(value2, settings2.get(key2)); + } + + private List testGenerateParse(Map settings) { + SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator()); + + List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onSettings(SettingsFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateSettings(settings, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + } + + return frames; + } + + @Test + void testGenerateParseInvalidSettings() { + SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator()); + + AtomicInteger errorRef = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onConnectionFailure(int error, String reason) { + errorRef.set(error); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + Map settings1 = new HashMap<>(); + settings1.put(13, 17); + FrameBytes frameBytes = generator.generateSettings(settings1, true); + // Modify the length of the frame to make it invalid + ByteBuffer bytes = frameBytes.getByteBuffers().get(0); + bytes.putShort(1, (short) (bytes.getShort(1) - 1)); + + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(ErrorCode.FRAME_SIZE_ERROR.code, errorRef.get()); + } + + @Test + void testGenerateParseOneByteAtATime() { + SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator()); + + List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onSettings(SettingsFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + Map settings1 = new HashMap<>(); + int key = 13; + Integer value = 17; + settings1.put(key, value); + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateSettings(settings1, true); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + SettingsFrame frame = frames.get(0); + Map settings2 = frame.getSettings(); + assertEquals(1, settings2.size()); + assertEquals(value, settings2.get(key)); + assertTrue(frame.isReply()); + } + } + + @Test + void testGenerateParseTooManyDifferentSettingsInOneFrame() { + SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator()); + + AtomicInteger errorRef = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onConnectionFailure(int error, String reason) { + errorRef.set(error); + } + }, 4096, 8192); + int maxSettingsKeys = 32; + parser.setMaxSettingsKeys(maxSettingsKeys); + parser.init(UnaryOperator.identity()); + + Map settings = new HashMap<>(); + for (int i = 0; i < maxSettingsKeys + 1; ++i) + settings.put(i + 10, i); + + FrameBytes frameBytes = generator.generateSettings(settings, false); + + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) + parser.parse(buffer); + } + + assertEquals(ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, errorRef.get()); + } + + @Test + void testGenerateParseTooManySameSettingsInOneFrame() throws Exception { + int keyValueLength = 6; + int pairs = Frame.DEFAULT_MAX_LENGTH / keyValueLength; + int maxSettingsKeys = pairs / 2; + + AtomicInteger errorRef = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter(), 4096, 8192); + parser.setMaxSettingsKeys(maxSettingsKeys); + parser.setMaxFrameLength(Frame.DEFAULT_MAX_LENGTH); + parser.init(listener -> new Parser.Listener.Wrapper(listener) { + @Override + public void onConnectionFailure(int error, String reason) { + errorRef.set(error); + } + }); + + int length = pairs * keyValueLength; + ByteBuffer buffer = ByteBuffer.allocate(1 + 9 + length); + buffer.putInt(length); + buffer.put((byte) FrameType.SETTINGS.getType()); + buffer.put((byte) 0); // Flags. + buffer.putInt(0); // Stream ID. + // Add the same setting over and over again. + for (int i = 0; i < pairs; ++i) { + buffer.putShort((short) SettingsFrame.MAX_CONCURRENT_STREAMS); + buffer.putInt(i); + } + // Only 3 bytes for the length, skip the first. + buffer.flip().position(1); + + while (buffer.hasRemaining()) + parser.parse(buffer); + + assertEquals(ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, errorRef.get()); + } + + @Test + void testGenerateParseTooManySettingsInMultipleFrames() { + SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator()); + + AtomicInteger errorRef = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onConnectionFailure(int error, String reason) { + errorRef.set(error); + } + }, 4096, 8192); + int maxSettingsKeys = 32; + parser.setMaxSettingsKeys(maxSettingsKeys); + parser.init(UnaryOperator.identity()); + + Map settings = new HashMap<>(); + settings.put(13, 17); + + List list = new LinkedList<>(); + for (int i = 0; i < maxSettingsKeys + 1; ++i) { + FrameBytes frameBytes = generator.generateSettings(settings, false); + list.addAll(frameBytes.getByteBuffers()); + } + + for (ByteBuffer buffer : list) { + while (buffer.hasRemaining()) + parser.parse(buffer); + } + + assertEquals(ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, errorRef.get()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/UnknownParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/UnknownParseTest.java new file mode 100644 index 000000000..a7e4ef4a0 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/UnknownParseTest.java @@ -0,0 +1,68 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class UnknownParseTest { + + @Test + void testParse() { + testParse(Function.identity()); + } + + @Test + void testParseOneByteAtATime() { + testParse(buffer -> ByteBuffer.wrap(new byte[]{buffer.get()})); + } + + @Test + void testInvalidFrameSize() { + AtomicInteger failure = new AtomicInteger(); + Parser parser = new Parser(new Parser.Listener.Adapter(), 4096, 8192); + parser.init(listener -> new Parser.Listener.Wrapper(listener) { + @Override + public void onConnectionFailure(int error, String reason) { + failure.set(error); + } + }); + parser.setMaxFrameLength(Frame.DEFAULT_MAX_LENGTH); + + // 0x4001 == 16385 which is > Frame.DEFAULT_MAX_LENGTH. + byte[] bytes = new byte[]{0, 0x40, 0x01, 64, 0, 0, 0, 0, 0}; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + while (buffer.hasRemaining()) + parser.parse(buffer); + + assertEquals(ErrorCode.FRAME_SIZE_ERROR.code, failure.get()); + } + + private void testParse(Function fn) { + AtomicBoolean failure = new AtomicBoolean(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onConnectionFailure(int error, String reason) { + failure.set(true); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + // Iterate a few times to be sure the parser is properly reset. + for (int i = 0; i < 2; ++i) { + byte[] bytes = new byte[]{0, 0, 4, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + while (buffer.hasRemaining()) + parser.parse(fn.apply(buffer)); + } + + assertFalse(failure.get()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/WindowUpdateGenerateParseTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/WindowUpdateGenerateParseTest.java new file mode 100644 index 000000000..607dd3bdf --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/frame/WindowUpdateGenerateParseTest.java @@ -0,0 +1,85 @@ +package com.fireflysource.net.http.common.v2.frame; + +import com.fireflysource.net.http.common.v2.decoder.Parser; +import com.fireflysource.net.http.common.v2.encoder.FrameBytes; +import com.fireflysource.net.http.common.v2.encoder.HeaderGenerator; +import com.fireflysource.net.http.common.v2.encoder.WindowUpdateGenerator; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WindowUpdateGenerateParseTest { + + @Test + void testGenerateParse() { + WindowUpdateGenerator generator = new WindowUpdateGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onWindowUpdate(WindowUpdateFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int windowUpdate = 17; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateWindowUpdate(streamId, windowUpdate); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(buffer); + } + } + } + + assertEquals(1, frames.size()); + WindowUpdateFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(windowUpdate, frame.getWindowDelta()); + } + + @Test + public void testGenerateParseOneByteAtATime() { + WindowUpdateGenerator generator = new WindowUpdateGenerator(new HeaderGenerator()); + + final List frames = new ArrayList<>(); + Parser parser = new Parser(new Parser.Listener.Adapter() { + @Override + public void onWindowUpdate(WindowUpdateFrame frame) { + frames.add(frame); + } + }, 4096, 8192); + parser.init(UnaryOperator.identity()); + + int streamId = 13; + int windowUpdate = 17; + + // Iterate a few times to be sure generator and parser are properly reset. + for (int i = 0; i < 2; ++i) { + FrameBytes frameBytes = generator.generateWindowUpdate(streamId, windowUpdate); + + frames.clear(); + for (ByteBuffer buffer : frameBytes.getByteBuffers()) { + while (buffer.hasRemaining()) { + parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()})); + } + } + + assertEquals(1, frames.size()); + WindowUpdateFrame frame = frames.get(0); + assertEquals(streamId, frame.getStreamId()); + assertEquals(windowUpdate, frame.getWindowDelta()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackContextTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackContextTest.java new file mode 100644 index 000000000..4e2b4d0c5 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackContextTest.java @@ -0,0 +1,395 @@ +package com.fireflysource.net.http.common.v2.hpack; + + +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.v2.hpack.HpackContext.Entry; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + + +class HpackContextTest { + + @Test + void testStaticName() { + HpackContext ctx = new HpackContext(4096); + Entry entry = ctx.get(":method"); + assertEquals(":method", entry.getHttpField().getName()); + assertTrue(entry.isStatic()); + assertTrue(entry.toString().startsWith("{S,2,:method: ")); + } + + @Test + void testEmptyAdd() { + HpackContext ctx = new HpackContext(0); + HttpField field = new HttpField("foo", "bar"); + assertNull(ctx.add(field)); + } + + @Test + void testTooBigAdd() { + HpackContext ctx = new HpackContext(37); + HttpField field = new HttpField("foo", "bar"); + assertNull(ctx.add(field)); + } + + @Test + void testJustRight() { + HpackContext ctx = new HpackContext(38); + HttpField field = new HttpField("foo", "bar"); + Entry entry = ctx.add(field); + assertNotNull(entry); + assertTrue(entry.toString().startsWith("{D,0,foo: bar,")); + } + + @Test + void testEvictOne() { + HpackContext ctx = new HpackContext(38); + HttpField field0 = new HttpField("foo", "bar"); + + assertEquals(field0, ctx.add(field0).getHttpField()); + assertEquals(field0, ctx.get("foo").getHttpField()); + + HttpField field1 = new HttpField("xxx", "yyy"); + assertEquals(field1, ctx.add(field1).getHttpField()); + + assertNull(ctx.get(field0)); + assertNull(ctx.get("foo")); + assertEquals(field1, ctx.get(field1).getHttpField()); + assertEquals(field1, ctx.get("xxx").getHttpField()); + + } + + @Test + void testEvictNames() { + HpackContext ctx = new HpackContext(38 * 2); + HttpField[] field = + { + new HttpField("name", "v0"), + new HttpField("name", "v1"), + new HttpField("name", "v2"), + new HttpField("name", "v3"), + new HttpField("name", "v4"), + new HttpField("name", "v5"), + }; + + Entry[] entry = new Entry[field.length]; + + // Add 2 name entries to fill table + for (int i = 0; i <= 1; i++) + entry[i] = ctx.add(field[i]); + + // check there is a name reference and it is the most recent added + assertEquals(entry[1], ctx.get("name")); + + // Add 1 other entry to table and evict 1 + ctx.add(new HttpField("xxx", "yyy")); + + // check the name reference has been not been evicted + assertEquals(entry[1], ctx.get("name")); + + // Add 1 other entry to table and evict 1 + ctx.add(new HttpField("foo", "bar")); + + // name is evicted + assertNull(ctx.get("name")); + } + + @Test + void testGetAddStatic() { + HpackContext ctx = new HpackContext(4096); + + // Look for the field. Should find static version. + HttpField methodGet = new HttpField(":method", "GET"); + assertEquals(methodGet, ctx.get(methodGet).getHttpField()); + assertTrue(ctx.get(methodGet).isStatic()); + + // Add static version to dynamic table + Entry e0 = ctx.add(ctx.get(methodGet).getHttpField()); + + // Look again and should see dynamic version + assertEquals(methodGet, ctx.get(methodGet).getHttpField()); + assertNotSame(methodGet, ctx.get(methodGet).getHttpField()); + assertFalse(ctx.get(methodGet).isStatic()); + + // Duplicates allows + Entry e1 = ctx.add(ctx.get(methodGet).getHttpField()); + + // Look again and should see dynamic version + assertEquals(methodGet, ctx.get(methodGet).getHttpField()); + assertNotSame(methodGet, ctx.get(methodGet).getHttpField()); + assertFalse(ctx.get(methodGet).isStatic()); + assertNotSame(e0, e1); + } + + @Test + void testGetAddStaticName() { + HpackContext ctx = new HpackContext(4096); + HttpField methodOther = new HttpField(":method", "OTHER"); + + // Look for the field by name. Should find static version. + assertEquals(":method", ctx.get(":method").getHttpField().getName()); + assertTrue(ctx.get(":method").isStatic()); + + // Add dynamic entry with method + ctx.add(methodOther); + + // Look for the field by name. Should find static version. + assertEquals(":method", ctx.get(":method").getHttpField().getName()); + assertTrue(ctx.get(":method").isStatic()); + } + + @Test + void testIndexes() { + // Only enough space for 5 entries + HpackContext ctx = new HpackContext(38 * 5); + + HttpField methodPost = new HttpField(":method", "POST"); + HttpField[] field = { + new HttpField("fo0", "b0r"), + new HttpField("fo1", "b1r"), + new HttpField("fo2", "b2r"), + new HttpField("fo3", "b3r"), + new HttpField("fo4", "b4r"), + new HttpField("fo5", "b5r"), + new HttpField("fo6", "b6r"), + new HttpField("fo7", "b7r"), + new HttpField("fo8", "b8r"), + new HttpField("fo9", "b9r"), + new HttpField("foA", "bAr"), + }; + + Entry[] entry = new Entry[100]; + + // Lookup the index of a static field + assertEquals(0, ctx.size()); + assertEquals(":authority", ctx.get(1).getHttpField().getName()); + assertEquals(3, ctx.index(ctx.get(methodPost))); + assertEquals(methodPost, ctx.get(3).getHttpField()); + assertEquals("www-authenticate", ctx.get(61).getHttpField().getName()); + assertNull(ctx.get(62)); + + // Add a single entry + entry[0] = ctx.add(field[0]); + + // Check new entry is 62 + assertEquals(1, ctx.size()); + assertEquals(62, ctx.index(entry[0])); + assertEquals(entry[0], ctx.get(62)); + + // and statics still OK + assertEquals(":authority", ctx.get(1).getHttpField().getName()); + assertEquals(3, ctx.index(ctx.get(methodPost))); + assertEquals(methodPost, ctx.get(3).getHttpField()); + assertEquals("www-authenticate", ctx.get(61).getHttpField().getName()); + assertNull(ctx.get(62 + ctx.size())); + + + // Add 4 more entries + for (int i = 1; i <= 4; i++) + entry[i] = ctx.add(field[i]); + + // Check newest entry is at 62 oldest at 66 + assertEquals(5, ctx.size()); + int index = 66; + for (int i = 0; i <= 4; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + // and statics still OK + assertEquals(":authority", ctx.get(1).getHttpField().getName()); + assertEquals(3, ctx.index(ctx.get(methodPost))); + assertEquals(methodPost, ctx.get(3).getHttpField()); + assertEquals("www-authenticate", ctx.get(61).getHttpField().getName()); + assertNull(ctx.get(62 + ctx.size())); + + // add 1 more entry and this should cause an eviction! + entry[5] = ctx.add(field[5]); + + // Check newest entry is at 1 oldest at 5 + index = 66; + for (int i = 1; i <= 5; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + // check entry 0 evicted + assertNull(ctx.get(field[0])); + assertEquals(0, ctx.index(entry[0])); + + // and statics still OK + assertEquals(":authority", ctx.get(1).getHttpField().getName()); + assertEquals(3, ctx.index(ctx.get(methodPost))); + assertEquals(methodPost, ctx.get(3).getHttpField()); + assertEquals("www-authenticate", ctx.get(61).getHttpField().getName()); + assertNull(ctx.get(62 + ctx.size())); + + // Add 4 more entries + for (int i = 6; i <= 9; i++) + entry[i] = ctx.add(field[i]); + + // Check newest entry is at 1 oldest at 5 + index = 66; + for (int i = 5; i <= 9; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + // check entry 0-4 evicted + for (int i = 0; i <= 4; i++) { + assertNull(ctx.get(field[i])); + assertEquals(0, ctx.index(entry[i])); + } + + + // Add new entries enough so that array queue will wrap + for (int i = 10; i <= 52; i++) + entry[i] = ctx.add(new HttpField("n" + i, "v" + i)); + + index = 66; + for (int i = 48; i <= 52; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + } + + + @Test + void testResize() { + // Only enough space for 5 entries + HpackContext ctx = new HpackContext(38 * 5); + + HttpField[] field = { + new HttpField("fo0", "b0r"), + new HttpField("fo1", "b1r"), + new HttpField("fo2", "b2r"), + new HttpField("fo3", "b3r"), + new HttpField("fo4", "b4r"), + new HttpField("fo5", "b5r"), + new HttpField("fo6", "b6r"), + new HttpField("fo7", "b7r"), + new HttpField("fo8", "b8r"), + new HttpField("fo9", "b9r"), + new HttpField("foA", "bAr"), + }; + Entry[] entry = new Entry[field.length]; + + // Add 5 entries + for (int i = 0; i <= 4; i++) + entry[i] = ctx.add(field[i]); + + assertEquals(5, ctx.size()); + + // check indexes + int index = 66; + for (int i = 0; i <= 4; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + // resize so that only 2 entries may be held + ctx.resize(38 * 2); + assertEquals(2, ctx.size()); + + // check indexes + index = 63; + for (int i = 3; i <= 4; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + // resize so that 6.5 entries may be held + ctx.resize(38 * 6 + 19); + assertEquals(2, ctx.size()); + + // check indexes + index = 63; + for (int i = 3; i <= 4; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + + // Add 5 entries + for (int i = 5; i <= 9; i++) + entry[i] = ctx.add(field[i]); + + assertEquals(6, ctx.size()); + + // check indexes + index = 67; + for (int i = 4; i <= 9; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + + // resize so that only 100 entries may be held + ctx.resize(38 * 100); + assertEquals(6, ctx.size()); + // check indexes + index = 67; + for (int i = 4; i <= 9; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + // add 50 fields + for (int i = 0; i < 50; i++) + ctx.add(new HttpField("n" + i, "v" + i)); + + // check indexes + index = 67 + 50; + for (int i = 4; i <= 9; i++) { + assertEquals(index, ctx.index(entry[i])); + assertEquals(entry[i], ctx.get(index)); + index--; + } + + + } + + @Test + void testStaticHuffmanValues() throws Exception { + HpackContext ctx = new HpackContext(4096); + for (int i = 2; i <= 14; i++) { + Entry entry = ctx.get(i); + assertTrue(entry.isStatic()); + + ByteBuffer buffer = ByteBuffer.wrap(entry.getStaticHuffmanValue()); + int huff = 0xff & buffer.get(); + assertEquals(0x80, (0x80 & huff)); + + int len = NBitInteger.decode(buffer, 7); + + assertEquals(len, buffer.remaining()); + String value = Huffman.decode(buffer); + + assertEquals(entry.getHttpField().getValue(), value); + + } + } + + @Test + void testNameInsensitivity() { + HpackContext ctx = new HpackContext(4096); + assertEquals("content-length", ctx.get("content-length").getHttpField().getName()); + assertEquals("content-length", ctx.get("Content-Length").getHttpField().getName()); + assertTrue(ctx.get("Content-Length").isStatic()); + assertTrue(ctx.get("Content-Type").isStatic()); + + ctx.add(new HttpField("Wibble", "Wobble")); + assertEquals("Wibble", ctx.get("wibble").getHttpField().getName()); + assertEquals("Wibble", ctx.get("Wibble").getHttpField().getName()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackDecoderTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackDecoderTest.java new file mode 100644 index 000000000..ccf89348b --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackDecoderTest.java @@ -0,0 +1,490 @@ +package com.fireflysource.net.http.common.v2.hpack; + + +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpHeader; +import com.fireflysource.net.http.common.model.HttpScheme; +import com.fireflysource.net.http.common.model.MetaData; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +import static com.fireflysource.net.http.common.v2.hpack.HpackException.CompressionException; +import static com.fireflysource.net.http.common.v2.hpack.HpackException.StreamException; +import static org.junit.jupiter.api.Assertions.*; + + +class HpackDecoderTest { + + @Test + void testDecodeD_3() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + // First request + String encoded = "828684410f7777772e6578616d706c652e636f6d"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + MetaData.Request request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + assertFalse(request.iterator().hasNext()); + + // Second request + encoded = "828684be58086e6f2d6361636865"; + buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + Iterator iterator = request.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(new HttpField("cache-control", "no-cache"), iterator.next()); + assertFalse(iterator.hasNext()); + + // Third request + encoded = "828785bf400a637573746f6d2d6b65790c637573746f6d2d76616c7565"; + buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTPS.getValue(), request.getURI().getScheme()); + assertEquals("/index.html", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + iterator = request.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(new HttpField("custom-key", "custom-value"), iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test + void testDecodeD_4() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + // First request + String encoded = "828684418cf1e3c2e5f23a6ba0ab90f4ff"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + MetaData.Request request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + assertFalse(request.iterator().hasNext()); + + // Second request + encoded = "828684be5886a8eb10649cbf"; + buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + Iterator iterator = request.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(new HttpField("cache-control", "no-cache"), iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test + void testDecodeWithArrayOffset() throws Exception { + String value = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; + + HpackDecoder decoder = new HpackDecoder(4096, 8192); + String encoded = "8682418cF1E3C2E5F23a6bA0Ab90F4Ff841f0822426173696320515778685a475270626a70766347567549484e6c633246745a513d3d"; + byte[] bytes = TypeUtils.fromHexString(encoded); + byte[] array = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, array, 1, bytes.length); + ByteBuffer buffer = ByteBuffer.wrap(array, 1, bytes.length).slice(); + + MetaData.Request request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + assertEquals(1, request.getFields().size()); + HttpField field = request.iterator().next(); + assertEquals(HttpHeader.AUTHORIZATION, field.getHeader()); + assertEquals(value, field.getValue()); + } + + @Test + void testDecodeHuffmanWithArrayOffset() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "8286418cf1e3c2e5f23a6ba0ab90f4ff84"; + byte[] bytes = TypeUtils.fromHexString(encoded); + byte[] array = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, array, 1, bytes.length); + ByteBuffer buffer = ByteBuffer.wrap(array, 1, bytes.length).slice(); + + MetaData.Request request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("www.example.com", request.getURI().getHost()); + assertFalse(request.iterator().hasNext()); + } + + @Test + void testNghttpx() throws Exception { + // Response encoded by nghttpx + String encoded = "886196C361Be940b6a65B6850400B8A00571972e080a62D1Bf5f87497cA589D34d1f9a0f0d0234327690Aa69D29aFcA954D3A5358980Ae112e0f7c880aE152A9A74a6bF3"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + HpackDecoder decoder = new HpackDecoder(4096, 8192); + MetaData.Response response = (MetaData.Response) decoder.decode(buffer); + + assertEquals(200, response.getStatus()); + assertEquals(6, response.getFields().size()); + assertTrue(response.getFields().contains(new HttpField(HttpHeader.DATE, "Fri, 15 Jul 2016 02:36:20 GMT"))); + assertTrue(response.getFields().contains(new HttpField(HttpHeader.CONTENT_TYPE, "text/html"))); + assertTrue(response.getFields().contains(new HttpField(HttpHeader.CONTENT_ENCODING, ""))); + assertTrue(response.getFields().contains(new HttpField(HttpHeader.CONTENT_LENGTH, "42"))); + assertTrue(response.getFields().contains(new HttpField(HttpHeader.SERVER, "nghttpx nghttp2/1.12.0"))); + assertTrue(response.getFields().contains(new HttpField(HttpHeader.VIA, "1.1 nghttpx"))); + } + + @Test + void testResize() throws Exception { + String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + HpackDecoder decoder = new HpackDecoder(4096, 8192); + MetaData metaData = decoder.decode(buffer); + assertEquals("localhost0", metaData.getFields().get(HttpHeader.HOST)); + assertEquals("abcdefghij", metaData.getFields().get(HttpHeader.COOKIE)); + assertEquals(50, decoder.getHpackContext().getMaxDynamicTableSize()); + assertEquals(1, decoder.getHpackContext().size()); + + + } + + @Test + void testBadResize() throws Exception { + /* + 4. Dynamic Table Management + 4.2. Maximum Table Size + × 1: Sends a dynamic table size update at the end of header block + -> The endpoint MUST treat this as a decoding error. + Expected: GOAWAY Frame (Error Code: COMPRESSION_ERROR) + Connection closed + */ + + String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f20"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + HpackDecoder decoder = new HpackDecoder(4096, 8192); + try { + decoder.decode(buffer); + fail(); + } catch (CompressionException e) { + assertTrue(e.getMessage().contains("Dynamic table resize after fields")); + } + } + + @Test + void testTooBigToIndex() throws Exception { + String encoded = "3f610f17FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + HpackDecoder decoder = new HpackDecoder(128, 8192); + MetaData metaData = decoder.decode(buffer); + + assertEquals(0, decoder.getHpackContext().getDynamicTableSize()); + assertTrue(metaData.getFields().get("host").startsWith("This is a very large field")); + } + + @Test + void testUnknownIndex() throws Exception { + String encoded = "BE"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + HpackDecoder decoder = new HpackDecoder(128, 8192); + try { + decoder.decode(buffer); + fail(); + } catch (HpackException.SessionException e) { + assertTrue(e.getMessage().startsWith("Unknown index")); + } + + } + + /* 8.1.2.1. Pseudo-Header Fields */ + @Test + void test8_1_2_1_PsuedoHeaderFields() throws Exception { + // 1:Sends a HEADERS frame that contains a unknown pseudo-header field + MetaDataBuilder mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(":unknown", "value")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Unknown pseudo header")); + } + + // 2: Sends a HEADERS frame that contains the pseudo-header field defined for response + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/path")); + mdb.emit(new HttpField(HttpHeader.C_STATUS, "100")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Request and Response headers")); + } + + // 3: Sends a HEADERS frame that contains a pseudo-header field as trailers + + // 4: Sends a HEADERS frame that contains a pseudo-header field that appears in a header block after a regular header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/path")); + mdb.emit(new HttpField("Accept", "No Compromise")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Pseudo header :authority after fields")); + } + } + + @Test + void test8_1_2_2_ConnectionSpecificHeaderFields() throws Exception { + MetaDataBuilder mdb; + + // 1: Sends a HEADERS frame that contains the connection-specific header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.CONNECTION, "value")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Connection specific field 'Connection'")); + } + + // 2: Sends a HEADERS frame that contains the TE header field with any value other than "trailers" + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.TE, "not_trailers")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Unsupported TE value 'not_trailers'")); + } + + + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.CONNECTION, "TE")); + mdb.emit(new HttpField(HttpHeader.TE, "trailers")); + assertNotNull(mdb.build()); + } + + + @Test + void test8_1_2_3_RequestPseudoHeaderFields() throws Exception { + MetaDataBuilder mdb; + + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); + assertTrue(mdb.build() instanceof MetaData.Request); + + + // 1: Sends a HEADERS frame with empty ":path" pseudo-header field + mdb = new MetaDataBuilder(4096); + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("No Path")); + } + + // 2: Sends a HEADERS frame that omits ":method" pseudo-header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("No Method")); + } + + + // 3: Sends a HEADERS frame that omits ":scheme" pseudo-header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("No Scheme")); + } + + // 4: Sends a HEADERS frame that omits ":path" pseudo-header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("No Path")); + } + + // 5: Sends a HEADERS frame with duplicated ":method" pseudo-header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Duplicate")); + } + + // 6: Sends a HEADERS frame with duplicated ":scheme" pseudo-header field + mdb = new MetaDataBuilder(4096); + mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); + mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); + mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); + try { + mdb.build(); + fail(); + } catch (StreamException ex) { + assertTrue(ex.getMessage().contains("Duplicate")); + } + } + + + @Test + void testHuffmanEncodedStandard() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "82868441" + "83" + "49509F"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + MetaData.Request request = (MetaData.Request) decoder.decode(buffer); + + assertEquals("GET", request.getMethod()); + assertEquals(HttpScheme.HTTP.getValue(), request.getURI().getScheme()); + assertEquals("/", request.getURI().getPath()); + assertEquals("test", request.getURI().getHost()); + assertFalse(request.iterator().hasNext()); + } + + + /* 5.2.1: Sends a Huffman-encoded string literal representation with padding longer than 7 bits */ + @Test + void testHuffmanEncodedExtraPadding() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "82868441" + "84" + "49509FFF"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + try { + decoder.decode(buffer); + fail(); + } catch (CompressionException ex) { + assertTrue(ex.getMessage().contains("Bad termination")); + } + } + + + /* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */ + @Test + void testHuffmanEncodedZeroPadding() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "82868441" + "83" + "495090"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + try { + decoder.decode(buffer); + fail(); + } catch (CompressionException ex) { + assertTrue(ex.getMessage().contains("Incorrect padding")); + } + } + + + /* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */ + @Test + void testHuffmanEncodedWithEOS() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "82868441" + "87" + "497FFFFFFF427F"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + try { + decoder.decode(buffer); + fail(); + } catch (CompressionException ex) { + assertTrue(ex.getMessage().contains("EOS in content")); + } + } + + + @Test + void testHuffmanEncodedOneIncompleteOctet() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "82868441" + "81" + "FE"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + try { + decoder.decode(buffer); + fail(); + } catch (CompressionException ex) { + assertTrue(ex.getMessage().contains("Bad termination")); + } + } + + + @Test + void testHuffmanEncodedTwoIncompleteOctet() throws Exception { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "82868441" + "82" + "FFFE"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + + try { + decoder.decode(buffer); + fail(); + } catch (CompressionException ex) { + assertTrue(ex.getMessage().contains("Bad termination")); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackEncoderTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackEncoderTest.java new file mode 100644 index 000000000..03f41aa55 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackEncoderTest.java @@ -0,0 +1,241 @@ +package com.fireflysource.net.http.common.v2.hpack; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.http.common.model.HttpField; +import com.fireflysource.net.http.common.model.HttpFields; +import com.fireflysource.net.http.common.model.HttpVersion; +import com.fireflysource.net.http.common.model.MetaData; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class HpackEncoderTest { + + @Test + void testUnknownFieldsContextManagement() { + HpackEncoder encoder = new HpackEncoder(38 * 5); + HttpFields fields = new HttpFields(); + + + HttpField[] field = { + new HttpField("fo0", "b0r"), + new HttpField("fo1", "b1r"), + new HttpField("fo2", "b2r"), + new HttpField("fo3", "b3r"), + new HttpField("fo4", "b4r"), + new HttpField("fo5", "b5r"), + new HttpField("fo6", "b6r"), + new HttpField("fo7", "b7r"), + new HttpField("fo8", "b8r"), + new HttpField("fo9", "b9r"), + new HttpField("foA", "bAr"), + }; + + // Add 4 entries + for (int i = 0; i <= 3; i++) { + fields.add(field[i]); + } + + // encode them + ByteBuffer buffer = ByteBuffer.allocate(4096); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // something encoded! + assertTrue(buffer.remaining() > 0); + + // All are in the dynamic table + assertEquals(4, encoder.getHpackContext().size()); + + // encode exact same fields again! + buffer.clear(); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // All are in the dynamic table + assertEquals(4, encoder.getHpackContext().size()); + + // Add 4 more fields + for (int i = 4; i <= 7; i++) + fields.add(field[i]); + + // encode + buffer.clear(); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // something encoded! + assertTrue(buffer.remaining() > 0); + + // max dynamic table size reached + assertEquals(5, encoder.getHpackContext().size()); + + + // remove some fields + for (int i = 0; i <= 7; i += 2) + fields.remove(field[i].getName()); + + // encode + buffer.clear(); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // something encoded! + assertTrue(buffer.remaining() > 0); + + // max dynamic table size reached + assertEquals(5, encoder.getHpackContext().size()); + + + // remove another fields + fields.remove(field[1].getName()); + + // encode + buffer.clear(); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + // something encoded! + assertTrue(buffer.remaining() > 0); + + // max dynamic table size reached + assertEquals(5, encoder.getHpackContext().size()); + + + // re add the field + + fields.add(field[1]); + + // encode + buffer.clear(); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // something encoded! + assertTrue(buffer.remaining() > 0); + + // max dynamic table size reached + assertEquals(5, encoder.getHpackContext().size()); + + } + + + @Test + void testNeverIndexSetCookie() { + HpackEncoder encoder = new HpackEncoder(38 * 5); + ByteBuffer buffer = ByteBuffer.allocate(4096); + + HttpFields fields = new HttpFields(); + fields.put("set-cookie", "some cookie value"); + + // encode + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // something was encoded! + assertTrue(buffer.remaining() > 0); + + // empty dynamic table + assertEquals(0, encoder.getHpackContext().size()); + + + // encode again + buffer.clear(); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + // something encoded! + assertTrue(buffer.remaining() > 0); + + // empty dynamic table + assertEquals(0, encoder.getHpackContext().size()); + + } + + + @Test + public void testFieldLargerThanTable() { + HttpFields fields = new HttpFields(); + + HpackEncoder encoder = new HpackEncoder(128); + ByteBuffer buffer0 = BufferUtils.allocate(4096); + int pos = BufferUtils.flipToFill(buffer0); + encoder.encode(buffer0, new MetaData(HttpVersion.HTTP_2, fields)); + BufferUtils.flipToFlush(buffer0, pos); + + encoder = new HpackEncoder(128); + fields.add(new HttpField("user-agent", "firefly/test")); + ByteBuffer buffer1 = BufferUtils.allocate(4096); + pos = BufferUtils.flipToFill(buffer1); + encoder.encode(buffer1, new MetaData(HttpVersion.HTTP_2, fields)); + BufferUtils.flipToFlush(buffer1, pos); + + encoder = new HpackEncoder(128); + encoder.setValidateEncoding(false); + fields.add(new HttpField(":path", + "This is a very large field, whose size is larger than the dynamic table so it should not be indexed as it will not fit in the table ever!" + + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX " + + "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY " + + "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ ")); + ByteBuffer buffer2 = BufferUtils.allocate(4096); + pos = BufferUtils.flipToFill(buffer2); + encoder.encode(buffer2, new MetaData(HttpVersion.HTTP_2, fields)); + BufferUtils.flipToFlush(buffer2, pos); + + encoder = new HpackEncoder(128); + encoder.setValidateEncoding(false); + fields.add(new HttpField("host", "somehost")); + ByteBuffer buffer = BufferUtils.allocate(4096); + pos = BufferUtils.flipToFill(buffer); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + BufferUtils.flipToFlush(buffer, pos); + + //System.err.println(BufferUtils.toHexString(buffer0)); + //System.err.println(BufferUtils.toHexString(buffer1)); + //System.err.println(BufferUtils.toHexString(buffer2)); + //System.err.println(BufferUtils.toHexString(buffer)); + + // something encoded! + assertTrue(buffer.remaining() > 0); + + // check first field is static index name and dynamic index body + assertEquals(1, (buffer.get(buffer0.remaining()) & 0xFF) >> 6); + + // check first field is static index name and literal body + assertEquals(0, (buffer.get(buffer1.remaining()) & 0xFF) >> 4); + + // check first field is static index name and dynamic index body + assertEquals(1, (buffer.get(buffer2.remaining()) & 0xFF) >> 6); + + // Only first and third fields are put in the table + HpackContext context = encoder.getHpackContext(); + assertEquals(2, context.size()); + assertEquals("host", context.get(HpackContext.STATIC_SIZE + 1).getHttpField().getName()); + assertEquals("user-agent", context.get(HpackContext.STATIC_SIZE + 2).getHttpField().getName()); + assertEquals(context.get(HpackContext.STATIC_SIZE + 1).getSize() + context.get(HpackContext.STATIC_SIZE + 2).getSize(), context.getDynamicTableSize()); + } + + @Test + void testResize() { + HttpFields fields = new HttpFields(); + fields.add("host", "localhost0"); + fields.add("cookie", "abcdefghij"); + + HpackEncoder encoder = new HpackEncoder(4096); + + ByteBuffer buffer = ByteBuffer.allocate(4096); + encoder.encodeMaxDynamicTableSize(buffer, 0); + encoder.setRemoteMaxDynamicTableSize(50); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields)); + buffer.flip(); + + HpackContext context = encoder.getHpackContext(); + + assertEquals(50, context.getMaxDynamicTableSize()); + assertEquals(1, context.size()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackTest.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackTest.java new file mode 100644 index 000000000..4354db2fc --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/HpackTest.java @@ -0,0 +1,172 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.net.http.common.codec.DateGenerator; +import com.fireflysource.net.http.common.codec.PreEncodedHttpField; +import com.fireflysource.net.http.common.model.*; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import static com.fireflysource.net.http.common.model.MetaData.Response; +import static org.junit.jupiter.api.Assertions.*; + + +class HpackTest { + + final static HttpField ServerFirefly = new PreEncodedHttpField(HttpHeader.SERVER, "firefly"); + final static HttpField XPowerFirefly = new PreEncodedHttpField(HttpHeader.X_POWERED_BY, "firefly"); + final static HttpField Date = new PreEncodedHttpField(HttpHeader.DATE, DateGenerator.formatDate(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()))); + + @Test + void encodeDecodeResponseTest() { + HpackEncoder encoder = new HpackEncoder(); + HpackDecoder decoder = new HpackDecoder(4096, 8192); + ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); + + HttpFields fields0 = new HttpFields(); + fields0.add(HttpHeader.CONTENT_TYPE, "text/html"); + fields0.add(HttpHeader.CONTENT_LENGTH, "1024"); + fields0.add(new HttpField(HttpHeader.CONTENT_ENCODING, (String) null)); + fields0.add(ServerFirefly); + fields0.add(XPowerFirefly); + fields0.add(Date); + fields0.add(HttpHeader.SET_COOKIE, "abcdefghijklmnopqrstuvwxyz"); + fields0.add("custom-key", "custom-value"); + MetaData.Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0); + + buffer.clear(); + encoder.encode(buffer, original0); + buffer.flip(); + Response decoded0 = (Response) decoder.decode(buffer); + original0.getFields().put(new HttpField(HttpHeader.CONTENT_ENCODING, "")); + assertMetadataSame(original0, decoded0); + + // Same again? + buffer.clear(); + encoder.encode(buffer, original0); + buffer.flip(); + Response decoded0b = (Response) decoder.decode(buffer); + + assertMetadataSame(original0, decoded0b); + + HttpFields fields1 = new HttpFields(); + fields1.add(HttpHeader.CONTENT_TYPE, "text/plain"); + fields1.add(HttpHeader.CONTENT_LENGTH, "1234"); + fields1.add(HttpHeader.CONTENT_ENCODING, " "); + fields1.add(ServerFirefly); + fields1.add(XPowerFirefly); + fields1.add(Date); + fields1.add("Custom-Key", "Other-Value"); + Response original1 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields1); + + // Same again? + buffer.clear(); + encoder.encode(buffer, original1); + buffer.flip(); + Response decoded1 = (Response) decoder.decode(buffer); + + assertMetadataSame(original1, decoded1); + assertEquals("custom-key", decoded1.getFields().getField("Custom-Key").getName()); + } + + @Test + void encodeDecodeTooLargeTest() { + HpackEncoder encoder = new HpackEncoder(); + HpackDecoder decoder = new HpackDecoder(4096, 164); + ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); + + HttpFields fields0 = new HttpFields(); + fields0.add("1234567890", "1234567890123456789012345678901234567890"); + fields0.add("Cookie", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); + MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0); + + buffer.clear(); + encoder.encode(buffer, original0); + buffer.flip(); + MetaData decoded0 = (MetaData) decoder.decode(buffer); + + assertMetadataSame(original0, decoded0); + + HttpFields fields1 = new HttpFields(); + fields1.add("1234567890", "1234567890123456789012345678901234567890"); + fields1.add("Cookie", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); + fields1.add("x", "y"); + MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1); + + buffer.clear(); + encoder.encode(buffer, original1); + buffer.flip(); + try { + decoder.decode(buffer); + fail(); + } catch (HpackException.SessionException e) { + assertTrue(e.getMessage().contains("Header too large")); + } + } + + @Test + void evictReferencedFieldTest() { + HpackEncoder encoder = new HpackEncoder(200, 200); + HpackDecoder decoder = new HpackDecoder(200, 1024); + ByteBuffer buffer = ByteBuffer.allocate(16 * 1024); + + HttpFields fields0 = new HttpFields(); + fields0.add("123456789012345678901234567890123456788901234567890", "value"); + fields0.add("foo", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); + MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0); + + buffer.clear(); + encoder.encode(buffer, original0); + buffer.flip(); + MetaData decoded0 = (MetaData) decoder.decode(buffer); + + assertEquals(2, encoder.getHpackContext().size()); + assertEquals(2, decoder.getHpackContext().size()); + assertEquals("123456789012345678901234567890123456788901234567890", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName()); + assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 0).getHttpField().getName()); + + assertMetadataSame(original0, decoded0); + + HttpFields fields1 = new HttpFields(); + fields1.add("123456789012345678901234567890123456788901234567890", "other_value"); + fields1.add("x", "y"); + MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1); + + buffer.clear(); + encoder.encode(buffer, original1); + buffer.flip(); + MetaData decoded1 = (MetaData) decoder.decode(buffer); + assertMetadataSame(original1, decoded1); + + assertEquals(2, encoder.getHpackContext().size()); + assertEquals(2, decoder.getHpackContext().size()); + assertEquals("x", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length).getHttpField().getName()); + assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName()); + } + + private void assertMetadataSame(MetaData.Response expected, MetaData.Response actual) { + assertEquals(expected.getStatus(), actual.getStatus()); + assertEquals(expected.getReason(), actual.getReason()); + assertMetadataSame((MetaData) expected, (MetaData) actual); + } + + private void assertMetadataSame(MetaData expected, MetaData actual) { + assertEquals(expected.getContentLength(), actual.getContentLength()); + assertEquals(expected.getHttpVersion(), actual.getHttpVersion()); + assertHttpFieldsSame("Metadata.fields", expected.getFields(), actual.getFields()); + } + + private void assertHttpFieldsSame(String message, HttpFields expected, HttpFields actual) { + assertEquals(expected.size(), actual.size(), message); + + for (HttpField actualField : actual) { + if ("DATE".equalsIgnoreCase(actualField.getName())) { + // skip comparison on Date, as these values can often differ by 1 second + // during testing. + continue; + } + assertTrue(expected.contains(actualField), message + ".contains(" + actualField + ")"); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/TestHuffman.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/TestHuffman.java new file mode 100644 index 000000000..dd719ceaf --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/TestHuffman.java @@ -0,0 +1,62 @@ +package com.fireflysource.net.http.common.v2.hpack; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.object.TypeUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TestHuffman { + + static Stream data() { + return Stream.of( + new String[][]{ + {"D.4.1", "f1e3c2e5f23a6ba0ab90f4ff", "www.example.com"}, + {"D.4.2", "a8eb10649cbf", "no-cache"}, + {"D.6.1k", "6402", "302"}, + {"D.6.1v", "aec3771a4b", "private"}, + {"D.6.1d", "d07abe941054d444a8200595040b8166e082a62d1bff", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"D.6.1l", "9d29ad171863c78f0b97c8e9ae82ae43d3", "https://www.example.com"}, + {"D.6.2te", "640cff", "303"}, + }).map(Arguments::of); + } + + @ParameterizedTest(name = "[{index}] spec={0}") + @MethodSource("data") + void testDecode(String specSection, String hex, String expected) throws Exception { + byte[] encoded = TypeUtils.fromHexString(hex); + String decoded = Huffman.decode(ByteBuffer.wrap(encoded)); + assertEquals(expected, decoded, specSection); + } + + @ParameterizedTest(name = "[{index}] spec={0}") + @MethodSource("data") + void testEncode(String specSection, String hex, String expected) { + ByteBuffer buf = ByteBuffer.allocate(1024); + Huffman.encode(buf, expected); + buf.flip(); + String encoded = TypeUtils.toHexString(BufferUtils.toArray(buf)).toLowerCase(Locale.ENGLISH); + assertEquals(hex, encoded, specSection); + assertEquals(hex.length() / 2, Huffman.octetsNeeded(expected)); + } + + @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name + @ValueSource(chars = {(char) 128, (char) 0, (char) -1, ' ' - 1}) + void testEncode8859Only(char bad) { + String s = "bad '" + bad + "'"; + + assertEquals(-1, Huffman.octetsNeeded(s)); + assertThrows(BufferOverflowException.class, () -> Huffman.encode(BufferUtils.allocate(32), s)); + } + + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/TestNBitInteger.java b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/TestNBitInteger.java new file mode 100644 index 000000000..6e4f29cf7 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/http/common/v2/hpack/TestNBitInteger.java @@ -0,0 +1,177 @@ +package com.fireflysource.net.http.common.v2.hpack; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.object.TypeUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class TestNBitInteger { + + @Test + void testOctetsNeeded() { + assertEquals(0, NBitInteger.octectsNeeded(5, 10)); + assertEquals(2, NBitInteger.octectsNeeded(5, 1337)); + assertEquals(1, NBitInteger.octectsNeeded(8, 42)); + assertEquals(3, NBitInteger.octectsNeeded(8, 1337)); + + assertEquals(0, NBitInteger.octectsNeeded(6, 62)); + assertEquals(1, NBitInteger.octectsNeeded(6, 63)); + assertEquals(1, NBitInteger.octectsNeeded(6, 64)); + assertEquals(2, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x01)); + assertEquals(3, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80)); + assertEquals(4, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80)); + } + + @Test + void testEncode() { + testEncode(6, 0, "00"); + testEncode(6, 1, "01"); + testEncode(6, 62, "3e"); + testEncode(6, 63, "3f00"); + testEncode(6, 63 + 1, "3f01"); + testEncode(6, 63 + 0x7e, "3f7e"); + testEncode(6, 63 + 0x7f, "3f7f"); + testEncode(6, 63 + 0x00 + 0x80 * 0x01, "3f8001"); + testEncode(6, 63 + 0x01 + 0x80 * 0x01, "3f8101"); + testEncode(6, 63 + 0x7f + 0x80 * 0x01, "3fFf01"); + testEncode(6, 63 + 0x00 + 0x80 * 0x02, "3f8002"); + testEncode(6, 63 + 0x01 + 0x80 * 0x02, "3f8102"); + testEncode(6, 63 + 0x7f + 0x80 * 0x7f, "3fFf7f"); + testEncode(6, 63 + 0x00 + 0x80 * 0x80, "3f808001"); + testEncode(6, 63 + 0x7f + 0x80 * 0x80 * 0x7f, "3fFf807f"); + testEncode(6, 63 + 0x00 + 0x80 * 0x80 * 0x80, "3f80808001"); + + testEncode(8, 0, "00"); + testEncode(8, 1, "01"); + testEncode(8, 128, "80"); + testEncode(8, 254, "Fe"); + testEncode(8, 255, "Ff00"); + testEncode(8, 255 + 1, "Ff01"); + testEncode(8, 255 + 0x7e, "Ff7e"); + testEncode(8, 255 + 0x7f, "Ff7f"); + testEncode(8, 255 + 0x80, "Ff8001"); + testEncode(8, 255 + 0x00 + 0x80 * 0x80, "Ff808001"); + } + + void testEncode(int n, int i, String expected) { + ByteBuffer buf = ByteBuffer.allocate(16); + if (n < 8) + buf.put((byte) 0x00); + NBitInteger.encode(buf, n, i); + buf.flip(); + String r = TypeUtils.toHexString(BufferUtils.toArray(buf)); + assertEquals(expected, r); + + assertEquals(expected.length() / 2, (n < 8 ? 1 : 0) + NBitInteger.octectsNeeded(n, i)); + } + + @Test + void testDecode() { + testDecode(6, 0, "00"); + testDecode(6, 1, "01"); + testDecode(6, 62, "3e"); + testDecode(6, 63, "3f00"); + testDecode(6, 63 + 1, "3f01"); + testDecode(6, 63 + 0x7e, "3f7e"); + testDecode(6, 63 + 0x7f, "3f7f"); + testDecode(6, 63 + 0x80, "3f8001"); + testDecode(6, 63 + 0x81, "3f8101"); + testDecode(6, 63 + 0x7f + 0x80 * 0x01, "3fFf01"); + testDecode(6, 63 + 0x00 + 0x80 * 0x02, "3f8002"); + testDecode(6, 63 + 0x01 + 0x80 * 0x02, "3f8102"); + testDecode(6, 63 + 0x7f + 0x80 * 0x7f, "3fFf7f"); + testDecode(6, 63 + 0x00 + 0x80 * 0x80, "3f808001"); + testDecode(6, 63 + 0x7f + 0x80 * 0x80 * 0x7f, "3fFf807f"); + testDecode(6, 63 + 0x00 + 0x80 * 0x80 * 0x80, "3f80808001"); + + testDecode(8, 0, "00"); + testDecode(8, 1, "01"); + testDecode(8, 128, "80"); + testDecode(8, 254, "Fe"); + testDecode(8, 255, "Ff00"); + testDecode(8, 255 + 1, "Ff01"); + testDecode(8, 255 + 0x7e, "Ff7e"); + testDecode(8, 255 + 0x7f, "Ff7f"); + testDecode(8, 255 + 0x80, "Ff8001"); + testDecode(8, 255 + 0x00 + 0x80 * 0x80, "Ff808001"); + } + + void testDecode(int n, int expected, String encoded) { + ByteBuffer buf = ByteBuffer.wrap(TypeUtils.fromHexString(encoded)); + buf.position(n == 8 ? 0 : 1); + assertEquals(expected, NBitInteger.decode(buf, n)); + } + + @Test + void testEncodeExampleD_1_1() { + ByteBuffer buf = ByteBuffer.allocate(16); + buf.put((byte) 0x77); + buf.put((byte) 0xFF); + NBitInteger.encode(buf, 5, 10); + buf.flip(); + + String r = TypeUtils.toHexString(BufferUtils.toArray(buf)); + + assertEquals("77Ea", r); + + } + + @Test + void testDecodeExampleD_1_1() { + ByteBuffer buf = ByteBuffer.wrap(TypeUtils.fromHexString("77EaFF")); + buf.position(2); + + assertEquals(10, NBitInteger.decode(buf, 5)); + } + + @Test + void testEncodeExampleD_1_2() { + ByteBuffer buf = ByteBuffer.allocate(16); + + buf.put((byte) 0x88); + buf.put((byte) 0x00); + NBitInteger.encode(buf, 5, 1337); + buf.flip(); + + String r = TypeUtils.toHexString(BufferUtils.toArray(buf)); + + assertEquals("881f9a0a", r); + + } + + @Test + void testDecodeExampleD_1_2() { + ByteBuffer buf = ByteBuffer.wrap(TypeUtils.fromHexString("881f9a0aff")); + buf.position(2); + + assertEquals(1337, NBitInteger.decode(buf, 5)); + } + + @Test + void testEncodeExampleD_1_3() { + ByteBuffer buf = ByteBuffer.allocate(16); + buf.put((byte) 0x88); + buf.put((byte) 0xFF); + NBitInteger.encode(buf, 8, 42); + buf.flip(); + + String r = TypeUtils.toHexString(BufferUtils.toArray(buf)); + + assertEquals("88Ff2a", r); + + } + + @Test + void testDecodeExampleD_1_3() { + ByteBuffer buf = ByteBuffer.wrap(TypeUtils.fromHexString("882aFf")); + buf.position(1); + + assertEquals(42, NBitInteger.decode(buf, 8)); + } + +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/ClosePayloadParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/ClosePayloadParserTest.java new file mode 100644 index 000000000..aff1c973d --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/ClosePayloadParserTest.java @@ -0,0 +1,43 @@ +package com.fireflysource.net.websocket.common.decoder; + + +import com.fireflysource.net.websocket.common.model.*; +import com.fireflysource.net.websocket.common.utils.MaskedByteBuffer; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class ClosePayloadParserTest { + @Test + public void testGameOver() { + String expectedReason = "Game Over"; + + byte[] utf = expectedReason.getBytes(StandardCharsets.UTF_8); + ByteBuffer payload = ByteBuffer.allocate(utf.length + 2); + payload.putChar((char) StatusCode.NORMAL); + payload.put(utf, 0, utf.length); + payload.flip(); + + ByteBuffer buf = ByteBuffer.allocate(24); + buf.put((byte) (0x80 | OpCode.CLOSE)); // fin + close + buf.put((byte) (0x80 | payload.remaining())); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, payload); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.CLOSE, 1); + CloseInfo close = new CloseInfo(capture.getFrames().poll()); + assertEquals(StatusCode.NORMAL, close.getStatusCode()); + assertEquals(expectedReason, close.getReason()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/ParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/ParserTest.java new file mode 100644 index 000000000..7947821f5 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/ParserTest.java @@ -0,0 +1,206 @@ +package com.fireflysource.net.websocket.common.decoder; + +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.encoder.UnitGenerator; +import com.fireflysource.net.websocket.common.exception.ProtocolException; +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.model.*; +import com.fireflysource.net.websocket.common.utils.Hex; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ParserTest { + /** + * Similar to the server side 5.15 testcase. A normal 2 fragment text text message, followed by another continuation. + */ + @Test + public void testParseCase515() { + List send = new ArrayList<>(); + send.add(new TextFrame().setPayload("fragment1").setFin(false)); + send.add(new ContinuationFrame().setPayload("fragment2").setFin(true)); + send.add(new ContinuationFrame().setPayload("fragment3").setFin(false)); // bad frame + send.add(new TextFrame().setPayload("fragment4").setFin(true)); + send.add(new CloseInfo(StatusCode.NORMAL).asFrame()); + + ByteBuffer completeBuf = UnitGenerator.generate(send); + UnitParser parser = new UnitParser(); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + ProtocolException x = assertThrows(ProtocolException.class, () -> parser.parseQuietly(completeBuf)); + assertTrue(x.getMessage().contains("CONTINUATION frame without prior !FIN")); + } + + /** + * Similar to the server side 5.18 testcase. Text message fragmented as 2 frames, both as opcode=TEXT + */ + @Test + public void testParseCase518() { + List send = new ArrayList<>(); + send.add(new TextFrame().setPayload("fragment1").setFin(false)); + send.add(new TextFrame().setPayload("fragment2").setFin(true)); // bad frame, must be continuation + send.add(new CloseInfo(StatusCode.NORMAL).asFrame()); + + ByteBuffer completeBuf = UnitGenerator.generate(send); + UnitParser parser = new UnitParser(); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + ProtocolException x = assertThrows(ProtocolException.class, () -> parser.parseQuietly(completeBuf)); + assertTrue(x.getMessage().contains("Unexpected TEXT frame")); + } + + /** + * Similar to the server side 5.19 testcase. text message, send in 5 frames/fragments, with 2 pings in the mix. + */ + @Test + public void testParseCase519() { + List send = new ArrayList<>(); + send.add(new TextFrame().setPayload("f1").setFin(false)); + send.add(new ContinuationFrame().setPayload(",f2").setFin(false)); + send.add(new PingFrame().setPayload("pong-1")); + send.add(new ContinuationFrame().setPayload(",f3").setFin(false)); + send.add(new ContinuationFrame().setPayload(",f4").setFin(false)); + send.add(new PingFrame().setPayload("pong-2")); + send.add(new ContinuationFrame().setPayload(",f5").setFin(true)); + send.add(new CloseInfo(StatusCode.NORMAL).asFrame()); + + ByteBuffer completeBuf = UnitGenerator.generate(send); + UnitParser parser = new UnitParser(); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parseQuietly(completeBuf); + + capture.assertHasFrame(OpCode.TEXT, 1); + capture.assertHasFrame(OpCode.CONTINUATION, 4); + capture.assertHasFrame(OpCode.CLOSE, 1); + capture.assertHasFrame(OpCode.PING, 2); + } + + /** + * Similar to the server side 5.6 testcase. pong, then text, then close frames. + */ + @Test + public void testParseCase56() { + List send = new ArrayList<>(); + send.add(new PongFrame().setPayload("ping")); + send.add(new TextFrame().setPayload("hello, world")); + send.add(new CloseInfo(StatusCode.NORMAL).asFrame()); + + ByteBuffer completeBuf = UnitGenerator.generate(send); + UnitParser parser = new UnitParser(); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(completeBuf); + + capture.assertHasFrame(OpCode.TEXT, 1); + capture.assertHasFrame(OpCode.CLOSE, 1); + capture.assertHasFrame(OpCode.PONG, 1); + } + + /** + * Similar to the server side 6.2.3 testcase. Lots of small 1 byte UTF8 Text frames, representing 1 overall text message. + */ + @Test + public void testParseCase623() { + String utf8 = "Hello-\uC2B5@\uC39F\uC3A4\uC3BC\uC3A0\uC3A1-UTF-8!!"; + byte[] msg = StringUtils.getUtf8Bytes(utf8); + + List send = new ArrayList<>(); + int textCount = 0; + int continuationCount = 0; + int len = msg.length; + boolean continuation = false; + byte[] mini; + for (int i = 0; i < len; i++) { + DataFrame frame = null; + if (continuation) { + frame = new ContinuationFrame(); + continuationCount++; + } else { + frame = new TextFrame(); + textCount++; + } + mini = new byte[1]; + mini[0] = msg[i]; + frame.setPayload(ByteBuffer.wrap(mini)); + boolean isLast = (i >= (len - 1)); + frame.setFin(isLast); + send.add(frame); + continuation = true; + } + send.add(new CloseInfo(StatusCode.NORMAL).asFrame()); + + ByteBuffer completeBuf = UnitGenerator.generate(send); + UnitParser parser = new UnitParser(); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(completeBuf); + + capture.assertHasFrame(OpCode.TEXT, textCount); + capture.assertHasFrame(OpCode.CONTINUATION, continuationCount); + capture.assertHasFrame(OpCode.CLOSE, 1); + } + + @Test + public void testParseNothing() { + ByteBuffer buf = ByteBuffer.allocate(16); + // Put nothing in the buffer. + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + assertEquals(0, capture.getFrames().size()); + } + + @Test + public void testWindowedParseLargeFrame() { + // Create frames + byte[] payload = new byte[65536]; + Arrays.fill(payload, (byte) '*'); + + List frames = new ArrayList<>(); + TextFrame text = new TextFrame(); + text.setPayload(ByteBuffer.wrap(payload)); + text.setMask(Hex.asByteArray("11223344")); + frames.add(text); + frames.add(new CloseInfo(StatusCode.NORMAL).asFrame()); + + // Build up raw (network bytes) buffer + ByteBuffer networkBytes = UnitGenerator.generate(frames); + + // Parse, in 4096 sized windows + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + while (networkBytes.remaining() > 0) { + ByteBuffer window = networkBytes.slice(); + int windowSize = Math.min(window.remaining(), 4096); + window.limit(windowSize); + parser.parse(window); + networkBytes.position(networkBytes.position() + windowSize); + } + + assertEquals(2, capture.getFrames().size()); + WebSocketFrame frame = capture.getFrames().poll(); + assertEquals(OpCode.TEXT, frame.getOpCode()); + ByteBuffer actualPayload = frame.getPayload(); + assertEquals(payload.length, actualPayload.remaining()); + // Should be all '*' characters (if masking is correct) + for (int i = actualPayload.position(); i < actualPayload.remaining(); i++) { + assertEquals((byte) '*', actualPayload.get(i)); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/PingPayloadParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/PingPayloadParserTest.java new file mode 100644 index 000000000..19017e52d --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/PingPayloadParserTest.java @@ -0,0 +1,39 @@ +package com.fireflysource.net.websocket.common.decoder; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.frame.PingFrame; +import com.fireflysource.net.websocket.common.model.IncomingFramesCapture; +import com.fireflysource.net.websocket.common.model.OpCode; +import com.fireflysource.net.websocket.common.model.WebSocketBehavior; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class PingPayloadParserTest { + + @Test + public void testBasicPingParsing() { + ByteBuffer buf = ByteBuffer.allocate(16); + BufferUtils.clearToFill(buf); + buf.put(new byte[] + {(byte) 0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}); + BufferUtils.flipToFlush(buf, 0); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.PING, 1); + PingFrame ping = (PingFrame) capture.getFrames().poll(); + + String actual = BufferUtils.toUTF8String(ping.getPayload()); + assertEquals("Hello", actual); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/RFC6455ExamplesParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/RFC6455ExamplesParserTest.java new file mode 100644 index 000000000..5057fbf79 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/RFC6455ExamplesParserTest.java @@ -0,0 +1,218 @@ +package com.fireflysource.net.websocket.common.decoder; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; +import com.fireflysource.net.websocket.common.model.IncomingFramesCapture; +import com.fireflysource.net.websocket.common.model.OpCode; +import com.fireflysource.net.websocket.common.model.WebSocketBehavior; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * Collection of Example packets as found in RFC 6455 Examples section + */ +public class RFC6455ExamplesParserTest { + + @Test + public void testFragmentedUnmaskedTextMessage() { + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + ByteBuffer buf = ByteBuffer.allocate(16); + BufferUtils.clearToFill(buf); + + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // A fragmented unmasked text message (part 1 of 2 "Hel") + buf.put(new byte[] + {(byte) 0x01, (byte) 0x03, 0x48, (byte) 0x65, 0x6c}); + + // Parse #1 + BufferUtils.flipToFlush(buf, 0); + parser.parse(buf); + + // part 2 of 2 "lo" (A continuation frame of the prior text message) + BufferUtils.flipToFill(buf); + buf.put(new byte[] + {(byte) 0x80, 0x02, 0x6c, 0x6f}); + + // Parse #2 + BufferUtils.flipToFlush(buf, 0); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + capture.assertHasFrame(OpCode.CONTINUATION, 1); + + WebSocketFrame txt = capture.getFrames().poll(); + String actual = BufferUtils.toUTF8String(txt.getPayload()); + assertEquals("Hel", actual); + txt = capture.getFrames().poll(); + actual = BufferUtils.toUTF8String(txt.getPayload()); + assertEquals("lo", actual); + } + + @Test + public void testSingleMaskedPongRequest() { + ByteBuffer buf = ByteBuffer.allocate(16); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // Unmasked Pong request + buf.put(new byte[] + {(byte) 0x8a, (byte) 0x85, 0x37, (byte) 0xfa, 0x21, 0x3d, 0x7f, (byte) 0x9f, 0x4d, 0x51, 0x58}); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.PONG, 1); + + WebSocketFrame pong = capture.getFrames().poll(); + String actual = BufferUtils.toUTF8String(pong.getPayload()); + assertEquals("Hello", actual); + } + + @Test + public void testSingleMaskedTextMessage() { + ByteBuffer buf = ByteBuffer.allocate(16); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // A single-frame masked text message + buf.put(new byte[] + {(byte) 0x81, (byte) 0x85, 0x37, (byte) 0xfa, 0x21, 0x3d, 0x7f, (byte) 0x9f, 0x4d, 0x51, 0x58}); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + + WebSocketFrame txt = capture.getFrames().poll(); + String actual = BufferUtils.toUTF8String(txt.getPayload()); + assertEquals("Hello", actual); + } + + @Test + public void testSingleUnmasked256ByteBinaryMessage() { + int dataSize = 256; + + ByteBuffer buf = ByteBuffer.allocate(dataSize + 10); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // 256 bytes binary message in a single unmasked frame + buf.put(new byte[] + {(byte) 0x82, 0x7E}); + buf.putShort((short) 0x01_00); // 16 bit size + for (int i = 0; i < dataSize; i++) { + buf.put((byte) 0x44); + } + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.BINARY, 1); + + Frame bin = capture.getFrames().poll(); + + assertEquals(dataSize, bin.getPayloadLength()); + + ByteBuffer data = bin.getPayload(); + assertEquals(dataSize, data.remaining()); + + for (int i = 0; i < dataSize; i++) { + assertEquals((byte) 0x44, data.get(i)); + } + } + + @Test + public void testSingleUnmasked64KByteBinaryMessage() { + int dataSize = 1024 * 64; + + ByteBuffer buf = ByteBuffer.allocate((dataSize + 10)); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // 64 Kbytes binary message in a single unmasked frame + buf.put(new byte[] + {(byte) 0x82, 0x7F}); + buf.putLong(dataSize); // 64bit size + for (int i = 0; i < dataSize; i++) { + buf.put((byte) 0x77); + } + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.BINARY, 1); + + Frame bin = capture.getFrames().poll(); + + assertEquals(dataSize, bin.getPayloadLength()); + ByteBuffer data = bin.getPayload(); + assertEquals(dataSize, data.remaining()); + + for (int i = 0; i < dataSize; i++) { + assertEquals((byte) 0x77, data.get(i)); + } + } + + @Test + public void testSingleUnmaskedPingRequest() { + ByteBuffer buf = ByteBuffer.allocate(16); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // Unmasked Ping request + buf.put(new byte[] + {(byte) 0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.PING, 1); + + WebSocketFrame ping = capture.getFrames().poll(); + String actual = BufferUtils.toUTF8String(ping.getPayload()); + assertEquals("Hello", actual); + } + + @Test + public void testSingleUnmaskedTextMessage() { + ByteBuffer buf = ByteBuffer.allocate(16); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // A single-frame unmasked text message + buf.put(new byte[] + {(byte) 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + + WebSocketFrame txt = capture.getFrames().poll(); + String actual = BufferUtils.toUTF8String(txt.getPayload()); + assertEquals("Hello", actual); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/TextPayloadParserTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/TextPayloadParserTest.java new file mode 100644 index 000000000..82357df65 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/TextPayloadParserTest.java @@ -0,0 +1,197 @@ +package com.fireflysource.net.websocket.common.decoder; + +import com.fireflysource.net.websocket.common.exception.MessageTooLargeException; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; +import com.fireflysource.net.websocket.common.model.IncomingFramesCapture; +import com.fireflysource.net.websocket.common.model.OpCode; +import com.fireflysource.net.websocket.common.model.WebSocketBehavior; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import com.fireflysource.net.websocket.common.utils.MaskedByteBuffer; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +public class TextPayloadParserTest { + + @Test + public void testFrameTooLargeDueToPolicy() throws Exception { + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + // Artificially small buffer/payload + policy.setInputBufferSize(1024); // read buffer + policy.setMaxTextMessageBufferSize(1024); // streaming buffer (not used in this test) + policy.setMaxTextMessageSize(1024); // actual maximum text message size policy + byte[] utf = new byte[2048]; + Arrays.fill(utf, (byte) 'a'); + + assertTrue(utf.length > 0x7E && utf.length < 0xFFFF); + + ByteBuffer buf = ByteBuffer.allocate(utf.length + 8); + buf.put((byte) 0x81); // text frame, fin = true + buf.put((byte) (0x80 | 0x7E)); // 0x7E == 126 (a 2 byte payload length) + buf.putShort((short) utf.length); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, utf); + buf.flip(); + + UnitParser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + assertThrows(MessageTooLargeException.class, () -> parser.parseQuietly(buf)); + } + + @Test + public void testLongMaskedText() throws Exception { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 3500; i++) { + sb.append("Hell\uFF4f Big W\uFF4Frld "); + } + sb.append(". The end."); + + String expectedText = sb.toString(); + byte[] utf = expectedText.getBytes(StandardCharsets.UTF_8); + + assertTrue(utf.length > 0xFFFF); + + ByteBuffer buf = ByteBuffer.allocate(utf.length + 32); + buf.put((byte) 0x81); // text frame, fin = true + buf.put((byte) (0x80 | 0x7F)); // 0x7F == 127 (a 8 byte payload length) + buf.putLong(utf.length); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, utf); + buf.flip(); + + WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); + policy.setMaxTextMessageSize(100000); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + WebSocketFrame txt = capture.getFrames().poll(); + assertEquals(expectedText, txt.getPayloadAsUTF8()); + } + + @Test + public void testMediumMaskedText() throws Exception { + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < 14; i++) { + sb.append("Hell\uFF4f Medium W\uFF4Frld "); + } + sb.append(". The end."); + + String expectedText = sb.toString(); + byte[] utf = expectedText.getBytes(StandardCharsets.UTF_8); + + assertTrue(utf.length > 0x7E && utf.length < 0xFFFF); + + ByteBuffer buf = ByteBuffer.allocate(utf.length + 10); + buf.put((byte) 0x81); + buf.put((byte) (0x80 | 0x7E)); // 0x7E == 126 (a 2 byte payload length) + buf.putShort((short) utf.length); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, utf); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + WebSocketFrame txt = capture.getFrames().poll(); + assertEquals(expectedText, txt.getPayloadAsUTF8()); + } + + @Test + public void testShortMaskedFragmentedText() throws Exception { + String part1 = "Hello "; + String part2 = "World"; + + byte[] b1 = part1.getBytes(StandardCharsets.UTF_8); + byte[] b2 = part2.getBytes(StandardCharsets.UTF_8); + + ByteBuffer buf = ByteBuffer.allocate(32); + + // part 1 + buf.put((byte) 0x01); // no fin + text + buf.put((byte) (0x80 | b1.length)); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, b1); + + // part 2 + buf.put((byte) 0x80); // fin + continuation + buf.put((byte) (0x80 | b2.length)); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, b2); + + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + capture.assertHasFrame(OpCode.CONTINUATION, 1); + WebSocketFrame txt = capture.getFrames().poll(); + assertEquals(part1, txt.getPayloadAsUTF8()); + txt = capture.getFrames().poll(); + assertEquals(part2, txt.getPayloadAsUTF8()); + } + + @Test + public void testShortMaskedText() throws Exception { + String expectedText = "Hello World"; + byte[] utf = expectedText.getBytes(StandardCharsets.UTF_8); + + ByteBuffer buf = ByteBuffer.allocate(24); + buf.put((byte) 0x81); + buf.put((byte) (0x80 | utf.length)); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, utf); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + WebSocketFrame txt = capture.getFrames().poll(); + assertEquals(expectedText, txt.getPayloadAsUTF8()); + } + + @Test + public void testShortMaskedUtf8Text() throws Exception { + String expectedText = "Hell\uFF4f W\uFF4Frld"; + + byte[] utf = expectedText.getBytes(StandardCharsets.UTF_8); + + ByteBuffer buf = ByteBuffer.allocate(24); + buf.put((byte) 0x81); + buf.put((byte) (0x80 | utf.length)); + MaskedByteBuffer.putMask(buf); + MaskedByteBuffer.putPayload(buf, utf); + buf.flip(); + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + parser.parse(buf); + + capture.assertHasFrame(OpCode.TEXT, 1); + WebSocketFrame txt = capture.getFrames().poll(); + assertEquals(expectedText, txt.getPayloadAsUTF8()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/UnitParser.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/UnitParser.java new file mode 100644 index 000000000..babf03459 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/decoder/UnitParser.java @@ -0,0 +1,40 @@ +package com.fireflysource.net.websocket.common.decoder; + + +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +import java.nio.ByteBuffer; + +public class UnitParser extends Parser { + public UnitParser() { + this(WebSocketPolicy.newServerPolicy()); + } + + public UnitParser(WebSocketPolicy policy) { + super(policy); + } + + private void parsePartial(ByteBuffer buf, int numBytes) { + int len = Math.min(numBytes, buf.remaining()); + byte[] arr = new byte[len]; + buf.get(arr, 0, len); + this.parse(ByteBuffer.wrap(arr)); + } + + /** + * Parse a buffer, but do so in a quiet fashion, squelching stacktraces if encountered. + *

+ * Use if you know the parse will cause an exception and just don't want to make the test console all noisy. + * + * @param buf the buffer to parse + */ + public void parseQuietly(ByteBuffer buf) { + parse(buf); + } + + public void parseSlowly(ByteBuffer buf, int segmentSize) { + while (buf.remaining() > 0) { + parsePartial(buf, segmentSize); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/GeneratorParserRoundtripTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/GeneratorParserRoundtripTest.java new file mode 100644 index 000000000..f2186e9cc --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/GeneratorParserRoundtripTest.java @@ -0,0 +1,86 @@ +package com.fireflysource.net.websocket.common.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.decoder.Parser; +import com.fireflysource.net.websocket.common.frame.TextFrame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; +import com.fireflysource.net.websocket.common.model.IncomingFramesCapture; +import com.fireflysource.net.websocket.common.model.OpCode; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GeneratorParserRoundtripTest { + + @Test + public void testParserAndGenerator() { + WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); + Generator gen = new Generator(policy); + Parser parser = new Parser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + String message = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; + + ByteBuffer out = BufferUtils.allocate(8192); + // Generate Buffer + BufferUtils.flipToFill(out); + WebSocketFrame frame = new TextFrame().setPayload(message); + ByteBuffer header = gen.generateHeaderBytes(frame); + ByteBuffer payload = frame.getPayload(); + out.put(header); + out.put(payload); + + // Parse Buffer + BufferUtils.flipToFlush(out, 0); + parser.parse(out); + + // Validate + capture.assertHasFrame(OpCode.TEXT, 1); + + TextFrame txt = (TextFrame) capture.getFrames().poll(); + assertEquals(message, txt.getPayloadAsUTF8()); + } + + @Test + public void testParserAndGeneratorMasked() { + Generator gen = new Generator(WebSocketPolicy.newClientPolicy()); + Parser parser = new Parser(WebSocketPolicy.newServerPolicy()); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + String message = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; + + ByteBuffer out = BufferUtils.allocate(8192); + BufferUtils.flipToFill(out); + // Setup Frame + WebSocketFrame frame = new TextFrame().setPayload(message); + + // Add masking + byte[] mask = new byte[4]; + Arrays.fill(mask, (byte) 0xFF); + frame.setMask(mask); + + // Generate Buffer + ByteBuffer header = gen.generateHeaderBytes(frame); + ByteBuffer payload = frame.getPayload(); + out.put(header); + out.put(payload); + + // Parse Buffer + BufferUtils.flipToFlush(out, 0); + parser.parse(out); + + // Validate + capture.assertHasFrame(OpCode.TEXT, 1); + + TextFrame txt = (TextFrame) capture.getFrames().poll(); + assertTrue(txt.isMasked()); + assertEquals(message, txt.getPayloadAsUTF8()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/GeneratorTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/GeneratorTest.java new file mode 100644 index 000000000..e45f9daa2 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/GeneratorTest.java @@ -0,0 +1,270 @@ +package com.fireflysource.net.websocket.common.encoder; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.decoder.Parser; +import com.fireflysource.net.websocket.common.decoder.UnitParser; +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.model.*; +import com.fireflysource.net.websocket.common.utils.Hex; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class GeneratorTest { + private static final LazyLogger LOG = SystemLogger.create(WindowHelper.class); + + public static class WindowHelper { + final int windowSize; + int totalParts; + int totalBytes; + + public WindowHelper(int windowSize) { + this.windowSize = windowSize; + this.totalParts = 0; + this.totalBytes = 0; + } + + public ByteBuffer generateWindowed(Frame... frames) { + // Create Buffer to hold all generated frames in a single buffer + int completeBufSize = 0; + for (Frame f : frames) { + completeBufSize += Generator.MAX_HEADER_LENGTH + f.getPayloadLength(); + } + + ByteBuffer completeBuf = ByteBuffer.allocate(completeBufSize); + BufferUtils.clearToFill(completeBuf); + + // Generate from all frames + Generator generator = new UnitGenerator(); + + for (Frame f : frames) { + ByteBuffer header = generator.generateHeaderBytes(f); + totalBytes += BufferUtils.put(header, completeBuf); + + if (f.hasPayload()) { + ByteBuffer payload = f.getPayload(); + totalBytes += payload.remaining(); + totalParts++; + completeBuf.put(payload.slice()); + } + } + + // Return results + BufferUtils.flipToFlush(completeBuf, 0); + return completeBuf; + } + + public void assertTotalParts(int expectedParts) { + assertEquals(expectedParts, totalParts); + } + + public void assertTotalBytes(int expectedBytes) { + assertEquals(expectedBytes, totalBytes); + } + } + + private void assertGeneratedBytes(CharSequence expectedBytes, Frame... frames) { + // collect up all frames as single ByteBuffer + ByteBuffer allframes = UnitGenerator.generate(frames); + // Get hex String form of all frames bytebuffer. + String actual = Hex.asHex(allframes); + // Validate + assertEquals(expectedBytes.toString(), actual); + } + + private String asMaskedHex(String str, byte[] maskingKey) { + byte[] utf = StringUtils.getUtf8Bytes(str); + mask(utf, maskingKey); + return Hex.asHex(utf); + } + + private void mask(byte[] buf, byte[] maskingKey) { + int size = buf.length; + for (int i = 0; i < size; i++) { + buf[i] ^= maskingKey[i % 4]; + } + } + + @Test + public void testCloseEmpty() { + // 0 byte payload (no status code) + assertGeneratedBytes("8800", new CloseFrame()); + } + + @Test + public void testCloseCodeNoReason() { + CloseInfo close = new CloseInfo(StatusCode.NORMAL); + // 2 byte payload (2 bytes for status code) + assertGeneratedBytes("880203E8", close.asFrame()); + } + + @Test + public void testCloseCodeOkReason() { + CloseInfo close = new CloseInfo(StatusCode.NORMAL, "OK"); + // 4 byte payload (2 bytes for status code, 2 more for "OK") + assertGeneratedBytes("880403E84F4B", close.asFrame()); + } + + @Test + public void testTextHello() { + WebSocketFrame frame = new TextFrame().setPayload("Hello"); + byte[] utf = StringUtils.getUtf8Bytes("Hello"); + assertGeneratedBytes("8105" + Hex.asHex(utf), frame); + } + + @Test + public void testTextMasked() { + WebSocketFrame frame = new TextFrame().setPayload("Hello"); + byte[] maskingKey = Hex.asByteArray("11223344"); + frame.setMask(maskingKey); + + // what is expected + StringBuilder expected = new StringBuilder(); + expected.append("8185").append("11223344"); + expected.append(asMaskedHex("Hello", maskingKey)); + + // validate + assertGeneratedBytes(expected, frame); + } + + @Test + public void testTextMaskedOffsetSourceByteBuffer() { + ByteBuffer payload = ByteBuffer.allocate(100); + payload.position(5); + payload.put(StringUtils.getUtf8Bytes("Hello")); + payload.flip(); + payload.position(5); + // at this point, we have a ByteBuffer of 100 bytes. + // but only a few bytes in the middle are made available for the payload. + // we are testing that masking works as intended, even if the provided + // payload does not start at position 0. + LOG.debug("Payload = {}", BufferUtils.toDetailString(payload)); + WebSocketFrame frame = new TextFrame().setPayload(payload); + byte[] maskingKey = Hex.asByteArray("11223344"); + frame.setMask(maskingKey); + + // what is expected + StringBuilder expected = new StringBuilder(); + expected.append("8185").append("11223344"); + expected.append(asMaskedHex("Hello", maskingKey)); + + // validate + assertGeneratedBytes(expected, frame); + } + + /** + * Prevent regression of masking of many packets. + */ + @Test + public void testManyMasked() { + int pingCount = 2; + + // Prepare frames + WebSocketFrame[] frames = new WebSocketFrame[pingCount + 1]; + for (int i = 0; i < pingCount; i++) { + frames[i] = new PingFrame().setPayload(String.format("ping-%d", i)); + } + frames[pingCount] = new CloseInfo(StatusCode.NORMAL).asFrame(); + + // Mask All Frames + byte[] maskingKey = Hex.asByteArray("11223344"); + for (WebSocketFrame f : frames) { + f.setMask(maskingKey); + } + + // Validate result of generation + StringBuilder expected = new StringBuilder(); + expected.append("8986").append("11223344"); + expected.append(asMaskedHex("ping-0", maskingKey)); // ping 0 + expected.append("8986").append("11223344"); + expected.append(asMaskedHex("ping-1", maskingKey)); // ping 1 + expected.append("8882").append("11223344"); + byte[] closure = Hex.asByteArray("03E8"); + mask(closure, maskingKey); + expected.append(Hex.asHex(closure)); // normal closure + + assertGeneratedBytes(expected, frames); + } + + /** + * Test the windowed generate of a frame that has no masking. + */ + @Test + public void testWindowedGenerate() { + // A decent sized frame, no masking + byte[] payload = new byte[10240]; + Arrays.fill(payload, (byte) 0x44); + + WebSocketFrame frame = new BinaryFrame().setPayload(payload); + + // Generate + int windowSize = 1024; + WindowHelper helper = new WindowHelper(windowSize); + ByteBuffer completeBuffer = helper.generateWindowed(frame); + + // Validate + int expectedHeaderSize = 4; + int expectedSize = payload.length + expectedHeaderSize; + int expectedParts = 1; + + helper.assertTotalParts(expectedParts); + helper.assertTotalBytes(payload.length + expectedHeaderSize); + + assertEquals(expectedSize, completeBuffer.remaining()); + } + + @Test + public void testWindowedGenerateWithMasking() { + // A decent sized frame, with masking + byte[] payload = new byte[10240]; + Arrays.fill(payload, (byte) 0x55); + + byte[] mask = new byte[] + {0x2A, (byte) 0xF0, 0x0F, 0x00}; + + WebSocketFrame frame = new BinaryFrame().setPayload(payload); + frame.setMask(mask); // masking! + + // Generate + int windowSize = 2929; + WindowHelper helper = new WindowHelper(windowSize); + ByteBuffer completeBuffer = helper.generateWindowed(frame); + + // Validate + int expectedHeaderSize = 8; + int expectedSize = payload.length + expectedHeaderSize; + int expectedParts = 1; + + helper.assertTotalParts(expectedParts); + helper.assertTotalBytes(payload.length + expectedHeaderSize); + + assertEquals(expectedSize, completeBuffer.remaining()); + + // Parse complete buffer. + WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); + Parser parser = new UnitParser(policy); + IncomingFramesCapture capture = new IncomingFramesCapture(); + parser.setIncomingFramesHandler(capture); + + parser.parse(completeBuffer); + + // Assert validity of frame + WebSocketFrame actual = capture.getFrames().poll(); + assertEquals(OpCode.BINARY, actual.getOpCode()); + assertEquals(payload.length, actual.getPayloadLength()); + + // Validate payload contents for proper masking + ByteBuffer actualData = actual.getPayload().slice(); + assertEquals(payload.length, actualData.remaining()); + while (actualData.remaining() > 0) { + assertEquals((byte) 0x55, actualData.get()); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/RFC6455ExamplesGeneratorTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/RFC6455ExamplesGeneratorTest.java new file mode 100644 index 000000000..5e1632a79 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/RFC6455ExamplesGeneratorTest.java @@ -0,0 +1,158 @@ +package com.fireflysource.net.websocket.common.encoder; + +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.utils.ByteBufferAssert; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class RFC6455ExamplesGeneratorTest { + private static final int FUDGE = 32; + + @Test + public void testFragmentedUnmaskedTextMessage() { + WebSocketFrame text1 = new TextFrame().setPayload("Hel").setFin(false); + WebSocketFrame text2 = new ContinuationFrame().setPayload("lo"); + + ByteBuffer actual1 = UnitGenerator.generate(text1); + ByteBuffer actual2 = UnitGenerator.generate(text2); + + ByteBuffer expected1 = ByteBuffer.allocate(5); + + expected1.put(new byte[] + {(byte) 0x01, (byte) 0x03, (byte) 0x48, (byte) 0x65, (byte) 0x6c}); + + ByteBuffer expected2 = ByteBuffer.allocate(4); + + expected2.put(new byte[] + {(byte) 0x80, (byte) 0x02, (byte) 0x6c, (byte) 0x6f}); + + expected1.flip(); + expected2.flip(); + + ByteBufferAssert.assertEquals("t1 buffers are not equal", expected1, actual1); + ByteBufferAssert.assertEquals("t2 buffers are not equal", expected2, actual2); + } + + @Test + public void testSingleMaskedPongRequest() { + PongFrame pong = new PongFrame().setPayload("Hello"); + pong.setMask(new byte[] + {0x37, (byte) 0xfa, 0x21, 0x3d}); + + ByteBuffer actual = UnitGenerator.generate(pong); + + ByteBuffer expected = ByteBuffer.allocate(11); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // Unmasked Pong request + expected.put(new byte[] + {(byte) 0x8a, (byte) 0x85, 0x37, (byte) 0xfa, 0x21, 0x3d, 0x7f, (byte) 0x9f, 0x4d, 0x51, 0x58}); + expected.flip(); // make readable + + ByteBufferAssert.assertEquals("pong buffers are not equal", expected, actual); + } + + @Test + public void testSingleMaskedTextMessage() { + WebSocketFrame text = new TextFrame().setPayload("Hello"); + text.setMask(new byte[] + {0x37, (byte) 0xfa, 0x21, 0x3d}); + + ByteBuffer actual = UnitGenerator.generate(text); + + ByteBuffer expected = ByteBuffer.allocate(11); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // A single-frame masked text message + expected.put(new byte[] + {(byte) 0x81, (byte) 0x85, 0x37, (byte) 0xfa, 0x21, 0x3d, 0x7f, (byte) 0x9f, 0x4d, 0x51, 0x58}); + expected.flip(); // make readable + + ByteBufferAssert.assertEquals("masked text buffers are not equal", expected, actual); + } + + @Test + public void testSingleUnmasked256ByteBinaryMessage() { + int dataSize = 256; + + BinaryFrame binary = new BinaryFrame(); + byte[] payload = new byte[dataSize]; + Arrays.fill(payload, (byte) 0x44); + binary.setPayload(ByteBuffer.wrap(payload)); + + ByteBuffer actual = UnitGenerator.generate(binary); + + ByteBuffer expected = ByteBuffer.allocate(dataSize + FUDGE); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // 256 bytes binary message in a single unmasked frame + expected.put(new byte[] + {(byte) 0x82, (byte) 0x7E}); + expected.putShort((short) 0x01_00); + + for (int i = 0; i < dataSize; i++) { + expected.put((byte) 0x44); + } + + expected.flip(); + + ByteBufferAssert.assertEquals("binary buffers are not equal", expected, actual); + } + + @Test + public void testSingleUnmasked64KBinaryMessage() { + int dataSize = 1024 * 64; + + BinaryFrame binary = new BinaryFrame(); + byte[] payload = new byte[dataSize]; + Arrays.fill(payload, (byte) 0x44); + binary.setPayload(ByteBuffer.wrap(payload)); + + ByteBuffer actual = UnitGenerator.generate(binary); + + ByteBuffer expected = ByteBuffer.allocate(dataSize + 10); + // Raw bytes as found in RFC 6455, Section 5.7 - Examples + // 64k bytes binary message in a single unmasked frame + expected.put(new byte[] + {(byte) 0x82, (byte) 0x7F}); + expected.putInt(0x00_00_00_00); + expected.putInt(0x00_01_00_00); + + for (int i = 0; i < dataSize; i++) { + expected.put((byte) 0x44); + } + + expected.flip(); + + ByteBufferAssert.assertEquals("binary buffers are not equal", expected, actual); + } + + @Test + public void testSingleUnmaskedPingRequest() throws Exception { + PingFrame ping = new PingFrame().setPayload("Hello"); + + ByteBuffer actual = UnitGenerator.generate(ping); + + ByteBuffer expected = ByteBuffer.allocate(10); + expected.put(new byte[] + {(byte) 0x89, (byte) 0x05, (byte) 0x48, (byte) 0x65, (byte) 0x6c, (byte) 0x6c, (byte) 0x6f}); + expected.flip(); // make readable + + ByteBufferAssert.assertEquals("Ping buffers", expected, actual); + } + + @Test + public void testSingleUnmaskedTextMessage() { + WebSocketFrame text = new TextFrame().setPayload("Hello"); + + ByteBuffer actual = UnitGenerator.generate(text); + + ByteBuffer expected = ByteBuffer.allocate(10); + + expected.put(new byte[] + {(byte) 0x81, (byte) 0x05, (byte) 0x48, (byte) 0x65, (byte) 0x6c, (byte) 0x6c, (byte) 0x6f}); + + expected.flip(); + + ByteBufferAssert.assertEquals("t1 buffers are not equal", expected, actual); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/UnitGenerator.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/UnitGenerator.java new file mode 100644 index 000000000..debf4d5ed --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/encoder/UnitGenerator.java @@ -0,0 +1,98 @@ +package com.fireflysource.net.websocket.common.encoder; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Convenience Generator. + */ +public class UnitGenerator extends Generator { + + private static final LazyLogger LOG = SystemLogger.create(UnitGenerator.class); + + public static ByteBuffer generate(Frame frame) { + return generate(new Frame[]{frame}); + } + + /** + * Generate All Frames into a single ByteBuffer. + *

+ * This is highly inefficient and is not used in production! (This exists to make testing of the Generator easier) + * + * @param frames the frames to generate from + * @return the ByteBuffer representing all of the generated frames provided. + */ + public static ByteBuffer generate(Frame[] frames) { + Generator generator = new UnitGenerator(); + + // Generate into single bytebuffer + int buflen = 0; + for (Frame f : frames) { + buflen += f.getPayloadLength() + MAX_HEADER_LENGTH; + } + ByteBuffer completeBuf = ByteBuffer.allocate(buflen); + BufferUtils.clearToFill(completeBuf); + + // Generate frames + for (Frame f : frames) { + generator.generateWholeFrame(f, completeBuf); + } + + BufferUtils.flipToFlush(completeBuf, 0); + if (LOG.isDebugEnabled()) { + LOG.debug("generate({} frames) - {}", frames.length, BufferUtils.toDetailString(completeBuf)); + } + return completeBuf; + } + + /** + * Generate a single giant buffer of all provided frames Not appropriate for production code, but useful for testing. + * + * @param frames the list of frames to generate from + * @return the bytebuffer representing all of the generated frames + */ + public static ByteBuffer generate(List frames) { + // Create non-symmetrical mask (helps show mask bytes order issues) + final byte[] MASK = + {0x11, 0x22, 0x33, 0x44}; + + // the generator + Generator generator = new UnitGenerator(); + + // Generate into single bytebuffer + int buflen = 0; + for (Frame f : frames) { + buflen += f.getPayloadLength() + MAX_HEADER_LENGTH; + } + ByteBuffer completeBuf = ByteBuffer.allocate(buflen); + BufferUtils.clearToFill(completeBuf); + + // Generate frames + for (WebSocketFrame f : frames) { + f.setMask(MASK); // make sure we have the test mask set + BufferUtils.put(generator.generateHeaderBytes(f), completeBuf); + ByteBuffer window = f.getPayload(); + if (BufferUtils.hasContent(window)) { + BufferUtils.put(window, completeBuf); + } + } + + BufferUtils.flipToFlush(completeBuf, 0); + if (LOG.isDebugEnabled()) { + LOG.debug("generate({} frames) - {}", frames.size(), BufferUtils.toDetailString(completeBuf)); + } + return completeBuf; + } + + public UnitGenerator() { + super(WebSocketPolicy.newServerPolicy()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/AbstractExtensionTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/AbstractExtensionTest.java new file mode 100644 index 000000000..ebd423c79 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/AbstractExtensionTest.java @@ -0,0 +1,17 @@ +package com.fireflysource.net.websocket.common.extension; + + +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import org.junit.jupiter.api.BeforeEach; + +public abstract class AbstractExtensionTest { + + protected ExtensionTool clientExtensions; + protected ExtensionTool serverExtensions; + + @BeforeEach + public void init() { + clientExtensions = new ExtensionTool(WebSocketPolicy.newClientPolicy()); + serverExtensions = new ExtensionTool(WebSocketPolicy.newServerPolicy()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/ExtensionTool.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/ExtensionTool.java new file mode 100644 index 000000000..d057c7b24 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/ExtensionTool.java @@ -0,0 +1,105 @@ +package com.fireflysource.net.websocket.common.extension; + +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.net.websocket.common.decoder.Parser; +import com.fireflysource.net.websocket.common.decoder.UnitParser; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.frame.TextFrame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; +import com.fireflysource.net.websocket.common.model.Extension; +import com.fireflysource.net.websocket.common.model.ExtensionConfig; +import com.fireflysource.net.websocket.common.model.IncomingFramesCapture; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import com.fireflysource.net.websocket.common.utils.ByteBufferAssert; + +import java.nio.ByteBuffer; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExtensionTool { + public class Tester { + private String requestedExtParams; + private ExtensionConfig extConfig; + private Extension ext; + private Parser parser; + private IncomingFramesCapture capture; + + private Tester(String parameterizedExtension) { + this.requestedExtParams = parameterizedExtension; + this.extConfig = ExtensionConfig.parse(parameterizedExtension); + Class extClass = factory.getExtension(extConfig.getName()); + assertNotNull(extClass); + + this.parser = new UnitParser(policy); + } + + public String getRequestedExtParams() { + return requestedExtParams; + } + + public void assertNegotiated(String expectedNegotiation) { + this.ext = factory.newInstance(extConfig); + if (ext instanceof AbstractExtension) { + ((AbstractExtension) ext).setPolicy(policy); + } + + this.capture = new IncomingFramesCapture(); + this.ext.setNextIncomingFrames(capture); + + this.parser.configureFromExtensions(Collections.singletonList(ext)); + this.parser.setIncomingFramesHandler(ext); + } + + public void parseIncomingHex(String... rawhex) { + int parts = rawhex.length; + byte[] net; + + for (int i = 0; i < parts; i++) { + String hex = rawhex[i].replaceAll("\\s*(0x)?", ""); + net = TypeUtils.fromHexString(hex); + parser.parse(ByteBuffer.wrap(net)); + } + } + + public void assertHasFrames(String... textFrames) { + Frame[] frames = new Frame[textFrames.length]; + for (int i = 0; i < frames.length; i++) { + frames[i] = new TextFrame().setPayload(textFrames[i]); + } + assertHasFrames(frames); + } + + public void assertHasFrames(Frame... expectedFrames) { + int expectedCount = expectedFrames.length; + capture.assertFrameCount(expectedCount); + + for (int i = 0; i < expectedCount; i++) { + WebSocketFrame actual = capture.getFrames().poll(); + + String prefix = String.format("frame[%d]", i); + assertEquals(expectedFrames[i].getOpCode(), actual.getOpCode()); + assertEquals(expectedFrames[i].isFin(), actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = expectedFrames[i].getPayload().slice(); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); + } + } + } + + private final WebSocketPolicy policy; + private final ExtensionFactory factory; + + public ExtensionTool(WebSocketPolicy policy) { + this.policy = policy; + this.factory = new WebSocketExtensionFactory(); + } + + public Tester newTester(String parameterizedExtension) { + return new Tester(parameterizedExtension); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/ByteAccumulatorTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/ByteAccumulatorTest.java new file mode 100644 index 000000000..be0d076aa --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/ByteAccumulatorTest.java @@ -0,0 +1,71 @@ +package com.fireflysource.net.websocket.common.extension.compress; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.exception.MessageTooLargeException; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.*; + +public class ByteAccumulatorTest { + + @Test + public void testCopyNormal() { + ByteAccumulator accumulator = new ByteAccumulator(10_000); + + byte[] hello = "Hello".getBytes(UTF_8); + byte[] space = " ".getBytes(UTF_8); + byte[] world = "World".getBytes(UTF_8); + + accumulator.copyChunk(hello, 0, hello.length); + accumulator.copyChunk(space, 0, space.length); + accumulator.copyChunk(world, 0, world.length); + + assertEquals(hello.length + space.length + world.length, accumulator.getLength()); + + ByteBuffer out = ByteBuffer.allocate(200); + accumulator.transferTo(out); + String result = BufferUtils.toUTF8String(out); + assertEquals("Hello World", result); + } + + @Test + public void testTransferToNotEnoughSpace() { + ByteAccumulator accumulator = new ByteAccumulator(10_000); + + byte[] hello = "Hello".getBytes(UTF_8); + byte[] space = " ".getBytes(UTF_8); + byte[] world = "World".getBytes(UTF_8); + + accumulator.copyChunk(hello, 0, hello.length); + accumulator.copyChunk(space, 0, space.length); + accumulator.copyChunk(world, 0, world.length); + + int length = hello.length + space.length + world.length; + assertEquals(length, accumulator.getLength()); + + ByteBuffer out = ByteBuffer.allocate(length - 2); // intentionally too small ByteBuffer + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> accumulator.transferTo(out)); + assertTrue(e.getMessage().contains("Not enough space in ByteBuffer")); + } + + @Test + public void testCopyChunkNotEnoughSpace() { + byte[] hello = "Hello".getBytes(UTF_8); + byte[] space = " ".getBytes(UTF_8); + byte[] world = "World".getBytes(UTF_8); + + int length = hello.length + space.length + world.length; + ByteAccumulator accumulator = new ByteAccumulator(length - 2); // intentionally too small of a max + + accumulator.copyChunk(hello, 0, hello.length); + accumulator.copyChunk(space, 0, space.length); + + MessageTooLargeException e = assertThrows(MessageTooLargeException.class, () -> accumulator.copyChunk(world, 0, world.length)); + System.out.println(e.getMessage()); + assertTrue(e.getMessage().contains("too large for configured max")); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/CapturedHexPayloads.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/CapturedHexPayloads.java new file mode 100644 index 000000000..ab41d1406 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/CapturedHexPayloads.java @@ -0,0 +1,27 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.model.OutgoingFrames; +import com.fireflysource.net.websocket.common.utils.Hex; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class CapturedHexPayloads implements OutgoingFrames { + private List captured = new ArrayList<>(); + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + String hexPayload = Hex.asHex(frame.getPayload()); + captured.add(hexPayload); + if (result != null) { + result.accept(Result.SUCCESS); + } + } + + public List getCaptured() { + return captured; + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/DeflateFrameExtensionTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/DeflateFrameExtensionTest.java new file mode 100644 index 000000000..0113a354f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/DeflateFrameExtensionTest.java @@ -0,0 +1,383 @@ +package com.fireflysource.net.websocket.common.extension.compress; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.common.slf4j.LazyLogger; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.common.sys.Result; +import com.fireflysource.common.sys.SystemLogger; +import com.fireflysource.net.websocket.common.decoder.Parser; +import com.fireflysource.net.websocket.common.decoder.UnitParser; +import com.fireflysource.net.websocket.common.encoder.Generator; +import com.fireflysource.net.websocket.common.extension.AbstractExtensionTest; +import com.fireflysource.net.websocket.common.extension.ExtensionTool.Tester; +import com.fireflysource.net.websocket.common.frame.BinaryFrame; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.frame.TextFrame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; +import com.fireflysource.net.websocket.common.model.*; +import com.fireflysource.net.websocket.common.utils.ByteBufferAssert; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +import static org.junit.jupiter.api.Assertions.*; + +public class DeflateFrameExtensionTest extends AbstractExtensionTest { + private static final LazyLogger LOG = SystemLogger.create(DeflateFrameExtensionTest.class); + + private void assertIncoming(byte[] raw, String... expectedTextDatas) { + WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); + + DeflateFrameExtension ext = new DeflateFrameExtension(); + ext.setPolicy(policy); + + ExtensionConfig config = ExtensionConfig.parse("deflate-frame"); + ext.setConfig(config); + + // Setup capture of incoming frames + IncomingFramesCapture capture = new IncomingFramesCapture(); + + // Wire up stack + ext.setNextIncomingFrames(capture); + + Parser parser = new UnitParser(policy); + parser.configureFromExtensions(Collections.singletonList(ext)); + parser.setIncomingFramesHandler(ext); + + parser.parse(ByteBuffer.wrap(raw)); + + int len = expectedTextDatas.length; + capture.assertFrameCount(len); + capture.assertHasFrame(OpCode.TEXT, len); + + int i = 0; + for (WebSocketFrame actual : capture.getFrames()) { + String prefix = "Frame[" + i + "]"; + assertEquals(OpCode.TEXT, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(expectedTextDatas[i], StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); + i++; + } + } + + private void assertOutgoing(String text, String expectedHex) throws IOException { + WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); + + DeflateFrameExtension ext = new DeflateFrameExtension(); + ext.setPolicy(policy); + + ExtensionConfig config = ExtensionConfig.parse("deflate-frame"); + ext.setConfig(config); + + Generator generator = new Generator(policy, true); + generator.configureFromExtensions(Collections.singletonList(ext)); + + OutgoingNetworkBytesCapture capture = new OutgoingNetworkBytesCapture(generator); + ext.setNextOutgoingFrames(capture); + + Frame frame = new TextFrame().setPayload(text); + ext.outgoingFrame(frame, null); + + capture.assertBytes(0, expectedHex); + } + + @Test + public void testBlockheadClientHelloThere() { + Tester tester = serverExtensions.newTester("deflate-frame"); + + tester.assertNegotiated("deflate-frame"); + + tester.parseIncomingHex(// Captured from Blockhead Client - "Hello" then "There" via unit test + "c18700000000f248cdc9c90700", // "Hello" + "c187000000000ac9482d4a0500" // "There" + ); + + tester.assertHasFrames("Hello", "There"); + } + + @Test + public void testChrome20Hello() { + Tester tester = serverExtensions.newTester("deflate-frame"); + + tester.assertNegotiated("deflate-frame"); + + tester.parseIncomingHex(// Captured from Chrome 20.x - "Hello" (sent from browser) + "c187832b5c11716391d84a2c5c" // "Hello" + ); + + tester.assertHasFrames("Hello"); + } + + @Test + public void testChrome20HelloThere() { + Tester tester = serverExtensions.newTester("deflate-frame"); + + tester.assertNegotiated("deflate-frame"); + + tester.parseIncomingHex(// Captured from Chrome 20.x - "Hello" then "There" (sent from browser) + "c1877b1971db8951bc12b21e71", // "Hello" + "c18759edc8f4532480d913e8c8" // There + ); + + tester.assertHasFrames("Hello", "There"); + } + + @Test + public void testChrome20Info() { + Tester tester = serverExtensions.newTester("deflate-frame"); + + tester.assertNegotiated("deflate-frame"); + + tester.parseIncomingHex(// Captured from Chrome 20.x - "info:" (sent from browser) + "c187ca4def7f0081a4b47d4fef" // example payload + ); + + tester.assertHasFrames("info:"); + } + + @Test + public void testChrome20TimeTime() { + Tester tester = serverExtensions.newTester("deflate-frame"); + + tester.assertNegotiated("deflate-frame"); + + tester.parseIncomingHex(// Captured from Chrome 20.x - "time:" then "time:" once more (sent from browser) + "c18782467424a88fb869374474", // "time:" + "c1853cfda17f16fcb07f3c" // "time:" + ); + + tester.assertHasFrames("time:", "time:"); + } + + @Test + public void testPyWebSocketTimeTimeTime() { + Tester tester = serverExtensions.newTester("deflate-frame"); + + tester.assertNegotiated("deflate-frame"); + + tester.parseIncomingHex(// Captured from Pywebsocket (r781) - "time:" sent 3 times. + "c1876b100104" + "41d9cd49de1201", // "time:" + "c1852ae3ff01" + "00e2ee012a", // "time:" + "c18435558caa" + "37468caa" // "time:" + ); + + tester.assertHasFrames("time:", "time:", "time:"); + } + + @Test + public void testCompressTimeTimeTime() { + // What pywebsocket produces for "time:", "time:", "time:" + String[] expected = new String[] + {"2AC9CC4DB50200", "2A01110000", "02130000"}; + + // Lets see what we produce + CapturedHexPayloads capture = new CapturedHexPayloads(); + DeflateFrameExtension ext = new DeflateFrameExtension(); + init(ext); + ext.setNextOutgoingFrames(capture); + + ext.outgoingFrame(new TextFrame().setPayload("time:"), null); + ext.outgoingFrame(new TextFrame().setPayload("time:"), null); + ext.outgoingFrame(new TextFrame().setPayload("time:"), null); + + List actual = capture.getCaptured(); + assertTrue(actual.containsAll(Arrays.asList(expected))); + } + + private void init(DeflateFrameExtension ext) { + ext.setConfig(new ExtensionConfig(ext.getName())); + } + + @Test + public void testDeflateBasics() { + // Setup deflater basics + Deflater compressor = new Deflater(Deflater.BEST_COMPRESSION, true); + compressor.setStrategy(Deflater.DEFAULT_STRATEGY); + + // Text to compress + String text = "info:"; + byte[] uncompressed = StringUtils.getUtf8Bytes(text); + + // Prime the compressor + compressor.reset(); + compressor.setInput(uncompressed, 0, uncompressed.length); + compressor.finish(); + + // Perform compression + ByteBuffer outbuf = ByteBuffer.allocate(64); + BufferUtils.clearToFill(outbuf); + + while (!compressor.finished()) { + byte[] out = new byte[64]; + int len = compressor.deflate(out, 0, out.length, Deflater.SYNC_FLUSH); + if (len > 0) { + outbuf.put(out, 0, len); + } + } + compressor.end(); + + BufferUtils.flipToFlush(outbuf, 0); + byte[] compressed = BufferUtils.toArray(outbuf); + // Clear the BFINAL bit that has been set by the compressor.end() call. + // In the real implementation we never end() the compressor. + compressed[0] &= 0xFE; + + String actual = TypeUtils.toHexString(compressed); + String expected = "CaCc4bCbB70200"; // what pywebsocket produces + + assertEquals(expected, actual); + } + + @Test + public void testGeneratedTwoFrames() throws IOException { + WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); + + DeflateFrameExtension ext = new DeflateFrameExtension(); + ext.setPolicy(policy); + ext.setConfig(new ExtensionConfig(ext.getName())); + + Generator generator = new Generator(policy, true); + generator.configureFromExtensions(Collections.singletonList(ext)); + + OutgoingNetworkBytesCapture capture = new OutgoingNetworkBytesCapture(generator); + ext.setNextOutgoingFrames(capture); + + ext.outgoingFrame(new TextFrame().setPayload("Hello"), null); + ext.outgoingFrame(new TextFrame(), null); + ext.outgoingFrame(new TextFrame().setPayload("There"), null); + + capture.assertBytes(0, "c107f248cdc9c90700"); + } + + @Test + public void testInflateBasics() throws Exception { + // should result in "info:" text if properly inflated + byte[] rawbuf = TypeUtils.fromHexString("CaCc4bCbB70200"); // what pywebsocket produces + // byte[] rawbuf = TypeUtil.fromHexString("CbCc4bCbB70200"); // what java produces + + Inflater inflater = new Inflater(true); + inflater.reset(); + inflater.setInput(rawbuf, 0, rawbuf.length); + + byte[] outbuf = new byte[64]; + int len = inflater.inflate(outbuf); + inflater.end(); + assertTrue(len > 4); + + String actual = new String(outbuf, 0, len, StandardCharsets.UTF_8); + assertEquals("info:", actual); + } + + @Test + public void testPyWebSocketServerHello() { + // Captured from PyWebSocket - "Hello" (echo from server) + byte[] rawbuf = TypeUtils.fromHexString("c107f248cdc9c90700"); + assertIncoming(rawbuf, "Hello"); + } + + @Test + public void testPyWebSocketServerLong() { + // Captured from PyWebSocket - Long Text (echo from server) + byte[] rawbuf = TypeUtils.fromHexString("c1421cca410a80300c44d1abccce9df7" + + "f018298634d05631138ab7b7b8fdef1f" + + "dc0282e2061d575a45f6f2686bab25e1" + + "3fb7296fa02b5885eb3b0379c394f461" + + "98cafd03"); + assertIncoming(rawbuf, "It's a big enough umbrella but it's always me that ends up getting wet."); + } + + @Test + public void testPyWebSocketServerMedium() { + // Captured from PyWebSocket - "stackoverflow" (echo from server) + byte[] rawbuf = TypeUtils.fromHexString("c10f2a2e494ccece2f4b2d4acbc92f0700"); + assertIncoming(rawbuf, "stackoverflow"); + } + + /** + * Make sure that the server generated compressed form for "Hello" is consistent with what PyWebSocket creates. + * + * @throws IOException on test failure + */ + @Test + public void testServerGeneratedHello() throws IOException { + assertOutgoing("Hello", "c107f248cdc9c90700"); + } + + /** + * Make sure that the server generated compressed form for "There" is consistent with what PyWebSocket creates. + * + * @throws IOException on test failure + */ + @Test + public void testServerGeneratedThere() throws IOException { + assertOutgoing("There", "c1070ac9482d4a0500"); + } + + @Test + public void testCompressAndDecompressBigPayload() throws Exception { + byte[] input = new byte[1024 * 1024]; + // Make them not compressible. + new Random().nextBytes(input); + + int maxMessageSize = (1024 * 1024) + 8192; + + DeflateFrameExtension clientExtension = new DeflateFrameExtension(); + clientExtension.setPolicy(WebSocketPolicy.newClientPolicy()); + clientExtension.getPolicy().setMaxBinaryMessageSize(maxMessageSize); + clientExtension.getPolicy().setMaxBinaryMessageBufferSize(maxMessageSize); + clientExtension.setConfig(ExtensionConfig.parse("deflate-frame")); + + final DeflateFrameExtension serverExtension = new DeflateFrameExtension(); + serverExtension.setPolicy(WebSocketPolicy.newServerPolicy()); + serverExtension.getPolicy().setMaxBinaryMessageSize(maxMessageSize); + serverExtension.getPolicy().setMaxBinaryMessageBufferSize(maxMessageSize); + serverExtension.setConfig(ExtensionConfig.parse("deflate-frame")); + + // Chain the next element to decompress. + clientExtension.setNextOutgoingFrames(new OutgoingFrames() { + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + LOG.debug("outgoingFrame({})", frame); + serverExtension.incomingFrame(frame); + result.accept(Result.SUCCESS); + } + }); + + final ByteArrayOutputStream result = new ByteArrayOutputStream(input.length); + serverExtension.setNextIncomingFrames(new IncomingFrames() { + @Override + public void incomingFrame(Frame frame) { + LOG.debug("incomingFrame({})", frame); + try { + result.write(BufferUtils.toArray(frame.getPayload())); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + }); + + BinaryFrame frame = new BinaryFrame(); + frame.setPayload(input); + frame.setFin(true); + clientExtension.outgoingFrame(frame, null); + + assertArrayEquals(input, result.toByteArray()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/PerMessageDeflateExtensionTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/PerMessageDeflateExtensionTest.java new file mode 100644 index 000000000..058b2e1e4 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/compress/PerMessageDeflateExtensionTest.java @@ -0,0 +1,514 @@ +package com.fireflysource.net.websocket.common.extension.compress; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.net.websocket.common.exception.ProtocolException; +import com.fireflysource.net.websocket.common.extension.AbstractExtensionTest; +import com.fireflysource.net.websocket.common.extension.ExtensionTool.Tester; +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.model.*; +import com.fireflysource.net.websocket.common.utils.ByteBufferAssert; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Client side behavioral tests for permessage-deflate extension. + *

+ * See: http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-15 + */ +public class PerMessageDeflateExtensionTest extends AbstractExtensionTest { + + private void assertEndsWithTail(String hexStr, boolean expectedResult) { + ByteBuffer buf = ByteBuffer.wrap(TypeUtils.fromHexString(hexStr)); + assertEquals(expectedResult, CompressExtension.endsWithTail(buf)); + } + + @Test + public void testEndsWithTailBytes() { + assertEndsWithTail("11223344", false); + assertEndsWithTail("00", false); + assertEndsWithTail("0000", false); + assertEndsWithTail("FFFF0000", false); + assertEndsWithTail("880000FFFF", true); + assertEndsWithTail("0000FFFF", true); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.1: A message compressed using 1 compressed DEFLATE block + */ + @Test + public void testDraft21HelloUnCompressedBlock() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex( + // basic, 1 block, compressed with 0 compression level (aka, uncompressed). + "0xc1 0x07", // (HEADER added for this test) + "0xf2 0x48 0xcd 0xc9 0xc9 0x07 0x00" // example frame from RFC + ); + + tester.assertHasFrames("Hello"); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.1: A message compressed using 1 compressed DEFLATE block (with fragmentation) + */ + @Test + public void testDraft21HelloUnCompressedBlockFragmented() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// basic, 1 block, compressed with 0 compression level (aka, uncompressed). + // Fragment 1 + "0x41 0x03 0xf2 0x48 0xcd", + // Fragment 2 + "0x80 0x04 0xc9 0xc9 0x07 0x00"); + + tester.assertHasFrames( + new TextFrame().setPayload("He").setFin(false), + new ContinuationFrame().setPayload("llo").setFin(true)); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.2: Sharing LZ77 Sliding Window + */ + @Test + public void testDraft21SharingL77SlidingWindowContextTakeover() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// context takeover (2 messages) + // message 1 + "0xc1 0x07", // (HEADER added for this test) + "0xf2 0x48 0xcd 0xc9 0xc9 0x07 0x00", + // message 2 + "0xc1 0x07", // (HEADER added for this test) + "0xf2 0x48 0xcd 0xc9 0xc9 0x07 0x00"); + + tester.assertHasFrames("Hello", "Hello"); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.2: Sharing LZ77 Sliding Window + */ + @Test + public void testDraft21SharingL77SlidingWindowNoContextTakeover() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// 2 message, shared LZ77 window + // message 1 + "0xc1 0x07", // (HEADER added for this test) + "0xf2 0x48 0xcd 0xc9 0xc9 0x07 0x00", + // message 2 + "0xc1 0x05", // (HEADER added for this test) + "0xf2 0x00 0x11 0x00 0x00" + ); + + tester.assertHasFrames("Hello", "Hello"); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.3: Using a DEFLATE Block with No Compression + */ + @Test + public void testDraft21DeflateBlockWithNoCompression() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// 1 message / no compression + "0xc1 0x0b 0x00 0x05 0x00 0xfa 0xff 0x48 0x65 0x6c 0x6c 0x6f 0x00" // example frame + ); + + tester.assertHasFrames("Hello"); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.4: Using a DEFLATE Block with BFINAL Set to 1 + */ + @Test + public void testDraft21DeflateBlockWithBFinal1() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// 1 message + "0xc1 0x08", // header + "0xf3 0x48 0xcd 0xc9 0xc9 0x07 0x00 0x00" // example payload + ); + + tester.assertHasFrames("Hello"); + } + + /** + * Decode payload example as seen in draft-ietf-hybi-permessage-compression-21. + *

+ * Section 8.2.3.5: Two DEFLATE Blocks in 1 Message + */ + @Test + public void testDraft21TwoDeflateBlocksOneMessage() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// 1 message, 1 frame, 2 deflate blocks + "0xc1 0x0d", // (HEADER added for this test) + "0xf2 0x48 0x05 0x00 0x00 0x00 0xff 0xff 0xca 0xc9 0xc9 0x07 0x00" + ); + + tester.assertHasFrames("Hello"); + } + + /** + * Decode fragmented message (3 parts: TEXT, CONTINUATION, CONTINUATION) + */ + @Test + public void testParseFragmentedMessageGood() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// 1 message, 3 frame + "410C", // HEADER TEXT / fin=false / rsv1=true + "F248CDC9C95700000000FFFF", + "000B", // HEADER CONTINUATION / fin=false / rsv1=false + "0ACF2FCA4901000000FFFF", + "8003", // HEADER CONTINUATION / fin=true / rsv1=false + "520400" + ); + + Frame txtFrame = new TextFrame().setPayload("Hello ").setFin(false); + Frame con1Frame = new ContinuationFrame().setPayload("World").setFin(false); + Frame con2Frame = new ContinuationFrame().setPayload("!").setFin(true); + + tester.assertHasFrames(txtFrame, con1Frame, con2Frame); + } + + /** + * Decode fragmented message (3 parts: TEXT, CONTINUATION, CONTINUATION) + *

+ * Continuation frames have RSV1 set, which MUST result in Failure + *

+ */ + @Test + public void testParseFragmentedMessageBadRsv1() { + Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + assertThrows(ProtocolException.class, () -> + tester.parseIncomingHex(// 1 message, 3 frame + "410C", // Header TEXT / fin=false / rsv1=true + "F248CDC9C95700000000FFFF", // Payload + "400B", // Header CONTINUATION / fin=false / rsv1=true + "0ACF2FCA4901000000FFFF", // Payload + "C003", // Header CONTINUATION / fin=true / rsv1=true + "520400" // Payload + )); + } + + /** + * Incoming PING (Control Frame) should pass through extension unmodified + */ + @Test + public void testIncomingPing() { + PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("permessage-deflate"); + ext.setConfig(config); + + // Setup capture of incoming frames + IncomingFramesCapture capture = new IncomingFramesCapture(); + + // Wire up stack + ext.setNextIncomingFrames(capture); + + String payload = "Are you there?"; + Frame ping = new PingFrame().setPayload(payload); + ext.incomingFrame(ping); + + capture.assertFrameCount(1); + capture.assertHasFrame(OpCode.PING, 1); + WebSocketFrame actual = capture.getFrames().poll(); + + assertEquals(OpCode.PING, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(payload, StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); + } + + /** + * Incoming Text Message fragmented into 3 pieces. + */ + @Test + public void testIncomingFragmented() { + PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("permessage-deflate"); + ext.setConfig(config); + + // Setup capture of incoming frames + IncomingFramesCapture capture = new IncomingFramesCapture(); + + // Wire up stack + ext.setNextIncomingFrames(capture); + + String payload = "Are you there?"; + Frame ping = new PingFrame().setPayload(payload); + ext.incomingFrame(ping); + + capture.assertFrameCount(1); + capture.assertHasFrame(OpCode.PING, 1); + WebSocketFrame actual = capture.getFrames().poll(); + + assertEquals(OpCode.PING, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(payload, StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); + } + + /** + * Verify that incoming uncompressed frames are properly passed through + */ + @Test + public void testIncomingUncompressedFrames() { + PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("permessage-deflate"); + ext.setConfig(config); + + // Setup capture of incoming frames + IncomingFramesCapture capture = new IncomingFramesCapture(); + + // Wire up stack + ext.setNextIncomingFrames(capture); + + // Quote + List quote = new ArrayList<>(); + quote.add("No amount of experimentation can ever prove me right;"); + quote.add("a single experiment can prove me wrong."); + quote.add("-- Albert Einstein"); + + // leave frames as-is, no compression, and pass into extension + for (String q : quote) { + TextFrame frame = new TextFrame().setPayload(q); + frame.setRsv1(false); // indication to extension that frame is not compressed (ie: a normal frame) + ext.incomingFrame(frame); + } + + int len = quote.size(); + capture.assertFrameCount(len); + capture.assertHasFrame(OpCode.TEXT, len); + + String prefix; + int i = 0; + for (WebSocketFrame actual : capture.getFrames()) { + prefix = "Frame[" + i + "]"; + + assertEquals(OpCode.TEXT, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(quote.get(i), StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); + i++; + } + } + + /** + * Outgoing PING (Control Frame) should pass through extension unmodified + * + * @throws IOException on test failure + */ + @Test + public void testOutgoingPing() throws IOException { + PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("permessage-deflate"); + ext.setConfig(config); + + // Setup capture of outgoing frames + OutgoingFramesCapture capture = new OutgoingFramesCapture(); + + // Wire up stack + ext.setNextOutgoingFrames(capture); + + String payload = "Are you there?"; + Frame ping = new PingFrame().setPayload(payload); + + ext.outgoingFrame(ping, null); + + capture.assertFrameCount(1); + capture.assertHasFrame(OpCode.PING, 1); + + WebSocketFrame actual = capture.getFrames().getFirst(); + + assertEquals(OpCode.PING, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(payload, StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); + } + + /** + * Outgoing Fragmented Message + * + * @throws IOException on test failure + */ + @Test + public void testOutgoingFragmentedMessage() throws InterruptedException { + PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("permessage-deflate"); + ext.setConfig(config); + + // Setup capture of outgoing frames + OutgoingFramesCapture capture = new OutgoingFramesCapture(); + + // Wire up stack + ext.setNextOutgoingFrames(capture); + + Frame txtFrame = new TextFrame().setPayload("Hello ").setFin(false); + Frame con1Frame = new ContinuationFrame().setPayload("World").setFin(false); + Frame con2Frame = new ContinuationFrame().setPayload("!").setFin(true); + ext.outgoingFrame(txtFrame, null); + ext.outgoingFrame(con1Frame, null); + ext.outgoingFrame(con2Frame, null); + + capture.assertFrameCount(3); + + WebSocketFrame capturedFrame; + + capturedFrame = capture.getFrames().poll(1, TimeUnit.SECONDS); + assertEquals(OpCode.TEXT, capturedFrame.getOpCode()); + assertFalse(capturedFrame.isFin()); + assertTrue(capturedFrame.isRsv1()); + assertFalse(capturedFrame.isRsv2()); + assertFalse(capturedFrame.isRsv3()); + + capturedFrame = capture.getFrames().poll(1, TimeUnit.SECONDS); + assertEquals(OpCode.CONTINUATION, capturedFrame.getOpCode()); + assertFalse(capturedFrame.isFin()); + assertFalse(capturedFrame.isRsv1()); + assertFalse(capturedFrame.isRsv2()); + assertFalse(capturedFrame.isRsv3()); + + capturedFrame = capture.getFrames().poll(1, TimeUnit.SECONDS); + assertEquals(OpCode.CONTINUATION, capturedFrame.getOpCode()); + assertTrue(capturedFrame.isFin()); + assertFalse(capturedFrame.isRsv1()); + assertFalse(capturedFrame.isRsv2()); + assertFalse(capturedFrame.isRsv3()); + } + + @Test + public void testPyWebSocketClientNoContextTakeoverThreeOra() { + Tester tester = clientExtensions.newTester("permessage-deflate; client_max_window_bits; client_no_context_takeover"); + + tester.assertNegotiated("permessage-deflate"); + + // Captured from Pywebsocket (r790) - 3 messages with similar parts. + + tester.parseIncomingHex(// context takeover (3 messages) + "c1 09 0a c9 2f 4a 0c 01 62 00 00", // ToraTora + "c1 0b 72 2c c9 2f 4a 74 cb 01 12 00 00", // AtoraFlora + "c1 0b 0a c8 c8 c9 2f 4a 0c 01 62 00 00" // PhloraTora + ); + + tester.assertHasFrames("ToraTora", "AtoraFlora", "PhloraTora"); + } + + @Test + public void testPyWebSocketClientToraToraTora() { + Tester tester = clientExtensions.newTester("permessage-deflate; client_max_window_bits"); + + tester.assertNegotiated("permessage-deflate"); + + // Captured from Pywebsocket (r790) - "tora" sent 3 times. + + tester.parseIncomingHex(// context takeover (3 messages) + "c1 06 2a c9 2f 4a 04 00", // tora 1 + "c1 05 2a 01 62 00 00", // tora 2 + "c1 04 02 61 00 00" // tora 3 + ); + + tester.assertHasFrames("tora", "tora", "tora"); + } + + @Test + public void testPyWebSocketServerNoContextTakeoverThreeOra() { + Tester tester = serverExtensions.newTester("permessage-deflate; client_max_window_bits; client_no_context_takeover"); + + tester.assertNegotiated("permessage-deflate"); + + // Captured from Pywebsocket (r790) - 3 messages with similar parts. + + tester.parseIncomingHex(// context takeover (3 messages) + "c1 89 88 bc 1b b1 82 75 34 fb 84 bd 79 b1 88", // ToraTora + "c1 8b 50 86 88 b2 22 aa 41 9d 1a f2 43 b3 42 86 88", // AtoraFlora + "c1 8b e2 3e 05 53 e8 f6 cd 9a cd 74 09 52 80 3e 05" // PhloraTora + ); + + tester.assertHasFrames("ToraTora", "AtoraFlora", "PhloraTora"); + } + + @Test + public void testPyWebSocketServerToraToraTora() { + Tester tester = serverExtensions.newTester("permessage-deflate; client_max_window_bits"); + + tester.assertNegotiated("permessage-deflate"); + + // Captured from Pywebsocket (r790) - "tora" sent 3 times. + + tester.parseIncomingHex(// context takeover (3 messages) + "c1 86 69 39 fe 91 43 f0 d1 db 6d 39", // tora 1 + "c1 85 2d f3 eb 96 07 f2 89 96 2d", // tora 2 + "c1 84 53 ad a5 34 51 cc a5 34" // tora 3 + ); + + tester.assertHasFrames("tora", "tora", "tora"); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/fragment/FragmentExtensionTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/fragment/FragmentExtensionTest.java new file mode 100644 index 000000000..fecb523ca --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/extension/fragment/FragmentExtensionTest.java @@ -0,0 +1,335 @@ +package com.fireflysource.net.websocket.common.extension.fragment; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.frame.*; +import com.fireflysource.net.websocket.common.model.*; +import com.fireflysource.net.websocket.common.utils.ByteBufferAssert; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.Consumer; + +import static com.fireflysource.common.sys.Result.futureToConsumer; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.*; + + +public class FragmentExtensionTest { + + /** + * Verify that incoming frames are passed thru without modification + */ + @Test + public void testIncomingFrames() { + IncomingFramesCapture capture = new IncomingFramesCapture(); + + FragmentExtension ext = new FragmentExtension(); + ext.setPolicy(WebSocketPolicy.newClientPolicy()); + ExtensionConfig config = ExtensionConfig.parse("fragment;maxLength=4"); + ext.setConfig(config); + + ext.setNextIncomingFrames(capture); + + // Quote + List quote = new ArrayList<>(); + quote.add("No amount of experimentation can ever prove me right;"); + quote.add("a single experiment can prove me wrong."); + quote.add("-- Albert Einstein"); + + // Manually create frame and pass into extension + for (String q : quote) { + Frame frame = new TextFrame().setPayload(q); + ext.incomingFrame(frame); + } + + int len = quote.size(); + capture.assertFrameCount(len); + capture.assertHasFrame(OpCode.TEXT, len); + + String prefix; + int i = 0; + for (WebSocketFrame actual : capture.getFrames()) { + prefix = "Frame[" + i + "]"; + + assertEquals(OpCode.TEXT, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(quote.get(i), StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); + i++; + } + } + + /** + * Incoming PING (Control Frame) should pass through extension unmodified + */ + @Test + public void testIncomingPing() { + IncomingFramesCapture capture = new IncomingFramesCapture(); + + FragmentExtension ext = new FragmentExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("fragment;maxLength=4"); + ext.setConfig(config); + + ext.setNextIncomingFrames(capture); + + String payload = "Are you there?"; + Frame ping = new PingFrame().setPayload(payload); + ext.incomingFrame(ping); + + capture.assertFrameCount(1); + capture.assertHasFrame(OpCode.PING, 1); + WebSocketFrame actual = capture.getFrames().poll(); + + assertEquals(OpCode.PING, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(payload, StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); + } + + /** + * Verify that outgoing text frames are fragmented by the maxLength configuration. + * + * @throws IOException on test failure + */ + @Test + public void testOutgoingFramesByMaxLength() throws IOException, InterruptedException { + OutgoingFramesCapture capture = new OutgoingFramesCapture(); + + FragmentExtension ext = new FragmentExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("fragment;maxLength=20"); + ext.setConfig(config); + + ext.setNextOutgoingFrames(capture); + + // Quote + List quote = new ArrayList<>(); + quote.add("No amount of experimentation can ever prove me right;"); + quote.add("a single experiment can prove me wrong."); + quote.add("-- Albert Einstein"); + + // Write quote as separate frames + for (String section : quote) { + Frame frame = new TextFrame().setPayload(section); + ext.outgoingFrame(frame, null); + } + + // Expected Frames + List expectedFrames = new ArrayList<>(); + expectedFrames.add(new TextFrame().setPayload("No amount of experim").setFin(false)); + expectedFrames.add(new ContinuationFrame().setPayload("entation can ever pr").setFin(false)); + expectedFrames.add(new ContinuationFrame().setPayload("ove me right;").setFin(true)); + + expectedFrames.add(new TextFrame().setPayload("a single experiment ").setFin(false)); + expectedFrames.add(new ContinuationFrame().setPayload("can prove me wrong.").setFin(true)); + + expectedFrames.add(new TextFrame().setPayload("-- Albert Einstein").setFin(true)); + + // capture.dump(); + + int len = expectedFrames.size(); + capture.assertFrameCount(len); + + String prefix; + LinkedBlockingDeque frames = capture.getFrames(); + for (int i = 0; i < len; i++) { + prefix = "Frame[" + i + "]"; + WebSocketFrame actualFrame = frames.poll(1, SECONDS); + WebSocketFrame expectedFrame = expectedFrames.get(i); + + // System.out.printf("actual: %s%n",actualFrame); + // System.out.printf("expect: %s%n",expectedFrame); + + // Validate Frame + assertEquals(expectedFrame.getOpCode(), actualFrame.getOpCode()); + assertEquals(expectedFrame.isFin(), actualFrame.isFin()); + assertEquals(expectedFrame.isRsv1(), actualFrame.isRsv1()); + assertEquals(expectedFrame.isRsv2(), actualFrame.isRsv2()); + assertEquals(expectedFrame.isRsv3(), actualFrame.isRsv3()); + + // Validate Payload + ByteBuffer expectedData = expectedFrame.getPayload().slice(); + ByteBuffer actualData = actualFrame.getPayload().slice(); + + assertEquals(expectedData.remaining(), actualData.remaining()); + ByteBufferAssert.assertEquals(prefix + ".payload", expectedData, actualData); + } + } + + /** + * Verify that outgoing text frames are not fragmented by default configuration (which has no maxLength specified) + * + * @throws IOException on test failure + */ + @Test + public void testOutgoingFramesDefaultConfig() throws Exception { + OutgoingFramesCapture capture = new OutgoingFramesCapture(); + + FragmentExtension ext = new FragmentExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("fragment"); + ext.setConfig(config); + + ext.setNextOutgoingFrames(capture); + + // Quote + List quote = new ArrayList<>(); + quote.add("No amount of experimentation can ever prove me right;"); + quote.add("a single experiment can prove me wrong."); + quote.add("-- Albert Einstein"); + + // Write quote as separate frames + for (String section : quote) { + Frame frame = new TextFrame().setPayload(section); + ext.outgoingFrame(frame, null); + } + + // Expected Frames + List expectedFrames = new ArrayList<>(); + expectedFrames.add(new TextFrame().setPayload("No amount of experimentation can ever prove me right;")); + expectedFrames.add(new TextFrame().setPayload("a single experiment can prove me wrong.")); + expectedFrames.add(new TextFrame().setPayload("-- Albert Einstein")); + + // capture.dump(); + + int len = expectedFrames.size(); + capture.assertFrameCount(len); + + String prefix; + LinkedBlockingDeque frames = capture.getFrames(); + for (int i = 0; i < len; i++) { + prefix = "Frame[" + i + "]"; + WebSocketFrame actualFrame = frames.poll(1, SECONDS); + WebSocketFrame expectedFrame = expectedFrames.get(i); + + // Validate Frame + assertEquals(expectedFrame.getOpCode(), actualFrame.getOpCode()); + assertEquals(expectedFrame.isFin(), actualFrame.isFin()); + assertEquals(expectedFrame.isRsv1(), actualFrame.isRsv1()); + assertEquals(expectedFrame.isRsv2(), actualFrame.isRsv2()); + assertEquals(expectedFrame.isRsv3(), actualFrame.isRsv3()); + + // Validate Payload + ByteBuffer expectedData = expectedFrame.getPayload().slice(); + ByteBuffer actualData = actualFrame.getPayload().slice(); + + assertEquals(expectedData.remaining(), actualData.remaining()); + ByteBufferAssert.assertEquals(prefix + ".payload", expectedData, actualData); + } + } + + /** + * Outgoing PING (Control Frame) should pass through extension unmodified + * + * @throws IOException on test failure + */ + @Test + public void testOutgoingPing() throws IOException { + OutgoingFramesCapture capture = new OutgoingFramesCapture(); + + FragmentExtension ext = new FragmentExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("fragment;maxLength=4"); + ext.setConfig(config); + + ext.setNextOutgoingFrames(capture); + + String payload = "Are you there?"; + Frame ping = new PingFrame().setPayload(payload); + + ext.outgoingFrame(ping, null); + + capture.assertFrameCount(1); + capture.assertHasFrame(OpCode.PING, 1); + + WebSocketFrame actual = capture.getFrames().getFirst(); + + assertEquals(OpCode.PING, actual.getOpCode()); + assertTrue(actual.isFin()); + assertFalse(actual.isRsv1()); + assertFalse(actual.isRsv2()); + assertFalse(actual.isRsv3()); + + ByteBuffer expected = BufferUtils.toBuffer(payload, StandardCharsets.UTF_8); + assertEquals(expected.remaining(), actual.getPayloadLength()); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); + } + + /** + * Ensure that FragmentExtension honors the correct order of websocket frames. + */ + @Test + public void testLargeSmallTextAlternating() throws Exception { + final int largeMessageSize = 60000; + byte[] buf = new byte[largeMessageSize]; + Arrays.fill(buf, (byte) 'x'); + String largeMessage = new String(buf, UTF_8); + + final int fragmentCount = 10; + final int fragmentLength = largeMessageSize / fragmentCount; + final int messageCount = 10000; + + FragmentExtension ext = new FragmentExtension(); + ext.setPolicy(WebSocketPolicy.newServerPolicy()); + ExtensionConfig config = ExtensionConfig.parse("fragment;maxLength=" + fragmentLength); + ext.setConfig(config); + SaneFrameOrderingAssertion saneFrameOrderingAssertion = new SaneFrameOrderingAssertion(); + ext.setNextOutgoingFrames(saneFrameOrderingAssertion); + + CompletableFuture enqueuedFrameCountFut = new CompletableFuture<>(); + + CompletableFuture.runAsync(() -> + { + // Run Server Task + int frameCount = 0; + try { + for (int i = 0; i < messageCount; i++) { + int messageId = i; + CompletableFuture future = new CompletableFuture<>(); + Consumer> result = futureToConsumer(future); + WebSocketFrame frame; + if (i % 2 == 0) { + frame = new TextFrame().setPayload(largeMessage); + frameCount += fragmentCount; + } else { + frame = new TextFrame().setPayload("Short Message: " + i); + frameCount++; + } + ext.outgoingFrame(frame, result); + future.get(); + } + enqueuedFrameCountFut.complete(frameCount); + } catch (Throwable t) { + enqueuedFrameCountFut.completeExceptionally(t); + } + }); + + int enqueuedFrameCount = enqueuedFrameCountFut.get(5, SECONDS); + + int expectedFrameCount = (messageCount / 2) * fragmentCount; // large messages + expectedFrameCount += (messageCount / 2); // + short messages + + assertEquals(expectedFrameCount, saneFrameOrderingAssertion.frameCount); + assertEquals(expectedFrameCount, enqueuedFrameCount); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/frame/WebSocketFrameTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/frame/WebSocketFrameTest.java new file mode 100644 index 000000000..d3a27c329 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/frame/WebSocketFrameTest.java @@ -0,0 +1,106 @@ +package com.fireflysource.net.websocket.common.frame; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.encoder.Generator; +import com.fireflysource.net.websocket.common.model.CloseInfo; +import com.fireflysource.net.websocket.common.model.StatusCode; +import com.fireflysource.net.websocket.common.model.WebSocketPolicy; +import com.fireflysource.net.websocket.common.utils.Hex; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class WebSocketFrameTest { + + private Generator strictGenerator; + private Generator laxGenerator; + + private ByteBuffer generateWholeFrame(Generator generator, Frame frame) { + ByteBuffer buf = ByteBuffer.allocate(frame.getPayloadLength() + Generator.MAX_HEADER_LENGTH); + generator.generateWholeFrame(frame, buf); + BufferUtils.flipToFlush(buf, 0); + return buf; + } + + @BeforeEach + public void initGenerator() { + WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); + strictGenerator = new Generator(policy); + laxGenerator = new Generator(policy, false); + } + + private void assertFrameHex(String message, String expectedHex, ByteBuffer actual) { + String actualHex = Hex.asHex(actual); + assertEquals(expectedHex, actualHex, message); + } + + @Test + public void testLaxInvalidClose() { + WebSocketFrame frame = new CloseFrame().setFin(false); + ByteBuffer actual = generateWholeFrame(laxGenerator, frame); + String expected = "0800"; + assertFrameHex("Lax Invalid Close Frame", expected, actual); + } + + @Test + public void testLaxInvalidPing() { + WebSocketFrame frame = new PingFrame().setFin(false); + ByteBuffer actual = generateWholeFrame(laxGenerator, frame); + String expected = "0900"; + assertFrameHex("Lax Invalid Ping Frame", expected, actual); + } + + @Test + public void testStrictValidClose() { + CloseInfo close = new CloseInfo(StatusCode.NORMAL); + ByteBuffer actual = generateWholeFrame(strictGenerator, close.asFrame()); + String expected = "880203E8"; + assertFrameHex("Strict Valid Close Frame", expected, actual); + } + + @Test + public void testStrictValidPing() { + WebSocketFrame frame = new PingFrame(); + ByteBuffer actual = generateWholeFrame(strictGenerator, frame); + String expected = "8900"; + assertFrameHex("Strict Valid Ping Frame", expected, actual); + } + + @Test + public void testRsv1() { + TextFrame frame = new TextFrame(); + frame.setPayload("Hi"); + frame.setRsv1(true); + laxGenerator.setRsv1InUse(true); + ByteBuffer actual = generateWholeFrame(laxGenerator, frame); + String expected = "C1024869"; + assertFrameHex("Lax Text Frame with RSV1", expected, actual); + } + + @Test + public void testRsv2() { + TextFrame frame = new TextFrame(); + frame.setPayload("Hi"); + frame.setRsv2(true); + laxGenerator.setRsv2InUse(true); + ByteBuffer actual = generateWholeFrame(laxGenerator, frame); + String expected = "A1024869"; + assertFrameHex("Lax Text Frame with RSV2", expected, actual); + } + + @Test + public void testRsv3() { + TextFrame frame = new TextFrame(); + frame.setPayload("Hi"); + frame.setRsv3(true); + laxGenerator.setRsv3InUse(true); + ByteBuffer actual = generateWholeFrame(laxGenerator, frame); + String expected = "91024869"; + assertFrameHex("Lax Text Frame with RSV3", expected, actual); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/AcceptHashTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/AcceptHashTest.java new file mode 100644 index 000000000..88b6f427e --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/AcceptHashTest.java @@ -0,0 +1,43 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.object.TypeUtils; +import org.junit.jupiter.api.Test; + +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class AcceptHashTest { + + @Test + public void testHash() { + byte[] key = TypeUtils.fromHexString("00112233445566778899AABBCCDDEEFF"); + assertEquals(16, key.length); + + // what the client sends + String clientKey = Base64.getEncoder().encodeToString(key); + // what the server responds with + String serverHash = AcceptHash.hashKey(clientKey); + + // how the client validates + assertEquals("mVL6JKtNRC4tluIaFAW2hhMffgE=", serverHash); + } + + /** + * Test of values present in RFC-6455. + *

+ * Note: client key bytes are "7468652073616d706c65206e6f6e6365" + */ + @Test + public void testRfcHashExample() { + // What the client sends in the RFC + String clientKey = "dGhlIHNhbXBsZSBub25jZQ=="; + + // What the server responds with + String serverAccept = AcceptHash.hashKey(clientKey); + String expectedHash = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="; + + assertEquals(expectedHash, serverAccept); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/CloseInfoTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/CloseInfoTest.java new file mode 100644 index 000000000..11ad7f9b2 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/CloseInfoTest.java @@ -0,0 +1,131 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.string.StringUtils; +import com.fireflysource.net.websocket.common.frame.CloseFrame; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +public class CloseInfoTest { + + /** + * A test where no close is provided + */ + @Test + public void testAnonymousClose() { + CloseInfo close = new CloseInfo(); + Assertions.assertEquals(StatusCode.NO_CODE, close.getStatusCode()); + assertNull(close.getReason()); + + CloseFrame frame = close.asFrame(); + assertEquals(OpCode.CLOSE, frame.getOpCode()); + // should result in no payload + assertFalse(frame.hasPayload()); + assertEquals(0, frame.getPayloadLength()); + } + + /** + * A test where NO_CODE (1005) is provided + */ + @Test + public void testNoCode() { + CloseInfo close = new CloseInfo(StatusCode.NO_CODE); + Assertions.assertEquals(StatusCode.NO_CODE, close.getStatusCode()); + assertNull(close.getReason()); + + CloseFrame frame = close.asFrame(); + assertEquals(OpCode.CLOSE, frame.getOpCode()); + // should result in no payload + assertFalse(frame.hasPayload()); + assertEquals(0, frame.getPayloadLength()); + } + + /** + * A test where NO_CLOSE (1006) is provided + */ + @Test + public void testNoClose() { + CloseInfo close = new CloseInfo(StatusCode.NO_CLOSE); + Assertions.assertEquals(StatusCode.NO_CLOSE, close.getStatusCode()); + assertNull(close.getReason()); + + CloseFrame frame = close.asFrame(); + assertEquals(OpCode.CLOSE, frame.getOpCode()); + // should result in no payload + assertFalse(frame.hasPayload()); + assertEquals(0, frame.getPayloadLength()); + } + + /** + * A test of FAILED_TLS_HANDSHAKE (1007) + */ + @Test + public void testFailedTlsHandshake() { + CloseInfo close = new CloseInfo(StatusCode.FAILED_TLS_HANDSHAKE); + Assertions.assertEquals(StatusCode.FAILED_TLS_HANDSHAKE, close.getStatusCode()); + assertNull(close.getReason()); + + CloseFrame frame = close.asFrame(); + assertEquals(OpCode.CLOSE, frame.getOpCode()); + // should result in no payload + assertFalse(frame.hasPayload()); + assertEquals(0, frame.getPayloadLength()); + } + + /** + * A test of NORMAL (1000) + */ + @Test + public void testNormal() { + CloseInfo close = new CloseInfo(StatusCode.NORMAL); + Assertions.assertEquals(StatusCode.NORMAL, close.getStatusCode()); + assertNull(close.getReason()); + + CloseFrame frame = close.asFrame(); + assertEquals(OpCode.CLOSE, frame.getOpCode()); + assertEquals(2, frame.getPayloadLength()); + } + + private ByteBuffer asByteBuffer(int statusCode, String reason) { + int len = 2; // status code length + byte[] utf = null; + if (StringUtils.hasText(reason)) { + utf = StringUtils.getUtf8Bytes(reason); + len += utf.length; + } + + ByteBuffer buf = BufferUtils.allocate(len); + BufferUtils.flipToFill(buf); + buf.put((byte) ((statusCode >>> 8) & 0xFF)); + buf.put((byte) ((statusCode >>> 0) & 0xFF)); + + if (utf != null) { + buf.put(utf, 0, utf.length); + } + BufferUtils.flipToFlush(buf, 0); + + return buf; + } + + @Test + public void testFromFrame() { + ByteBuffer payload = asByteBuffer(StatusCode.NORMAL, null); + assertEquals(2, payload.remaining()); + CloseFrame frame = new CloseFrame(); + frame.setPayload(payload); + + // create from frame + CloseInfo close = new CloseInfo(frame); + Assertions.assertEquals(StatusCode.NORMAL, close.getStatusCode()); + assertNull(close.getReason()); + + // and back again + frame = close.asFrame(); + assertEquals(OpCode.CLOSE, frame.getOpCode()); + assertEquals(2, frame.getPayloadLength()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/ExtensionConfigTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/ExtensionConfigTest.java new file mode 100644 index 000000000..24e58361e --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/ExtensionConfigTest.java @@ -0,0 +1,92 @@ +package com.fireflysource.net.websocket.common.model; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExtensionConfigTest { + + private void assertConfig(ExtensionConfig cfg, String expectedName, Map expectedParams) { + assertEquals(expectedName, cfg.getName()); + + Map actualParams = cfg.getParameters(); + assertNotNull(actualParams); + assertEquals(expectedParams.size(), actualParams.size()); + + for (String expectedKey : expectedParams.keySet()) { + assertTrue(actualParams.containsKey(expectedKey)); + + String expectedValue = expectedParams.get(expectedKey); + String actualValue = actualParams.get(expectedKey); + + assertEquals(expectedValue, actualValue); + } + } + + @Test + public void testParseMuxExample() { + ExtensionConfig cfg = ExtensionConfig.parse("mux; max-channels=4; flow-control"); + Map expectedParams = new HashMap<>(); + expectedParams.put("max-channels", "4"); + expectedParams.put("flow-control", null); + assertConfig(cfg, "mux", expectedParams); + } + + @Test + public void testParsePerMessageCompressExample1() { + ExtensionConfig cfg = ExtensionConfig.parse("permessage-compress; method=foo"); + Map expectedParams = new HashMap<>(); + expectedParams.put("method", "foo"); + assertConfig(cfg, "permessage-compress", expectedParams); + } + + @Test + public void testParsePerMessageCompressExample2() { + ExtensionConfig cfg = ExtensionConfig.parse("permessage-compress; method=\"foo; x=10\""); + Map expectedParams = new HashMap<>(); + expectedParams.put("method", "foo; x=10"); + assertConfig(cfg, "permessage-compress", expectedParams); + } + + @Test + public void testParsePerMessageCompressExample3() { + ExtensionConfig cfg = ExtensionConfig.parse("permessage-compress; method=\"foo, bar\""); + Map expectedParams = new HashMap<>(); + expectedParams.put("method", "foo, bar"); + assertConfig(cfg, "permessage-compress", expectedParams); + } + + @Test + public void testParsePerMessageCompressExample4() { + ExtensionConfig cfg = ExtensionConfig.parse("permessage-compress; method=\"foo; use_x, foo\""); + Map expectedParams = new HashMap<>(); + expectedParams.put("method", "foo; use_x, foo"); + assertConfig(cfg, "permessage-compress", expectedParams); + } + + @Test + public void testParsePerMessageCompressExample5() { + ExtensionConfig cfg = ExtensionConfig.parse("permessage-compress; method=\"foo; x=\\\"Hello World\\\", bar\""); + Map expectedParams = new HashMap<>(); + expectedParams.put("method", "foo; x=\"Hello World\", bar"); + assertConfig(cfg, "permessage-compress", expectedParams); + } + + @Test + public void testParseSimpleBasicParameters() { + ExtensionConfig cfg = ExtensionConfig.parse("bar; baz=2"); + Map expectedParams = new HashMap<>(); + expectedParams.put("baz", "2"); + assertConfig(cfg, "bar", expectedParams); + } + + @Test + public void testParseSimpleNoParameters() { + ExtensionConfig cfg = ExtensionConfig.parse("foo"); + Map expectedParams = new HashMap<>(); + assertConfig(cfg, "foo", expectedParams); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/IncomingFramesCapture.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/IncomingFramesCapture.java new file mode 100644 index 000000000..b084f374e --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/IncomingFramesCapture.java @@ -0,0 +1,84 @@ +package com.fireflysource.net.websocket.common.model; + + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; + +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class IncomingFramesCapture implements IncomingFrames { + private LinkedBlockingQueue frames = new LinkedBlockingQueue<>(); + + public void assertFrameCount(int expectedCount) { + if (frames.size() != expectedCount) { + // dump details + System.err.printf("Expected %d frame(s)%n", expectedCount); + System.err.printf("But actually captured %d frame(s)%n", frames.size()); + int i = 0; + for (Frame frame : frames) { + System.err.printf(" [%d] Frame[%s] - %s%n", i++, + OpCode.name(frame.getOpCode()), + BufferUtils.toDetailString(frame.getPayload())); + } + } + assertEquals(expectedCount, frames.size()); + } + + public void assertHasFrame(byte op) { + assertTrue(op >= 1); + } + + public void assertHasFrame(byte op, int expectedCount) { + String msg = String.format("%s frame count", OpCode.name(op)); + assertEquals(expectedCount, getFrameCount(op)); + } + + public void assertHasNoFrames() { + assertEquals(0, frames.size()); + } + + public void clear() { + frames.clear(); + } + + public void dump() { + System.err.printf("Captured %d incoming frames%n", frames.size()); + int i = 0; + for (Frame frame : frames) { + System.err.printf("[%3d] %s%n", i++, frame); + System.err.printf(" payload: %s%n", BufferUtils.toDetailString(frame.getPayload())); + } + } + + public int getFrameCount(byte op) { + int count = 0; + for (WebSocketFrame frame : frames) { + if (frame.getOpCode() == op) { + count++; + } + } + return count; + } + + public Queue getFrames() { + return frames; + } + + @Override + public void incomingFrame(Frame frame) { + WebSocketFrame copy = WebSocketFrame.copy(frame); + // TODO: might need to make this optional (depending on use by client vs server tests) + // assertThat("frame.masking must be set",frame.isMasked(),is(true)); + frames.add(copy); + } + + public int size() { + return frames.size(); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/OutgoingFramesCapture.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/OutgoingFramesCapture.java new file mode 100644 index 000000000..de9e3fc1f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/OutgoingFramesCapture.java @@ -0,0 +1,64 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.frame.Frame; +import com.fireflysource.net.websocket.common.frame.WebSocketFrame; + +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OutgoingFramesCapture implements OutgoingFrames { + private LinkedBlockingDeque frames = new LinkedBlockingDeque<>(); + + public void assertFrameCount(int expectedCount) { + assertEquals(expectedCount, frames.size()); + } + + public void assertHasFrame(byte op) { + assertTrue(getFrameCount(op) >= 1); + } + + public void assertHasFrame(byte op, int expectedCount) { + assertEquals(expectedCount, getFrameCount(op)); + } + + public void assertHasNoFrames() { + assertEquals(0, frames.size()); + } + + public void dump() { + System.out.printf("Captured %d outgoing writes%n", frames.size()); + int i = 0; + for (WebSocketFrame frame : frames) { + System.out.printf("[%3d] %s%n", i, frame); + System.out.printf(" %s%n", BufferUtils.toDetailString(frame.getPayload())); + i++; + } + } + + public int getFrameCount(byte op) { + int count = 0; + for (WebSocketFrame frame : frames) { + if (frame.getOpCode() == op) { + count++; + } + } + return count; + } + + public LinkedBlockingDeque getFrames() { + return frames; + } + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + frames.add(WebSocketFrame.copy(frame)); + if (result != null) { + result.accept(Result.SUCCESS); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/OutgoingNetworkBytesCapture.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/OutgoingNetworkBytesCapture.java new file mode 100644 index 000000000..56e02f67f --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/OutgoingNetworkBytesCapture.java @@ -0,0 +1,52 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.io.BufferUtils; +import com.fireflysource.common.object.TypeUtils; +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.encoder.Generator; +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Capture outgoing network bytes. + */ +public class OutgoingNetworkBytesCapture implements OutgoingFrames { + private final Generator generator; + private List captured; + + public OutgoingNetworkBytesCapture(Generator generator) { + this.generator = generator; + this.captured = new ArrayList<>(); + } + + public void assertBytes(int idx, String expectedHex) { + assertTrue(idx < captured.size()); + ByteBuffer buf = captured.get(idx); + String actualHex = TypeUtils.toHexString(BufferUtils.toArray(buf)).toUpperCase(Locale.ENGLISH); + assertEquals(expectedHex.toUpperCase(Locale.ENGLISH), actualHex); + } + + public List getCaptured() { + return captured; + } + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + ByteBuffer buf = ByteBuffer.allocate(Generator.MAX_HEADER_LENGTH + frame.getPayloadLength()); + generator.generateWholeFrame(frame, buf); + BufferUtils.flipToFlush(buf, 0); + captured.add(buf); + if (result != null) { + result.accept(Result.SUCCESS); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/SaneFrameOrderingAssertion.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/SaneFrameOrderingAssertion.java new file mode 100644 index 000000000..c4d21c801 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/model/SaneFrameOrderingAssertion.java @@ -0,0 +1,53 @@ +package com.fireflysource.net.websocket.common.model; + +import com.fireflysource.common.sys.Result; +import com.fireflysource.net.websocket.common.frame.Frame; + +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Useful for testing the production of sane frame ordering from various components. + */ +public class SaneFrameOrderingAssertion implements OutgoingFrames { + boolean priorDataFrame = false; + public int frameCount = 0; + + @Override + public void outgoingFrame(Frame frame, Consumer> result) { + byte opcode = frame.getOpCode(); + assertTrue(OpCode.isKnown(opcode)); + + switch (opcode) { + case OpCode.TEXT: + assertFalse(priorDataFrame, "Unexpected " + OpCode.name(opcode) + " frame, was expecting CONTINUATION"); + break; + case OpCode.BINARY: + assertFalse(priorDataFrame, "Unexpected " + OpCode.name(opcode) + " frame, was expecting CONTINUATION"); + break; + case OpCode.CONTINUATION: + assertTrue(priorDataFrame, "CONTINUATION frame without prior !FIN"); + break; + case OpCode.CLOSE: + assertFalse(frame.isFin(), "Fragmented Close Frame [" + OpCode.name(opcode) + "]"); + break; + case OpCode.PING: + assertFalse(frame.isFin(), "Fragmented Close Frame [" + OpCode.name(opcode) + "]"); + break; + case OpCode.PONG: + assertFalse(frame.isFin(), "Fragmented Close Frame [" + OpCode.name(opcode) + "]"); + break; + } + + if (OpCode.isDataFrame(opcode)) { + priorDataFrame = !frame.isFin(); + } + + frameCount++; + + if (result != null) + result.accept(Result.SUCCESS); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/ByteBufferAssert.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/ByteBufferAssert.java new file mode 100644 index 000000000..e869056b6 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/ByteBufferAssert.java @@ -0,0 +1,45 @@ +package com.fireflysource.net.websocket.common.utils; + + +import com.fireflysource.common.io.BufferUtils; +import org.junit.jupiter.api.Assertions; + +import java.nio.ByteBuffer; + + +public class ByteBufferAssert { + + public static void assertEquals(String message, byte[] expected, byte[] actual) { + Assertions.assertEquals(expected.length, actual.length); + int len = expected.length; + for (int i = 0; i < len; i++) { + Assertions.assertEquals(expected[i], actual[i]); + } + } + + public static void assertEquals(ByteBuffer expectedBuffer, ByteBuffer actualBuffer, String message) { + assertEquals(message, expectedBuffer, actualBuffer); + } + + public static void assertEquals(String message, ByteBuffer expectedBuffer, ByteBuffer actualBuffer) { + if (expectedBuffer == null) { + Assertions.assertNull(actualBuffer); + } else { + byte[] expectedBytes = BufferUtils.toArray(expectedBuffer); + byte[] actualBytes = BufferUtils.toArray(actualBuffer); + assertEquals(message, expectedBytes, actualBytes); + } + } + + public static void assertEquals(String message, String expectedString, ByteBuffer actualBuffer) { + String actualString = BufferUtils.toString(actualBuffer); + Assertions.assertEquals(expectedString, actualString); + } + + public static void assertSize(String message, int expectedSize, ByteBuffer buffer) { + if ((expectedSize == 0) && (buffer == null)) { + return; + } + Assertions.assertEquals(expectedSize, buffer.remaining()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/Hex.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/Hex.java new file mode 100644 index 000000000..3136c0b54 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/Hex.java @@ -0,0 +1,54 @@ +package com.fireflysource.net.websocket.common.utils; + + +import com.fireflysource.common.io.BufferUtils; + +import java.nio.ByteBuffer; + +public final class Hex { + private static final char[] hexcodes = "0123456789ABCDEF".toCharArray(); + + public static byte[] asByteArray(String hstr) { + if ((hstr.length() < 0) || ((hstr.length() % 2) != 0)) { + throw new IllegalArgumentException(String.format("Invalid string length of <%d>", hstr.length())); + } + + int size = hstr.length() / 2; + byte[] buf = new byte[size]; + byte hex; + int len = hstr.length(); + + int idx = (int) Math.floor(((size * 2) - (double) len) / 2); + for (int i = 0; i < len; i++) { + hex = 0; + if (i >= 0) { + hex = (byte) (Character.digit(hstr.charAt(i), 16) << 4); + } + i++; + hex += (byte) (Character.digit(hstr.charAt(i), 16)); + + buf[idx] = hex; + idx++; + } + + return buf; + } + + public static ByteBuffer asByteBuffer(String hstr) { + return ByteBuffer.wrap(asByteArray(hstr)); + } + + public static String asHex(byte[] buf) { + int len = buf.length; + char[] out = new char[len * 2]; + for (int i = 0; i < len; i++) { + out[i * 2] = hexcodes[(buf[i] & 0xF0) >> 4]; + out[(i * 2) + 1] = hexcodes[(buf[i] & 0x0F)]; + } + return String.valueOf(out); + } + + public static String asHex(ByteBuffer buffer) { + return asHex(BufferUtils.toArray(buffer)); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/MaskedByteBuffer.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/MaskedByteBuffer.java new file mode 100644 index 000000000..5c7df20f8 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/MaskedByteBuffer.java @@ -0,0 +1,26 @@ +package com.fireflysource.net.websocket.common.utils; + +import java.nio.ByteBuffer; + +public class MaskedByteBuffer { + private static byte[] mask = new byte[] + {0x00, (byte) 0xF0, 0x0F, (byte) 0xFF}; + + public static void putMask(ByteBuffer buffer) { + buffer.put(mask, 0, mask.length); + } + + public static void putPayload(ByteBuffer buffer, byte[] payload) { + int len = payload.length; + for (int i = 0; i < len; i++) { + buffer.put((byte) (payload[i] ^ mask[i % 4])); + } + } + + public static void putPayload(ByteBuffer buffer, ByteBuffer payload) { + int len = payload.remaining(); + for (int i = 0; i < len; i++) { + buffer.put((byte) (payload.get() ^ mask[i % 4])); + } + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/QuoteUtilQuoteTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/QuoteUtilQuoteTest.java new file mode 100644 index 000000000..63457528b --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/QuoteUtilQuoteTest.java @@ -0,0 +1,53 @@ +package com.fireflysource.net.websocket.common.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test QuoteUtil.quote(), and QuoteUtil.dequote() + */ +public class QuoteUtilQuoteTest { + public static Stream data() { + // The various quoting of a String + List data = new ArrayList<>(); + + data.add(new Object[]{"Hi", "\"Hi\""}); + data.add(new Object[]{"Hello World", "\"Hello World\""}); + data.add(new Object[]{"9.0.0", "\"9.0.0\""}); + data.add(new Object[]{ + "Something \"Special\"", + "\"Something \\\"Special\\\"\"" + }); + data.add(new Object[]{ + "A Few\n\"Good\"\tMen", + "\"A Few\\n\\\"Good\\\"\\tMen\"" + }); + + return data.stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("data") + public void testDequoting(final String unquoted, final String quoted) { + String actual = QuoteUtil.dequote(quoted); + actual = QuoteUtil.unescape(actual); + assertEquals(unquoted, actual); + } + + @ParameterizedTest + @MethodSource("data") + public void testQuoting(final String unquoted, final String quoted) { + StringBuilder buf = new StringBuilder(); + QuoteUtil.quote(buf, unquoted); + + String actual = buf.toString(); + assertEquals(quoted, actual); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/QuoteUtilTest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/QuoteUtilTest.java new file mode 100644 index 000000000..bc6aead2c --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/QuoteUtilTest.java @@ -0,0 +1,118 @@ +package com.fireflysource.net.websocket.common.utils; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test QuoteUtil + */ +public class QuoteUtilTest { + private void assertSplitAt(Iterator iter, String... expectedParts) { + int len = expectedParts.length; + for (int i = 0; i < len; i++) { + String expected = expectedParts[i]; + assertTrue(iter.hasNext()); + assertEquals(expected, iter.next()); + } + } + + @Test + public void testSplitAtPreserveQuoting() { + Iterator iter = QuoteUtil.splitAt("permessage-compress; method=\"foo, bar\"", ";"); + assertSplitAt(iter, "permessage-compress", "method=\"foo, bar\""); + } + + @Test + public void testSplitAtPreserveQuotingWithNestedDelim() { + Iterator iter = QuoteUtil.splitAt("permessage-compress; method=\"foo; x=10\"", ";"); + assertSplitAt(iter, "permessage-compress", "method=\"foo; x=10\""); + } + + @Test + public void testSplitAtAllWhitespace() { + Iterator iter = QuoteUtil.splitAt(" ", "="); + assertFalse(iter.hasNext()); + assertThrows(NoSuchElementException.class, () -> iter.next()); + } + + @Test + public void testSplitAtEmpty() { + Iterator iter = QuoteUtil.splitAt("", "="); + assertFalse(iter.hasNext()); + assertThrows(NoSuchElementException.class, () -> iter.next()); + } + + @Test + public void testSplitAtHelloWorld() { + Iterator iter = QuoteUtil.splitAt("Hello World", " ="); + assertSplitAt(iter, "Hello", "World"); + } + + @Test + public void testSplitAtKeyValueMessage() { + Iterator iter = QuoteUtil.splitAt("method=\"foo, bar\"", "="); + assertSplitAt(iter, "method", "foo, bar"); + } + + @Test + public void testSplitAtQuotedDelim() { + // test that split ignores delimiters that occur within a quoted + // part of the sequence. + Iterator iter = QuoteUtil.splitAt("A,\"B,C\",D", ","); + assertSplitAt(iter, "A", "B,C", "D"); + } + + @Test + public void testSplitAtSimple() { + Iterator iter = QuoteUtil.splitAt("Hi", "="); + assertSplitAt(iter, "Hi"); + } + + @Test + public void testSplitKeyValueQuoted() { + Iterator iter = QuoteUtil.splitAt("Key = \"Value\"", "="); + assertSplitAt(iter, "Key", "Value"); + } + + @Test + public void testSplitKeyValueQuotedValueList() { + Iterator iter = QuoteUtil.splitAt("Fruit = \"Apple, Banana, Cherry\"", "="); + assertSplitAt(iter, "Fruit", "Apple, Banana, Cherry"); + } + + @Test + public void testSplitKeyValueQuotedWithDelim() { + Iterator iter = QuoteUtil.splitAt("Key = \"Option=Value\"", "="); + assertSplitAt(iter, "Key", "Option=Value"); + } + + @Test + public void testSplitKeyValueSimple() { + Iterator iter = QuoteUtil.splitAt("Key=Value", "="); + assertSplitAt(iter, "Key", "Value"); + } + + @Test + public void testSplitKeyValueWithWhitespace() { + Iterator iter = QuoteUtil.splitAt("Key = Value", "="); + assertSplitAt(iter, "Key", "Value"); + } + + @Test + public void testQuoteIfNeeded() { + StringBuilder buf = new StringBuilder(); + QuoteUtil.quoteIfNeeded(buf, "key", ","); + assertEquals("key", buf.toString()); + } + + @Test + public void testQuoteIfNeedednull() { + StringBuilder buf = new StringBuilder(); + QuoteUtil.quoteIfNeeded(buf, null, ";="); + assertEquals("", buf.toString()); + } +} diff --git a/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/WSURITest.java b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/WSURITest.java new file mode 100644 index 000000000..fdb0d95e8 --- /dev/null +++ b/firefly-net/src/test/java/com/fireflysource/net/websocket/common/utils/WSURITest.java @@ -0,0 +1,58 @@ +package com.fireflysource.net.websocket.common.utils; + +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WSURITest { + private void assertURI(URI actual, URI expected) { + assertEquals(expected.toASCIIString(), actual.toASCIIString()); + } + + @Test + public void testHttpsToHttps() throws URISyntaxException { + assertURI(WSURI.toHttp(URI.create("https://localhost/")), URI.create("https://localhost/")); + } + + @Test + public void testHttpsToWss() throws URISyntaxException { + assertURI(WSURI.toWebsocket(URI.create("https://localhost/")), URI.create("wss://localhost/")); + } + + @Test + public void testHttpToHttp() throws URISyntaxException { + assertURI(WSURI.toHttp(URI.create("http://localhost/")), URI.create("http://localhost/")); + } + + @Test + public void testHttpToWs() throws URISyntaxException { + assertURI(WSURI.toWebsocket(URI.create("http://localhost/")), URI.create("ws://localhost/")); + assertURI(WSURI.toWebsocket(URI.create("http://localhost:8080/deeper/")), URI.create("ws://localhost:8080/deeper/")); + assertURI(WSURI.toWebsocket("http://localhost/"), URI.create("ws://localhost/")); + assertURI(WSURI.toWebsocket("http://localhost/", null), URI.create("ws://localhost/")); + assertURI(WSURI.toWebsocket("http://localhost/", "a=b"), URI.create("ws://localhost/?a=b")); + } + + @Test + public void testWssToHttps() throws URISyntaxException { + assertURI(WSURI.toHttp(URI.create("wss://localhost/")), URI.create("https://localhost/")); + } + + @Test + public void testWssToWss() throws URISyntaxException { + assertURI(WSURI.toWebsocket(URI.create("wss://localhost/")), URI.create("wss://localhost/")); + } + + @Test + public void testWsToHttp() throws URISyntaxException { + assertURI(WSURI.toHttp(URI.create("ws://localhost/")), URI.create("http://localhost/")); + } + + @Test + public void testWsToWs() throws URISyntaxException { + assertURI(WSURI.toWebsocket(URI.create("ws://localhost/")), URI.create("ws://localhost/")); + } +} diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/common/v2/stream/TestAsyncHttp2Connection.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/common/v2/stream/TestAsyncHttp2Connection.kt new file mode 100644 index 000000000..5a4ba7720 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/common/v2/stream/TestAsyncHttp2Connection.kt @@ -0,0 +1,894 @@ +package com.fireflysource.net.common.v2.stream + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.common.sys.Result.futureToConsumer +import com.fireflysource.net.http.client.impl.Http2ClientConnection +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.common.v2.frame.* +import com.fireflysource.net.http.common.v2.frame.SettingsFrame.DEFAULT_SETTINGS_FRAME +import com.fireflysource.net.http.common.v2.stream.AsyncHttp2Stream +import com.fireflysource.net.http.common.v2.stream.Http2Connection +import com.fireflysource.net.http.common.v2.stream.SimpleFlowControlStrategy +import com.fireflysource.net.http.common.v2.stream.Stream +import com.fireflysource.net.http.server.impl.Http2ServerConnection +import com.fireflysource.net.tcp.aio.AioTcpClient +import com.fireflysource.net.tcp.aio.AioTcpServer +import com.fireflysource.net.tcp.aio.TcpConfig +import com.fireflysource.net.tcp.onAcceptAsync +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.net.URL +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import kotlin.system.measureTimeMillis + +class TestAsyncHttp2Connection { + + @Test + @DisplayName("should send priority frame successfully") + fun testPriority() = runBlocking { + val host = "localhost" + val port = 4026 + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream: $stream . the headers: $frame .") + + val fields = HttpFields() + fields.put("Test-New-Stream-Response", "R1") + if (frame.priority != null) { + fields.put("Stream-Priority", "${frame.priority.weight}") + fields.put("Dependency-Stream", "${frame.priority.parentStreamId}") + } + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val headersFrame = HeadersFrame(stream.id, response, null, false) + stream.headers(headersFrame) { println("Server response success.") } + + return Stream.Listener.Adapter() + } + } + ).begin() + }.listen(host, port) + + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val responseHeadersChannel = Channel(UNLIMITED) + val newStreamChannel = Channel(UNLIMITED) + val headersFrame = createRequestHeadersFrame() + http2Connection.newStream(headersFrame, + { + if (it.isSuccess) { + val success = newStreamChannel.trySend(it.value) + println("offer new stream success: $success .") + } else { + println("new a stream failed") + it.throwable?.printStackTrace() + } + }, + object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + responseHeadersChannel.trySend(frame) + } + }) + + val stream = newStreamChannel.receive() + val responseHeadersFrame = responseHeadersChannel.receive() + assertEquals(1, responseHeadersFrame.streamId) + assertTrue(responseHeadersFrame.metaData.isResponse) + assertEquals("R1", responseHeadersFrame.metaData.fields["Test-New-Stream-Response"]) + assertNull(responseHeadersFrame.metaData.fields["Dependency-Stream"]) + + + val priorityFrame = PriorityFrame(stream.id + 2, stream.id, 10, false) + val headersFrameWithPriority = createRequestHeadersFrame(priorityFrame) + http2Connection.newStream(headersFrameWithPriority, + { + if (it.isSuccess) { + val success = newStreamChannel.trySend(it.value) + println("offer new stream success: $success .") + } else { + println("new a stream failed") + it.throwable?.printStackTrace() + } + }, + object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + responseHeadersChannel.trySend(frame) + } + }) + + val future = CompletableFuture() + http2Connection.priority( + PriorityFrame(stream.id + 2, stream.id, 15, false), + futureToConsumer(future) + ) + + val stream2 = newStreamChannel.receive() + assertEquals(3, stream2.id) + assertFalse(stream2.isReset) + val responseHeadersFrame2 = responseHeadersChannel.receive() + assertEquals(3, responseHeadersFrame2.streamId) + assertTrue(responseHeadersFrame2.metaData.isResponse) + assertEquals("1", responseHeadersFrame2.metaData.fields["Dependency-Stream"]) + assertEquals("10", responseHeadersFrame2.metaData.fields["Stream-Priority"]) + + future.await() + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should reset stream successfully after the stream sends reset frame") + fun testResetFrame() = runBlocking { + val host = "localhost" + val port = 4025 + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + + val resetFrameChannel = Channel(UNLIMITED) + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onReset(http2Connection: Http2Connection, frame: ResetFrame) { + println("Server receives reset frame for an unknown stream. frame: $frame") + resetFrameChannel.trySend(frame) + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream: $stream . the headers: $frame .") + + val fields = HttpFields() + fields.put("Test-New-Stream-Response", "R1") + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val headersFrame = HeadersFrame(stream.id, response, null, false) + stream.headers(headersFrame) { println("Server response success.") } + + return object : Stream.Listener.Adapter() { + override fun onReset(stream: Stream, frame: ResetFrame) { + println("Server receives the reset frame: $frame .") + resetFrameChannel.trySend(frame) + } + } + } + } + ).begin() + }.listen(host, port) + + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val newStreamChannel = Channel(UNLIMITED) + val responseHeadersChannel = Channel(UNLIMITED) + val headersFrame = createRequestHeadersFrame() + http2Connection.newStream(headersFrame, + { + if (it.isSuccess) { + val success = newStreamChannel.trySend(it.value) + println("offer new stream success: $success .") + } else { + println("new a stream failed") + it.throwable?.printStackTrace() + } + }, + object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client receives headers: $frame") + responseHeadersChannel.trySend(frame) + } + + override fun onReset(stream: Stream, frame: ResetFrame) { + println("Client receives reset frame: $frame") + } + }) + + val time = measureTimeMillis { + val newStream = newStreamChannel.receive() + assertEquals(1, newStream.id) + assertFalse(newStream.isReset) + + val responseHeadersFrame = responseHeadersChannel.receive() + assertEquals(1, responseHeadersFrame.streamId) + assertTrue(responseHeadersFrame.metaData.isResponse) + assertEquals("R1", responseHeadersFrame.metaData.fields["Test-New-Stream-Response"]) + + val resetFrame = ResetFrame(newStream.id, ErrorCode.INTERNAL_ERROR.code) + newStream.reset(resetFrame) { + println("reset frame success. $it") + } + val serverReceivedResetFrame = resetFrameChannel.receive() + assertTrue(newStream.isReset) + assertEquals(1, serverReceivedResetFrame.streamId) + assertEquals(ErrorCode.INTERNAL_ERROR.code, serverReceivedResetFrame.error) + } + + println("reset stream time: $time ms") + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should create server push stream successfully") + fun testPushPromise() = runBlocking { + val host = "localhost" + val port = 4024 + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onPreface(http2Connection: Http2Connection): MutableMap { + println("Server receives the preface frame.") + return DEFAULT_SETTINGS_FRAME.settings + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream. stream: ${stream}, headers: $frame") + + val fields = HttpFields() + fields.put("Test-Push-Promise-Stream", "P1") + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val pushPromiseFrame = PushPromiseFrame(stream.id, 0, response) + stream.push(pushPromiseFrame, { + if (it.isSuccess) { + println("Server creates new push stream success. stream: ${it.value}") + } else { + println("new a push stream failed") + it.throwable?.printStackTrace() + } + }, Stream.Listener.Adapter()) + + return Stream.Listener.Adapter() + } + } + ).begin() + }.listen(host, port) + + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val newPushStreamChannel = Channel(UNLIMITED) + val pushPromiseChannel = Channel(UNLIMITED) + val headersFrame = createRequestHeadersFrame() + http2Connection.newStream(headersFrame, + { + if (it.isSuccess) { + println("Client creates new stream success. stream: ${it.value}") + } else { + println("new a stream failed") + it.throwable?.printStackTrace() + } + }, + object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + } + + override fun onPush(stream: Stream, frame: PushPromiseFrame): Stream.Listener { + val success = newPushStreamChannel.trySend(stream) + println("Client received push stream: $stream . $success , $frame") + + pushPromiseChannel.trySend(frame) + return Stream.Listener.Adapter() + } + }) + + val time = measureTimeMillis { + val newStream = newPushStreamChannel.receive() + assertEquals(2, newStream.id) + assertFalse(newStream.isReset) + + val frame = pushPromiseChannel.receive() + assertEquals(1, frame.streamId) + assertEquals(2, frame.promisedStreamId) + assertTrue(frame.metaData.isResponse) + assertEquals("P1", frame.metaData.fields["Test-Push-Promise-Stream"]) + } + println("push promise stream time: $time ms") + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should send data frame successfully after the stream creates.") + fun testData(): Unit = runTest { + val host = "localhost" + val port = 4027 + val tcpConfig = TcpConfig(30, true) + val httpConfig = HttpConfig() + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream: ${stream}. the headers: ${frame}.") + + val fields = HttpFields() + fields.put("Test-New-Stream-Response", "R1") + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val headersFrame = HeadersFrame(stream.id, response, null, false) + stream.headers(headersFrame) { println("Server response header success.") } + + val data = BufferUtils.toBuffer("test data frame.") + val dataFrame = DataFrame(stream.id, data, true) + stream.data(dataFrame) { println("Server response data success.") } + + return Stream.Listener.Adapter() + } + } + ).begin() + }.listen(host, port) + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val dataFrameChannel = Channel(UNLIMITED) + val headersFrame = createRequestHeadersFrame() + val future = http2Connection.newStream(headersFrame, object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + } + + override fun onData(stream: Stream, frame: DataFrame, result: Consumer>) { + println("Client received data frame: $frame") + dataFrameChannel.trySend(frame) + result.accept(Result.SUCCESS) + } + }) + val time = measureTimeMillis { + val newStream = future.await() + val dataFrame = dataFrameChannel.receive() + assertEquals(1, newStream.id) + assertFalse(newStream.isReset) + assertEquals("test data frame.", BufferUtils.toString(dataFrame.data)) + } + + println("receive data time: $time") + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should create a new stream successfully") + fun testNewStream() = runTest { + val host = "localhost" + val port = 4023 + val tcpConfig = TcpConfig(30, true) + val httpConfig = HttpConfig() + + val requestHeadersChannel = Channel(UNLIMITED) + val responseHeadersChannel = Channel(UNLIMITED) + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream: $stream . the headers: $frame .") + requestHeadersChannel.trySend(frame) + + val fields = HttpFields() + fields.put("Test-New-Stream-Response", "R1") + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val headersFrame = HeadersFrame(stream.id, response, null, true) + stream.headers(headersFrame) { println("Server response success.") } + + return Stream.Listener.Adapter() + } + } + ).begin() + }.listen(host, port) + + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val headersFrame = createRequestHeadersFrame() + val future = http2Connection.newStream(headersFrame, object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + responseHeadersChannel.trySend(frame) + } + }) + + val time = measureTimeMillis { + val newStream = future.await() + assertEquals(1, newStream.id) + assertFalse(newStream.isReset) + if (newStream is AsyncHttp2Stream) { + assertTrue(newStream.getSendWindow() > 0) + assertEquals(httpConfig.initialStreamRecvWindow, newStream.getRecvWindow()) + } + val http2ClientConnection = http2Connection as Http2ClientConnection + assertTrue(http2ClientConnection.getSendWindow() > 0) + assertEquals(httpConfig.initialSessionRecvWindow, http2ClientConnection.getRecvWindow()) + + val requestHeadersFrame = requestHeadersChannel.receive() + assertEquals(1, requestHeadersFrame.streamId) + assertTrue(requestHeadersFrame.metaData.isRequest) + assertEquals("V1", requestHeadersFrame.metaData.fields["Test-New-Stream"]) + + val responseHeadersFrame = responseHeadersChannel.receive() + assertEquals(1, responseHeadersFrame.streamId) + assertTrue(responseHeadersFrame.metaData.isResponse) + assertEquals("R1", responseHeadersFrame.metaData.fields["Test-New-Stream-Response"]) + } + println("new stream time: $time ms") + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + private fun createClientHttp2ConnectionListener(): Http2Connection.Listener.Adapter { + return object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Client receives go away frame: $frame") + } + } + } + + private fun createRequestHeadersFrame(priorityFrame: PriorityFrame? = null): HeadersFrame { + val httpFields = HttpFields() + httpFields.put("Test-New-Stream", "V1") + @Suppress("BlockingMethodInNonBlockingContext") + val request = MetaData.Request( + HttpMethod.GET.value, + HttpURI(URL("http://localhost:8888/test").toURI()), + HttpVersion.HTTP_2, + httpFields + ) + return HeadersFrame(request, priorityFrame, true) + } + + @Test + @DisplayName("should send go away frame successfully") + fun testGoAway() = runTest { + val host = "localhost" + val port = 4022 + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + } + ).begin() + }.listen(host, port) + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Client receives go away frame: $frame") + } + } + ) + + val success = http2Connection.close(ErrorCode.INTERNAL_ERROR.code, "test error message") + assertTrue(success) + + client.stop() + server.stop() + } + + @Test + @DisplayName("should send settings frame successfully") + fun testSettings() = runTest { + val host = "localhost" + val port = 4021 + val channel = Channel(UNLIMITED) + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + + val settingsFrame = SettingsFrame( + mutableMapOf( + SettingsFrame.HEADER_TABLE_SIZE to 8192, + SettingsFrame.ENABLE_PUSH to 1, + SettingsFrame.MAX_CONCURRENT_STREAMS to 300, + SettingsFrame.INITIAL_WINDOW_SIZE to 128 * 1024, + SettingsFrame.MAX_FRAME_SIZE to 1024 * 1024, + SettingsFrame.MAX_HEADER_LIST_SIZE to 64 + ), false + ) + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onSettings(http2Connection: Http2Connection, frame: SettingsFrame) { + println("server receives settings: $frame") + + if (frame.settings == settingsFrame.settings) { + val success = channel.trySend(frame) + println("put result settings frame: $success") + } + } + } + ).begin() + }.listen(host, port) + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onSettings(http2Connection: Http2Connection, frame: SettingsFrame) { + println("client receives settings: $frame") + } + } + ) + + http2Connection.settings(settingsFrame) { println("send settings success. $it") } + + val receivedSettings = channel.receive()//withTimeout(2000) { channel.receive() } + assertEquals(settingsFrame.settings, receivedSettings.settings) + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should send ping frame successfully") + fun testPing() = runTest { + val host = "localhost" + val port = 4020 + val count = 10L + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + + val channel = Channel(UNLIMITED) + + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + } + ).begin() + }.listen(host, port) + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onPing(http2Connection: Http2Connection, frame: PingFrame) { + println("Client receives the ping frame. ${frame.payloadAsLong}: ${frame.isReply}") + if (frame.payloadAsLong == count) { + val success = channel.trySend(frame.payloadAsLong) + println("put result ping frame: $success") + } + } + } + ) + + (1..count).forEach { index -> + val pingFrame = PingFrame(index, false) + http2Connection.ping(pingFrame) { println("send ping success. $it") } + } + + val pingCount = channel.receive()//withTimeout(20000) { channel.receive() } + assertTrue(pingCount > 0) + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should receive reset frame when the stream idle timeout") + fun testStreamIdleTimeout(): Unit = runTest { + val host = "localhost" + val port = 4100 + val tcpConfig = TcpConfig(30, false) + val serverHttpConfig = HttpConfig() + serverHttpConfig.streamIdleTimeout = 1 + + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + serverHttpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream: $stream . the headers: $frame .") + + val fields = HttpFields() + fields.put("Test-Idle-Timeout", "R1") + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val headersFrame = HeadersFrame(stream.id, response, null, false) + stream.headers(headersFrame) { println("Server response success.") } + return object : Stream.Listener.Adapter() { + override fun onReset(stream: Stream, frame: ResetFrame) { + println("Server received reset: $frame") + } + + override fun onIdleTimeout(stream: Stream, x: Throwable): Boolean { + println("${x.message}: $stream") + return true + } + } + } + } + ).begin() + }.listen(host, port) + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + HttpConfig(), connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val headersFrame = createRequestHeadersFrame() + val channel: Channel = Channel(UNLIMITED) + val future = http2Connection.newStream(headersFrame, object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + } + + override fun onReset(stream: Stream, frame: ResetFrame) { + println("Client received reset: $frame") + channel.trySend(frame) + } + }) + + val time = measureTimeMillis { + val newStream = future.await() + assertEquals(1, newStream.id) + val resetFrame = channel.receive() + assertEquals(1, resetFrame.streamId) + } + println("new stream time: $time ms") + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + + @Test + @DisplayName("should set the stream idle timeout successfully") + fun testSetStreamIdleTimeout(): Unit = runTest { + val host = "localhost" + val port = 4101 + val tcpConfig = TcpConfig(30, false) + val httpConfig = HttpConfig() + httpConfig.streamIdleTimeout = 30 + + val channel: Channel = Channel(UNLIMITED) + val server = AioTcpServer(tcpConfig).onAcceptAsync { connection -> + connection.beginHandshake().await() + Http2ServerConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + object : Http2Connection.Listener.Adapter() { + + override fun onFailure(http2Connection: Http2Connection, failure: Throwable) { + failure.printStackTrace() + } + + override fun onClose(http2Connection: Http2Connection, frame: GoAwayFrame) { + println("Server receives go away frame: $frame") + } + + override fun onNewStream(stream: Stream, frame: HeadersFrame): Stream.Listener { + println("Server creates the remote stream: $stream . the headers: $frame .") + + val fields = HttpFields() + fields.put("Test-Idle-Timeout", "R1") + val response = MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields) + val headersFrame = HeadersFrame(stream.id, response, null, false) + stream.headers(headersFrame) { println("Server response success.") } + return object : Stream.Listener.Adapter() { + override fun onReset(stream: Stream, frame: ResetFrame) { + println("Server received reset: $frame") + channel.trySend(frame) + } + + override fun onIdleTimeout(stream: Stream, x: Throwable): Boolean { + println("${x.message}: $stream") + return true + } + } + } + } + ).begin() + }.listen(host, port) + + val client = AioTcpClient(tcpConfig) + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val http2Connection: Http2Connection = Http2ClientConnection( + httpConfig, connection, SimpleFlowControlStrategy(), + createClientHttp2ConnectionListener() + ) + + val headersFrame = createRequestHeadersFrame() + + val future = http2Connection.newStream(headersFrame, object : Stream.Listener.Adapter() { + override fun onHeaders(stream: Stream, frame: HeadersFrame) { + println("Client received headers: $frame") + } + + override fun onReset(stream: Stream, frame: ResetFrame) { + println("Client received reset: $frame") + } + }) + + val time = measureTimeMillis { + val newStream = future.await() + assertEquals(1, newStream.id) + newStream.idleTimeout = 1 + val resetFrame = channel.receive() + assertEquals(1, resetFrame.streamId) + } + println("new stream time: $time ms") + + http2Connection.close(ErrorCode.NO_ERROR.code, "exit test") {} + + client.stop() + server.stop() + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/common/v2/stream/TestAsyncHttp2Stream.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/common/v2/stream/TestAsyncHttp2Stream.kt new file mode 100644 index 000000000..13976f56c --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/common/v2/stream/TestAsyncHttp2Stream.kt @@ -0,0 +1,80 @@ +package com.fireflysource.net.common.v2.stream + +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.net.http.common.v2.frame.CloseState +import com.fireflysource.net.http.common.v2.frame.ErrorCode +import com.fireflysource.net.http.common.v2.frame.ResetFrame +import com.fireflysource.net.http.common.v2.stream.AsyncHttp2Connection +import com.fireflysource.net.http.common.v2.stream.AsyncHttp2Stream +import com.fireflysource.net.http.common.v2.stream.Stream +import com.fireflysource.net.http.common.v2.stream.getAndIncreaseStreamId +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger + +class TestAsyncHttp2Stream { + + private val asyncHttp2Connection = Mockito.mock(AsyncHttp2Connection::class.java) + + @Test + @DisplayName("should update stream window successfully") + fun testWindowUpdate() { + val stream = AsyncHttp2Stream(asyncHttp2Connection, 1, true, Stream.Listener.Adapter()) + val initRecvWindow = stream.updateRecvWindow(25) + assertEquals(0, initRecvWindow) + assertEquals(25, stream.getRecvWindow()) + + val initSendWindow = stream.updateSendWindow(13) + assertEquals(0, initSendWindow) + assertEquals(13, stream.getSendWindow()) + } + + @Test + @DisplayName("should close stream after the stream remote close and local close") + fun testCloseStream() { + val stream = AsyncHttp2Stream(asyncHttp2Connection, 1, true, Stream.Listener.Adapter()) + assertFalse(stream.isClosed) + + val result = stream.updateClose(true, CloseState.Event.RECEIVED) + assertFalse(result) + + val result1 = stream.updateClose(true, CloseState.Event.BEFORE_SEND) + assertFalse(result1) + + val result2 = stream.updateClose(true, CloseState.Event.AFTER_SEND) + assertTrue(result2) + assertTrue(stream.isClosed) + } + + @Test + @DisplayName("should stream reset when the reset frame sent") + fun testReset() { + val stream = AsyncHttp2Stream(asyncHttp2Connection, 1, true, Stream.Listener.Adapter()) + assertFalse(stream.isReset) + + val frame = ResetFrame(1, ErrorCode.INTERNAL_ERROR.code) + val future = CompletableFuture() + future.complete(0) + `when`(asyncHttp2Connection.sendControlFrame(stream, frame)).thenReturn(future) + stream.reset(frame, discard()) + verify(asyncHttp2Connection).sendControlFrame(stream, frame) + assertTrue(stream.isReset) + } + + @Test + @DisplayName("should get the initial stream id when the id exceeds max integer") + fun testStreamIdIncrease() { + val id1 = AtomicInteger(Integer.MAX_VALUE) + assertEquals(Integer.MAX_VALUE, getAndIncreaseStreamId(id1, 3)) + assertEquals(3, getAndIncreaseStreamId(id1, 3)) + + val id2 = AtomicInteger(Integer.MAX_VALUE - 1) + assertEquals(Integer.MAX_VALUE - 1, getAndIncreaseStreamId(id2, 2)) + assertEquals(2, getAndIncreaseStreamId(id2, 2)) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestAsyncHttpClientConnectionManager.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestAsyncHttpClientConnectionManager.kt new file mode 100644 index 000000000..50f1ff3a3 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestAsyncHttpClientConnectionManager.kt @@ -0,0 +1,90 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.Http1ServerConnection +import com.fireflysource.net.tcp.TcpClientConnectionFactory +import com.fireflysource.net.tcp.TcpServer +import com.fireflysource.net.tcp.TcpServerFactory +import com.fireflysource.net.tcp.aio.AioTcpChannelGroup +import com.fireflysource.net.tcp.onAcceptAsync +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.net.InetSocketAddress +import java.net.URL +import java.util.concurrent.CompletableFuture +import kotlin.random.Random + +class TestAsyncHttpClientConnectionManager { + + private lateinit var address: InetSocketAddress + private lateinit var httpServer: TcpServer + + @BeforeEach + fun init() { + address = InetSocketAddress("localhost", Random.nextInt(2000, 5000)) + val listener = object : HttpServerConnection.Listener.Adapter() { + override fun onHttpRequestComplete(ctx: RoutingContext): CompletableFuture { + return ctx.put(HttpHeader.CONTENT_LENGTH, "7").end("test ok") + } + + override fun onException(context: RoutingContext, e: Throwable): CompletableFuture { + e.printStackTrace() + return Result.DONE + } + } + httpServer = TcpServerFactory.create().timeout(120 * 1000L).enableOutputBuffer() + .onAcceptAsync { connection -> + println("accept connection. ${connection.id}") + connection.beginHandshake().await() + val http1Connection = Http1ServerConnection(HttpConfig(), connection) + http1Connection.setListener(listener).begin() + }.listen(address) + } + + @AfterEach + fun destroy() { + httpServer.stop() + } + + @Test + fun test() = runTest { + val config = HttpConfig() + val connectionFactory = TcpClientConnectionFactory( + AioTcpChannelGroup("async-http-client"), + config.isStopTcpChannelGroup, + config.timeout, + config.secureEngineFactory + ) + val manager = AsyncHttpClientConnectionManager(config, connectionFactory) + + repeat(5) { + val request = AsyncHttpClientRequest() + request.method = HttpMethod.GET.value + @Suppress("BlockingMethodInNonBlockingContext") + request.uri = HttpURI(URL("http://${address.hostName}:${address.port}/test1").toURI()) + + val response = manager.send(request).await() + println("${response.status} ${response.reason}") + println(response.httpFields) + println(response.stringBody) + println() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals(7L, response.contentLength) + assertEquals("test ok", response.stringBody) + } + + manager.stop() + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestAsyncHttpClientRequestBuilder.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestAsyncHttpClientRequestBuilder.kt new file mode 100644 index 000000000..049c55c70 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestAsyncHttpClientRequestBuilder.kt @@ -0,0 +1,175 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.client.HttpClientConnectionManager +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.stringBody +import com.fireflysource.net.http.client.impl.content.provider.ByteBufferContentProvider +import com.fireflysource.net.http.client.impl.content.provider.StringContentProvider +import com.fireflysource.net.http.common.model.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import java.net.URL +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets + +class TestAsyncHttpClientRequestBuilder { + + private val connectionManager = mock(HttpClientConnectionManager::class.java) + + @Test + fun testPostStringBodyData() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.POST.value, uri, HttpVersion.HTTP_1_1) + builder.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_PLAIN.value).body("body 123") + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + println(metadata.fields) + + assertTrue(metadata.isRequest) + + assertNotNull(builder.httpRequest.contentProvider) + assertTrue(builder.httpRequest.contentProvider is StringContentProvider) + } + + @Test + fun testPostByteBufferBodyData() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.POST.value, uri, HttpVersion.HTTP_1_1) + val buffer = ByteBuffer.allocate(20) + builder.put(HttpHeader.CONTENT_TYPE, "application/octet-stream").body(buffer) + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + println(metadata.fields) + + assertTrue(metadata.isRequest) + + assertNotNull(builder.httpRequest.contentProvider) + assertTrue(builder.httpRequest.contentProvider is ByteBufferContentProvider) + } + + @Test + fun testPostFormData() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.POST.value, uri, HttpVersion.HTTP_1_1) + builder.putFormInput("p1", "v1") + .putFormInput("p2", "v2") + .addFormInput("p3", "v3") + .addFormInputs("p3", listOf("v31", "v32")) + .putFormInputs("p4", listOf()) + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + println(metadata.fields) + + assertTrue(metadata.isRequest) + assertEquals(MimeTypes.Type.FORM_ENCODED.value, metadata.fields[HttpHeader.CONTENT_TYPE]) + assertTrue(metadata.fields[HttpHeader.CONTENT_LENGTH].toLong() > 0) + + assertNotNull(builder.httpRequest.contentProvider) + assertTrue(builder.httpRequest.contentProvider is StringContentProvider) + assertTrue(builder.httpRequest.contentProvider!!.length() > 0) + assertTrue((builder.httpRequest.contentProvider as StringContentProvider).content.contains("p1=v1&p2=v2&p3=v3&p3=v31&p3=v32&p4=")) + + println((builder.httpRequest.contentProvider as StringContentProvider).content) + } + + @Test + fun testQueryParam() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.GET.value, uri, HttpVersion.HTTP_1_1) + builder.putQueryString("q1", "v1") + .putQueryString("q2", "v2") + .addQueryString("q2", "v22") + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + + assertTrue(metadata.isRequest) + assertTrue(metadata.uri.query.contains("q1=v1&q2=v2&q2=v22")) + + assertEquals(0, metadata.fields.size()) + } + + @Test + fun testQueryParam2() { + val uri = HttpURI(URL("https://www.fireflysource.com?a1=c1&q1=v1").toURI()) + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.GET.value, uri, HttpVersion.HTTP_1_1) + builder.putQueryString("q1", "v1") + .putQueryString("q2", "v2") + .addQueryString("q2", "v22") + .putQueryStrings("q3", listOf("v31", "v32", "v33")) + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + + assertTrue(metadata.isRequest) + assertTrue(metadata.uri.query.contains("a1=c1&q1=v1&q1=v1&q2=v2&q2=v22&q3=v31&q3=v32&q3=v33")) + + assertEquals(0, metadata.fields.size()) + } + + @Test + fun testPostMultiPartData() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.POST.value, uri, HttpVersion.HTTP_1_1) + + val fields = HttpFields() + fields.add("t1", "x1") + builder.addPart("text1", stringBody("plain text1", StandardCharsets.UTF_8), fields) + + val fields2 = HttpFields() + fields2.add("t2", "x2") + builder.addPart("text2", stringBody("plain text2", StandardCharsets.UTF_8), fields2) + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + println(metadata.fields) + + assertTrue(metadata.isRequest) + assertTrue(metadata.fields[HttpHeader.CONTENT_TYPE].contains("multipart/form-data")) + assertEquals("327", metadata.fields[HttpHeader.CONTENT_LENGTH]) + } + + @Test + fun testPostFileMultiPartData() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.POST.value, uri, HttpVersion.HTTP_1_1) + + val fields = HttpFields() + fields.add("t1", "x1") + builder.addFilePart( + "text1", + "file1.txt", + stringBody("mock file text1", StandardCharsets.UTF_8), + fields + ) + + val metadata = toMetaDataRequest(builder.httpRequest) + println(metadata) + println(metadata.fields) + + assertTrue(metadata.isRequest) + assertTrue(metadata.fields[HttpHeader.CONTENT_TYPE].contains("multipart/form-data")) + } + + @Test + fun testCookie() { + val uri = HttpURI("https://www.fireflysource.com") + val builder = AsyncHttpClientRequestBuilder(connectionManager, HttpMethod.POST.value, uri, HttpVersion.HTTP_1_1) + + builder.cookies( + mutableListOf( + Cookie("c1", "v1"), + Cookie("c2", "v2"), + Cookie("c3", "v3") + ) + ) + + val metadata = toMetaDataRequest(builder.httpRequest) + assertTrue(metadata.isRequest) + assertTrue(metadata.fields[HttpHeader.COOKIE].contains("c1=v1;c2=v2;c3=v3")) + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttp1ClientConnection.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttp1ClientConnection.kt new file mode 100644 index 000000000..9273137ad --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttp1ClientConnection.kt @@ -0,0 +1,52 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpHeaderValue +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpURI +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class TestHttp1ClientConnection { + + @Test + @DisplayName("should add the HOST and KEEP_ALIVE headers") + fun testPrepareHttp1Headers() { + val request = AsyncHttpClientRequest() + request.method = HttpMethod.GET.value + request.uri = HttpURI("https://www.fireflysource.com/") + prepareHttp1Headers(request) { "defaultHost" } + + assertTrue(request.httpFields.getValuesList(HttpHeader.HOST.value).isNotEmpty()) + assertEquals("www.fireflysource.com", request.httpFields[HttpHeader.HOST.value]) + assertEquals(HttpHeaderValue.KEEP_ALIVE.value, request.httpFields[HttpHeader.CONNECTION.value]) + } + + @Test + @DisplayName("should set default host header") + fun testDefaultHost() { + val request = AsyncHttpClientRequest() + request.method = HttpMethod.GET.value + request.uri = HttpURI("/echo0") + prepareHttp1Headers(request) { "defaultHost" } + assertTrue(request.httpFields.getValuesList(HttpHeader.HOST.value).isNotEmpty()) + assertEquals("defaultHost", request.httpFields[HttpHeader.HOST.value]) + } + + @Test + @DisplayName("should not remove user setting headers") + fun testExistConnectionHeaders() { + val request = AsyncHttpClientRequest() + request.method = HttpMethod.GET.value + request.uri = HttpURI("https://www.fireflysource.com/") + request.httpFields.addCSV(HttpHeader.CONNECTION, HttpHeaderValue.UPGRADE.value, "HTTP2-Settings") + prepareHttp1Headers(request) { "" } + + assertEquals( + "${HttpHeaderValue.UPGRADE.value}, HTTP2-Settings, ${HttpHeaderValue.KEEP_ALIVE.value}", + request.httpFields[HttpHeader.CONNECTION.value] + ) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttp2ClientConnection.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttp2ClientConnection.kt new file mode 100644 index 000000000..65721b272 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttp2ClientConnection.kt @@ -0,0 +1,176 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpHeaderValue +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.HttpServerConnection +import com.fireflysource.net.http.server.RoutingContext +import com.fireflysource.net.http.server.impl.Http2ServerConnection +import com.fireflysource.net.tcp.TcpServer +import com.fireflysource.net.tcp.TcpServerFactory +import com.fireflysource.net.tcp.onAcceptAsync +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.net.InetSocketAddress +import java.util.concurrent.CompletableFuture +import kotlin.math.roundToLong +import kotlin.random.Random +import kotlin.system.measureTimeMillis + +class TestHttp2ClientConnection { + + private lateinit var address: InetSocketAddress + + @BeforeEach + fun init() { + address = InetSocketAddress("localhost", Random.nextInt(20000, 40000)) + } + + private fun finish(count: Int, time: Long, httpClient: HttpClient, httpServer: TcpServer) { + val throughput = count / (time / 1000.00) + println("success. $time ms, ${throughput.roundToLong()} qps") + httpClient.stop() + httpServer.stop() + } + + private fun createHttpServer(listener: HttpServerConnection.Listener): TcpServer { + val server = TcpServerFactory.create().timeout(120 * 1000L).enableSecureConnection() + + return server.onAcceptAsync { connection -> + println("accept connection. ${connection.id}") + connection.beginHandshake().await() + val http2Connection = Http2ServerConnection(HttpConfig(), connection) + http2Connection.setListener(listener).begin() + }.listen(address) + } + + @Test + @DisplayName("should send request and receive response successfully.") + fun testSendRequest(): Unit = runBlocking { + val count = 10 + + val httpServer = createHttpServer(object : HttpServerConnection.Listener.Adapter() { + override fun onHttpRequestComplete(ctx: RoutingContext): CompletableFuture { + return ctx.put("Test-Http-Exchange", "R1") + .end("http exchange success.") + } + }) + + val httpClient = HttpClientFactory.create() + + val time = measureTimeMillis { + val futures = (1..count).map { i -> + httpClient.get("https://${address.hostName}:${address.port}/test/$i").submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("http exchange success.", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should receive 100 continue response.") + fun test100Continue(): Unit = runBlocking { + val count = 100 + + val httpServer = createHttpServer(object : HttpServerConnection.Listener.Adapter() { + override fun onHeaderComplete(ctx: RoutingContext): CompletableFuture { + return if (ctx.expect100Continue()) ctx.response100Continue() else Result.DONE + } + + override fun onHttpRequestComplete(ctx: RoutingContext): CompletableFuture { + return ctx.put("Test-100-Continue", "100") + .end("receive data success.") + } + }) + + val httpClient = HttpClientFactory.create() + + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("https://${address.hostName}:${address.port}/data") + .put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.value) + .body("Some test data!") + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("receive data success.", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should receive trailers successfully.") + fun testTrailer(): Unit = runBlocking { + val count = 100 + + val httpServer = createHttpServer(object : HttpServerConnection.Listener.Adapter() { + override fun onHeaderComplete(ctx: RoutingContext): CompletableFuture { + return if (ctx.expect100Continue()) ctx.response100Continue() else Result.DONE + } + + override fun onHttpRequestComplete(ctx: RoutingContext): CompletableFuture { + return ctx.put("Test-100-Continue", "100") + .addCSV(HttpHeader.TRAILER, "t1", "t2", "t3") + .setTrailerSupplier { + val trailers = HttpFields() + trailers.put("t1", "v1") + trailers.put("t2", "v2") + trailers.put("t3", "v3") + trailers + } + .end("receive data success.") + } + }) + + val httpClient = HttpClientFactory.create() + + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("https://${address.hostName}:${address.port}/data") + .put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.value) + .body("Some test data!") + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("receive data success.", response.stringBody) + assertTrue(response.trailerSupplier != null) + val trailers = response.trailerSupplier.get() + println(trailers) + assertEquals("v1", trailers["t1"]) + assertEquals("v2", trailers["t2"]) + assertEquals("v3", trailers["t3"]) + } + + finish(count, time, httpClient, httpServer) + } + + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttpClient.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttpClient.kt new file mode 100644 index 000000000..df9f85cd5 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttpClient.kt @@ -0,0 +1,242 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.useAwait +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.client.impl.content.provider.ByteBufferContentProvider +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.ProxyConfig +import com.fireflysource.net.http.common.exception.MissingRemoteHostException +import com.fireflysource.net.http.common.model.ContentEncoding +import com.fireflysource.net.http.common.model.Cookie +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.net.http.server.HttpServerFactory +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture +import kotlin.math.roundToLong +import kotlin.random.Random +import kotlin.system.measureTimeMillis + +class TestHttpClient { + + private lateinit var address: InetSocketAddress + private lateinit var httpServer: HttpServer + private val content = (1..50000).joinToString("") { it.toString() } + + @BeforeEach + fun init() { + address = InetSocketAddress("localhost", Random.nextInt(2000, 5000)) + httpServer = HttpServerFactory.create() + httpServer + .router().path("/testHttpClient").handler { ctx -> + ctx.setCookies(listOf(Cookie("cookie1", "value1"), Cookie("cookie2", "value2"))) + .put(HttpHeader.CONTENT_LENGTH, "14") + .write("test client ok") + .end() + } + .router().path("/testChunkedEncoding").handler { ctx -> + ctx.end("test chunked encoding success") + } + .router().path("/testNoChunkedEncoding").handler { ctx -> + ctx.put(HttpHeader.CONTENT_LENGTH, "32").end("test no chunked encoding success") + } + .router().get("/testCompressedContent").handler { ctx -> + ctx.put(HttpHeader.CONTENT_ENCODING, ContentEncoding.GZIP.value) + .write("测试压缩内容:") + .write( + mutableListOf( + BufferUtils.toBuffer("跳过", StandardCharsets.UTF_8), + BufferUtils.toBuffer("跳过", StandardCharsets.UTF_8), + BufferUtils.toBuffer("今天,", StandardCharsets.UTF_8), + BufferUtils.toBuffer("非常愉快的", StandardCharsets.UTF_8), + BufferUtils.toBuffer("搞定了这个功能。", StandardCharsets.UTF_8) + ), 2, 3 + ) + .end() + } + .router().get("/echo0").handler { it.end("ok") } + .timeout(120 * 1000L) + .listen(address) + } + + @AfterEach + fun destroy() { + val time = measureTimeMillis { + httpServer.stop() + } + println("shutdown time: $time ms") + } + + @Test + @DisplayName("should send the HTTP request no content successfully") + fun testNoContent() = runBlocking { + val httpClient = HttpClientFactory.create() + val count = 100 + + val time = measureTimeMillis { + val futures = + (1..count).map { httpClient.get("http://${address.hostName}:${address.port}/testHttpClient").submit() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals(14L, response.contentLength) + assertEquals("test client ok", response.stringBody) + + assertEquals(2, response.cookies.size) + assertEquals("value1", response.cookies.filter { it.name == "cookie1" }.map { it.value }[0]) + assertEquals("value2", response.cookies.filter { it.name == "cookie2" }.map { it.value }[0]) + + } + + val throughput = count / (time / 1000.00) + println("success. $time ms, ${throughput.roundToLong()} qps") + httpClient.stop() + } + + @Test + @DisplayName("should send the HTTP request with content using chucked encoding successfully") + fun testContentWithChunkedEncoding() = runBlocking { + val httpClient = HttpClientFactory.create() + + val data = ByteBuffer.wrap(content.toByteArray(StandardCharsets.UTF_8)) + println("data length: ${data.remaining()}") + val response = httpClient + .post("http://${address.hostName}:${address.port}/testChunkedEncoding") + .contentProvider(MockChunkByteBufferContentProvider(data)).submit().await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("test chunked encoding success", response.stringBody) + + httpClient.stop() + } + + @Test + @DisplayName("should send the HTTP request with content and content length successfully") + fun testContentWithoutChunkedEncoding() = runBlocking { + val httpClient = HttpClientFactory.create() + + (1..10).map { + val data = ByteBuffer.wrap(content.toByteArray(StandardCharsets.UTF_8)) + val length = data.remaining() + println("data length: $length") + httpClient.post("http://${address.hostName}:${address.port}/testNoChunkedEncoding") + .contentProvider(ByteBufferContentProvider(data)) + .submit() + }.forEach { + val response = it.await() + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("test no chunked encoding success", response.stringBody) + assertEquals(32, response.contentLength) + } + + httpClient.stop() + } + + @Test + @DisplayName("should get compressed content successfully") + fun testCompressedContent() = runBlocking { + val httpClient = HttpClientFactory.create() + val response = + httpClient.get("http://${address.hostName}:${address.port}/testCompressedContent").submit().await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("测试压缩内容:今天,非常愉快的搞定了这个功能。", response.stringBody) + println(response) + + httpClient.stop() + } + + @Test + @DisplayName("should retry to send request successfully") + fun testConnectionTimeoutRetry() = runBlocking { + val server2 = HttpServerFactory.create() + val addr = InetSocketAddress("localhost", Random.nextInt(12000, 15000)) + server2.router().get("/timeout/echo") + .handler { + val cmd = it.getQueryString("cmd") + it.end("test timeout $cmd") + } + .timeout(1) + .listen(addr) + + val config = HttpConfig() + config.connectionPoolSize = 1 + val httpClient = HttpClientFactory.create(config) + + suspend fun echo() { + val url = "http://${addr.hostName}:${addr.port}/timeout/echo" + val response = httpClient.get(url).addQueryString("cmd", "xx").submit().await() + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("test timeout xx", response.stringBody) + println(response) + } + + echo() + delay(2000) + echo() + + httpClient.stop() + server2.stop() + } + + @Test + @DisplayName("should create HTTP client connection and send request successfully") + fun testCreateHttpClientConnection() = runBlocking { + val httpClient = HttpClientFactory.create() + val uri = "http://${address.hostName}:${address.port}" + val connection = httpClient.createHttpClientConnection(uri).await() + + connection.useAwait { + repeat(3) { + val response = connection.get("/echo0").submit().await() + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("ok", response.stringBody) + println(response) + } + } + + httpClient.stop() + } + + @Test + @DisplayName("should send request failure when the host does not set") + fun testNoHostException() { + val httpClient = HttpClientFactory.create() + assertThrows(MissingRemoteHostException::class.java) { + httpClient.get("/echo0").submit() + } + } + +// @Test + fun testProxy() = runBlocking { + val proxyConfig = ProxyConfig() + proxyConfig.host = "127.0.0.1" + proxyConfig.port = 1091 + val httpConfig = HttpConfig() + httpConfig.proxyConfig = proxyConfig + val client = HttpClientFactory.create(httpConfig) + val response = client.get("https://www.google.com/").submit().await() + println(response) + println(response.stringBody) + } + + class MockChunkByteBufferContentProvider(content: ByteBuffer) : ByteBufferContentProvider(content) { + override fun length(): Long = -1 + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttpProtocolNegotiator.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttpProtocolNegotiator.kt new file mode 100644 index 000000000..5bff0c272 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/TestHttpProtocolNegotiator.kt @@ -0,0 +1,73 @@ +package com.fireflysource.net.http.client.impl + +import com.fireflysource.common.codec.base64.Base64Utils +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.addHttp2UpgradeHeader +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.defaultSettingsFrameBytes +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.expectUpgradeHttp2 +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.isUpgradeSuccess +import com.fireflysource.net.http.client.impl.HttpProtocolNegotiator.removeHttp2UpgradeHeader +import com.fireflysource.net.http.common.model.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * @author Pengtao Qiu + */ +class TestHttpProtocolNegotiator { + + @Test + @DisplayName("should remove h2c headers successfully") + fun testRemoveHttp2UpgradeHeader() { + val request = AsyncHttpClientRequest() + addHttp2UpgradeHeader(request) + assertTrue( + request.httpFields.getCSV(HttpHeader.CONNECTION, false).containsAll( + arrayListOf( + "Upgrade", + "HTTP2-Settings" + ) + ) + ) + assertEquals("h2c", request.httpFields[HttpHeader.UPGRADE]) + assertEquals( + Base64Utils.encodeToUrlSafeString(defaultSettingsFrameBytes), + request.httpFields[HttpHeader.HTTP2_SETTINGS] + ) + + removeHttp2UpgradeHeader(request) + assertFalse(expectUpgradeHttp2(request)) + assertFalse(request.httpFields.contains(HttpHeader.HTTP2_SETTINGS)) + assertFalse(request.httpFields.contains(HttpHeader.UPGRADE)) + assertEquals("keep-alive", request.httpFields[HttpHeader.CONNECTION]) + } + + @Test + @DisplayName("should add h2c headers successfully") + fun testAddHttp2UpgradeHeader() { + val request = AsyncHttpClientRequest() + request.httpFields.put(HttpHeader.CONNECTION, "keep-alive") + addHttp2UpgradeHeader(request) + assertTrue( + request.httpFields.getCSV(HttpHeader.CONNECTION, false).containsAll( + arrayListOf( + "keep-alive", + "Upgrade", + "HTTP2-Settings" + ) + ) + ) + assertTrue(expectUpgradeHttp2(request)) + } + + @Test + @DisplayName("should check response upgrade http2 successfully") + fun testUpgradeSuccess() { + val metadata = MetaData.Response(HttpVersion.HTTP_1_1, HttpStatus.SWITCHING_PROTOCOLS_101, HttpFields()) + metadata.fields.put(HttpHeader.CONNECTION, "Upgrade") + metadata.fields.put(HttpHeader.UPGRADE, "h2c") + val response = AsyncHttpClientResponse(metadata, null) + assertTrue(isUpgradeSuccess(response)) + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/handler/TestFileContentHandler.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/handler/TestFileContentHandler.kt new file mode 100644 index 000000000..420b30437 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/handler/TestFileContentHandler.kt @@ -0,0 +1,53 @@ +package com.fireflysource.net.http.client.impl.content.handler + +import com.fireflysource.common.io.readFileBytesAsync +import com.fireflysource.net.http.client.HttpClientContentHandlerFactory.fileHandler +import com.fireflysource.net.http.client.HttpClientResponse +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import java.nio.ByteBuffer +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardOpenOption.WRITE +import java.util.* + +class TestFileContentHandler { + + private val response = Mockito.mock(HttpClientResponse::class.java) + + private val tmpFile = Paths.get(System.getProperty("user.home"), "tmpFile${UUID.randomUUID()}.txt") + + @BeforeEach + fun init() { + Files.createFile(tmpFile) + println("create a file: $tmpFile") + } + + @AfterEach + fun destroy() { + Files.delete(tmpFile) + println("delete file: $tmpFile") + } + + @Test + @DisplayName("should write data to file successfully") + fun test() = runTest { + val handler = fileHandler(tmpFile, WRITE) + arrayOf( + ByteBuffer.wrap("hello".toByteArray()), + ByteBuffer.wrap(" file".toByteArray()), + ByteBuffer.wrap(" handler".toByteArray()) + ).forEach { handler.accept(it, response) } + + handler.closeAsync().await() + + val str = readFileBytesAsync(tmpFile).await() + assertEquals("hello file handler", String(str)) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/handler/TestStringContentHandler.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/handler/TestStringContentHandler.kt new file mode 100644 index 000000000..43cb7e3c3 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/handler/TestStringContentHandler.kt @@ -0,0 +1,23 @@ +package com.fireflysource.net.http.client.impl.content.handler + +import com.fireflysource.net.http.client.HttpClientContentHandlerFactory.stringHandler +import com.fireflysource.net.http.client.HttpClientResponse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import java.nio.ByteBuffer + +class TestStringContentHandler { + + private val response = mock(HttpClientResponse::class.java) + + @Test + fun test() { + val handler = stringHandler() + arrayOf( + ByteBuffer.wrap("hello".toByteArray()), + ByteBuffer.wrap(" buffer".toByteArray()) + ).forEach { handler.accept(it, response) } + assertEquals("hello buffer", handler.toString()) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestByteBufferContentProvider.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestByteBufferContentProvider.kt new file mode 100644 index 000000000..6a9808042 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestByteBufferContentProvider.kt @@ -0,0 +1,65 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.bytesBody +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class TestByteBufferContentProvider { + + @Test + @DisplayName("should get byte buffer successfully") + fun testToByteBuffer() { + val content = BufferUtils.allocate(12) + val pos = BufferUtils.flipToFill(content) + content.putInt(333) + content.putLong(7777) + BufferUtils.flipToFlush(content, pos) + + val provider = bytesBody(content) + val buffer = provider.toByteBuffer() + assertEquals(333, buffer.int) + assertEquals(7777, buffer.long) + } + + @Test + @DisplayName("should read buffer successfully") + fun testRead() = runTest { + val content = BufferUtils.allocate(12) + val pos = BufferUtils.flipToFill(content) + content.putInt(333) + content.putLong(7777) + BufferUtils.flipToFlush(content, pos) + + val provider = bytesBody(content) + assertEquals(12, provider.length()) + + val buffer = BufferUtils.allocate(6) + val bufPos = BufferUtils.flipToFill(buffer) + val len1 = provider.read(buffer).await() + BufferUtils.flipToFlush(buffer, bufPos) + assertEquals(6, len1) + assertEquals(333, buffer.int) + + val buffer2 = BufferUtils.allocate(10) + val bufPos2 = BufferUtils.flipToFill(buffer2) + val len2 = provider.read(buffer2).await() + BufferUtils.flipToFlush(buffer2, bufPos2) + assertEquals(6, len2) + + val buffer3 = BufferUtils.allocate(8) + val bufPos3 = BufferUtils.flipToFill(buffer3) + buffer3.put(buffer).put(buffer2) + BufferUtils.flipToFlush(buffer3, bufPos3) + assertEquals(7777, buffer3.long) + + val buffer4 = BufferUtils.allocate(8) + val bufPos4 = BufferUtils.flipToFill(buffer4) + val len4 = provider.read(buffer2).await() + BufferUtils.flipToFlush(buffer4, bufPos4) + assertEquals(-1, len4) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestFileContentProvider.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestFileContentProvider.kt new file mode 100644 index 000000000..a5bdcf476 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestFileContentProvider.kt @@ -0,0 +1,98 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.openFileChannelAsync +import com.fireflysource.common.io.useAwait +import com.fireflysource.common.io.writeAwait +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.fileBody +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardOpenOption.READ +import java.nio.file.StandardOpenOption.WRITE +import java.util.* + +class TestFileContentProvider { + + private val tmpFile = Paths.get(System.getProperty("user.home"), "tmpFile${UUID.randomUUID()}.txt") + + @BeforeEach + fun init() { + Files.createFile(tmpFile) + println("create file: $tmpFile") + } + + @AfterEach + fun destroy() { + Files.delete(tmpFile) + println("delete file: $tmpFile") + } + + @Test + @DisplayName("should read file successfully") + fun test(): Unit = runBlocking { + val capacity = 24 + val fileChannel = openFileChannelAsync(tmpFile, WRITE).await() + fileChannel.useAwait { + val writeBuffer = BufferUtils.allocate(capacity) + val writePos = BufferUtils.flipToFill(writeBuffer) + writeBuffer.putInt(1).putInt(2).putInt(3) + .putInt(4).putInt(5).putInt(6) + BufferUtils.flipToFlush(writeBuffer, writePos) + + val writeLen = fileChannel.writeAwait(writeBuffer, 0L) + assertEquals(capacity, writeLen) + } + + val provider = fileBody(tmpFile, READ) as FileContentProvider + val readBuffer = BufferUtils.allocate(capacity) + val readPos = BufferUtils.flipToFill(readBuffer) + val readLen = provider.read(readBuffer).await() + BufferUtils.flipToFlush(readBuffer, readPos) + assertEquals(capacity, readLen) + + (1..6).forEach { i -> + assertEquals(i, readBuffer.int) + } + provider.closeAsync().await() + Unit + } + + @Test + @DisplayName("should seek position and read file successfully") + fun testSeekPosition(): Unit = runBlocking { + val capacity = 24 + val fileChannel = openFileChannelAsync(tmpFile, WRITE).await() + fileChannel.useAwait { + val writeBuffer = BufferUtils.allocate(capacity) + val writePos = BufferUtils.flipToFill(writeBuffer) + writeBuffer.putInt(1).putInt(2).putInt(3) + .putInt(4).putInt(5).putInt(6) + BufferUtils.flipToFlush(writeBuffer, writePos) + + val writeLen = fileChannel.writeAwait(writeBuffer, 0L) + assertEquals(capacity, writeLen) + } + + val pos: Long = 2 * 4 + val length = capacity - pos + val provider = fileBody(tmpFile, setOf(READ), pos, length) as FileContentProvider + val readBuffer = BufferUtils.allocate(capacity) + val readPos = BufferUtils.flipToFill(readBuffer) + val readLen = provider.read(readBuffer).await() + BufferUtils.flipToFlush(readBuffer, readPos) + assertEquals(length, readLen.toLong()) + + (3..6).forEach { i -> + assertEquals(i, readBuffer.int) + } + provider.closeAsync().await() + Unit + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestMultiPartContentProvider.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestMultiPartContentProvider.kt new file mode 100644 index 000000000..3ae5a2179 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestMultiPartContentProvider.kt @@ -0,0 +1,51 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.stringBody +import com.fireflysource.net.http.common.model.HttpFields +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.nio.charset.StandardCharsets + +class TestMultiPartContentProvider { + + @Test + @DisplayName("should generate multi-part format successfully") + fun testRead() = runBlocking { + val provider = MultiPartContentProvider() + + val str = "Hello string body" + val strProvider = stringBody(str, StandardCharsets.UTF_8) + provider.addPart("hello string", strProvider, null) + + val str2 = "string body 2" + val strProvider2 = stringBody(str2, StandardCharsets.UTF_8) + val httpFields = HttpFields() + httpFields.put("x1", "y1") + provider.addPart("string 2", strProvider2, httpFields) + + val buffer = BufferUtils.allocate(provider.length().toInt()) + val pos = BufferUtils.flipToFill(buffer) + while (buffer.hasRemaining()) { + val len = provider.read(buffer).await() + if (len < 0) { + break + } + } + BufferUtils.flipToFlush(buffer, pos) + println() + + println("Content-Type: ${provider.contentType}") + println() + provider.closeAsync().await() + + val content = BufferUtils.toUTF8String(buffer) + println(content) + assertTrue(content.contains("Hello string body")) + assertTrue(content.contains("string body 2")) + assertTrue(content.contains("x1: y1")) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestStringContentProvider.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestStringContentProvider.kt new file mode 100644 index 000000000..52f4cfe84 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/client/impl/content/provider/TestStringContentProvider.kt @@ -0,0 +1,55 @@ +package com.fireflysource.net.http.client.impl.content.provider + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.stringBody +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.nio.charset.StandardCharsets + +class TestStringContentProvider { + + @Test + @DisplayName("should get string successfully") + fun testToByteBuffer() { + val str = "Hello string body" + val provider = stringBody(str, StandardCharsets.UTF_8) + val byteBuffer = provider.toByteBuffer() + assertEquals(str, BufferUtils.toString(byteBuffer, StandardCharsets.UTF_8)) + } + + @Test + @DisplayName("should read string successfully") + fun testRead() = runBlocking { + val str = "Hello string body" + val provider = stringBody(str, StandardCharsets.UTF_8) + + val byteBuffer = BufferUtils.allocate(5) + val pos = BufferUtils.flipToFill(byteBuffer) + val len = provider.read(byteBuffer).await() + BufferUtils.flipToFlush(byteBuffer, pos) + + assertEquals(5, len) + assertEquals(5, byteBuffer.remaining()) + assertEquals("Hello", BufferUtils.toString(byteBuffer, StandardCharsets.UTF_8)) + + val byteBuffer2 = BufferUtils.allocate(20) + val pos2 = BufferUtils.flipToFill(byteBuffer2) + val len2 = provider.read(byteBuffer2).await() + BufferUtils.flipToFlush(byteBuffer2, pos2) + + assertEquals(str.length - 5, len2) + assertEquals(str.length - 5, byteBuffer2.remaining()) + assertEquals(" string body", BufferUtils.toString(byteBuffer2, StandardCharsets.UTF_8)) + + val byteBuffer3 = BufferUtils.allocate(10) + val pos3 = BufferUtils.flipToFill(byteBuffer3) + val len3 = provider.read(byteBuffer3).await() + BufferUtils.flipToFlush(byteBuffer3, pos3) + + assertEquals(-1, len3) + assertEquals(0, byteBuffer3.remaining()) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/AbstractHttpServerTestBase.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/AbstractHttpServerTestBase.kt new file mode 100644 index 000000000..7f2c2ac40 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/AbstractHttpServerTestBase.kt @@ -0,0 +1,62 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.net.http.server.HttpServerFactory +import com.fireflysource.net.tcp.aio.ApplicationProtocol +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.provider.Arguments +import java.net.InetSocketAddress +import java.util.stream.Stream +import kotlin.math.roundToLong +import kotlin.random.Random + +abstract class AbstractHttpServerTestBase { + + companion object { + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + Arguments.arguments("http1", "http"), + Arguments.arguments("http1", "https"), + Arguments.arguments("http2", "https") + ) + } + } + + protected lateinit var address: InetSocketAddress + + @BeforeEach + fun init() { + address = InetSocketAddress("localhost", Random.nextInt(20000, 40000)) + } + + fun createHttpServer(protocol: String, schema: String, httpConfig: HttpConfig = HttpConfig()): HttpServer { + val server = HttpServerFactory.create(httpConfig) + when (protocol) { + "http1" -> server.supportedProtocols(listOf(ApplicationProtocol.HTTP1.value)) + "http2" -> server.supportedProtocols( + listOf( + ApplicationProtocol.HTTP2.value, + ApplicationProtocol.HTTP1.value + ) + ) + } + if (schema == "https") { + server.enableSecureConnection() + } + return server + } + + fun finish(count: Int, time: Long, httpClient: HttpClient, httpServer: HttpServer) { + try { + val throughput = count / (time / 1000.00) + println("success. $time ms, ${throughput.roundToLong()} qps") + httpClient.stop() + httpServer.stop() + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/TestHttpServer.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/TestHttpServer.kt new file mode 100644 index 000000000..534a096da --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/TestHttpServer.kt @@ -0,0 +1,503 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.ProxyConfig +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.HttpServerContentProviderFactory +import com.fireflysource.net.http.server.HttpServerFactory +import com.fireflysource.net.http.server.impl.router.asyncBlockingHandler +import com.fireflysource.net.http.server.impl.router.asyncHandler +import com.fireflysource.net.http.server.impl.router.getCurrentRoutingContext +import com.fireflysource.net.tcp.TcpClientFactory +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture +import kotlin.random.Random +import kotlin.system.measureTimeMillis + +@Suppress("HttpUrlsUsage") +class TestHttpServer : AbstractHttpServerTestBase() { + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should visit router chain successfully.") + fun testRouterChain(protocol: String, schema: String) = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer + .router().path("*").asyncHandler { ctx -> + assertTrue(getCurrentRoutingContext() != null) + ctx.write("into router -> ") + ctx.next().await() + ctx.end("end router.") + } + .router().get("/hello/:foo").handler { ctx -> + val p = ctx.getPathParameter("foo") + ctx.write("visit foo: $p |").next() + } + .router().get("/hello/*").handler { ctx -> + val p = ctx.getPathParameter(0) + ctx.write("visit pattern: $p |").next() + } + .router().method(HttpMethod.GET).pathRegex("/hello/([a-z]+)").handler { ctx -> + val p = ctx.getPathParameterByRegexGroup(1) + ctx.write("regex group: $p |").next() + } + .router().get("/hello/test").handler { ctx -> + ctx.write("visit test ").next() + } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/hello/bar").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals( + "into router -> visit foo: bar |visit pattern: bar |regex group: bar |end router.", + response.stringBody + ) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should enable and disable router successfully.") + fun testEnableAndDisableRouter(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer + .router().enable().get("/hello/:foo").handler { ctx -> + val p = ctx.getPathParameter("foo") + ctx.write("visit foo: $p ").next() + } + .router().disable().get("/hello/*").handler { ctx -> + val p = ctx.getPathParameter(0) + ctx.write("visit pattern: $p ").next() + } + .router().get("/hello/test").handler { ctx -> + ctx.write("visit test ").next() + } + .router().path("*").handler { ctx -> ctx.end() } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/hello/test").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals( + "visit foo: test visit test ", + response.stringBody + ) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should get and set attributes successfully.") + fun testContextAttributes(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer + .router().get("/hello/:foo").handler { ctx -> + ctx.attributes["A"] = "TestA" + ctx.next() + } + .router().get("/hello/:bar").handler { ctx -> + ctx.attributes["B"] = "TestB" + ctx.setAttribute("C", "TestC") + ctx.setAttribute("D", "TestD") + ctx.removeAttribute("D") + ctx.removeAttribute("E") + ctx.next() + } + .router().method("GET").path("/hello/*").handler { ctx -> + ctx.write("${ctx.attributes["A"]} ${ctx.attributes["B"]} ${ctx.getAttribute("C")}").next() + } + .router().method(HttpMethod.GET).handler { ctx -> ctx.end() } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/hello/test").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals( + "TestA TestB TestC", + response.stringBody + ) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response 500 when the router does not commit.") + fun testRouterNotCommit(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/hello").handler { + Result.DONE + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/hello").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + try { + val response = futures[0].await() + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.status) + println(response) + } catch (e: Exception) { + e.printStackTrace() + } + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response 400 when the resource not found.") + fun testResourceNotFound(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/hello").handler { + Result.DONE + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/hellox").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.NOT_FOUND_404, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response 500 when the router happens exception.") + fun testRouterException(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer + .router().get("/exception").handler { + throw IllegalStateException("test exception") + } + .router().get("/exception").handler { ctx -> + println("bad end?") + ctx.end("bad end") + } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/exception").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should response 400 when the host header is missing.") + fun testNoHostHeader(): Unit = runTest { + val httpServer = createHttpServer("http1", "http") + httpServer + .router().get("/testNoHost").handler { it.end("ok") } + .listen(address) + + val config = HttpConfig() + config.isAutoGeneratedClientHttp1Headers = false + val httpClient = HttpClientFactory.create(config) + + val time = measureTimeMillis { + val response = httpClient.get("http://${address.hostName}:${address.port}/testNoHost").submit().await() + assertEquals(HttpStatus.BAD_REQUEST_400, response.status) + println(response) + } + + finish(1, time, httpClient, httpServer) + } + + @Test + @DisplayName("should establish HTTP tunnel successfully.") + fun testHttpTunnel(): Unit = runTest { + val httpServer = createHttpServer("http1", "http") + httpServer + .onAcceptHttpTunnel { request -> + println("Accept http tunnel request. $request") + CompletableFuture.completedFuture(true) + } + .onHttpTunnelHandshakeComplete { connection, address -> + println("target address: $address") + connection.write(BufferUtils.toBuffer("1234")) + .thenCompose { connection.closeAsync() } + } + .listen(address) + + val message = "CONNECT p54-caldav.icloud.com:443 HTTP/1.1\r\n" + + "Host: p54-caldav.icloud.com\r\n" + + "User-Agent: Mac+OS+X/10.15.7 (19H114) CalendarAgent/930.5.1\r\n" + + "Connection: keep-alive\r\n" + + "Proxy-Connection: keep-alive\r\n\r\n" + val tcpClient = TcpClientFactory.create() + val connection = tcpClient.connect(address).await() + connection.write(BufferUtils.toBuffer(message)).await() + val receivedData = mutableListOf() + while (true) { + try { + val data = connection.read().await() + receivedData.add(data) + } catch (e: ClosedChannelException) { + break + } + } + val receivedMessages = BufferUtils.toString(receivedData, StandardCharsets.UTF_8) + println(receivedMessages) + assertTrue(receivedMessages.isNotBlank()) + assertTrue(receivedMessages.contains("HTTP/1.1 200 Connection Established\r\n\r\n")) + assertTrue(receivedMessages.contains("1234")) + + tcpClient.stop() + httpServer.stop() + } + + @Test + @DisplayName("should establish HTTP tunnel failure.") + fun testHttpTunnelFailure(): Unit = runTest { + val httpServer = createHttpServer("http1", "http") + httpServer + .onAcceptHttpTunnel { request -> + println("Accept http tunnel request. $request") + CompletableFuture.completedFuture(false) + } + .onHttpTunnelHandshakeComplete { connection, address -> + println("target address: $address") + connection.write(BufferUtils.toBuffer("1234")) + .thenCompose { connection.closeAsync() } + } + .listen(address) + + val message = "CONNECT p54-caldav.icloud.com:443 HTTP/1.1\r\n" + + "Host: p54-caldav.icloud.com\r\n" + + "User-Agent: Mac+OS+X/10.15.7 (19H114) CalendarAgent/930.5.1\r\n" + + "Connection: keep-alive\r\n" + + "Proxy-Connection: keep-alive\r\n\r\n" + val tcpClient = TcpClientFactory.create() + val connection = tcpClient.connect(address).await() + connection.write(BufferUtils.toBuffer(message)).await() + val receivedData = mutableListOf() + while (true) { + try { + val data = connection.read().await() + receivedData.add(data) + } catch (e: ClosedChannelException) { + break + } + } + val receivedMessages = BufferUtils.toString(receivedData, StandardCharsets.UTF_8) + println(receivedMessages) + assertTrue(receivedMessages.isNotBlank()) + assertTrue(receivedMessages.contains("Proxy Authentication Required")) + assertTrue(receivedMessages.contains("The proxy authentication must be required")) + assertFalse(receivedMessages.contains("1234")) + + tcpClient.stop() + httpServer.stop() + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should send request via the http proxy successfully.") + fun testHttpProxy(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + httpServer + .router().path("/hello/*").asyncHandler { ctx -> + assertTrue(getCurrentRoutingContext() != null) + ctx.write("into router -> ") + ctx.next().await() + ctx.end("end router.") + } + .router().get("/hello/:foo").handler { ctx -> + val p = ctx.getPathParameter("foo") + ctx.write("visit foo: $p |").next() + } + .router().get("/hello/*").handler { ctx -> + val p = ctx.getPathParameter(0) + ctx.write("visit pattern: $p |").next() + } + .router().method(HttpMethod.GET).pathRegex("/hello/([a-z]+)").handler { ctx -> + val p = ctx.getPathParameterByRegexGroup(1) + ctx.write("regex group: $p |").next() + } + .router().get("/hello/test").handler { ctx -> + ctx.write("visit test ").next() + } + .router().get("/length/test").handler { ctx -> + ctx.contentProvider(HttpServerContentProviderFactory.stringBody("1234567", StandardCharsets.UTF_8)) + .end() + } + .listen(address) + + val proxyAddress = InetSocketAddress("localhost", Random.nextInt(10000, 20000)) + val proxy = HttpServerFactory.createHttpProxy() + proxy.listen(proxyAddress) + + val clientHttpConfig = HttpConfig() + val proxyConfig = ProxyConfig() + proxyConfig.host = proxyAddress.hostName + proxyConfig.port = proxyAddress.port + clientHttpConfig.proxyConfig = proxyConfig + val httpClient = HttpClientFactory.create(clientHttpConfig) + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/hello/bar").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals( + "into router -> visit foo: bar |visit pattern: bar |regex group: bar |end router.", + response.stringBody + ) + println(response) + + val lengthResponse = + httpClient.get("$schema://${address.hostName}:${address.port}/length/test").submit().await() + assertEquals(HttpStatus.OK_200, lengthResponse.status) + assertEquals("1234567", lengthResponse.stringBody) + println(lengthResponse) + } + + finish(count, time, httpClient, httpServer) + proxy.stop() + } + + @Test + fun testCopy() = runTest { + val httpServer = createHttpServer("http1", "http") + httpServer.router().get("/testCopy") + .asyncBlockingHandler { + println(Thread.currentThread().name) + it.end("Origin server") + } + .router().get("/blockingTask") + .blockingHandler { + Thread.sleep(100) + println(Thread.currentThread().name) + it.end("Blocking task").get() + } + .listen(address) + + var newAddress = InetSocketAddress("localhost", Random.nextInt(20000, 40000)) + while (newAddress == address) { + newAddress = InetSocketAddress("localhost", Random.nextInt(20000, 40000)) + } + val newHttpServer = httpServer.copy() + newHttpServer.enableSecureConnection().listen(newAddress) + + val httpClient = HttpClientFactory.create() + + var response = httpClient.get("http://${address.hostName}:${address.port}/testCopy").submit().await() + assertEquals("Origin server", response.stringBody) + println(response) + + response = httpClient.get("http://${address.hostName}:${address.port}/blockingTask").submit().await() + assertEquals("Blocking task", response.stringBody) + println(response) + + response = httpClient.get("https://${newAddress.hostName}:${newAddress.port}/testCopy").submit().await() + assertEquals("Origin server", response.stringBody) + println(response) + + response = httpClient.get("https://${newAddress.hostName}:${newAddress.port}/blockingTask").submit().await() + assertEquals("Blocking task", response.stringBody) + println(response) + + httpClient.stop() + httpServer.stop() + newHttpServer.stop() + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/TestHttpServerConnection.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/TestHttpServerConnection.kt new file mode 100644 index 000000000..96e4b3baf --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/TestHttpServerConnection.kt @@ -0,0 +1,690 @@ +package com.fireflysource.net.http.server.impl + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.client.HttpClientContentProviderFactory +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.client.impl.content.provider.ByteBufferContentProvider +import com.fireflysource.net.http.common.HttpConfig +import com.fireflysource.net.http.common.model.* +import com.fireflysource.net.http.server.HttpServerContentProviderFactory.stringBody +import com.fireflysource.net.http.server.impl.content.provider.DefaultContentProvider +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.time.Duration +import java.util.concurrent.CompletableFuture +import kotlin.system.measureTimeMillis + +class TestHttpServerConnection : AbstractHttpServerTestBase() { + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should receive request and response texts successfully.") + fun test(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/test-*").handler { ctx -> + ctx.write(BufferUtils.toBuffer("response buffer.", StandardCharsets.UTF_8)) + val arr = arrayOf( + BufferUtils.toBuffer("array 1.", StandardCharsets.UTF_8), + BufferUtils.toBuffer("array 2.", StandardCharsets.UTF_8), + BufferUtils.toBuffer("array 3.", StandardCharsets.UTF_8) + ) + val list = listOf( + BufferUtils.toBuffer("list 1.", StandardCharsets.UTF_8), + BufferUtils.toBuffer("list 2.", StandardCharsets.UTF_8), + BufferUtils.toBuffer("list 3.", StandardCharsets.UTF_8) + ) + ctx.write(arr, 0, arr.size) + .write(list, 1, 2) + .end("hello http1 server!") + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/test-$it").submit() } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals( + "response buffer.array 1.array 2.array 3.list 2.list 3.hello http1 server!", + response.stringBody + ) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response texts with content length successfully.") + fun testResponseContentLength(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/length-*").handler { ctx -> + val buffer = BufferUtils.toBuffer("response text with content length.", StandardCharsets.UTF_8) + ctx.put(HttpHeader.CONTENT_LENGTH, buffer.remaining().toString()) + ctx.write(buffer).end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/length-$it").submit() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("response text with content length.", response.stringBody) + assertEquals(34L, response.contentLength) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should receive query strings and form inputs successfully.") + fun testQueryStringsAndFormInputs(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().post("/query-form-*").handler { ctx -> + val query = ctx.getQueryString("key1") + val queryList = ctx.getQueryStrings("list1") + val querySize = ctx.queryStrings.size + val message = ctx.getFormInput("key1") + val formList = ctx.getFormInputs("list1") + val formSize = ctx.formInputs.size + val method = ctx.method + ctx.write(method).write(", ") + .write(query).write(queryList.toString()).write(", size: $querySize") + .write(", ") + .write(message).write(formList.toString()).write(", size: $formSize") + .end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("$schema://${address.hostName}:${address.port}/query-form-$it") + .addQueryString("key1", "query") + .addQueryStrings("list1", listOf("q1", "q2", "q3")) + .addFormInput("key1", "message") + .addFormInputs("list1", listOf("v1", "v2", "v3", "v4", "v5")) + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("POST, query[q1, q2, q3], size: 2, message[v1, v2, v3, v4, v5], size: 2", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response default html successfully.") + fun testContentProvider(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/not-found-*").handler { ctx -> + ctx.setStatus(HttpStatus.NOT_FOUND_404).setReason("Just so so") + .setHttpVersion(HttpVersion.HTTP_1_1) + .contentProvider(DefaultContentProvider(HttpStatus.NOT_FOUND_404, null, ctx)) + .end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/not-found-$it").submit() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.NOT_FOUND_404, response.status) + if (protocol == "http1") { + assertEquals("Just so so", response.reason) + } + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response trailer successfully.") + fun testTrailer(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/trailer-*").handler { ctx -> + ctx.addCSV(HttpHeader.TRAILER, "t1", "t2", "t3") + .setTrailerSupplier { + val fields = HttpFields() + fields.put("t1", "trailer1") + fields.put("t2", "trailer2") + fields.put("t3", "trailer3") + fields + } + .write("response text success.") + .write("trailer.") + .end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/trailer-$it").submit() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("t1, t2, t3", response.httpFields[HttpHeader.TRAILER]) + assertEquals("response text success.trailer.", response.stringBody) + assertEquals("trailer1", response.trailerSupplier.get()["t1"]) + assertEquals("trailer2", response.trailerSupplier.get()["t2"]) + assertEquals("trailer3", response.trailerSupplier.get()["t3"]) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should redirect successfully.") + fun testRedirect(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().get("/redirect-*").handler { ctx -> + ctx.redirect("http://${address.hostName}:${address.port}/r0") + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { httpClient.get("$schema://${address.hostName}:${address.port}/redirect-$it").submit() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.FOUND_302, response.status) + assertEquals("http://${address.hostName}:${address.port}/r0", response.httpFields[HttpHeader.LOCATION]) + } + + finish(count, time, httpClient, httpServer) + } + + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should get cookies and set cookies successfully.") + fun testCookies(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().post("/cookies-*").handler { ctx -> + ctx.cookies = listOf( + Cookie("s1", "v1"), + Cookie("s2", "v2"), + Cookie("s3", ctx.cookies[0].value), + Cookie("s4", ctx.cookies[1].value) + ) + ctx.end("receive ${ctx.stringBody} ok.") + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("$schema://${address.hostName}:${address.port}/cookies-$it") + .body("cookies c1, c2.") + .cookies(listOf(Cookie("c1", "client1"), Cookie("c2", "client2"))) + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals(4, response.cookies.size) + assertEquals("receive cookies c1, c2. ok.", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should receive gbk content successfully.") + fun testGBK(protocol: String, schema: String): Unit = runTest { + val count = 100 + val charset = Charset.forName("GBK") + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().post("/gbk-*").handler { ctx -> + val content = ctx.getStringBody(charset) + ctx.contentProvider(stringBody("收到:${content}。长度:${ctx.contentLength}", charset)).end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("$schema://${address.hostName}:${address.port}/gbk-$it") + .body("发射!!Oooo", charset) + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response.getStringBody(charset)) + assertEquals(HttpStatus.OK_200, response.status) +// assertEquals("收到:发射!!Oooo。长度:12", response.getStringBody(charset)) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should accept 100 continue.") + fun testAccept100Continue(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().post("/100-continue-*").handler { ctx -> + ctx.end("receive ${ctx.stringBody} OK") + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("$schema://${address.hostName}:${address.port}/100-continue-$it") + .put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.value) + .body("100 continue content") + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("receive 100 continue content OK", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should not accept 100 continue and receive the error status successfully.") + fun testNotAccept100Continue(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer + .onHeaderComplete { ctx -> + ctx.setStatus(HttpStatus.PAYLOAD_TOO_LARGE_413).end("Content too large") + } + .router().post("/100-continue-*").handler { ctx -> + if (ctx.response.isCommitted) Result.DONE + else ctx.end("receive ${ctx.stringBody} OK") + } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("$schema://${address.hostName}:${address.port}/100-continue-$it") + .put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.value) + .body("100 continue content") + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, response.status) + assertEquals("Content too large", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should upgrade http2 protocol successfully.") + fun testUpgradeHttp2(): Unit = runTest { + val count = 30 + + val httpServer = createHttpServer("http1", "http") + httpServer.router().get("/upgrade-http2-*").handler { ctx -> + ctx.end("Upgrade http2 success") + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.get("http://${address.hostName}:${address.port}/upgrade-http2-$it").upgradeHttp2().submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + val response = futures[0].await() + + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("Upgrade http2 success", response.stringBody) + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should trigger window update successfully.") + fun testBufferedWindowUpdate(): Unit = runTest { + val count = 1 + val content = (1..30_000_000).joinToString("") { "a" } + val httpConfig = HttpConfig() + httpConfig.initialSessionRecvWindow = HttpConfig.DEFAULT_WINDOW_SIZE + httpConfig.initialStreamRecvWindow = HttpConfig.DEFAULT_WINDOW_SIZE + + val httpServer = createHttpServer("http2", "https", httpConfig) + httpServer.router().post("/big-data-http2-*").handler { ctx -> + ctx.end("Received data success. length: ${ctx.stringBody.length}") + }.listen(address) + + val httpClient = HttpClientFactory.create(httpConfig) + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("https://${address.hostName}:${address.port}/big-data-http2-$it") + .contentProvider(ByteBufferContentProvider(ByteBuffer.wrap(content.toByteArray(StandardCharsets.UTF_8)))) + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + val response = futures[0].await() + + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertTrue(response.stringBody.contains("Received data success.")) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should receive multi part content successfully.") + fun testMultiPartContent(protocol: String, schema: String): Unit = runTest { + val count = 100 + + val httpServer = createHttpServer(protocol, schema) + httpServer.router().post("/multi-part-content-*").handler { ctx -> + val part1 = ctx.getPart("part1") + val part2 = ctx.getPart("part2") + val content = """ + |received part1: + |${part1.httpFields} + |${part1.stringBody} + | + |received part2: + |${part2.stringBody} + """.trimMargin() + ctx.end(content) + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + val part1 = HttpClientContentProviderFactory.stringBody("string content 1") + val fields1 = HttpFields() + fields1.put("hello", "hello part 1") + fields1.addCSV("part123", "1", "2", "3") + + val part2 = HttpClientContentProviderFactory.stringBody("file content 2") + httpClient.post("$schema://${address.hostName}:${address.port}/multi-part-content-$it") + .addPart("part1", part1, fields1) + .addFilePart("part2", "fileContent.txt", part2, HttpFields()) + .submit() + } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertTrue(response.stringBody.contains("hello")) + assertTrue(response.stringBody.contains("part123")) + assertTrue(response.stringBody.contains("string content 1")) + assertTrue(response.stringBody.contains("file content 2")) + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should send request by non persistence connection successfully.") + fun testClientShortConnection(): Unit = runTest { + val count = 30 + + val httpServer = createHttpServer("http1", "http") + httpServer.router().get("/close-*").handler { ctx -> + ctx.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.value) + .contentProvider(stringBody("Close connection success", StandardCharsets.UTF_8)) + .end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.get("http://${address.hostName}:${address.port}/close-$it") + .put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.value) + .submit() + } + + try { + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + val response = futures[0].await() + + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("Close connection success", response.stringBody) + } catch (e: Exception) { + println(e.message) + } + } + + finish(count, time, httpClient, httpServer) + } + + @Test + @DisplayName("should close connection by server successfully.") + fun testCloseConnectionByServer(): Unit = runTest { + val count = 30 + + val httpServer = createHttpServer("http1", "http") + httpServer.router().get("/close-*").handler { ctx -> + ctx.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.value) + .contentProvider(stringBody("Close connection success", StandardCharsets.UTF_8)) + .end() + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.get("http://${address.hostName}:${address.port}/close-$it").submit() + } + + try { + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + val response = futures[0].await() + val failureCount = futures.count { it.isCompletedExceptionally } + println("exception: $failureCount") + println(response) + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("Close connection success", response.stringBody) + } catch (e: Exception) { + println(e.message) + } + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response 413 when the multi-part too large.") + fun testMultiPartTooLarge(protocol: String, schema: String): Unit = runTest { + val count = 20 // TODO http1 client close connection exception + + val httpConfig = HttpConfig() + httpConfig.maxUploadFileSize = 10 + httpConfig.maxRequestBodySize = 10 + httpConfig.uploadFileSizeThreshold = 10 + val httpServer = createHttpServer(protocol, schema, httpConfig) + httpServer.router().post("/multi-part-content-*").handler { ctx -> + val part1 = ctx.getPart("part1") + val part2 = ctx.getPart("part2") + val content = """ + |received part1: + |${part1.httpFields} + |${part1.stringBody} + | + |received part2: + |${part2.stringBody} + """.trimMargin() + ctx.end(content) + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + val part1 = HttpClientContentProviderFactory.stringBody("string content 1") + val fields1 = HttpFields() + fields1.put("hello", "hello part 1") + fields1.addCSV("part123", "1", "2", "3") + + val part2 = HttpClientContentProviderFactory.stringBody("file content 2") + + httpClient.post("$schema://${address.hostName}:${address.port}/multi-part-content-$it") + .addPart("part1", part1, fields1) + .addFilePart("part2", "fileContent.txt", part2, HttpFields()) + .submit() + } + try { + withTimeout(Duration.ofSeconds(5).toMillis()) { + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + val failureCount = futures.count { it.isCompletedExceptionally } + println("failure count: $failureCount") + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, response.status) + } + } catch (e: Exception) { + println(e.message) + } + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response 413 when the http body too large.") + fun testHttpBodyTooLarge(protocol: String, schema: String): Unit = runTest { + val count = 20 // TODO http1 client close connection exception + + val content = (1..30_000_000).joinToString("") { "a" } + val httpConfig = HttpConfig() + httpConfig.maxUploadFileSize = 10 + httpConfig.maxRequestBodySize = 10 + httpConfig.uploadFileSizeThreshold = 10 + val httpServer = createHttpServer(protocol, schema, httpConfig) + httpServer.router().post("/content-*").handler { ctx -> + ctx.end("ok") + }.listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count).map { + httpClient.post("$schema://${address.hostName}:${address.port}/content-$it") + .body(content) + .submit() + } + try { + withTimeout(Duration.ofSeconds(5).toMillis()) { + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + assertTrue(allDone) + val failureCount = futures.count { it.isCompletedExceptionally } + println("failure count: $failureCount") + + val response = futures[0].await() + println(response) + assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, response.status) + } + } catch (e: Exception) { + println(e.message) + } + } + + finish(count, time, httpClient, httpServer) + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/content/handler/TestFormInputsContentHandler.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/content/handler/TestFormInputsContentHandler.kt new file mode 100644 index 000000000..aa989ceb0 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/content/handler/TestFormInputsContentHandler.kt @@ -0,0 +1,37 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.net.http.common.codec.UrlEncoded +import com.fireflysource.net.http.server.RoutingContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import java.nio.charset.StandardCharsets + +class TestFormInputsContentHandler { + + private val context = mock(RoutingContext::class.java) + + @Test + @DisplayName("should decode web form inputs successfully") + fun test() { + val encoded = UrlEncoded() + encoded["key1"] = listOf("测试1", "$%^&====") + encoded["key2"] = listOf("v2") + encoded.add("key3", "v3") + encoded.add("key3", "v4") + val string = encoded.encode(StandardCharsets.UTF_8, true) + println(string) + val buffer = BufferUtils.toBuffer(string, StandardCharsets.UTF_8) + + val handler = FormInputsContentHandler() + handler.accept(buffer, context) + + assertEquals(listOf("测试1", "$%^&===="), handler.getFormInputs("key1")) + assertEquals("v2", handler.getFormInput("key2")) + assertEquals(listOf("v3", "v4"), handler.getFormInputs("key3")) + assertEquals(3, handler.getFormInputs().size) + println(handler.getFormInputs()) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/content/handler/TestMultiPartContentHandler.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/content/handler/TestMultiPartContentHandler.kt new file mode 100644 index 000000000..b41ba4ad8 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/content/handler/TestMultiPartContentHandler.kt @@ -0,0 +1,169 @@ +package com.fireflysource.net.http.server.impl.content.handler + +import com.fireflysource.common.io.BufferUtils +import com.fireflysource.common.io.flipToFill +import com.fireflysource.common.io.flipToFlush +import com.fireflysource.common.io.useAwait +import com.fireflysource.net.http.client.HttpClientContentProviderFactory.stringBody +import com.fireflysource.net.http.client.impl.content.provider.MultiPartContentProvider +import com.fireflysource.net.http.common.exception.BadMessageException +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.server.RoutingContext +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.* + +class TestMultiPartContentHandler { + + @Test + @DisplayName("should handle multi part content successfully.") + fun test(): Unit = runTest { + val provider = MultiPartContentProvider() + val buffer = createMultiPartContent(provider) + val ctx = mockRoutingContext(provider) + provider.closeAsync().await() + val handler = MultiPartContentHandler() + + handler.accept(buffer, ctx) + handler.closeAsync().await() + handler.getParts().forEach { + val body = BufferUtils.allocate(64) + val pos = body.flipToFill() + it.read(body) + body.flipToFlush(pos) + println( + """ + |------------------- + |${it.httpFields} + |${BufferUtils.toString(body)} + |------------------- + """.trimMargin() + ) + } + + assertEquals("Hello string body", handler.getPart("hello string")?.stringBody) + + val string2Part = handler.getPart("string 2") + requireNotNull(string2Part) + assertEquals("string body 2", string2Part.stringBody) + assertEquals("y1", string2Part.httpFields.get("x1")) + + val filePart = handler.getPart("file body") + requireNotNull(filePart) + assertEquals("file body 1", filePart.stringBody) + assertEquals("testFile.txt", filePart.fileName) + assertEquals("g1", filePart.httpFields.get("f1")) + } + + @Test + @DisplayName("should get the bad message exception.") + fun testEof(): Unit = runTest { + val provider = MultiPartContentProvider() + val ctx = mockRoutingContext(provider) + val handler = MultiPartContentHandler() + provider.closeAsync().await() + + val buf = BufferUtils.toBuffer((1..100).joinToString { "a" }) + handler.accept(buf, ctx) + + val success = try { + handler.closeAsync().await() + true + } catch (e: Exception) { + assertTrue(e is BadMessageException) + false + } + assertFalse(success) + } + + @Test + @DisplayName("should save content to temp file successfully") + fun testFileSizeThreshold(): Unit = runTest { + val provider = MultiPartContentProvider() + val buffer = createMultiPartContent(provider) + val ctx = mockRoutingContext(provider) + provider.closeAsync().await() + val handler = MultiPartContentHandler(uploadFileSizeThreshold = 100) + + val buffers = LinkedList() + while (buffer.hasRemaining()) { + val b = BufferUtils.allocate(32) + val pos = b.flipToFill() + BufferUtils.put(buffer, b) + b.flipToFlush(pos) + buffers.add(b) + } + + buffers.forEach { handler.accept(it, ctx) } + handler.closeAsync().await() + + val filePart = handler.getPart("file body") + requireNotNull(filePart) + assertEquals("file body 1", filePart.stringBody) + assertEquals("testFile.txt", filePart.fileName) + assertEquals("g1", filePart.httpFields.get("f1")) + + val bigFilePart = handler.getPart("bigFile") + requireNotNull(bigFilePart) + bigFilePart.useAwait { + assertTrue(bigFilePart.stringBody.isBlank()) + val bigFileBuffer = BufferUtils.allocate(500) + val pos = bigFileBuffer.flipToFill() + bigFilePart.read(bigFileBuffer).await() + bigFileBuffer.flipToFlush(pos) + + assertEquals(500, bigFileBuffer.remaining()) + val content = BufferUtils.toString(bigFileBuffer) + assertTrue(content.contains("ccccc")) + } + } + + private fun mockRoutingContext(provider: MultiPartContentProvider): RoutingContext { + val ctx = Mockito.mock(RoutingContext::class.java) + val httpFields = HttpFields() + httpFields.put(HttpHeader.CONTENT_TYPE, provider.contentType) + `when`(ctx.httpFields).thenReturn(httpFields) + return ctx + } + + private suspend fun createMultiPartContent(provider: MultiPartContentProvider): ByteBuffer { + val string1 = "Hello string body" + val string1Provider = stringBody(string1, StandardCharsets.UTF_8) + provider.addPart("hello string", string1Provider, null) + + val string2 = "string body 2" + val string2Provider = stringBody(string2, StandardCharsets.UTF_8) + val string2HttpFields = HttpFields() + string2HttpFields.put("x1", "y1") + provider.addPart("string 2", string2Provider, string2HttpFields) + + val file1 = "file body 1" + val file1Provider = stringBody(file1, StandardCharsets.UTF_8) + val file1HttpFields = HttpFields() + file1HttpFields.put("f1", "g1") + provider.addFilePart("file body", "testFile.txt", file1Provider, file1HttpFields) + + val bigFile = (1..500).joinToString(separator = "") { "c" } + val bigFileProvider = stringBody(bigFile, StandardCharsets.UTF_8) + provider.addFilePart("bigFile", "bigFile.txt", bigFileProvider, HttpFields()) + + val buffer = BufferUtils.allocate(provider.length().toInt()) + val pos = BufferUtils.flipToFill(buffer) + while (buffer.hasRemaining()) { + val len = provider.read(buffer).await() + if (len < 0) { + break + } + } + BufferUtils.flipToFlush(buffer, pos) + return buffer + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestAcceptHeaderMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestAcceptHeaderMatcher.kt new file mode 100644 index 000000000..b53e8f55a --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestAcceptHeaderMatcher.kt @@ -0,0 +1,41 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestAcceptHeaderMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by the accept-type successfully.") + fun test() { + val matcher = AcceptHeaderMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + matcher.add("application/json", router1) + matcher.add("application/json", router2) + matcher.add("text/html", router3) + + val result1 = matcher.match("text/html,application/xml;q=0.9,application/json;q=0.8") + requireNotNull(result1) + assertEquals(1, result1.routers.size) + assertEquals(Matcher.MatchType.ACCEPT, result1.matchType) + + val result2 = matcher.match("text/html;q=0.6,application/xml;q=0.7,application/json;q=0.8") + requireNotNull(result2) + assertEquals(2, result2.routers.size) + assertEquals(Matcher.MatchType.ACCEPT, result2.matchType) + + val result3 = matcher.match("text/html,application/xml,application/json") + requireNotNull(result3) + assertEquals(1, result3.routers.size) + assertEquals(Matcher.MatchType.ACCEPT, result3.matchType) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestHttpMethodMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestHttpMethodMatcher.kt new file mode 100644 index 000000000..fea260536 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestHttpMethodMatcher.kt @@ -0,0 +1,42 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestHttpMethodMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by the http method successfully.") + fun test() { + val matcher = HttpMethodMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + matcher.add(HttpMethod.GET.value, router1) + matcher.add("get", router2) + matcher.add("POST", router3) + + val result1 = matcher.match("GET") + requireNotNull(result1) + assertEquals(2, result1.routers.size) + assertEquals(Matcher.MatchType.METHOD, result1.matchType) + + val result2 = matcher.match("get") + requireNotNull(result2) + assertEquals(2, result2.routers.size) + assertEquals(Matcher.MatchType.METHOD, result2.matchType) + + val result3 = matcher.match("POST") + requireNotNull(result3) + assertEquals(1, result3.routers.size) + assertEquals(Matcher.MatchType.METHOD, result3.matchType) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestParameterPathMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestParameterPathMatcher.kt new file mode 100644 index 000000000..903ceaecf --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestParameterPathMatcher.kt @@ -0,0 +1,46 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestParameterPathMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by parameter path successfully.") + fun test() { + val matcher = ParameterPathMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + + matcher.add("/hello/:foo", router1) + matcher.add("/:hello/:foo/", router2) + matcher.add("/hello/:foo/:bar", router3) + + val result1 = matcher.match("/hello/abc") + requireNotNull(result1) + assertEquals(2, result1.routers.size) + assertEquals(Matcher.MatchType.PATH, result1.matchType) + assertEquals("abc", result1.parameters[router1]?.get("foo")) + assertEquals("abc", result1.parameters[router2]?.get("foo")) + assertEquals("hello", result1.parameters[router2]?.get("hello")) + + val result2 = matcher.match("/hello/abc/eee/") + requireNotNull(result2) + assertEquals(1, result2.routers.size) + assertEquals(Matcher.MatchType.PATH, result2.matchType) + assertEquals("abc", result2.parameters[router3]?.get("foo")) + assertEquals("eee", result2.parameters[router3]?.get("bar")) + + val result3 = matcher.match("/hello/abc/eee/ddd") + assertNull(result3) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPatternedContentTypeMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPatternedContentTypeMatcher.kt new file mode 100644 index 000000000..3f40a579b --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPatternedContentTypeMatcher.kt @@ -0,0 +1,35 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestPatternedContentTypeMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by content type successfully.") + fun test() { + val matcher = PatternedContentTypeMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + matcher.add("*/json", router1) + matcher.add("*/json", router2) + + val result = matcher.match("application/json;charset=utf-8") + requireNotNull(result) + assertEquals(2, result.routers.size) + assertEquals(Matcher.MatchType.CONTENT_TYPE, result.matchType) + assertEquals("application", result.parameters[router1]?.get("param0")) + assertEquals("application", result.parameters[router2]?.get("param0")) + + val result2 = matcher.match("text/html") + assertNull(result2) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPatternedPathMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPatternedPathMatcher.kt new file mode 100644 index 000000000..067df09b1 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPatternedPathMatcher.kt @@ -0,0 +1,44 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestPatternedPathMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by the path successfully.") + fun test() { + val matcher = PatternedPathMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + val router4 = AsyncRouter(4, routerManager) + matcher.add("*", router1) + matcher.add("/*", router2) + matcher.add("/he*/*", router3) + matcher.add("/hello*", router4) + + val result1 = matcher.match("/test") + requireNotNull(result1) + assertEquals(Matcher.MatchType.PATH, result1.matchType) + assertEquals(2, result1.routers.size) + assertEquals("/test", result1.parameters[router1]?.get("param0")) + assertEquals("test", result1.parameters[router2]?.get("param0")) + + val result2 = matcher.match("/hello/1") + requireNotNull(result2) + assertEquals(Matcher.MatchType.PATH, result2.matchType) + assertEquals(4, result2.routers.size) + assertEquals("llo", result2.parameters[router3]?.get("param0")) + assertEquals("1", result2.parameters[router3]?.get("param1")) + assertEquals("/1", result2.parameters[router4]?.get("param0")) + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPreciseContentTypeMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPreciseContentTypeMatcher.kt new file mode 100644 index 000000000..c5011eebc --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPreciseContentTypeMatcher.kt @@ -0,0 +1,40 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestPreciseContentTypeMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by the precise content type successfully.") + fun test() { + val matcher = PreciseContentTypeMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + matcher.add("application/json", router1) + matcher.add("application/json", router2) + matcher.add("text/json", router3) + + val result1 = matcher.match("application/json;charset=utf-8") + requireNotNull(result1) + assertEquals(2, result1.routers.size) + assertEquals(Matcher.MatchType.CONTENT_TYPE, result1.matchType) + + val result2 = matcher.match("text/json") + requireNotNull(result2) + assertEquals(1, result2.routers.size) + assertEquals(Matcher.MatchType.CONTENT_TYPE, result2.matchType) + + val result3 = matcher.match("text/html") + assertNull(result3) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPrecisePathMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPrecisePathMatcher.kt new file mode 100644 index 000000000..558c35f73 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestPrecisePathMatcher.kt @@ -0,0 +1,49 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestPrecisePathMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by the precise path successfully.") + fun test() { + val matcher = PrecisePathMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + val router4 = AsyncRouter(4, routerManager) + matcher.add("/", router1) + matcher.add("/hello", router2) + matcher.add("/hello/", router3) + matcher.add("/hello/foo", router4) + + val result1 = matcher.match("/") + requireNotNull(result1) + assertEquals(Matcher.MatchType.PATH, result1.matchType) + assertEquals(1, result1.routers.size) + assertEquals(1, result1.routers.first().id) + + val result2 = matcher.match("/hello") + requireNotNull(result2) + assertEquals(Matcher.MatchType.PATH, result2.matchType) + assertEquals(2, result2.routers.size) + + val result3 = matcher.match("/hello/foo/") + requireNotNull(result3) + assertEquals(Matcher.MatchType.PATH, result3.matchType) + assertEquals(1, result3.routers.size) + assertEquals(4, result3.routers.first().id) + + val result4 = matcher.match("/hello/foo/bar") + assertNull(result4) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestRegexPathMatcher.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestRegexPathMatcher.kt new file mode 100644 index 000000000..22521d956 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/matcher/TestRegexPathMatcher.kt @@ -0,0 +1,43 @@ +package com.fireflysource.net.http.server.impl.matcher + +import com.fireflysource.net.http.server.Matcher +import com.fireflysource.net.http.server.impl.router.AsyncRouter +import com.fireflysource.net.http.server.impl.router.AsyncRouterManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class TestRegexPathMatcher { + + private val routerManager = Mockito.mock(AsyncRouterManager::class.java) + + @Test + @DisplayName("should match router by regex path successfully.") + fun test() { + val matcher = RegexPathMatcher() + val router1 = AsyncRouter(1, routerManager) + val router2 = AsyncRouter(2, routerManager) + val router3 = AsyncRouter(3, routerManager) + + matcher.add("/hello(\\d*)", router1) + matcher.add("/hello(\\d*)", router2) + matcher.add("/foo/([a-c]+)", router3) + + val result1 = matcher.match("/hello345") + requireNotNull(result1) + assertEquals(2, result1.routers.size) + assertEquals(Matcher.MatchType.PATH, result1.matchType) + assertEquals("345", result1.parameters[router2]?.get("group1")) + + val result2 = matcher.match("/foo/abccc") + requireNotNull(result2) + assertEquals(1, result2.routers.size) + assertEquals(Matcher.MatchType.PATH, result2.matchType) + assertEquals("abccc", result2.parameters[router3]?.get("group1")) + + val result3 = matcher.match("/foo/abcde") + assertNull(result3) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/TestAsyncRouterManager.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/TestAsyncRouterManager.kt new file mode 100644 index 000000000..8241916c8 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/TestAsyncRouterManager.kt @@ -0,0 +1,206 @@ +package com.fireflysource.net.http.server.impl.router + +import com.fireflysource.net.http.common.model.HttpFields +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpURI +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.net.http.server.Matcher.MatchType.* +import com.fireflysource.net.http.server.RoutingContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.mockito.Mockito.`when` + +class TestAsyncRouterManager { + + private val httpServer: HttpServer = Mockito.mock(HttpServer::class.java) + + @Test + @DisplayName("should find routers") + fun test() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register().get("/hello") + routerManager.register().post("/hello") + routerManager.register().put("/hello") + routerManager.register().delete("/hello") + + var ctx = createContext("GET", "/hello") + var result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(0, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + + ctx = createContext("POST", "/hello") + result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(1, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + + ctx = createContext("PUT", "/hello") + result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(2, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + + ctx = createContext("DELETE", "/hello") + result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(3, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + } + + @Test + @DisplayName("should not find routers") + fun testNotFound() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register().get("/hello") + + val ctx = createContext("POST", "/hello") + val result1 = routerManager.findRouters(ctx) + assertTrue(result1.isEmpty()) + } + + @Test + @DisplayName("should find routers by paths successfully") + fun testPaths() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register().paths(listOf("/hello", "/hello/*")) + routerManager.register().paths(listOf("/hello/:name", "/foo")) + + val ctx1 = createContext("POST", "/hello") + val result1 = routerManager.findRouters(ctx1) + assertEquals(1, result1.size) + assertEquals(0, result1.first().router.id) + assertTrue(result1.first().matchTypes.containsAll(listOf(PATH))) + + val ctx2 = createContext("POST", "/hello/xxx") + val result2 = routerManager.findRouters(ctx2) + assertEquals(2, result2.size) + assertTrue(result2.first().matchTypes.containsAll(listOf(PATH))) + + val ctx3 = createContext("PUT", "/foo") + val result3 = routerManager.findRouters(ctx3) + assertEquals(1, result3.size) + assertEquals(1, result3.first().router.id) + assertTrue(result3.first().matchTypes.containsAll(listOf(PATH))) + } + + @Test + @DisplayName("should find routers by content type successfully") + fun testConsume() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register().path("/hello").consumes("*/json") + routerManager.register().path("/hello").consumes("text/html") + + val ctx1 = createContext("GET", "/hello", "application/json") + val result1 = routerManager.findRouters(ctx1) + assertEquals(1, result1.size) + assertEquals(0, result1.first().router.id) + assertTrue(result1.first().matchTypes.containsAll(listOf(PATH, CONTENT_TYPE))) + + val ctx2 = createContext("GET", "/hello", "text/html") + val result2 = routerManager.findRouters(ctx2) + assertEquals(1, result2.size) + assertEquals(1, result2.first().router.id) + assertTrue(result1.first().matchTypes.containsAll(listOf(PATH, CONTENT_TYPE))) + + val ctx3 = createContext("POST", "/test", "text/html") + val result3 = routerManager.findRouters(ctx3) + assertTrue(result3.isEmpty()) + } + + @Test + @DisplayName("should find routers by accept successfully") + fun testAccept() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register().path("/*").produces("application/json") + routerManager.register().path("/hello").produces("text/html") + + var ctx = createContext("GET", "/hello", "", "text/html,application/xml;q=0.9,application/json;q=0.8") + var result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(1, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(PATH, ACCEPT))) + + ctx = createContext("GET", "/hello", "", "text/html;q=0.6,application/xml;q=0.7,application/json;q=0.8") + result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(0, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(PATH, ACCEPT))) + } + + @Test + @DisplayName("should find routers by regex successfully") + fun testRegex() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register(30).pathRegex("/foo/([a-c]+)") + routerManager.register(20).path("/foo/*") + + var ctx = createContext("GET", "/foo/abc", "", "text/html,application/xml;q=0.9,application/json;q=0.8") + var result = routerManager.findRouters(ctx) + assertEquals(2, result.size) + assertEquals(20, result.first().router.id) + assertEquals(30, result.last().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(PATH))) + + ctx = createContext("GET", "/foo/ddd") + result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(20, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(PATH))) + } + + private fun createContext( + method: String, + uri: String, + contentType: String = "", + accept: String = "" + ): RoutingContext { + val ctx = Mockito.mock(RoutingContext::class.java) + `when`(ctx.method).thenReturn(method) + `when`(ctx.uri).thenReturn(HttpURI(uri)) + val fields = HttpFields() + fields.put(HttpHeader.CONTENT_TYPE, contentType) + fields.put(HttpHeader.ACCEPT, accept) + `when`(ctx.httpFields).thenReturn(fields) + `when`(ctx.contentType).thenReturn(contentType) + return ctx + } + + @Test + fun testCopy() { + val routerManager = AsyncRouterManager(httpServer) + routerManager.register().get("/hello") + routerManager.register().post("/hello") + routerManager.register().put("/hello") + routerManager.register().delete("/hello") + + var ctx = createContext("GET", "/hello") + var result = routerManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(0, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + + val newRouterManager = routerManager.copy(httpServer) + newRouterManager.register().get("/helloCopy") + + ctx = createContext("POST", "/hello") + result = newRouterManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(1, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + + ctx = createContext("GET", "/helloCopy") + result = newRouterManager.findRouters(ctx) + assertEquals(1, result.size) + assertEquals(4, result.first().router.id) + assertTrue(result.first().matchTypes.containsAll(listOf(METHOD, PATH))) + + ctx = createContext("GET", "/helloCopy") + result = routerManager.findRouters(ctx) + assertTrue(result.isEmpty()) + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/handler/TestCorsHandler.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/handler/TestCorsHandler.kt new file mode 100644 index 000000000..a64a96c1c --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/handler/TestCorsHandler.kt @@ -0,0 +1,369 @@ +package com.fireflysource.net.http.server.impl.router.handler + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.client.HttpClientResponse +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpMethod +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.impl.AbstractHttpServerTestBase +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.util.concurrent.CompletableFuture +import kotlin.system.measureTimeMillis + +class TestCorsHandler : AbstractHttpServerTestBase() { + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should allow request successfully.") + fun testSimpleRequest(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().get("/cors-simple-request/*").handler { it.end("success") } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/cors-simple-request/$it") + .put(HttpHeader.ORIGIN, "simple.request.cors.test.com") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("success", response.stringBody) + assertEquals("simple.request.cors.test.com", response.httpFields[HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN]) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response expose headers successfully.") + fun testExposeHeaders(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + corsConfig.exposeHeaders = setOf("x1-x2", "x3-x4") + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().get("/cors-simple-request/*").handler { it.end("success") } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/cors-simple-request/$it") + .put(HttpHeader.ORIGIN, "simple.request.cors.test.com") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("success", response.stringBody) + assertEquals("simple.request.cors.test.com", response.httpFields[HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN]) + assertEquals("x1-x2, x3-x4", response.httpFields[HttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS]) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should not allow origin via simple request.") + fun testNotAllowOriginSimpleRequest(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().get("/cors-simple-request/*").handler { it.end("success") } + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/cors-simple-request/$it") + .put(HttpHeader.ORIGIN, "www.fireflysource.com") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.FORBIDDEN_403, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should response preflight request successfully.") + fun testPreflightRequest(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().post("/cors-data-request/*").handler { it.end("success") } + .listen(address) + + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient + .request( + HttpMethod.OPTIONS, + "$schema://${address.hostName}:${address.port}/cors-data-request/$it" + ) + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.value) + .put(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, HttpHeader.CONTENT_TYPE.lowerCaseValue) + .submit() + .thenCompose { resp -> + if (resp.status == HttpStatus.NO_CONTENT_204) { + httpClient.post("$schema://${address.hostName}:${address.port}/cors-data-request/$it") + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .body( + """ + |{"id": 333} + """.trimMargin() + ) + .submit() + } else { + val future = CompletableFuture() + future.complete(resp) + future + } + } + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("success", response.stringBody) + assertEquals("data.request.cors.test.com", response.httpFields[HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN]) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should block by preflight request when the headers do not allow to access.") + fun testNotAllowHeader(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().post("/cors-data-request/*").handler { it.end("success") } + .listen(address) + + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient + .request( + HttpMethod.OPTIONS, + "$schema://${address.hostName}:${address.port}/cors-data-request/$it" + ) + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.value) + .put(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, "x1-x2") + .submit() + .thenCompose { resp -> + if (resp.status == HttpStatus.NO_CONTENT_204) { + httpClient.post("$schema://${address.hostName}:${address.port}/cors-data-request/$it") + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .body( + """ + |{"id": 333} + """.trimMargin() + ) + .submit() + } else { + val future = CompletableFuture() + future.complete(resp) + future + } + } + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.BAD_REQUEST_400, response.status) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should block by preflight request when the methods do not allow to access.") + fun testNotAllowMethod(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + corsConfig.allowMethods = setOf(HttpMethod.GET.value) + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().post("/cors-data-request/*").handler { it.end("success") } + .listen(address) + + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient + .request( + HttpMethod.OPTIONS, + "$schema://${address.hostName}:${address.port}/cors-data-request/$it" + ) + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.value) + .put(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .submit() + .thenCompose { resp -> + if (resp.status == HttpStatus.NO_CONTENT_204) { + httpClient.post("$schema://${address.hostName}:${address.port}/cors-data-request/$it") + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .body( + """ + |{"id": 333} + """.trimMargin() + ) + .submit() + } else { + val future = CompletableFuture() + future.complete(resp) + future + } + } + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.METHOD_NOT_ALLOWED_405, response.status) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should not allow origin via preflight.") + fun testNotAllowOriginPreflight(protocol: String, schema: String): Unit = runTest { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val corsConfig = CorsConfig("*.cors.test.com") + corsConfig.allowMethods = setOf(HttpMethod.GET.value) + httpServer + .router().path("*").handler(CorsHandler(corsConfig)) + .router().post("/cors-data-request/*").handler { it.end("success") } + .listen(address) + + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient + .request( + HttpMethod.OPTIONS, + "$schema://${address.hostName}:${address.port}/cors-data-request/$it" + ) + .put(HttpHeader.ORIGIN, "www.fireflysource.com") + .put(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.value) + .put(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .submit() + .thenCompose { resp -> + if (resp.status == HttpStatus.NO_CONTENT_204) { + httpClient.post("$schema://${address.hostName}:${address.port}/cors-data-request/$it") + .put(HttpHeader.ORIGIN, "data.request.cors.test.com") + .put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .body( + """ + |{"id": 333} + """.trimMargin() + ) + .submit() + } else { + val future = CompletableFuture() + future.complete(resp) + future + } + } + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.FORBIDDEN_403, response.status) + } + + finish(count, time, httpClient, httpServer) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/handler/TestFileHandler.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/handler/TestFileHandler.kt new file mode 100644 index 000000000..400d5bb1b --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/http/server/impl/router/handler/TestFileHandler.kt @@ -0,0 +1,200 @@ +package com.fireflysource.net.http.server.impl.router.handler + +import com.fireflysource.common.concurrent.exceptionallyAccept +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.server.impl.AbstractHttpServerTestBase +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.CompletableFuture +import kotlin.system.measureTimeMillis + +class TestFileHandler : AbstractHttpServerTestBase() { + + private fun createFileHandler(): FileHandler { + val path = Optional.ofNullable(FileHandler::class.java.classLoader.getResource("files")) + .map { it.toURI() } + .map { Paths.get(it) } + .map { it.toString() } + .orElse("") + val fileConfig = FileConfig(path) + return FileHandler(fileConfig) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should get file successfully.") + fun testFile(protocol: String, schema: String): Unit = runBlocking { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val fileHandler = createFileHandler() + httpServer + .router().paths(listOf("/favicon.ico", "/*.html", "/*.txt")).handler(fileHandler) + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/poem.html") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.OK_200, response.status) + assertEquals("text/html", response.httpFields[HttpHeader.CONTENT_TYPE]) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should not find the file.") + fun testFileNotFound(protocol: String, schema: String): Unit = runBlocking { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val fileHandler = createFileHandler() + httpServer + .router().paths(listOf("/favicon.ico", "/*.html", "/*.txt")).handler(fileHandler) + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/poem-$it.html") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.NOT_FOUND_404, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should get partial file successfully.") + fun testPartialFile(protocol: String, schema: String): Unit = runBlocking { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val fileHandler = createFileHandler() + httpServer + .router().paths(listOf("/favicon.ico", "/*.html", "/*.txt")).handler(fileHandler) + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/poem.html") + .put(HttpHeader.RANGE, "bytes=5-10") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.PARTIAL_CONTENT_206, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should get partial file unsuccessfully.") + fun testRangeNotSatisfiable(protocol: String, schema: String): Unit = runBlocking { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val fileHandler = createFileHandler() + httpServer + .router().paths(listOf("/favicon.ico", "/*.html", "/*.txt")).handler(fileHandler) + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/poem.html") + .put(HttpHeader.RANGE, "bytes=1000000000000000000-") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.RANGE_NOT_SATISFIABLE_416, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should not response multi-range.") + fun testMultiRange(protocol: String, schema: String): Unit = runBlocking { + val count = 1 + + val httpServer = createHttpServer(protocol, schema) + val fileHandler = createFileHandler() + httpServer + .router().paths(listOf("/favicon.ico", "/*.html", "/*.txt")).handler(fileHandler) + .listen(address) + + val httpClient = HttpClientFactory.create() + val time = measureTimeMillis { + val futures = (1..count) + .map { + httpClient.get("$schema://${address.hostName}:${address.port}/poem.html") + .put(HttpHeader.RANGE, "bytes=5-20,35-65,-5") + .submit() + } + futures.forEach { f -> f.exceptionallyAccept { println(it.message) } } + CompletableFuture.allOf(*futures.toTypedArray()).await() + val allDone = futures.all { it.isDone } + Assertions.assertTrue(allDone) + + val response = futures[0].await() + + assertEquals(HttpStatus.RANGE_NOT_SATISFIABLE_416, response.status) + println(response) + } + + finish(count, time, httpClient, httpServer) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/aio/TestAdaptiveBufferSize.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/aio/TestAdaptiveBufferSize.kt new file mode 100644 index 000000000..f88037712 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/aio/TestAdaptiveBufferSize.kt @@ -0,0 +1,45 @@ +package com.fireflysource.net.tcp.aio + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * @author Pengtao Qiu + */ +class TestAdaptiveBufferSize { + + @Test + @DisplayName("should increase or decrease the buffer size when the data size changes.") + fun test() { + val bufSize = AdaptiveBufferSize() + assertEquals(128, bufSize.getBufferSize()) + + bufSize.update(1024) + assertEquals(256, bufSize.getBufferSize()) + + bufSize.update(1024) + assertEquals(512, bufSize.getBufferSize()) + + bufSize.update(1024) + assertEquals(1024, bufSize.getBufferSize()) + + bufSize.update(2048) + assertEquals(2048, bufSize.getBufferSize()) + + bufSize.update(4096) + assertEquals(4096, bufSize.getBufferSize()) + + bufSize.update(500) + assertEquals(2048, bufSize.getBufferSize()) + + bufSize.update(100) + assertEquals(1024, bufSize.getBufferSize()) + + repeat(100) { bufSize.update(512 * 1024) } + assertEquals(512 * 1024, bufSize.getBufferSize()) + + repeat(100) { bufSize.update(15) } + assertEquals(128, bufSize.getBufferSize()) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/aio/TestAioServerAndClient.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/aio/TestAioServerAndClient.kt new file mode 100644 index 000000000..0ecc84905 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/aio/TestAioServerAndClient.kt @@ -0,0 +1,306 @@ +package com.fireflysource.net.tcp.aio + +import com.fireflysource.common.coroutine.CoroutineDispatchers.defaultPoolSize +import com.fireflysource.common.coroutine.event +import com.fireflysource.common.coroutine.eventAsync +import com.fireflysource.common.sys.Result.discard +import com.fireflysource.net.tcp.* +import com.fireflysource.net.tcp.onAcceptAsync +import com.fireflysource.net.tcp.secure.conscrypt.NoCheckConscryptSSLContextFactory +import com.fireflysource.net.tcp.secure.conscrypt.SelfSignedCertificateConscryptSSLContextFactory +import com.fireflysource.net.tcp.secure.wildfly.NoCheckWildflySSLContextFactory +import com.fireflysource.net.tcp.secure.wildfly.SelfSignedCertificateWildflySSLContextFactory +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.nio.ByteBuffer +import java.util.concurrent.atomic.AtomicInteger +import java.util.stream.Stream +import kotlin.math.roundToLong +import kotlin.random.Random +import kotlin.system.measureTimeMillis + + +/** + * @author Pengtao Qiu + */ +class TestAioServerAndClient { + + companion object { + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + arguments("single", true, false, "default"), + arguments("array", true, false, "default"), + arguments("list", true, false, "default"), + + arguments("single", true, false, "conscrypt"), + arguments("array", true, false, "conscrypt"), + arguments("list", true, false, "conscrypt"), + +// arguments("single", true, false, "wildfly"), +// arguments("array", true, false, "wildfly"), +// arguments("list", true, false, "wildfly"), + + arguments("single", true, true, "default"), + arguments("array", true, true, "default"), + arguments("list", true, true, "default"), + + arguments("single", false, false, "default"), + arguments("array", false, false, "default"), + arguments("list", false, false, "default"), + + arguments("single", false, true, "default"), + arguments("array", false, true, "default"), + arguments("list", false, true, "default") + ) + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should send and receive messages successfully.") + fun test(bufType: String, enableSecure: Boolean, enableBuffer: Boolean, securityProvider: String) = runTest { + val host = "localhost" + val port = Random.nextInt(30000, 50000) + + val connectionCount = defaultPoolSize + val maxMessageCountPerOneConnection = 32 + val expectMessageCount = maxMessageCountPerOneConnection * connectionCount + + val messageCount = AtomicInteger() + val tcpConfig = TcpConfig( + timeout = 30, + enableSecureConnection = enableSecure, + enableOutputBuffer = enableBuffer + ) + + val server = TcpServerFactory.create(tcpConfig) + when (securityProvider) { + "conscrypt" -> server.secureEngineFactory(SelfSignedCertificateConscryptSSLContextFactory()) + "wildfly" -> server.secureEngineFactory(SelfSignedCertificateWildflySSLContextFactory()) + } + server.onAcceptAsync { connection -> + println("accept connection. ${connection.id}") + connection.beginHandshake().await() + readLoop@ while (true) { + val buf = try { + connection.read(3000L) + } catch (e: Exception) { + println(e.message + "|" + e::class.java.name) + break@readLoop + } + + writeLoop@ while (buf.hasRemaining()) { + val num = buf.int + val newBuf = ByteBuffer.allocate(4) + newBuf.putInt(num).flip() + + if (num == maxMessageCountPerOneConnection) { + connection.write(newBuf) + try { + connection.flush(300L) + } catch (e: Exception) { + println(e.message + "|" + e::class.java.name) + } + break@readLoop + } else { + connection.write(newBuf, discard()) + } + } + } + }.listen(host, port) + + val client = TcpClientFactory.create(tcpConfig) + when (securityProvider) { + "conscrypt" -> client.secureEngineFactory(NoCheckConscryptSSLContextFactory()) + "wildfly" -> client.secureEngineFactory(NoCheckWildflySSLContextFactory()) + } + val time = measureTimeMillis { + val jobs = (1..connectionCount).map { + event { + val connection = withTimeout(1000L) { + val c = client.connect(host, port).await() + println("create connection. ${c.id}") + c.beginHandshake().await() + c + } + + val readingJob = connection.coroutineScope.launch { + readLoop@ while (true) { + val buf = try { + connection.read(3000L) + } catch (e: Exception) { + println(e.message + "|" + e::class.java.name) + break@readLoop + } + + writeLoop@ while (buf.hasRemaining()) { + val num = buf.int + messageCount.incrementAndGet() + + if (num == maxMessageCountPerOneConnection) { + try { + connection.close(1000L) + } catch (e: Exception) { + println(e.message + "|" + e::class.java.name) + } + break@readLoop + } + } + } + } + + when (bufType) { + "single" -> { + (1..maxMessageCountPerOneConnection).forEach { i -> + val buf = ByteBuffer.allocate(4) + buf.putInt(i).flip() + connection.write(buf, discard()) + } + connection.flush() + } + "array" -> { + val bufArray = Array(maxMessageCountPerOneConnection) { index -> + val buf = ByteBuffer.allocate(4) + buf.putInt(index + 1).flip() + buf + } + connection.write(bufArray, 0, bufArray.size, discard()) + connection.flush() + } + "list" -> { + val bufList = List(maxMessageCountPerOneConnection) { index -> + val buf = ByteBuffer.allocate(4) + buf.putInt(index + 1).flip() + buf + } + connection.write(bufList, 0, bufList.size, discard()) + connection.flush() + } + } + + readingJob.join() + + assertTrue(connection.readBytes > 0) + assertTrue(connection.writtenBytes > 0) + assertTrue(connection.lastActiveTime > 0L) + assertTrue(connection.lastReadTime > 0L) + assertTrue(connection.lastWrittenTime > 0L) + } + } + + jobs.forEach { it.join() } + + assertEquals(expectMessageCount, messageCount.get()) + } + + val throughput = expectMessageCount / (time / 1000.00) + println("success. $time ms, ${throughput.roundToLong()} qps") + println("connection: ${connectionCount}, messageCount: $expectMessageCount") + + val stopTime = measureTimeMillis { + client.stop() + server.stop() + } + println("stop success. $stopTime") + } + + @Test + @DisplayName("should close when the connection is timeout.") + fun testTimeout() = runTest { + val host = "localhost" + val port = Random.nextInt(10000, 50000) + + val server = TcpServerFactory.create(TcpConfig(30)).onAcceptAsync { conn -> + conn.setReadTimeout(1) + conn.setWriteTimeout(1) + try { + conn.read().await() + println("Server reads success.") + } catch (e: Exception) { + println("Server reads failure. ${e.javaClass.name}") + } + }.listen(host, port) + + val client = TcpClientFactory.create(TcpConfig(30)) + val connection = client.connect(host, port).await() + assertEquals(port, connection.remoteAddress.port) + + connection.setReadTimeout(1) + connection.setWriteTimeout(1) + val success = try { + connection.read().await() + true + } catch (e: Exception) { + println("Client reads failure. ${e.javaClass.name}") + false + } + delay(500) + assertFalse(success) + assertTrue(connection.duration > 0) + assertTrue(connection.isShutdownInput) + + val stopTime = measureTimeMillis { + client.stop() + server.stop() + } + println("stop success. $stopTime") + } + + @Test + @DisplayName("should close connection successfully.") + fun testClose(): Unit = runTest { + val host = "localhost" + val port = Random.nextInt(10000, 50000) + + val server = TcpServerFactory.create().onAcceptAsync { connection -> + try { + connection.read(2000L) + println("Server reads success.") + } catch (e: Exception) { + println("Server reads failure. ${e.javaClass.name}") + } + }.listen(host, port) + + val client = TcpClientFactory.create() + val connection = client.connect(host, port).await() + + val success = eventAsync { + try { + connection.read(2000L) + println("Client reads success.") + true + } catch (e: Exception) { + println("Client reads failure. ${e.javaClass.name}") + false + } + } + + connection.closeAsync().await() + println("close client connection success") + assertTrue(connection.isShutdownOutput) + assertTrue(connection.isShutdownInput) + assertTrue(connection.isClosed) + assertFalse(success.await()) + + val stopTime = measureTimeMillis { + withTimeoutOrNull(2000L) { + client.stop() + server.stop() + } + } + println("stop success. $stopTime") + } + +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/buffer/TestMessage.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/buffer/TestMessage.kt new file mode 100644 index 000000000..57005b0ec --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/buffer/TestMessage.kt @@ -0,0 +1,63 @@ +package com.fireflysource.net.tcp.buffer + +import com.fireflysource.common.sys.Result.discard +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import java.nio.ByteBuffer + +/** + * @author Pengtao Qiu + */ +class TestMessage { + + @ParameterizedTest + @CsvSource(value = ["4,2", "2,3", "-1,3", "0,0", "0,-5", "0,6"]) + @DisplayName("the length should be less than and equal buffers size subtracts offset") + fun testOutOfBoundException(offset: Int, length: Int) { + val size = 4 + assertThrows { + OutputBuffers(Array(size) { ByteBuffer.allocate(16) }, offset, length, discard()) + } + + assertThrows { + OutputBufferList(List(size) { ByteBuffer.allocate(16) }, offset, length, discard()) + } + } + + @ParameterizedTest + @CsvSource(value = ["0,3", "1,3", "2,2", "3,1", "0,6", "1,5", "2,3"]) + @DisplayName("the current offset and length should change by the buffers consume") + fun testCurrentOffsetAndLength(offset: Int, length: Int) { + val size = 6 + val capacity = 16 + val buffers = OutputBuffers(Array(size) { ByteBuffer.allocate(capacity) }, offset, length, discard()) + buffers.buffers[offset].putInt(1) + assertEquals(offset, buffers.getCurrentOffset()) + assertEquals(length, buffers.getCurrentLength()) + + buffers.buffers[offset].putLong(2) + buffers.buffers[offset].putInt(3) + assertEquals(offset + 1, buffers.getCurrentOffset()) + assertEquals(length - 1, buffers.getCurrentLength()) + } + + @ParameterizedTest + @CsvSource(value = ["0,3", "1,3", "2,2", "3,1", "0,6", "1,5", "2,3"]) + @DisplayName("should not get the remaining") + fun testHasRemaining(offset: Int, length: Int) { + val size = 6 + val capacity = 16 + val buffers = OutputBuffers(Array(size) { ByteBuffer.allocate(capacity) }, offset, length, discard()) + + val lastIndex = offset + length - 1 + val bytes = ByteArray(capacity) + (offset..lastIndex).forEach { i -> buffers.buffers[i].put(bytes) } + assertFalse(buffers.hasRemaining()) + assertEquals(0, buffers.getCurrentLength()) + assertEquals(offset + length, buffers.getCurrentOffset()) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/secure/TestSSLContextFactory.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/secure/TestSSLContextFactory.kt new file mode 100644 index 000000000..da02a10cf --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/tcp/secure/TestSSLContextFactory.kt @@ -0,0 +1,78 @@ +package com.fireflysource.net.tcp.secure + +import com.fireflysource.net.tcp.secure.common.AbstractSecureEngineFactory +import com.fireflysource.net.tcp.secure.conscrypt.FileConscryptSSLContextFactory +import com.fireflysource.net.tcp.secure.conscrypt.SelfSignedCertificateConscryptSSLContextFactory +import com.fireflysource.net.tcp.secure.jdk.FileOpenJdkSSLContextFactory +import com.fireflysource.net.tcp.secure.jdk.SelfSignedCertificateOpenJdkSSLContextFactory +import com.fireflysource.net.tcp.secure.utils.SecureUtils +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.security.KeyStore +import java.util.stream.Stream +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManagerFactory + +class TestSSLContextFactory { + + companion object { + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + arguments( + "jsse", + FileOpenJdkSSLContextFactory("fireflyKeystore.jks", "123456", "654321", "JKS") + ), + arguments( + "jsse", + SelfSignedCertificateOpenJdkSSLContextFactory() + ), + arguments( + "Conscrypt", + FileConscryptSSLContextFactory("fireflyKeystore.jks", "123456", "654321", "JKS") + ), + arguments( + "Conscrypt", + SelfSignedCertificateConscryptSSLContextFactory() + ) + ) + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should create secure engine factory successfully.") + fun test(name: String, factory: AbstractSecureEngineFactory) { + val sslContext = factory.sslContext + println(sslContext.provider.name) + println(sslContext.provider.info) + assertTrue(sslContext.provider.name.contains(name, true)) + } + + @Test + @DisplayName("should load self signed certificate successfully.") + fun testKeyStore() { + val ks = KeyStore.getInstance("JKS") + SecureUtils.getSelfSignedCertificate().use { + ks.load(it, "123456".toCharArray()) + println("size: ${ks.size()}") + assertTrue(ks.size() > 0) + val certificate = ks.getCertificate("fireflyselfcert") + println(certificate.type) + assertEquals("X.509", certificate.type) + + val km = KeyManagerFactory.getInstance("SunX509") + km.init(ks, "654321".toCharArray()) + assertTrue(km.keyManagers.isNotEmpty()) + val tmf = TrustManagerFactory.getInstance("SunX509") + tmf.init(ks) + assertTrue(tmf.trustManagers.isNotEmpty()) + } + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/websocket/TestWebSocketServerAndClient.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/websocket/TestWebSocketServerAndClient.kt new file mode 100644 index 000000000..ee4706e51 --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/websocket/TestWebSocketServerAndClient.kt @@ -0,0 +1,106 @@ +package com.fireflysource.net.websocket + +import com.fireflysource.common.sys.Result +import com.fireflysource.net.http.client.HttpClientFactory +import com.fireflysource.net.http.server.HttpServerFactory +import com.fireflysource.net.websocket.common.frame.Frame +import com.fireflysource.net.websocket.common.frame.TextFrame +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.random.Random + +/** + * @author Pengtao Qiu + */ +class TestWebSocketServerAndClient { + + companion object { + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + arguments("none", "ws"), + arguments("fragment", "ws"), + arguments("identity", "ws"), + arguments("deflate-frame", "ws"), + arguments("permessage-deflate", "ws"), + arguments("x-webkit-deflate-frame", "ws"), + arguments("identity,permessage-deflate", "ws"), + arguments("fragment,identity,permessage-deflate", "ws"), + + arguments("none", "wss"), + arguments("fragment", "wss"), + arguments("identity", "wss"), + arguments("deflate-frame", "wss"), + arguments("permessage-deflate", "wss"), + arguments("x-webkit-deflate-frame", "wss"), + arguments("identity,permessage-deflate", "wss"), + arguments("fragment,identity,permessage-deflate", "wss") + ) + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should receive websocket messages successfully.") + fun test(extensions: String, scheme: String) = runTest { + val host = "localhost" + val port = Random.nextInt(10000, 20000) + val count = 100 + + val serverChannel = Channel(Channel.UNLIMITED) + val server = HttpServerFactory.create() + if (scheme == "wss") { + server.enableSecureConnection() + } + server.websocket("/websocket/echo") + .onMessage { frame, _ -> + if (frame.type == Frame.Type.TEXT && frame is TextFrame) { + serverChannel.trySend(frame.payloadAsUTF8) + } + Result.DONE + } + .onAccept { connection -> + (1..count).forEach { i -> connection.sendText("Server $i") } + Result.DONE + } + .listen(host, port) + + val clientChannel = Channel(Channel.UNLIMITED) + val client = HttpClientFactory.create() + val webSocketConnection = client + .websocket("$scheme://$host:$port/websocket/echo") + .extensions(extensions.split(",")) + .onMessage { frame, _ -> + if (frame.type == Frame.Type.TEXT && frame is TextFrame) { + clientChannel.trySend(frame.payloadAsUTF8) + } + Result.DONE + } + .connect() + .await() + + (1..count).forEach { i -> webSocketConnection.sendText("Client $i") } + + (1..count).forEach { i -> + val serverReceivedMessage = serverChannel.receive() + assertEquals("Client $i", serverReceivedMessage) + } + + (1..count).forEach { i -> + val clientReceivedMessage = clientChannel.receive() + assertEquals("Server $i", clientReceivedMessage) + } + + webSocketConnection.closeAsync() + client.stop() + server.stop() + } +} \ No newline at end of file diff --git a/firefly-net/src/test/kotlin/com/fireflysource/net/websocket/common/impl/TestAsyncWebSocketConnection.kt b/firefly-net/src/test/kotlin/com/fireflysource/net/websocket/common/impl/TestAsyncWebSocketConnection.kt new file mode 100644 index 000000000..d37c16f3f --- /dev/null +++ b/firefly-net/src/test/kotlin/com/fireflysource/net/websocket/common/impl/TestAsyncWebSocketConnection.kt @@ -0,0 +1,125 @@ +package com.fireflysource.net.websocket.common.impl + +import com.fireflysource.common.sys.Result +import com.fireflysource.net.tcp.TcpClientFactory +import com.fireflysource.net.tcp.TcpConnection +import com.fireflysource.net.tcp.TcpServerFactory +import com.fireflysource.net.tcp.onAcceptAsync +import com.fireflysource.net.websocket.common.WebSocketConnection +import com.fireflysource.net.websocket.common.frame.TextFrame +import com.fireflysource.net.websocket.common.model.WebSocketBehavior +import com.fireflysource.net.websocket.common.model.WebSocketPolicy +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.random.Random + +/** + * @author Pengtao Qiu + */ +class TestAsyncWebSocketConnection { + + companion object { + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + arguments("none", false), + arguments("fragment", false), + arguments("identity", false), + arguments("deflate-frame", false), + arguments("permessage-deflate", false), + arguments("x-webkit-deflate-frame", false), + arguments("identity,permessage-deflate", false), + arguments("fragment,identity,permessage-deflate", false), + + arguments("none", true), + arguments("fragment", true), + arguments("identity", true), + arguments("deflate-frame", true), + arguments("permessage-deflate", true), + arguments("x-webkit-deflate-frame", true), + arguments("identity,permessage-deflate", true), + arguments("fragment,identity,permessage-deflate", true) + ) + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should send and receive websocket messages successfully.") + fun test(extension: String, tls: Boolean) = runTest { + val host = "localhost" + val port = Random.nextInt(10000, 20000) + + val server = TcpServerFactory.create() + if (tls) { + server.enableSecureConnection() + } + server.onAcceptAsync { connection -> + connection.beginHandshake().await() + val webSocketConnection = createWebSocketConnection(WebSocketBehavior.SERVER, extension, connection) + webSocketConnection.setWebSocketMessageHandler { frame, conn -> + println("Server receive: ${frame.type}") + when (frame) { + is TextFrame -> { + val payload = frame.payloadAsUTF8 + println("server receive: $payload") + conn.sendText("response $payload") + } + else -> Result.DONE + } + } + webSocketConnection.begin() + } + server.listen(host, port) + + val channel = Channel(Channel.UNLIMITED) + val client = TcpClientFactory.create() + if (tls) { + client.enableSecureConnection() + } + val connection = client.connect(host, port).await() + connection.beginHandshake().await() + val webSocketConnection = createWebSocketConnection(WebSocketBehavior.CLIENT, extension, connection) + webSocketConnection.setWebSocketMessageHandler { frame, _ -> + when (frame) { + is TextFrame -> { + val payload = frame.payloadAsUTF8 + println("Client receive: $payload") + channel.trySend(payload) + Result.DONE + } + else -> Result.DONE + } + } + webSocketConnection.begin() + + (1..10).forEach { webSocketConnection.sendText("text: $it") } + + (1..10).forEach { + val text = channel.receive() + assertEquals("response text: $it", text) + } + + webSocketConnection.closeAsync() + client.stop() + server.stop() + } + + private fun createWebSocketConnection( + behavior: WebSocketBehavior, + extension: String, + connection: TcpConnection + ): WebSocketConnection { + val policy = WebSocketPolicy(behavior) + val extensions = if (extension != "none") listOf(extension) else listOf() + return AsyncWebSocketConnection(connection, policy, "localhost", extensions) + } +} \ No newline at end of file diff --git a/firefly-net/src/test/resources/files/favicon.ico b/firefly-net/src/test/resources/files/favicon.ico new file mode 100644 index 000000000..d888a0c47 Binary files /dev/null and b/firefly-net/src/test/resources/files/favicon.ico differ diff --git a/firefly-net/src/test/resources/files/poem.html b/firefly-net/src/test/resources/files/poem.html new file mode 100644 index 000000000..415dcaccf --- /dev/null +++ b/firefly-net/src/test/resources/files/poem.html @@ -0,0 +1,20 @@ + + + + + Poem + + +

家庭

+

+ 我独自在横跨过田地的路上行走,夕阳像一个守财奴似的,正藏起它的最后的金子。 + 白昼更加深沉地陷入黑暗之中,那已经收割了的孤独的田地, + 默默地躺在那里。 + 天空里突然升起了一个男孩童的尖锐的歌声。他穿过看不见的黑暗,留下他的歌声的辙痕跨过黄昏的静谧。 + 他的乡村的家坐落在荒凉的边上,在甘蔗田的后面,躲藏在香蕉树,瘦长的槟榔树,椰子树和深绿色的贾克果树的阴影里。 + 我在星光下独自走着的路上停留了一会,我看见黑沉沉的大地展开在我的面前,用她的手臂拥抱着无量数的家庭, + 在那些家庭里有着摇篮和床铺,母亲们的心和夜晚的灯,还有年轻轻的生命, + 他们满心快乐,却浑然不知这样的快乐对于世界的价值。 +

+ + \ No newline at end of file diff --git a/firefly-net/src/test/resources/files/poem.txt b/firefly-net/src/test/resources/files/poem.txt new file mode 100644 index 000000000..a9ccadd62 --- /dev/null +++ b/firefly-net/src/test/resources/files/poem.txt @@ -0,0 +1,22 @@ +偷睡眠的人 +谁从孩童的眼里把睡眠偷了去呢?我一定要知道。 +妈妈把她的水罐挟在腰间,走到近村汲水去了。 +这是正午的时候,孩童们游戏的时间已经过去了,池中的鸭子缄默无声。 +牧童躺在榕树的荫下睡着了。 +白鹤庄重而安静地立在檬果树边的泥泽里。 + +就在这个时候,偷睡眠的人跑来从孩童的两眼里捉住睡眠,便飞去了。 +当妈妈回来时,她看见孩童四肢着地地在屋里爬着。 +谁从孩童的眼里把睡眠偷了去呢?我一定要知道。我一定要找到她,把她锁起来。 +我一定要向那个黑洞里张望,在这个洞里,有一道小泉从圆的和有皱纹的石上滴下来。 +我一定要到醉花林中的沉寂的树影里搜寻,在这林中,鸽子在它们住的地方咕咕地叫着,仙女的脚环在繁星满天的静夜里叮当地响着。 +我要在黄昏时,向静静的萧萧的竹林里窥望,在这林中,萤火虫闪闪地耗费它们的光明,只要遇见一个人,我便要问他:“谁能告诉我偷睡眠者住在什么地方?” + +谁从孩童的眼里把睡眠偷了去呢?我一定要知道。 +只要我能捉住她,怕不会给她一顿好教训! +我要闯入她的巢穴,看她把所有偷来的睡眠藏在什么地方。 +我要把它都夺回来,带回家去。 +我要把她的双翼缚得紧紧的,把她放在河边,然后叫她拿一根芦苇在灯心草和睡莲间钓鱼为戏。 +黄昏,街上已经收了市,村里的孩童们都坐在妈妈的膝上时, +夜鸟便会讥讽地在她耳边说: +“你现在还想偷谁的睡眠呢?” \ No newline at end of file diff --git a/firefly-net/src/test/resources/firefly-log.xml b/firefly-net/src/test/resources/firefly-log.xml new file mode 100644 index 000000000..d139af581 --- /dev/null +++ b/firefly-net/src/test/resources/firefly-log.xml @@ -0,0 +1,18 @@ + + + + + firefly-system + INFO + ${log.path} + false + + + + firefly-monitor + INFO + ${log.path} + + + diff --git a/firefly-net/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/firefly-net/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/firefly-net/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/firefly-nettool/.gitignore b/firefly-nettool/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/firefly-nettool/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/firefly-nettool/pom.xml b/firefly-nettool/pom.xml deleted file mode 100644 index e42569b5b..000000000 --- a/firefly-nettool/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - 4.0.0 - - com.firefly - firefly-nettool - 1.0-SNAPSHOT - jar - - firefly-nettool - http://maven.apache.org - - ${project.artifactId} - install - - - src/main/resources - true - - - - - true - src/test/resources - - - - - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - UTF-8 - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - UTF-8 - - - - - - - - com.firefly - firefly-common - 1.0-SNAPSHOT - - - - junit - junit - 4.8.1 - test - - - org.hamcrest - hamcrest-all - 1.1 - test - - - - - com.firefly - firefly-nettool - - - 1.1.2 - - INFO - D:/log/ - - - - mac - - INFO - /Users/qiupengtao/develop/logs/ - - - - macdebug - - DEBUG - /Users/qiupengtao/develop/logs/ - - - - windebug - - DEBUG - D:/log/ - - - - - - 3rdRepo - 3rd party - http://localhost:7777/nexus-webapp/content/repositories/thirdparty - - - dev - Snapshots - http://localhost:7777/nexus-webapp/content/repositories/snapshots - - - diff --git a/firefly-nettool/src/main/java/com/firefly/net/Client.java b/firefly-nettool/src/main/java/com/firefly/net/Client.java deleted file mode 100644 index b218b08e3..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Client.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.firefly.net; - -public interface Client { - - void setConfig(Config config); - - int connect(String host, int port); - - void connect(String host, int port, int id); - - void shutdown(); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/Config.java b/firefly-nettool/src/main/java/com/firefly/net/Config.java deleted file mode 100644 index e9fcfdd2f..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Config.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.firefly.net; - -public class Config { - - private int timeout = 10 * 1000; - private int handleThreads = -1; - private int receiveByteBufferSize = 0; - private int workerThreads; - { - int workers = Runtime.getRuntime().availableProcessors(); - if (workers > 4) - workerThreads = workers * 2; - else - workerThreads = workers + 1; - } - - private String serverName = "firefly-server"; - private String clientName = "firefly-client"; - - private Decoder decoder; - private Encoder encoder; - private Handler handler; - - /** - * @return 连接超时时间 - */ - public int getTimeout() { - return timeout; - } - - /** - * 设置连接超时时间 - * - * @param timeout - * 超时时间 - */ - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - /** - * @return handler执行线程数 - */ - public int getHandleThreads() { - return handleThreads; - } - - /** - * 设置handler执行线程数
- * 线程数>0: 使用固定线程数线程池。
- * 线程数=0: 使用cached线程池 。
- * 线程数<0: handler在worker线程执行。
- * - * @param handleThreads - * handler执行线程数 - */ - public void setHandleThreads(int handleThreads) { - this.handleThreads = handleThreads; - } - - /** - * @return 接受数据ByteBuffer大小 - */ - public int getReceiveByteBufferSize() { - return receiveByteBufferSize; - } - - /** - * 设置接受数据ByteBuffer大小,当设置为小于或等于0时,使用自适应buffer大小 - * @param receiveByteBufferSize 设置接受数据ByteBuffer大小 - */ - public void setReceiveByteBufferSize(int receiveByteBufferSize) { - this.receiveByteBufferSize = receiveByteBufferSize; - } - - public int getWorkerThreads() { - return workerThreads; - } - - public void setWorkerThreads(int workerThreads) { - this.workerThreads = workerThreads; - } - - public String getServerName() { - return serverName; - } - - public void setServerName(String serverName) { - this.serverName = serverName; - } - - public String getClientName() { - return clientName; - } - - public void setClientName(String clientName) { - this.clientName = clientName; - } - - public Decoder getDecoder() { - return decoder; - } - - public void setDecoder(Decoder decoder) { - this.decoder = decoder; - } - - public Encoder getEncoder() { - return encoder; - } - - public void setEncoder(Encoder encoder) { - this.encoder = encoder; - } - - public Handler getHandler() { - return handler; - } - - public void setHandler(Handler handler) { - this.handler = handler; - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/Decoder.java b/firefly-nettool/src/main/java/com/firefly/net/Decoder.java deleted file mode 100644 index c9097f1bd..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Decoder.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.net; - -import java.nio.ByteBuffer; - -public interface Decoder { - void decode(ByteBuffer buf, Session session) throws Throwable; -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/Encoder.java b/firefly-nettool/src/main/java/com/firefly/net/Encoder.java deleted file mode 100644 index 744629530..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Encoder.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.firefly.net; - -public interface Encoder { - void encode(Object message, Session session) throws Throwable; -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/EventManager.java b/firefly-nettool/src/main/java/com/firefly/net/EventManager.java deleted file mode 100644 index f886e1773..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/EventManager.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.net; - - -public interface EventManager { - void executeOpenTask(Session session); - void executeReceiveTask(Session session, Object message); - void executeCloseTask(Session session); - void executeExceptionTask(Session session, Throwable t); - void shutdown(); -} \ No newline at end of file diff --git a/firefly-nettool/src/main/java/com/firefly/net/Handler.java b/firefly-nettool/src/main/java/com/firefly/net/Handler.java deleted file mode 100644 index 0de511b2e..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Handler.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.firefly.net; - -public interface Handler { - void sessionOpened(Session session) throws Throwable; - void sessionClosed(Session session) throws Throwable; - void messageRecieved(Session session, Object message) throws Throwable; - void exceptionCaught(Session session, Throwable t) throws Throwable; -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/ReceiveBufferPool.java b/firefly-nettool/src/main/java/com/firefly/net/ReceiveBufferPool.java deleted file mode 100644 index 2bbe6091c..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/ReceiveBufferPool.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.firefly.net; - -import java.nio.ByteBuffer; - -public interface ReceiveBufferPool { - - /** - * 从池中获取指定容量的ByteBuffer - * @param size buffer的容量 - * @return ByteBuffer - */ - ByteBuffer acquire(int size); - - /** - * 将一个ByteBuffer返回到池中 - * @param buffer - */ - void release(ByteBuffer buffer); - -} \ No newline at end of file diff --git a/firefly-nettool/src/main/java/com/firefly/net/ReceiveBufferSizePredictor.java b/firefly-nettool/src/main/java/com/firefly/net/ReceiveBufferSizePredictor.java deleted file mode 100644 index f7de114e2..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/ReceiveBufferSizePredictor.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.net; - -public interface ReceiveBufferSizePredictor { - int nextReceiveBufferSize(); - - void previousReceiveBufferSize(int previousReceiveBufferSize); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/SendBufferPool.java b/firefly-nettool/src/main/java/com/firefly/net/SendBufferPool.java deleted file mode 100644 index b6aa6e464..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/SendBufferPool.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.net; - -import com.firefly.net.buffer.SocketSendBufferPool.SendBuffer; - -public interface SendBufferPool { - SendBuffer acquire(Object src); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/Server.java b/firefly-nettool/src/main/java/com/firefly/net/Server.java deleted file mode 100644 index 9362a088f..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Server.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.firefly.net; - -public interface Server { - void setConfig(Config config); - - void start(String host, int port); - - void shutdown(); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/Session.java b/firefly-nettool/src/main/java/com/firefly/net/Session.java deleted file mode 100644 index 9ebd820bd..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Session.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.firefly.net; - -import java.net.InetSocketAddress; - -public interface Session { - int CLOSE = 0; - int OPEN = 1; - String CLOSE_FLAG = "#firefly-session-close"; - - void setAttribute(String key, Object value); - - Object getAttribute(String key); - - void removeAttribute(String key); - - void clearAttributes(); - - void fireReceiveMessage(Object message); - - void encode(Object message); - - void write(Object object); - - int getInterestOps(); - - int getSessionId(); - - long getOpenTime(); - - long getLastReadTime(); - - long getLastWrittenTime(); - - long getLastActiveTime(); - - long getReadBytes(); - - long getWrittenBytes(); - - void close(boolean immediately); - - int getState(); - - boolean isOpen(); - - InetSocketAddress getLocalAddress(); - - InetSocketAddress getRemoteAddress(); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/Worker.java b/firefly-nettool/src/main/java/com/firefly/net/Worker.java deleted file mode 100644 index 49028bb49..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/Worker.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firefly.net; - -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; - -public interface Worker extends Runnable { - - void registerSelectableChannel(SelectableChannel selectableChannel, int sessionId); - - void close(SelectionKey key); - - int getWorkerId(); - - void shutdown(); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/buffer/AdaptiveReceiveBufferSizePredictor.java b/firefly-nettool/src/main/java/com/firefly/net/buffer/AdaptiveReceiveBufferSizePredictor.java deleted file mode 100644 index b49eb5d34..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/buffer/AdaptiveReceiveBufferSizePredictor.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.firefly.net.buffer; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.firefly.net.ReceiveBufferSizePredictor; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - - -public class AdaptiveReceiveBufferSizePredictor implements - ReceiveBufferSizePredictor { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - static final int DEFAULT_MINIMUM = 64; - static final int DEFAULT_INITIAL = 1024; - static final int DEFAULT_MAXIMUM = 65536; - - private static final int INDEX_INCREMENT = 4; - private static final int INDEX_DECREMENT = 1; - - private static final int[] SIZE_TABLE; - - static { - List sizeTable = new ArrayList(); - for (int i = 1; i <= 8; i++) { - sizeTable.add(i); - } - - for (int i = 4; i < 32; i++) { - long v = 1L << i; - long inc = v >>> 4; - v -= inc << 3; - - for (int j = 0; j < 8; j++) { - v += inc; - if (v > Integer.MAX_VALUE) { - sizeTable.add(Integer.MAX_VALUE); - } else { - sizeTable.add((int) v); - } - } - } - - SIZE_TABLE = new int[sizeTable.size()]; - for (int i = 0; i < SIZE_TABLE.length; i++) { - SIZE_TABLE[i] = sizeTable.get(i); - } - log.debug(Arrays.toString(SIZE_TABLE)); - } - - private static int getSizeTableIndex(final int size) { - if (size <= 16) { - return size - 1; - } - - int bits = 0; - int v = size; - do { - v >>>= 1; - bits++; - } while (v != 0); - - final int baseIdx = bits << 3; - final int startIdx = baseIdx - 18; - final int endIdx = baseIdx - 25; - - for (int i = startIdx; i >= endIdx; i--) { - if (size >= SIZE_TABLE[i]) { - return i; - } - } - - throw new Error("shouldn't reach here; please file a bug report."); - } - - private final int minIndex; - private final int maxIndex; - private int index; - private int nextReceiveBufferSize; - private boolean decreaseNow; - - /** - * Creates a new predictor with the default parameters. With the default - * parameters, the expected buffer size starts from {@code 1024}, does not - * go down below {@code 64}, and does not go up above {@code 65536}. - */ - public AdaptiveReceiveBufferSizePredictor() { - this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM); - } - - /** - * Creates a new predictor with the specified parameters. - * - * @param minimum - * the inclusive lower bound of the expected buffer size - * @param initial - * the initial buffer size when no feed back was received - * @param maximum - * the inclusive upper bound of the expected buffer size - */ - public AdaptiveReceiveBufferSizePredictor(int minimum, int initial, - int maximum) { - if (minimum <= 0) { - throw new IllegalArgumentException("minimum: " + minimum); - } - if (initial < minimum) { - throw new IllegalArgumentException("initial: " + initial); - } - if (maximum < initial) { - throw new IllegalArgumentException("maximum: " + maximum); - } - - int minIndex = getSizeTableIndex(minimum); - if (SIZE_TABLE[minIndex] < minimum) { - this.minIndex = minIndex + 1; - } else { - this.minIndex = minIndex; - } - - int maxIndex = getSizeTableIndex(maximum); - if (SIZE_TABLE[maxIndex] > maximum) { - this.maxIndex = maxIndex - 1; - } else { - this.maxIndex = maxIndex; - } - - index = getSizeTableIndex(initial); - nextReceiveBufferSize = SIZE_TABLE[index]; - } - - @Override - public int nextReceiveBufferSize() { - return nextReceiveBufferSize; - } - - @Override - public void previousReceiveBufferSize(int previousReceiveBufferSize) { - if (previousReceiveBufferSize <= SIZE_TABLE[Math.max(0, index - - INDEX_DECREMENT - 1)]) { - if (decreaseNow) { - index = Math.max(index - INDEX_DECREMENT, minIndex); - nextReceiveBufferSize = SIZE_TABLE[index]; - decreaseNow = false; - } else { - decreaseNow = true; - } - } else if (previousReceiveBufferSize >= nextReceiveBufferSize) { - index = Math.min(index + INDEX_INCREMENT, maxIndex); - nextReceiveBufferSize = SIZE_TABLE[index]; - decreaseNow = false; - } - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/buffer/FileRegion.java b/firefly-nettool/src/main/java/com/firefly/net/buffer/FileRegion.java deleted file mode 100644 index a570ade83..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/buffer/FileRegion.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.firefly.net.buffer; - -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.nio.channels.WritableByteChannel; - -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class FileRegion { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private final FileChannel file; - private final RandomAccessFile raf; - private final long position; - private final long count; - - public FileRegion(RandomAccessFile raf, long position, long count) { - this.raf = raf; - this.file = raf.getChannel(); - this.position = position; - this.count = count; - } - - public long getPosition() { - return position; - } - - public long getCount() { - return count; - } - - public long transferTo(WritableByteChannel target, long position) - throws IOException { - long count = this.count - position; - if (count < 0 || position < 0) { - throw new IllegalArgumentException("position out of range: " - + position + " (expected: 0 - " + (this.count - 1) + ")"); - } - if (count == 0) { - return 0L; - } - - return file.transferTo(this.position + position, count, target); - } - - public void releaseExternalResources() { - try { - log.debug("FileChannel close"); - file.close(); - raf.close(); - } catch (IOException e) { - log.error("Failed to close a file.", e); - } - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/buffer/FixedReceiveBufferSizePredictor.java b/firefly-nettool/src/main/java/com/firefly/net/buffer/FixedReceiveBufferSizePredictor.java deleted file mode 100644 index 9911904f6..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/buffer/FixedReceiveBufferSizePredictor.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.firefly.net.buffer; - -import com.firefly.net.ReceiveBufferSizePredictor; - - -public class FixedReceiveBufferSizePredictor implements - ReceiveBufferSizePredictor { - - private final int bufferSize; - - public FixedReceiveBufferSizePredictor(int bufferSize) { - if (bufferSize <= 0) { - throw new IllegalArgumentException( - "bufferSize must greater than 0: " + bufferSize); - } - this.bufferSize = bufferSize; - } - - @Override - public int nextReceiveBufferSize() { - return bufferSize; - } - - @Override - public void previousReceiveBufferSize(int previousReceiveBufferSize) { - // Ignore - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/buffer/SocketReceiveBufferPool.java b/firefly-nettool/src/main/java/com/firefly/net/buffer/SocketReceiveBufferPool.java deleted file mode 100644 index 2d05e0b99..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/buffer/SocketReceiveBufferPool.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.firefly.net.buffer; - -import java.lang.ref.SoftReference; -import java.nio.ByteBuffer; - -import com.firefly.net.ReceiveBufferPool; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class SocketReceiveBufferPool implements ReceiveBufferPool { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - private static final int POOL_SIZE = 8; - - @SuppressWarnings("unchecked") - private final SoftReference[] pool = new SoftReference[POOL_SIZE]; - - public final ByteBuffer acquire(int size) { - final SoftReference[] pool = this.pool; - for (int i = 0; i < POOL_SIZE; i++) { - SoftReference ref = pool[i]; - if (ref == null) { - continue; - } - - ByteBuffer buf = ref.get(); - if (buf == null) { - pool[i] = null; - continue; - } - - if (buf.capacity() < size) { - continue; - } - - pool[i] = null; - - buf.clear(); - return buf; - } - - int allocateSize = normalizeCapacity(size); - log.debug("acquire read size: {}", allocateSize); - - ByteBuffer buf = ByteBuffer.allocateDirect(allocateSize); - buf.clear(); - return buf; - } - - public final void release(ByteBuffer buffer) { - final SoftReference[] pool = this.pool; - for (int i = 0; i < POOL_SIZE; i++) { - SoftReference ref = pool[i]; - if (ref == null || ref.get() == null) { - pool[i] = new SoftReference(buffer); - return; - } - } - - // pool is full - replace one - final int capacity = buffer.capacity(); - for (int i = 0; i < POOL_SIZE; i++) { - SoftReference ref = pool[i]; - ByteBuffer pooled = ref.get(); - if (pooled == null) { - pool[i] = null; - continue; - } - - if (pooled.capacity() < capacity) { - pool[i] = new SoftReference(buffer); - return; - } - } - } - - /** - * 把容量变成1024的倍数 - * @param capacity - * @return - */ - public static final int normalizeCapacity(int capacity) { - int q = capacity >>> 10; - int r = capacity & 1023; - if (r != 0) { - q++; - } - return q << 10; - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/buffer/SocketSendBufferPool.java b/firefly-nettool/src/main/java/com/firefly/net/buffer/SocketSendBufferPool.java deleted file mode 100644 index 768729509..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/buffer/SocketSendBufferPool.java +++ /dev/null @@ -1,335 +0,0 @@ -package com.firefly.net.buffer; - -import java.io.IOException; -import java.lang.ref.SoftReference; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.WritableByteChannel; -import com.firefly.net.SendBufferPool; - -public final class SocketSendBufferPool implements SendBufferPool { - private static final SendBuffer EMPTY_BUFFER = new EmptySendBuffer(); - - private static final int DEFAULT_PREALLOCATION_SIZE = 65536; - private static final int ALIGN_SHIFT = 4; - private static final int ALIGN_MASK = 15; - - PreallocationRef poolHead = null; - Preallocation current = new Preallocation(DEFAULT_PREALLOCATION_SIZE); - - public SocketSendBufferPool() { - super(); - } - - @Override - public SendBuffer acquire(Object src) { - if (src instanceof ByteBuffer) { - return acquire((ByteBuffer) src); - } else if (src instanceof FileRegion) { - return acquire((FileRegion) src); - } - - throw new IllegalArgumentException("unsupported type: " - + src.getClass()); - } - - private final SendBuffer acquire(FileRegion src) { - if (src.getCount() == 0) { - return EMPTY_BUFFER; - } - return new FileSendBuffer(src); - } - - private final SendBuffer acquire(ByteBuffer src) { - final int size = src.remaining(); - if (size == 0) { - return EMPTY_BUFFER; - } - - if (src.isDirect()) { - return new UnpooledSendBuffer(src); - } - if (src.remaining() > DEFAULT_PREALLOCATION_SIZE) { - return new UnpooledSendBuffer(src); - } - - Preallocation current = this.current; - ByteBuffer buffer = current.buffer; - int remaining = buffer.remaining(); - PooledSendBuffer dst; - - if (size < remaining) { - int nextPos = buffer.position() + size; - ByteBuffer slice = buffer.duplicate(); - buffer.position(align(nextPos)); - slice.limit(nextPos); - current.refCnt++; - dst = new PooledSendBuffer(current, slice); - } else if (size > remaining) { - this.current = current = getPreallocation(); - buffer = current.buffer; - ByteBuffer slice = buffer.duplicate(); - buffer.position(align(size)); - slice.limit(size); - current.refCnt++; - dst = new PooledSendBuffer(current, slice); - } else { // size == remaining - current.refCnt++; - this.current = getPreallocation0(); - dst = new PooledSendBuffer(current, current.buffer); - } - - ByteBuffer dstbuf = dst.buffer; - dstbuf.mark(); - // src.getBytes(src.readerIndex(), dstbuf); - dstbuf.put(src.array(), src.position(), Math.min(src.remaining(), - dstbuf.remaining())); - dstbuf.reset(); - return dst; - } - - private final Preallocation getPreallocation() { - Preallocation current = this.current; - if (current.refCnt == 0) { - current.buffer.clear(); - return current; - } - - return getPreallocation0(); - } - - private final Preallocation getPreallocation0() { - PreallocationRef ref = poolHead; - if (ref != null) { - do { - Preallocation p = ref.get(); - ref = ref.next; - - if (p != null) { - poolHead = ref; - return p; - } - } while (ref != null); - - poolHead = ref; - } - - return new Preallocation(DEFAULT_PREALLOCATION_SIZE); - } - - private static final int align(int pos) { - int q = pos >>> ALIGN_SHIFT; - int r = pos & ALIGN_MASK; - if (r != 0) { - q++; - } - return q << ALIGN_SHIFT; - } - - private final class Preallocation { - final ByteBuffer buffer; - int refCnt; - - Preallocation(int capacity) { - buffer = ByteBuffer.allocateDirect(capacity); - } - } - - private final class PreallocationRef extends SoftReference { - final PreallocationRef next; - - PreallocationRef(Preallocation prealloation, PreallocationRef next) { - super(prealloation); - this.next = next; - } - } - - public interface SendBuffer { - boolean finished(); - - long writtenBytes(); - - long totalBytes(); - - long transferTo(WritableByteChannel ch) throws IOException; - - long transferTo(DatagramChannel ch, SocketAddress raddr) - throws IOException; - - void release(); - } - - class UnpooledSendBuffer implements SendBuffer { - - final ByteBuffer buffer; - final int initialPos; - - UnpooledSendBuffer(ByteBuffer buffer) { - this.buffer = buffer; - initialPos = buffer.position(); - } - - @Override - public final boolean finished() { - return !buffer.hasRemaining(); - } - - @Override - public final long writtenBytes() { - return buffer.position() - initialPos; - } - - @Override - public final long totalBytes() { - return buffer.limit() - initialPos; - } - - @Override - public final long transferTo(WritableByteChannel ch) throws IOException { - return ch.write(buffer); - } - - @Override - public final long transferTo(DatagramChannel ch, SocketAddress raddr) - throws IOException { - return ch.send(buffer, raddr); - } - - @Override - public void release() { - // Unpooled. - } - } - - final class PooledSendBuffer implements SendBuffer { - - private final Preallocation parent; - final ByteBuffer buffer; - final int initialPos; - - PooledSendBuffer(Preallocation parent, ByteBuffer buffer) { - this.parent = parent; - this.buffer = buffer; - initialPos = buffer.position(); - } - - @Override - public boolean finished() { - return !buffer.hasRemaining(); - } - - @Override - public long writtenBytes() { - return buffer.position() - initialPos; - } - - @Override - public long totalBytes() { - return buffer.limit() - initialPos; - } - - @Override - public long transferTo(WritableByteChannel ch) throws IOException { - return ch.write(buffer); - } - - @Override - public long transferTo(DatagramChannel ch, SocketAddress raddr) - throws IOException { - return ch.send(buffer, raddr); - } - - @Override - public void release() { - final Preallocation parent = this.parent; - if (--parent.refCnt == 0) { - parent.buffer.clear(); - if (parent != current) { - poolHead = new PreallocationRef(parent, poolHead); - } - } - } - } - - static final class EmptySendBuffer implements SendBuffer { - - EmptySendBuffer() { - super(); - } - - @Override - public final boolean finished() { - return true; - } - - @Override - public final long writtenBytes() { - return 0; - } - - @Override - public final long totalBytes() { - return 0; - } - - @Override - public final long transferTo(WritableByteChannel ch) throws IOException { - return 0; - } - - @Override - public final long transferTo(DatagramChannel ch, SocketAddress raddr) - throws IOException { - return 0; - } - - @Override - public void release() { - // Unpooled. - } - } - - final class FileSendBuffer implements SendBuffer { - - private final FileRegion file; - private long writtenBytes; - - FileSendBuffer(FileRegion file) { - this.file = file; - } - - @Override - public boolean finished() { - return writtenBytes >= file.getCount(); - } - - @Override - public long writtenBytes() { - return writtenBytes; - } - - @Override - public long totalBytes() { - return file.getCount(); - } - - @Override - public long transferTo(WritableByteChannel ch) throws IOException { - long localWrittenBytes = file.transferTo(ch, writtenBytes); - writtenBytes += localWrittenBytes; - return localWrittenBytes; - } - - @Override - public long transferTo(DatagramChannel ch, SocketAddress raddr) - throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public void release() { - file.releaseExternalResources(); - } - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/event/CurrentThreadEventManager.java b/firefly-nettool/src/main/java/com/firefly/net/event/CurrentThreadEventManager.java deleted file mode 100644 index 49469b47b..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/event/CurrentThreadEventManager.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.firefly.net.event; - -import com.firefly.net.Config; -import com.firefly.net.EventManager; -import com.firefly.net.Session; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -/** - * 使用worker线程执行事件,用于客户端操作 - * @author qiupengtao - * - */ -public class CurrentThreadEventManager implements EventManager { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private Config config; - - public CurrentThreadEventManager(Config config) { - log.info("CurrentThreadEventManager"); - this.config = config; - } - - @Override - public void executeCloseTask(Session session) { - try { - config.getHandler().sessionClosed(session); - } catch (Throwable t) { - executeExceptionTask(session, t); - } - } - - @Override - public void executeExceptionTask(Session session, Throwable t) { - try { - config.getHandler().exceptionCaught(session, t); - } catch (Throwable t0) { - log.error("handler exception", t0); - } - - } - - @Override - public void executeOpenTask(Session session) { - try { - config.getHandler().sessionOpened(session); - } catch (Throwable t) { - executeExceptionTask(session, t); - } - } - - @Override - public void executeReceiveTask(Session session, Object message) { - try { - log.debug("CurrentThreadEventManager"); - config.getHandler().messageRecieved(session, message); - } catch (Throwable t) { - executeExceptionTask(session, t); - } - } - - @Override - public void shutdown() { - // TODO Auto-generated method stub - - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/event/ThreadPoolEventManager.java b/firefly-nettool/src/main/java/com/firefly/net/event/ThreadPoolEventManager.java deleted file mode 100644 index e065e95a9..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/event/ThreadPoolEventManager.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.firefly.net.event; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.firefly.net.Config; -import com.firefly.net.EventManager; -import com.firefly.net.Session; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -/** - * 线程池事件管理,无法保证响应的顺序,多用于服务端短连接 - * @author qiupengtao - * - */ -public class ThreadPoolEventManager implements EventManager { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private ExecutorService executorService; - private Config config; - - public ThreadPoolEventManager(Config config) { - this.config = config; - if (config.getHandleThreads() > 0) { - log.info("FixedThreadPool: {}", config.getHandleThreads()); - executorService = Executors.newFixedThreadPool(config - .getHandleThreads()); - } else if (config.getHandleThreads() == 0) { - log.info("CachedThreadPool"); - executorService = Executors.newCachedThreadPool(); - } - } - - public void shutdown() { - executorService.shutdown(); - log.debug("executorService is shutdown: {}", executorService.isShutdown()); - } - - public void executeOpenTask(Session session) { - executorService.submit(new OpenTask(session)); - } - - public void executeReceiveTask(Session session, Object message) { - executorService.submit(new ReceiveTask(session, message)); - } - - public void executeCloseTask(Session session) { - executorService.submit(new CloseTask(session)); - } - - public void executeExceptionTask(Session session, Throwable t) { - executorService.submit(new ExceptionTask(session,t)); - } - - private class OpenTask implements Runnable { - private Session session; - - private OpenTask(Session session) { - this.session = session; - } - - @Override - public void run() { - try { - config.getHandler().sessionOpened(session); - } catch (Throwable t) { - ThreadPoolEventManager.this.executeExceptionTask(session, t); - } - } - - } - - private class ReceiveTask implements Runnable { - private Session session; - private Object message; - - private ReceiveTask(Session session, Object message) { - this.session = session; - this.message = message; - } - - @Override - public void run() { - try { - log.debug("thread pool event"); - config.getHandler().messageRecieved(session, message); - } catch (Throwable t) { - ThreadPoolEventManager.this.executeExceptionTask(session, t); - } - } - } - - private class CloseTask implements Runnable { - private Session session; - - private CloseTask(Session session) { - this.session = session; - } - - @Override - public void run() { - try { - config.getHandler().sessionClosed(session); - } catch (Throwable t) { - ThreadPoolEventManager.this.executeExceptionTask(session, t); - } - } - } - - private class ExceptionTask implements Runnable { - private Session session; - private Throwable t; - - private ExceptionTask(Session session, Throwable t) { - this.session = session; - this.t = t; - } - - @Override - public void run() { - try { - config.getHandler().exceptionCaught(session, t); - } catch (Throwable t) { - log.error("handler exception", t); - } - } - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/exception/NetException.java b/firefly-nettool/src/main/java/com/firefly/net/exception/NetException.java deleted file mode 100644 index c1362acad..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/exception/NetException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.net.exception; - -public class NetException extends RuntimeException { - - private static final long serialVersionUID = 5751160039001031850L; - - public NetException(String msg) { - super(msg); - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/MessageReceiveCallBack.java b/firefly-nettool/src/main/java/com/firefly/net/support/MessageReceiveCallBack.java deleted file mode 100644 index 51f4c3aad..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/MessageReceiveCallBack.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.net.support; - -import com.firefly.net.Session; - -public interface MessageReceiveCallBack { - void messageRecieved(Session session, Object obj); -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/SimpleTcpClient.java b/firefly-nettool/src/main/java/com/firefly/net/support/SimpleTcpClient.java deleted file mode 100644 index 93d8c0ae5..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/SimpleTcpClient.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.firefly.net.support; - -import com.firefly.net.Client; -import com.firefly.net.Decoder; -import com.firefly.net.Encoder; -import com.firefly.net.Handler; -import com.firefly.net.Session; -import com.firefly.net.tcp.TcpClient; - -public class SimpleTcpClient { - private String host; - private int port; - - private Synchronizer synchronizer = new Synchronizer(); - private Client client; - - public SimpleTcpClient(String host, int port, Decoder decoder, - Encoder encoder, Handler handler) { - this.host = host; - this.port = port; - client = new TcpClient(decoder, encoder, - handler == null ? new SimpleTcpClientHandler(synchronizer) - : handler); - } - - public SimpleTcpClient(String host, int port, Decoder decoder, - Encoder encoder) { - this(host, port, decoder, encoder, null); - } - - public TcpConnection connect() { - return connect(0); - } - - public TcpConnection connect(long timeout) { - int id = client.connect(host, port); - TcpConnection ret = new TcpConnection(synchronizer.get(id), timeout); - return ret; - } - - public void shutdown() { - client.shutdown(); - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/SimpleTcpClientHandler.java b/firefly-nettool/src/main/java/com/firefly/net/support/SimpleTcpClientHandler.java deleted file mode 100644 index c36f0912f..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/SimpleTcpClientHandler.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.firefly.net.support; - -import java.util.Queue; - -import com.firefly.net.Handler; -import com.firefly.net.Session; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class SimpleTcpClientHandler implements Handler { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private Synchronizer synchronizer; - - public SimpleTcpClientHandler(Synchronizer synchronizer) { - this.synchronizer = synchronizer; - } - - @Override - public void sessionOpened(Session session) throws Throwable { - log.debug("session: {} open", session.getSessionId()); - synchronizer.put(session.getSessionId(), session); - } - - @Override - public void sessionClosed(Session session) throws Throwable { - log.debug("session: {} close", session.getSessionId()); - } - - @SuppressWarnings("unchecked") - @Override - public void messageRecieved(Session session, Object message) throws Throwable { - log.debug("message: {}", message); - Queue queue = (Queue)session.getAttribute(TcpConnection.QUEUE_KEY); - queue.poll().messageRecieved(session, message); - } - - @Override - public void exceptionCaught(Session session, Throwable t) throws Throwable { - log.error("client session error", t); - session.close(true); - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/StringLineDecoder.java b/firefly-nettool/src/main/java/com/firefly/net/support/StringLineDecoder.java deleted file mode 100644 index e89292f85..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/StringLineDecoder.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.firefly.net.support; - -import java.nio.ByteBuffer; - -import com.firefly.net.Decoder; -import com.firefly.net.Session; - -public class StringLineDecoder implements Decoder { - private static final byte LINE_LIMITOR = '\n'; - - @Override - public void decode(ByteBuffer buffer, Session session) throws Throwable { - ByteBuffer now = buffer; - ByteBuffer prev = (ByteBuffer) session.getAttribute("buff"); - - if (prev != null) { - session.removeAttribute("buff"); - now = (ByteBuffer) ByteBuffer - .allocate(prev.remaining() + buffer.remaining()).put(prev) - .put(buffer).flip(); - } - - int dataLen = now.position() + now.remaining(); - - for (int i = now.position(), p = i; i < dataLen; i++) { - if (now.get(i) == LINE_LIMITOR) { - byte[] data = new byte[i - p + 1]; - now.get(data); - String line = new String(data).trim(); - p = i + 1; - session.fireReceiveMessage(line); - } - } - - if (now.hasRemaining()) - session.setAttribute("buff", now); - } - - public static void main(String[] args) { - ByteBuffer buf = ByteBuffer.allocate(16); - buf.putInt(1); - buf.putInt(2); - buf.putInt(3); - buf.putInt(4); - buf.flip(); - System.out.println(buf.getInt() + "|" + buf.getInt() + "\t" + buf.position() + "|" + buf.remaining()); - ByteBuffer buf2 = buf.slice(); - System.out.println(buf2.position() + "|" + buf2.remaining()); - System.out.println(buf2.getInt()); - System.out.println(buf2.getInt()); - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/StringLineEncoder.java b/firefly-nettool/src/main/java/com/firefly/net/support/StringLineEncoder.java deleted file mode 100644 index 1eb708929..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/StringLineEncoder.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.firefly.net.support; - -import java.nio.ByteBuffer; - -import com.firefly.net.Encoder; -import com.firefly.net.Session; - -public class StringLineEncoder implements Encoder { - - private static final String LINE_LIMITOR = System.getProperty("line.separator"); - - @Override - public void encode(Object message, Session session) throws Throwable { - String str = message + LINE_LIMITOR; - - ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes()); - session.write(byteBuffer); - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/Synchronizer.java b/firefly-nettool/src/main/java/com/firefly/net/support/Synchronizer.java deleted file mode 100644 index 004ef7503..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/Synchronizer.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.firefly.net.support; - -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class Synchronizer { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private SynchronousObject[] objs; - private final long timeout; - private int size; - - public Synchronizer() { - this(0, 0); - } - - public Synchronizer(int size, long timeout) { - if (size <= 0) { - this.size = 1024 * 4; - } else { - int i = 2; - while (i < size) - i <<= 1; - - this.size = i; - } - log.info("synchronizer size: {}", this.size); - this.timeout = timeout > 0 ? timeout : 5000; - init(); - log.debug("client timeout {}", timeout); - } - - public T get(int index) { - log.debug("get index {}", index); - return objs[index & (size - 1)].get(timeout); - } - - public void put(int index, T t) { - log.debug("put index {}", index); - objs[index & (size - 1)].put(t, timeout); - } - - @SuppressWarnings("unchecked") - public void init() { - objs = new SynchronousObject[size]; - - for (int i = 0; i < objs.length; i++) { - objs[i] = new SynchronousObject(); - } - - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/SynchronousObject.java b/firefly-nettool/src/main/java/com/firefly/net/support/SynchronousObject.java deleted file mode 100644 index af6255ddc..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/SynchronousObject.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.firefly.net.support; - -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.TimeUnit; - -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class SynchronousObject { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private SynchronousQueue queue = new SynchronousQueue(); - - public void put(T obj, long timeout) { - try { - queue.offer(obj, timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - log.error("put synchronous obj error", e); - } - } - - public T get(long timeout) { - T t = null; - try { - t = queue.poll(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - log.error("get synchronous obj error", e); - } - return t; - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/support/TcpConnection.java b/firefly-nettool/src/main/java/com/firefly/net/support/TcpConnection.java deleted file mode 100644 index 1e9caeafc..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/support/TcpConnection.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.firefly.net.support; - -import java.util.concurrent.BlockingQueue; - -import com.firefly.net.Session; -import com.firefly.utils.collection.LinkedTransferQueue; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class TcpConnection { - private Session session; - private long timeout; - private BlockingQueue queue; - public static final String QUEUE_KEY = "#message_queue"; - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public TcpConnection(Session session) { - this(session, 0); - } - - public TcpConnection(Session session, long timeout) { - this.queue = new LinkedTransferQueue(); - this.session = session; - this.session.setAttribute(QUEUE_KEY, queue); - this.timeout = timeout > 0 ? timeout : 5000L; - } - - public Object send(Object obj) { - final SynchronousObject ret = new SynchronousObject(); - send(obj, new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - ret.put(obj, timeout); - } - }); - - return ret.get(timeout); - } - - public void send(Object obj, MessageReceiveCallBack callback) { - if (queue.offer(callback)) - session.encode(obj); - else - log.warn("tcp connection queue offer failure!"); - } - - public int getId() { - return session.getSessionId(); - } - - public void close(boolean b) { - session.close(b); - } - - public boolean isOpen() { - return session.isOpen(); - } - - public Session getSession() { - return session; - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpClient.java b/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpClient.java deleted file mode 100644 index b24743a1e..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpClient.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.firefly.net.tcp; - -import com.firefly.net.*; -import com.firefly.net.event.CurrentThreadEventManager; -import com.firefly.net.event.ThreadPoolEventManager; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.time.Millisecond100Clock; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.channels.SocketChannel; -import java.util.concurrent.atomic.AtomicInteger; - -public class TcpClient implements Client { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private Config config; - private Worker[] workers; - private AtomicInteger sessionId = new AtomicInteger(0); - private volatile boolean started = false; - - public TcpClient() { - } - - public TcpClient(Decoder decoder, Encoder encoder, Handler handler) { - this(); - config = new Config(); - config.setDecoder(decoder); - config.setEncoder(encoder); - config.setHandler(handler); - } - - private synchronized Client init() { - if (started) - return this; - - if (config == null) - throw new IllegalArgumentException("init error config is null"); - - EventManager eventManager = null; - if (config.getHandleThreads() >= 0) { - eventManager = new ThreadPoolEventManager(config); - } else { - eventManager = new CurrentThreadEventManager(config); - } - - log.info("client worker num: {}", config.getWorkerThreads()); - workers = new Worker[config.getWorkerThreads()]; - for (int i = 0; i < config.getWorkerThreads(); i++) - workers[i] = new TcpWorker(config, i, eventManager); - - started = true; - return this; - } - - @Override - public void setConfig(Config config) { - this.config = config; - } - - @Override - public int connect(String host, int port) { - int id = sessionId.getAndIncrement(); - connect(host, port, id); - return id; - } - - @Override - public void connect(String host, int port, int id) { - if (!started) - init(); - try { - SocketChannel socketChannel = SocketChannel.open(); - socketChannel.socket().connect(new InetSocketAddress(host, port), config.getTimeout()); - accept(socketChannel, id); - } catch (IOException e) { - log.error("connect error", e); - } - } - - private void accept(SocketChannel socketChannel, int sessionId) { - try { - int workerIndex = Math.abs(sessionId) % workers.length; - log.debug("accept sessionId [{}] and worker index [{}]", - sessionId, workerIndex); - workers[workerIndex].registerSelectableChannel(socketChannel, - sessionId); - } catch (Exception e) { - log.error("Failed to initialize an accepted socket.", e); - try { - socketChannel.close(); - } catch (IOException e1) { - log.error("Failed to close a partially accepted socket.", - e1); - } - } - } - - @Override - public void shutdown() { - for(Worker worker : workers) - worker.shutdown(); - - Millisecond100Clock.stop(); - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpPerformanceParameter.java b/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpPerformanceParameter.java deleted file mode 100644 index 13e3246ad..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpPerformanceParameter.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.net.tcp; - -public interface TcpPerformanceParameter { - int CONNECTION_TIME = 1; - int LATENCY = 2; - int BANDWIDTH = 0; - int BACKLOG = 1024 * 16; - - int CLEANUP_INTERVAL = 256; - int WRITE_SPIN_COUNT = 16; - int WRITE_BUFFER_HIGH_WATER_MARK = 64 * 1024; - int WRITE_BUFFER_LOW_WATER_MARK = 32 * 1024; -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpServer.java b/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpServer.java deleted file mode 100644 index 95435f36f..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpServer.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.firefly.net.tcp; - -import static com.firefly.net.tcp.TcpPerformanceParameter.BACKLOG; -import static com.firefly.net.tcp.TcpPerformanceParameter.BANDWIDTH; -import static com.firefly.net.tcp.TcpPerformanceParameter.CONNECTION_TIME; -import static com.firefly.net.tcp.TcpPerformanceParameter.LATENCY; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; - -import com.firefly.net.Config; -import com.firefly.net.Decoder; -import com.firefly.net.Encoder; -import com.firefly.net.EventManager; -import com.firefly.net.Handler; -import com.firefly.net.Server; -import com.firefly.net.Worker; -import com.firefly.net.event.CurrentThreadEventManager; -import com.firefly.net.event.ThreadPoolEventManager; -import com.firefly.net.exception.NetException; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.time.Millisecond100Clock; - -public class TcpServer implements Server { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private Config config; - private Worker[] workers; - private Thread bossThread; - private boolean start; - - public TcpServer() { - - } - - public TcpServer(Decoder decoder, Encoder encoder, Handler handler) { - config = new Config(); - config.setDecoder(decoder); - config.setEncoder(encoder); - config.setHandler(handler); - } - - @Override - public void setConfig(Config config) { - this.config = config; - } - - @Override - public void start(String host, int port) { - if (config == null) - throw new NetException("server config is null"); - log.debug(config.toString()); - listen(bind(host, port)); - } - - private ServerSocketChannel bind(String host, int port) { - ServerSocketChannel serverSocketChannel = null; - try { - serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.configureBlocking(false); - serverSocketChannel.socket().setReuseAddress(true); - serverSocketChannel.socket().setPerformancePreferences( - CONNECTION_TIME, LATENCY, BANDWIDTH); - - log.debug("ServerSocket receiveBufferSize: [{}]", - serverSocketChannel.socket().getReceiveBufferSize()); - - serverSocketChannel.socket().bind( - new InetSocketAddress(host, port), BACKLOG); - - } catch (Exception e) { - log.error("ServerSocket bind error", e); - } - return serverSocketChannel; - } - - private void listen(ServerSocketChannel serverSocketChannel) { - EventManager eventManager = null; - if (config.getHandleThreads() >= 0) { - eventManager = new ThreadPoolEventManager(config); - } else { - eventManager = new CurrentThreadEventManager(config); - } - - log.info("server worker num: {}", config.getWorkerThreads()); - workers = new Worker[config.getWorkerThreads()]; - for (int i = 0; i < config.getWorkerThreads(); i++) - workers[i] = new TcpWorker(config, i, eventManager); - - Boss boss = null; - try { - boss = new Boss(serverSocketChannel); - } catch (IOException e) { - log.error("Boss create error", e); - } - bossThread = new Thread(boss, config.getServerName()); - start = true; - bossThread.start(); - } - - private final class Boss implements Runnable { - private final Selector selector; - private final ServerSocketChannel serverSocketChannel; - - public Boss(ServerSocketChannel serverSocketChannel) throws IOException { - selector = Selector.open(); - this.serverSocketChannel = serverSocketChannel; - this.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - } - - @Override - public void run() { - - int sessionId = 0; - try { - while (start) { - try { - if (selector.select(1000) > 0) - selector.selectedKeys().clear(); - - SocketChannel socketChannel = serverSocketChannel - .accept(); - if (socketChannel != null) { - accept(socketChannel, sessionId); - sessionId++; - } - } catch (ClosedChannelException e) { - // Closed as requested. - break; - } catch (Throwable e) { - log.error("Failed to accept a connection.", e); - } - } - } finally { - try { - selector.close(); - } catch (Exception e) { - log.error("Failed to close a selector.", e); - } - } - - } - - public void accept(SocketChannel socketChannel, int sessionId) { - try { - int workerIndex = Math.abs(sessionId) % workers.length; - log.debug("accept sessionId [{}] and worker index [{}]", - sessionId, workerIndex); - workers[workerIndex].registerSelectableChannel(socketChannel, - sessionId); - } catch (Exception e) { - log.error("Failed to initialize an accepted socket.", e); - try { - socketChannel.close(); - } catch (IOException e1) { - log.error("Failed to close a partially accepted socket.", - e1); - } - } - } - - } - - @Override - public void shutdown() { - for (Worker worker : workers) { - worker.shutdown(); - } - start = false; - Millisecond100Clock.stop(); - log.debug("thread {} is shutdown: {}", bossThread.getName(), - bossThread.isInterrupted()); - } - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpSession.java b/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpSession.java deleted file mode 100644 index eefb9d875..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpSession.java +++ /dev/null @@ -1,426 +0,0 @@ -package com.firefly.net.tcp; - -import static com.firefly.net.tcp.TcpPerformanceParameter.WRITE_BUFFER_HIGH_WATER_MARK; -import static com.firefly.net.tcp.TcpPerformanceParameter.WRITE_BUFFER_LOW_WATER_MARK; - -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.util.HashMap; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import com.firefly.net.Config; -import com.firefly.net.EventManager; -import com.firefly.net.ReceiveBufferSizePredictor; -import com.firefly.net.Session; -import com.firefly.net.buffer.AdaptiveReceiveBufferSizePredictor; -import com.firefly.net.buffer.FixedReceiveBufferSizePredictor; -import com.firefly.net.buffer.SocketSendBufferPool.SendBuffer; -import com.firefly.utils.ThreadLocalBoolean; -import com.firefly.utils.collection.LinkedTransferQueue; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public final class TcpSession implements Session { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private final int sessionId; - private final SelectionKey selectionKey; - private long openTime, lastReadTime, lastWrittenTime, readBytes, writtenBytes; - private final TcpWorker worker; - private final Config config; - private final Map map = new HashMap(); - private final Runnable writeTask = new WriteTask(); - private final AtomicInteger writeBufferSize = new AtomicInteger(); - private final AtomicInteger highWaterMarkCounter = new AtomicInteger(); - private final AtomicBoolean writeTaskInTaskQueue = new AtomicBoolean(); - private InetSocketAddress localAddress; - private volatile InetSocketAddress remoteAddress; - private volatile int interestOps = SelectionKey.OP_READ; - private boolean inWriteNowLoop; - private boolean writeSuspended; - private final Object interestOpsLock = new Object(); - private final Object writeLock = new Object(); - private final Queue writeBuffer = new WriteRequestQueue(); - private Object currentWrite; - private SendBuffer currentWriteBuffer; - private volatile int state; - private ReceiveBufferSizePredictor receiveBufferSizePredictor; - private EventManager eventManager; - - public TcpSession(int sessionId, TcpWorker worker, Config config, - long openTime, SelectionKey selectionKey, EventManager eventManager) { - super(); - this.sessionId = sessionId; - this.worker = worker; - this.config = config; - this.openTime = openTime; - this.selectionKey = selectionKey; - this.eventManager = eventManager; - if (config.getReceiveByteBufferSize() > 0) { - log.debug("fix buffer size: {}", config.getReceiveByteBufferSize()); - receiveBufferSizePredictor = new FixedReceiveBufferSizePredictor( - config.getReceiveByteBufferSize()); - } else { - log.debug("adaptive buffer size"); - receiveBufferSizePredictor = new AdaptiveReceiveBufferSizePredictor(); - } - state = OPEN; - } - - public InetSocketAddress getLocalAddress() { - if (localAddress == null) { - SocketChannel socket = (SocketChannel) selectionKey.channel(); - try { - localAddress = (InetSocketAddress) socket.socket() - .getLocalSocketAddress(); - - } catch (Throwable t) { - log.error("get localAddress error", t); - } - } - return localAddress; - } - - public InetSocketAddress getRemoteAddress() { - if (remoteAddress == null) { - SocketChannel socket = (SocketChannel) selectionKey.channel(); - try { - remoteAddress = (InetSocketAddress) socket.socket() - .getRemoteSocketAddress(); - } catch (Throwable t) { - log.error("get remoteAddress error", t); - } - } - return remoteAddress; - } - - ReceiveBufferSizePredictor getReceiveBufferSizePredictor() { - return receiveBufferSizePredictor; - } - - AtomicBoolean getWriteTaskInTaskQueue() { - return writeTaskInTaskQueue; - } - - public int getState() { - return state; - } - - void setState(int state) { - this.state = state; - } - - @Override - public boolean isOpen() { - return state > 0; - } - - SendBuffer getCurrentWriteBuffer() { - return currentWriteBuffer; - } - - void setCurrentWriteBuffer(SendBuffer currentWriteBuffer) { - this.currentWriteBuffer = currentWriteBuffer; - } - - void setCurrentWrite(Object currentWrite) { - this.currentWrite = currentWrite; - } - - void resetCurrentWriteAndWriteBuffer() { - this.currentWrite = null; - this.currentWriteBuffer = null; - } - - Object getCurrentWrite() { - return currentWrite; - } - - Queue getWriteBuffer() { - return writeBuffer; - } - - Runnable getWriteTask() { - return writeTask; - } - - SelectionKey getSelectionKey() { - return selectionKey; - } - - Object getInterestOpsLock() { - return interestOpsLock; - } - - Object getWriteLock() { - return writeLock; - } - - boolean isInWriteNowLoop() { - return inWriteNowLoop; - } - - void setInWriteNowLoop(boolean inWriteNowLoop) { - this.inWriteNowLoop = inWriteNowLoop; - } - - boolean isWriteSuspended() { - return writeSuspended; - } - - void setWriteSuspended(boolean writeSuspended) { - this.writeSuspended = writeSuspended; - } - - @Override - public long getOpenTime() { - return openTime; - } - - @Override - public int getSessionId() { - return sessionId; - } - - @Override - public void setAttribute(String key, Object value) { - map.put(key, value); - } - - @Override - public Object getAttribute(String key) { - return map.get(key); - } - - @Override - public void removeAttribute(String key) { - map.remove(key); - } - - @Override - public void clearAttributes() { - map.clear(); - } - - @Override - public void fireReceiveMessage(Object message) { - worker.getEventManager().executeReceiveTask(this, message); - } - - @Override - public void encode(Object message) { - try { - config.getEncoder().encode(message, this); - } catch (Throwable t) { - eventManager.executeExceptionTask(this, t); - } - } - - @Override - public void write(Object object) { - boolean offered = writeBuffer.offer(object); - assert offered; - worker.writeFromUserCode(this); - } - - int getRawInterestOps() { - return interestOps; - } - - void setInterestOpsNow(int interestOps) { - this.interestOps = interestOps; - } - - @Override - public void close(boolean immediately) { - if (immediately) - worker.close(selectionKey); - else - write(CLOSE_FLAG); - } - - private final class WriteTask implements Runnable { - - WriteTask() { - super(); - } - - @Override - public void run() { - writeTaskInTaskQueue.set(false); - worker.writeFromTaskLoop(TcpSession.this); - } - } - - private final class WriteRequestQueue extends LinkedTransferQueue { - private static final long serialVersionUID = -2493148252918843163L; - private final ThreadLocalBoolean notifying = new ThreadLocalBoolean(); - - private WriteRequestQueue() { - super(); - } - - @Override - public boolean offer(Object object) { - boolean success = super.offer(object); - assert success; - - int messageSize = getMessageSize(object); - int newWriteBufferSize = writeBufferSize.addAndGet(messageSize); - int highWaterMark = WRITE_BUFFER_HIGH_WATER_MARK; - - if (newWriteBufferSize >= highWaterMark) { - if (newWriteBufferSize - messageSize < highWaterMark) { - highWaterMarkCounter.incrementAndGet(); - if (!notifying.get()) { - notifying.set(Boolean.TRUE); - worker.setInterestOps(TcpSession.this, - SelectionKey.OP_READ); - notifying.set(Boolean.FALSE); - } - } - } - return true; - } - - @Override - public Object poll() { - Object object = super.poll(); - if (object != null) { - int messageSize = getMessageSize(object); - int newWriteBufferSize = writeBufferSize - .addAndGet(-messageSize); - int lowWaterMark = WRITE_BUFFER_LOW_WATER_MARK; - - if (newWriteBufferSize == 0 - || newWriteBufferSize < lowWaterMark) { - if (newWriteBufferSize + messageSize >= lowWaterMark) { - highWaterMarkCounter.decrementAndGet(); - if (!notifying.get()) { - notifying.set(Boolean.TRUE); - worker.setInterestOps(TcpSession.this, - SelectionKey.OP_READ); - notifying.set(Boolean.FALSE); - } - } - } - } - return object; - } - - private int getMessageSize(Object obj) { - if (obj instanceof ByteBuffer) { - return ((ByteBuffer) obj).remaining(); - } - return 0; - } - } - - @Override - public int getInterestOps() { - if (!isOpen()) { - return SelectionKey.OP_WRITE; - } - - int interestOps = getRawInterestOps(); - int writeBufferSize = this.writeBufferSize.get(); - if (writeBufferSize != 0) { - if (highWaterMarkCounter.get() > 0) { - int lowWaterMark = WRITE_BUFFER_LOW_WATER_MARK; - if (writeBufferSize >= lowWaterMark) { - interestOps |= SelectionKey.OP_WRITE; - } else { - interestOps &= ~SelectionKey.OP_WRITE; - } - } else { - int highWaterMark = WRITE_BUFFER_HIGH_WATER_MARK; - if (writeBufferSize >= highWaterMark) { - interestOps |= SelectionKey.OP_WRITE; - } else { - interestOps &= ~SelectionKey.OP_WRITE; - } - } - } else { - interestOps &= ~SelectionKey.OP_WRITE; - } - - return interestOps; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + sessionId; - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - TcpSession other = (TcpSession) obj; - return sessionId != other.sessionId; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(); - sb.append("TcpSession"); - sb.append("{sessionId=").append(sessionId); - sb.append('}'); - return sb.toString(); - } - - @Override - public long getLastWrittenTime() { - return lastWrittenTime; - } - - void setLastWrittenTime(long lastWrittenTime) { - this.lastWrittenTime = lastWrittenTime; - } - - @Override - public long getLastReadTime() { - return lastReadTime; - } - - void setLastReadTime(long lastReadTime) { - this.lastReadTime = lastReadTime; - } - - @Override - public long getLastActiveTime() { - return Math.max(lastReadTime, lastWrittenTime); - } - - @Override - public long getReadBytes() { - return readBytes; - } - - void setReadBytes(long readBytes) { - this.readBytes += readBytes; - } - - @Override - public long getWrittenBytes() { - return writtenBytes; - } - - void setWrittenBytes(long writtenBytes) { - this.writtenBytes += writtenBytes; - } - - - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpWorker.java b/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpWorker.java deleted file mode 100644 index 2b7fee216..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/tcp/TcpWorker.java +++ /dev/null @@ -1,643 +0,0 @@ -package com.firefly.net.tcp; - -import static com.firefly.net.tcp.TcpPerformanceParameter.CLEANUP_INTERVAL; -import static com.firefly.net.tcp.TcpPerformanceParameter.WRITE_SPIN_COUNT; - -import java.io.IOException; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.AsynchronousCloseException; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import com.firefly.net.Config; -import com.firefly.net.EventManager; -import com.firefly.net.ReceiveBufferPool; -import com.firefly.net.ReceiveBufferSizePredictor; -import com.firefly.net.SendBufferPool; -import com.firefly.net.Session; -import com.firefly.net.Worker; -import com.firefly.net.buffer.SocketReceiveBufferPool; -import com.firefly.net.buffer.SocketSendBufferPool; -import com.firefly.net.buffer.SocketSendBufferPool.SendBuffer; -import com.firefly.net.exception.NetException; -import com.firefly.utils.collection.LinkedTransferQueue; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.time.HashTimeWheel; -import com.firefly.utils.time.Millisecond100Clock; - -public final class TcpWorker implements Worker { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private final Config config; - private final Queue registerTaskQueue = new LinkedTransferQueue(); - private final Queue writeTaskQueue = new LinkedTransferQueue(); - private final AtomicBoolean wakenUp = new AtomicBoolean(); - private final ReceiveBufferPool receiveBufferPool = new SocketReceiveBufferPool(); - private final SendBufferPool sendBufferPool = new SocketSendBufferPool(); - private final Selector selector; - private final HashTimeWheel timeWheel = new HashTimeWheel(); - private final int workerId; - private volatile int cancelledKeys; - private Thread thread; - private EventManager eventManager; - private boolean start; - - public TcpWorker(Config config, int workerId, EventManager eventManager) { - try { - this.workerId = workerId; - this.config = config; - this.eventManager = eventManager; - timeWheel.start(); - - selector = Selector.open(); - start = true; - new Thread(this, "Tcp-worker: " + workerId).start(); - } catch (IOException e) { - log.error("worker init error", e); - throw new NetException("worker init error"); - } - } - - public int getWorkerId() { - return workerId; - } - - @Override - public void registerSelectableChannel(SelectableChannel selectableChannel, - int sessionId) { - SocketChannel socketChannel = (SocketChannel) selectableChannel; - registerTaskQueue.offer(new RegisterTask(socketChannel, sessionId)); - if (wakenUp.compareAndSet(false, true)) - selector.wakeup(); - } - - @Override - public void run() { - thread = Thread.currentThread(); - - while (start) { - wakenUp.set(false); - try { - select(selector); - if (wakenUp.get()) - selector.wakeup(); - - cancelledKeys = 0; - processRegisterTaskQueue(); - processWriteTaskQueue(); - processSelectedKeys(selector.selectedKeys()); - } catch (Throwable t) { - log.error("Unexpected exception in the selector loop.", t); - - // Prevent possible consecutive immediate failures that lead to - // excessive CPU consumption. - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - // Ignore. - } - } - } - - } - - EventManager getEventManager() { - return eventManager; - } - - private void processWriteTaskQueue() throws IOException { - while (true) { - Runnable task = writeTaskQueue.poll(); - if (task == null) - break; - task.run(); - cleanUpCancelledKeys(); - } - - } - - private void processSelectedKeys(Set selectedKeys) - throws IOException { - for (Iterator i = selectedKeys.iterator(); i.hasNext();) { - SelectionKey k = i.next(); - i.remove(); - try { - int readyOps = k.readyOps(); - if ((readyOps & SelectionKey.OP_READ) != 0 || readyOps == 0) { - if (!read(k)) { - // Connection already closed - no need to handle write. - continue; - } - } - if ((readyOps & SelectionKey.OP_WRITE) != 0) { - writeFromSelectorLoop(k); - } - } catch (CancelledKeyException e) { - log.debug("processSelectedKeys error close session", e); - close(k); - } - - if (cleanUpCancelledKeys()) - break; - } - - } - - void writeFromUserCode(final TcpSession session) { - if (!session.isOpen()) { - cleanUpWriteBuffer(session); - return; - } - - if (scheduleWriteIfNecessary(session)) { - return; - } - - // From here, we are sure Thread.currentThread() == workerThread. - if (session.isWriteSuspended() || session.isInWriteNowLoop()) - return; - - log.debug("worker thread write"); - write0(session); - } - - private boolean scheduleWriteIfNecessary(final TcpSession session) { - log.debug("worker thread {} | current thread {}", thread.toString(), - Thread.currentThread().toString()); - if (Thread.currentThread() != thread) { - log.debug("schedule write >>>>"); - if (session.getWriteTaskInTaskQueue().compareAndSet(false, true)) { - boolean offered = writeTaskQueue.offer(session.getWriteTask()); - assert offered; - } - if (wakenUp.compareAndSet(false, true)) - selector.wakeup(); - return true; - } - return false; - } - - void writeFromTaskLoop(final TcpSession session) { - if (!session.isWriteSuspended()) - write0(session); - } - - private void writeFromSelectorLoop(SelectionKey k) { - final TcpSession session = (TcpSession) k.attachment(); - session.setWriteSuspended(false); - write0(session); - } - - private void write0(TcpSession session) { - if (!session.isOpen()) - return; - - boolean open = true; - boolean addOpWrite = false; - boolean removeOpWrite = false; - long writtenBytes = 0; - - final SocketChannel ch = (SocketChannel) session.getSelectionKey() - .channel(); - final Queue writeBuffer = session.getWriteBuffer(); - final int writeSpinCount = WRITE_SPIN_COUNT; - synchronized (session.getWriteLock()) { - session.setInWriteNowLoop(true); - while (true) { - Object obj = session.getCurrentWrite(); - SendBuffer buf = null; - if (obj == null) { - obj = writeBuffer.poll(); - session.setCurrentWrite(obj); - if (session.getCurrentWrite() == null) { - removeOpWrite = true; - session.setWriteSuspended(false); - break; - } - if (obj == Session.CLOSE_FLAG) { - open = false; - } else { - buf = sendBufferPool.acquire(obj); - session.setCurrentWriteBuffer(buf); - } - } else { - if (obj == Session.CLOSE_FLAG) - open = false; - else - buf = session.getCurrentWriteBuffer(); - } - - try { - log.debug("0> session is open: {}", open); - if (!open) { - log.debug("receive close flag"); - assert buf == null; - - session.resetCurrentWriteAndWriteBuffer(); - // buf = null; - // obj = null; - clearOpWrite(session); - close(session.getSelectionKey()); - break; - } - - long localWrittenBytes; - for (int i = writeSpinCount; i > 0; i--) { - localWrittenBytes = buf.transferTo(ch); - if (localWrittenBytes != 0) { - writtenBytes += localWrittenBytes; - break; - } - if (buf.finished()) { - break; - } - } - - if (buf.finished()) { - // Successful write - proceed to the next message. - buf.release(); - session.resetCurrentWriteAndWriteBuffer(); - obj = null; - buf = null; - } else { - // Not written fully - perhaps the kernel buffer is - // full. - addOpWrite = true; - session.setWriteSuspended(true); - break; - } - } catch (AsynchronousCloseException e) { - // Doesn't need a user attention - ignore. - } catch (Throwable t) { - if(buf != null) - buf.release(); - - session.resetCurrentWriteAndWriteBuffer(); - buf = null; - obj = null; - eventManager.executeExceptionTask(session, t); - if (t instanceof IOException) { - log.debug("write0 IOException session close"); - open = false; - close(session.getSelectionKey()); - } - } - } - session.setInWriteNowLoop(false); - - // Initially, the following block was executed after releasing - // the writeLock, but there was a race condition, and it has to be - // executed before releasing the writeLock: - if (open) { - if (addOpWrite) { - setOpWrite(session); - } else if (removeOpWrite) { - clearOpWrite(session); - } - } - } - - if (writtenBytes > 0) { - session.setLastWrittenTime(Millisecond100Clock.currentTimeMillis()); - session.setWrittenBytes(writtenBytes); - log.debug("write complete size: {}", writtenBytes); - log.debug("1> session is open: {}", open); - log.debug("is in write loop: {}", session.isInWriteNowLoop()); - } - } - - private void cleanUpWriteBuffer(TcpSession session) { - Exception cause = null; - boolean fireExceptionCaught = false; - - // Clean up the stale messages in the write buffer. - synchronized (session.getWriteLock()) { - Object obj = session.getCurrentWrite(); - if (obj != null) { - cause = new NetException("cleanUpWriteBuffer error"); - session.getCurrentWriteBuffer().release(); - session.resetCurrentWriteAndWriteBuffer(); - fireExceptionCaught = true; - } - - Queue writeBuffer = session.getWriteBuffer(); - if (!writeBuffer.isEmpty()) { - // Create the exception only once to avoid the excessive - // overhead - // caused by fillStackTrace. - if (cause == null) { - cause = new NetException("cleanUpWriteBuffer error"); - } - - while (true) { - obj = writeBuffer.poll(); - if (obj == null) { - break; - } - log.warn("error clear obj: {}", obj.getClass().toString()); - fireExceptionCaught = true; - } - } - } - - if (fireExceptionCaught) - eventManager.executeExceptionTask(session, cause); - - } - - private boolean read(SelectionKey k) { - final SocketChannel ch = (SocketChannel) k.channel(); - final TcpSession session = (TcpSession) k.attachment(); - final ReceiveBufferSizePredictor predictor = session - .getReceiveBufferSizePredictor(); - final int predictedRecvBufSize = predictor.nextReceiveBufferSize(); - - int ret = 0; - int readBytes = 0; - boolean failure = true; - - ByteBuffer bb = receiveBufferPool.acquire(predictedRecvBufSize); - try { - while ((ret = ch.read(bb)) > 0) { - readBytes += ret; - if (!bb.hasRemaining()) - break; - } - failure = false; - } catch (ClosedChannelException e) { - // Can happen, and does not need a user attention. - } catch (Throwable t) { - eventManager.executeExceptionTask(session, t); - } - - if (readBytes > 0) { - bb.flip(); - receiveBufferPool.release(bb); - - // Update the predictor. - predictor.previousReceiveBufferSize(readBytes); - session.setReadBytes(readBytes); - session.setLastReadTime(Millisecond100Clock.currentTimeMillis()); - // Decode - - try { - config.getDecoder().decode(bb, session); - } catch (Throwable t) { - eventManager.executeExceptionTask(session, t); - } - // log.info("Worker {} decode", workerId); - } else { - receiveBufferPool.release(bb); - } - - if (ret < 0 || failure) { - log.debug("read failure session close"); - k.cancel(); - close(k); - return false; - } - - return true; - } - - private void processRegisterTaskQueue() throws IOException { - while (true) { - Runnable task = registerTaskQueue.poll(); - if (task == null) - break; - task.run(); - cleanUpCancelledKeys(); - } - } - - private final class RegisterTask implements Runnable { - - private SocketChannel socketChannel; - private int sessionId; - - public RegisterTask(SocketChannel socketChannel, int sessionId) { - this.socketChannel = socketChannel; - this.sessionId = sessionId; - } - - @Override - public void run() { - - SelectionKey key = null; - try { - socketChannel.configureBlocking(false); - socketChannel.socket().setReuseAddress(true); - socketChannel.socket().setTcpNoDelay(false); - socketChannel.socket().setKeepAlive(true); - - key = socketChannel.register(selector, SelectionKey.OP_READ); - Session session = new TcpSession(sessionId, TcpWorker.this, - config, Millisecond100Clock.currentTimeMillis(), key, eventManager); - key.attach(session); - - SocketAddress localAddress = session.getLocalAddress(); - SocketAddress remoteAddress = session.getRemoteAddress(); - if (localAddress == null || remoteAddress == null) { - TcpWorker.this.close(key); - } - - if (config.getTimeout() > 0) - timeWheel.add(config.getTimeout(), new TimeoutTask(session, - config.getTimeout())); - eventManager.executeOpenTask(session); - } catch (IOException e) { - log.error("socketChannel register error", e); - close(key); - } - - } - - } - - private final class TimeoutTask implements Runnable { - private Session session; - private final long timeout; - - public TimeoutTask(Session session, long timeout) { - this.session = session; - this.timeout = timeout; - } - - @Override - public void run() { - long t = Millisecond100Clock.currentTimeMillis() - - session.getLastActiveTime(); - // log.debug("check time: {}", t); - if (t > timeout) { - // log.debug("check timeout"); - if (session.isOpen()) - session.close(true); - } else { - long nextCheckTime = timeout - t; - // log.debug("next check time: {}", nextCheckTime); - timeWheel.add(nextCheckTime, TimeoutTask.this); - } - - } - - } - - public void close(SelectionKey key) { - try { - key.channel().close(); - increaseCancelledKey(); - TcpSession session = (TcpSession) key.attachment(); - session.setState(Session.CLOSE); - cleanUpWriteBuffer(session); - eventManager.executeCloseTask(session); - - } catch (IOException e) { - log.error("channel close error", e); - } - } - - static void select(Selector selector) throws IOException { - try { - selector.select(500); - } catch (CancelledKeyException e) { - // Harmless exception - log anyway - log.debug(CancelledKeyException.class.getSimpleName() - + " raised by a Selector - JDK bug?", e); - } - } - - private boolean cleanUpCancelledKeys() throws IOException { - if (cancelledKeys >= CLEANUP_INTERVAL) { - cancelledKeys = 0; - selector.selectNow(); - return true; - } - return false; - } - - private void increaseCancelledKey() { - int temp = cancelledKeys; - temp++; - cancelledKeys = temp; - } - - private void setOpWrite(TcpSession session) { - SelectionKey key = session.getSelectionKey(); - if (key == null) { - return; - } - if (!key.isValid()) { - log.debug("setOpWrite failure session close"); - close(key); - return; - } - - // interestOps can change at any time and at any thread. - // Acquire a lock to avoid possible race condition. - synchronized (session.getInterestOpsLock()) { - int interestOps = session.getRawInterestOps(); - if ((interestOps & SelectionKey.OP_WRITE) == 0) { - interestOps |= SelectionKey.OP_WRITE; - key.interestOps(interestOps); - session.setInterestOpsNow(interestOps); - } - } - } - - private void clearOpWrite(TcpSession session) { - SelectionKey key = session.getSelectionKey(); - if (key == null) { - return; - } - if (!key.isValid()) { - log.debug("clearOpWrite key valid false"); - close(key); - return; - } - - // interestOps can change at any time and at any thread. - // Acquire a lock to avoid possible race condition. - synchronized (session.getInterestOpsLock()) { - int interestOps = session.getRawInterestOps(); - if ((interestOps & SelectionKey.OP_WRITE) != 0) { - interestOps &= ~SelectionKey.OP_WRITE; - log.debug("clear write op >>> {}", interestOps); - key.interestOps(interestOps); - session.setInterestOpsNow(interestOps); - } - } - } - - void setInterestOps(TcpSession session, int interestOps) { - boolean changed = false; - try { - // interestOps can change at any time and at any thread. - // Acquire a lock to avoid possible race condition. - synchronized (session.getInterestOpsLock()) { - SelectionKey key = session.getSelectionKey(); - - if (key == null || selector == null) { - // Not registered to the worker yet. - // Set the rawInterestOps immediately; RegisterTask will - // pick it up. - session.setInterestOpsNow(interestOps); - return; - } - - // Override OP_WRITE flag - a user cannot change this flag. - interestOps &= ~SelectionKey.OP_WRITE; - interestOps |= session.getRawInterestOps() - & SelectionKey.OP_WRITE; - - /** - * 0 - no need to wake up to get / set interestOps (most cases) - * 1 - no need to wake up to get interestOps, but need to wake - * up to set. 2 - need to wake up to get / set interestOps (old - * providers) - */ - if (session.getRawInterestOps() != interestOps) { - key.interestOps(interestOps); - if (Thread.currentThread() != thread - && wakenUp.compareAndSet(false, true)) { - selector.wakeup(); - } - changed = true; - } - - if (changed) { - session.setInterestOpsNow(interestOps); - } - } - - if (changed) { - log.debug("interestOps change [{}]", interestOps); - setInterestOps(session, SelectionKey.OP_READ); - } - } catch (CancelledKeyException e) { - // setInterestOps() was called on a closed channel. - ClosedChannelException cce = new ClosedChannelException(); - eventManager.executeExceptionTask(session, cce); - } catch (Throwable t) { - eventManager.executeExceptionTask(session, t); - } - } - - @Override - public void shutdown() { - eventManager.shutdown(); - start = false; - timeWheel.stop(); - log.debug("thread {} is shutdown: {}", thread.getName(), - thread.isInterrupted()); - } -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/udp/UdpClient.java b/firefly-nettool/src/main/java/com/firefly/net/udp/UdpClient.java deleted file mode 100644 index 0e75c9c19..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/udp/UdpClient.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.firefly.net.udp; - -import com.firefly.net.Client; -import com.firefly.net.Config; - -public class UdpClient implements Client { - - @Override - public void setConfig(Config config) { - // TODO Auto-generated method stub - - } - - @Override - public int connect(String host, int port) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void shutdown() { - // TODO Auto-generated method stub - - } - - @Override - public void connect(String host, int port, int id) { - // TODO Auto-generated method stub - - } - - -} diff --git a/firefly-nettool/src/main/java/com/firefly/net/udp/UdpServer.java b/firefly-nettool/src/main/java/com/firefly/net/udp/UdpServer.java deleted file mode 100644 index cdbefef69..000000000 --- a/firefly-nettool/src/main/java/com/firefly/net/udp/UdpServer.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.net.udp; - -import com.firefly.net.Config; -import com.firefly.net.Server; - -public class UdpServer implements Server { - - @Override - public void setConfig(Config config) { - // TODO Auto-generated method stub - - } - - @Override - public void start(String host, int port) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void shutdown() { - // TODO Auto-generated method stub - - } - -} diff --git a/firefly-nettool/src/test/java/test/net/buffer/TestReceiveBufferSizePredictor.java b/firefly-nettool/src/test/java/test/net/buffer/TestReceiveBufferSizePredictor.java deleted file mode 100644 index cdc969800..000000000 --- a/firefly-nettool/src/test/java/test/net/buffer/TestReceiveBufferSizePredictor.java +++ /dev/null @@ -1,46 +0,0 @@ -package test.net.buffer; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.net.ReceiveBufferSizePredictor; -import com.firefly.net.buffer.AdaptiveReceiveBufferSizePredictor; -import com.firefly.net.buffer.FixedReceiveBufferSizePredictor; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -import static org.hamcrest.Matchers.*; - -public class TestReceiveBufferSizePredictor { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - @Test - public void testAdaptive() { - ReceiveBufferSizePredictor receiveBufferSizePredictor = new AdaptiveReceiveBufferSizePredictor(); - receiveBufferSizePredictor.previousReceiveBufferSize(960); - receiveBufferSizePredictor.previousReceiveBufferSize(960); - receiveBufferSizePredictor.previousReceiveBufferSize(960); - log.debug("current buf size: " + receiveBufferSizePredictor.nextReceiveBufferSize()); - Assert.assertThat(receiveBufferSizePredictor.nextReceiveBufferSize(), is(1024)); - - receiveBufferSizePredictor.previousReceiveBufferSize(1025); - receiveBufferSizePredictor.previousReceiveBufferSize(1300); - log.debug("current buf size: " + receiveBufferSizePredictor.nextReceiveBufferSize()); - Assert.assertThat(receiveBufferSizePredictor.nextReceiveBufferSize(), greaterThan(1024)); - - receiveBufferSizePredictor.previousReceiveBufferSize(4000); - log.debug("current buf size: " + receiveBufferSizePredictor.nextReceiveBufferSize()); - Assert.assertThat(receiveBufferSizePredictor.nextReceiveBufferSize(), greaterThan(2000)); - } - - @Test - public void testFix() { - ReceiveBufferSizePredictor receiveBufferSizePredictor = new FixedReceiveBufferSizePredictor(1024 * 8); - receiveBufferSizePredictor.previousReceiveBufferSize(960); - Assert.assertThat(receiveBufferSizePredictor.nextReceiveBufferSize(), is(1024 * 8)); - receiveBufferSizePredictor.previousReceiveBufferSize(40000); - Assert.assertThat(receiveBufferSizePredictor.nextReceiveBufferSize(), is(1024 * 8)); - } - -} diff --git a/firefly-nettool/src/test/java/test/net/buffer/TestSocketReceiveBufferPool.java b/firefly-nettool/src/test/java/test/net/buffer/TestSocketReceiveBufferPool.java deleted file mode 100644 index 33c60803b..000000000 --- a/firefly-nettool/src/test/java/test/net/buffer/TestSocketReceiveBufferPool.java +++ /dev/null @@ -1,24 +0,0 @@ -package test.net.buffer; - -import org.junit.Assert; -import org.junit.Test; -import com.firefly.net.buffer.SocketReceiveBufferPool; -import static org.hamcrest.Matchers.*; - - -public class TestSocketReceiveBufferPool { -// private static Logger log = LoggerFactory.getLogger(TestSocketReceiveBufferPool.class); - - @Test - public void testNormalizeCapacity() { - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(5), is(1024)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(1023), is(1024)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(1024), is(1024)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(70), is(1024)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(1025), is(1024 * 2)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(1900), is(1024 * 2)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(2048), is(1024 * 2)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(2049), is(1024 * 3)); - Assert.assertThat(SocketReceiveBufferPool.normalizeCapacity(5000), is(1024 * 5)); - } -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/FileTransferTcpServer.java b/firefly-nettool/src/test/java/test/net/tcp/FileTransferTcpServer.java deleted file mode 100644 index 038925d93..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/FileTransferTcpServer.java +++ /dev/null @@ -1,18 +0,0 @@ -package test.net.tcp; - -import java.net.URISyntaxException; -import com.firefly.net.Server; -import com.firefly.net.support.StringLineDecoder; -import com.firefly.net.support.StringLineEncoder; -import com.firefly.net.tcp.TcpServer; - -public class FileTransferTcpServer { - - public static void main(String[] args) throws URISyntaxException { -// System.out.println(SendFileHandler.class.getResource("/testFile.txt").toURI()); - Server server = new TcpServer(new StringLineDecoder(), - new StringLineEncoder(), new SendFileHandler()); - server.start("localhost", 9900); - } - -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/SendFileHandler.java b/firefly-nettool/src/test/java/test/net/tcp/SendFileHandler.java deleted file mode 100644 index 1ad046735..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/SendFileHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package test.net.tcp; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.net.URISyntaxException; - -import com.firefly.net.Handler; -import com.firefly.net.Session; -import com.firefly.net.buffer.FileRegion; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class SendFileHandler implements Handler { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - @Override - public void sessionOpened(Session session) throws Throwable { - log.debug("session open |" + session.getSessionId()); - log.debug("local: " + session.getLocalAddress()); - log.debug("remote: " + session.getRemoteAddress()); - } - - @Override - public void sessionClosed(Session session) throws Throwable { - log.debug("session close|" + session.getSessionId()); - } - - @Override - public void messageRecieved(Session session, Object message) throws Throwable { - String str = (String) message; - if (str.equals("quit")) { - session.encode("bye!"); - session.close(false); - } else if (str.equals("getfile")) { - RandomAccessFile raf = null; - File file = null; - try { - file = new File(SendFileHandler.class.getResource( - "/testFile.txt").toURI()); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - try { - raf = new RandomAccessFile(file, "r"); - } catch (FileNotFoundException fnfe) { - fnfe.printStackTrace(); - } - FileRegion fileRegion = null; - try { - assert raf != null; - fileRegion = new FileRegion(raf, 0, raf.length()); - } catch (IOException e) { - e.printStackTrace(); - } - session.write(fileRegion); - } else { - log.debug("recive: " + str); - session.encode(message); - } - log.debug("r {} {} | w {} {}", session.getReadBytes(), str, session.getWrittenBytes(), message); - } - - @Override - public void exceptionCaught(Session session, Throwable t) throws Throwable{ - log.error(t.getMessage() + "|" + session.getSessionId(), t); - } - -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/SimpleTcpClientExample.java b/firefly-nettool/src/test/java/test/net/tcp/SimpleTcpClientExample.java deleted file mode 100644 index fbaf1566e..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/SimpleTcpClientExample.java +++ /dev/null @@ -1,61 +0,0 @@ -package test.net.tcp; - -import com.firefly.net.Session; -import com.firefly.net.support.MessageReceiveCallBack; -import com.firefly.net.support.SimpleTcpClient; -import com.firefly.net.support.StringLineDecoder; -import com.firefly.net.support.StringLineEncoder; -import com.firefly.net.support.TcpConnection; -import com.firefly.utils.log.LogFactory; - -public class SimpleTcpClientExample { - public static void main(String[] args) throws Throwable { - final SimpleTcpClient client = new SimpleTcpClient("localhost", 9900, - new StringLineDecoder(), new StringLineEncoder()); - TcpConnection c = client.connect(); - c.send("hello client 1", new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - System.out.println("con1|" + obj.toString()); - - } - }); - - c.send("test client 2", new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - System.out.println("con1|" + obj.toString()); - - } - }); - - c.send("test client 3", new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - System.out.println("con1|" + obj.toString()); - } - }); - - c.send("quit", new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - System.out.println("con1|" + obj.toString()); - } - }); - - TcpConnection c2 = client.connect(); - System.out.println("con2|" + c2.send("getfile")); - c2.close(false); - - Thread.sleep(4000); - client.shutdown(); - LogFactory.getInstance().shutdown(); -// System.out.println("shutdown"); - - - } -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/StringLineHandler.java b/firefly-nettool/src/test/java/test/net/tcp/StringLineHandler.java deleted file mode 100644 index 7669e7940..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/StringLineHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package test.net.tcp; - -import com.firefly.net.Handler; -import com.firefly.net.Session; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class StringLineHandler implements Handler { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - @Override - public void sessionOpened(Session session) throws Throwable { - log.info("session open |" + session.getSessionId()); - log.info("local: " + session.getLocalAddress()); - log.info("remote: " + session.getRemoteAddress()); - } - - @Override - public void sessionClosed(Session session) throws Throwable { - log.debug("session close|" + session.getSessionId()); - } - - @Override - public void messageRecieved(Session session, Object message) throws Throwable { - String str = (String) message; - if (str.equals("quit")) { - session.encode("bye!"); - session.close(false); - } else { - log.debug("recive: " + str); - session.encode(message); - } - } - - @Override - public void exceptionCaught(Session session, Throwable t) throws Throwable { - log.error( t.getMessage() + "|" + session.getSessionId()); - } - -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/StringLinePerformance.java b/firefly-nettool/src/test/java/test/net/tcp/StringLinePerformance.java deleted file mode 100644 index 941217f88..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/StringLinePerformance.java +++ /dev/null @@ -1,132 +0,0 @@ -package test.net.tcp; - -import com.firefly.net.Session; -import com.firefly.net.support.StringLineDecoder; -import com.firefly.net.support.StringLineEncoder; -import com.firefly.net.support.TcpConnection; -import com.firefly.net.support.MessageReceiveCallBack; -import com.firefly.net.support.SimpleTcpClient; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import java.util.concurrent.*; - -public class StringLinePerformance { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - public static final int LOOP = 2000; - public static final int THREAD = 500; - - public static class ClientSynchronizeTask implements Runnable { - - private final SimpleTcpClient client; - private final CyclicBarrier barrier; - - @Override - public void run() { - TcpConnection c = client.connect(); - try { - for (int i = 0; i < LOOP; i++) { - String message = "hello world! " + c.getId(); - String ret = (String) c.send(message); - log.debug("rev: {}", ret); - } - } finally { - if (c != null) - c.close(false); - } - log.debug("session {} complete", c.getId()); - - try { - barrier.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (BrokenBarrierException e) { - e.printStackTrace(); - } - - } - - public ClientSynchronizeTask(SimpleTcpClient client, - CyclicBarrier barrier) { - this.client = client; - this.barrier = barrier; - } - } - - public static class ClientAsynchronousTask implements Runnable { - - private final SimpleTcpClient client; - private final CyclicBarrier barrier; - - @Override - public void run() { - TcpConnection c = client.connect(); - for (int i = 0; i < LOOP; i++) { - String message = "hello world! " + c.getId(); - c.send(message, new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - log.debug("rev: {}", obj); - } - }); - - } - c.send("quit", new MessageReceiveCallBack() { - - @Override - public void messageRecieved(Session session, Object obj) { - log.debug("rev: {}", obj); - log.debug("session {} complete", session.getSessionId()); - } - }); - try { - barrier.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (BrokenBarrierException e) { - e.printStackTrace(); - } - - } - - public ClientAsynchronousTask(SimpleTcpClient client, - CyclicBarrier barrier) { - this.client = client; - this.barrier = barrier; - } - } - - public static class StatTask implements Runnable { - - private long start; - - public StatTask() { - this.start = System.currentTimeMillis(); - } - - @Override - public void run() { - long time = System.currentTimeMillis() - start; - log.debug("start time: {}", start); - log.debug("total time: {}", time); - int reqs = LOOP * THREAD; - - double throughput = (reqs / (double) time) * 1000; - log.info("throughput: {}", throughput); - } - - } - - public static void main(String[] args) { - ExecutorService executorService = Executors.newFixedThreadPool(THREAD); - final SimpleTcpClient client = new SimpleTcpClient("localhost", 9900, - new StringLineDecoder(), new StringLineEncoder()); - final CyclicBarrier barrier = new CyclicBarrier(THREAD, new StatTask()); - - for (int i = 0; i < THREAD; i++) - executorService.submit(new ClientSynchronizeTask(client, barrier)); - -// for (int i = 0; i < THREAD; i++) -// executorService.submit(new ClientAsynchronousTask(client, barrier)); - } -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/StringLineTcpServer.java b/firefly-nettool/src/test/java/test/net/tcp/StringLineTcpServer.java deleted file mode 100644 index e99127df8..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/StringLineTcpServer.java +++ /dev/null @@ -1,13 +0,0 @@ -package test.net.tcp; - -import com.firefly.net.support.StringLineDecoder; -import com.firefly.net.support.StringLineEncoder; -import com.firefly.net.tcp.TcpServer; - -public class StringLineTcpServer { - - public static void main(String[] args) { - new TcpServer(new StringLineDecoder(), - new StringLineEncoder(), new StringLineHandler()).start("localhost", 9900); - } -} diff --git a/firefly-nettool/src/test/java/test/net/tcp/TestTcpClientAndServer.java b/firefly-nettool/src/test/java/test/net/tcp/TestTcpClientAndServer.java deleted file mode 100644 index cdb46ea4d..000000000 --- a/firefly-nettool/src/test/java/test/net/tcp/TestTcpClientAndServer.java +++ /dev/null @@ -1,60 +0,0 @@ -package test.net.tcp; - -import com.firefly.net.Config; -import com.firefly.net.Server; -import com.firefly.net.support.StringLineDecoder; -import com.firefly.net.support.StringLineEncoder; -import com.firefly.net.support.TcpConnection; -import com.firefly.net.support.SimpleTcpClient; -import com.firefly.net.tcp.TcpServer; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import org.junit.Assert; -import org.junit.Test; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import static org.hamcrest.Matchers.is; - -public class TestTcpClientAndServer { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - @Test - public void testHello() { - Server server = new TcpServer(); - Config config = new Config(); - config.setHandleThreads(100); - config.setDecoder(new StringLineDecoder()); - config.setEncoder(new StringLineEncoder()); - config.setHandler(new SendFileHandler()); - server.setConfig(config); - server.start("localhost", 9900); - - final int LOOP = 50; - ExecutorService executorService = Executors.newFixedThreadPool(LOOP); - final SimpleTcpClient client = new SimpleTcpClient("localhost", 9900, - new StringLineDecoder(), new StringLineEncoder()); - - for (int i = 0; i < LOOP; i++) { - executorService.submit(new Runnable() { - @Override - public void run() { - final TcpConnection c = client.connect(); - Assert.assertThat(c.isOpen(), is(true)); - log.debug("main thread {}", Thread.currentThread() - .toString()); - Assert.assertThat((String) c.send("hello client"), is("hello client")); - Assert.assertThat((String) c.send("hello multithread test"), is("hello multithread test")); - Assert.assertThat((String) c.send("getfile"), is("zero copy file transfers")); - Assert.assertThat((String) c.send("quit"), is("bye!")); - log.debug("complete session {}", c.getId()); - } - }); - - } - - final TcpConnection c = client.connect(); - Assert.assertThat((String) c.send("hello client 2"), is("hello client 2")); - Assert.assertThat((String) c.send("quit"), is("bye!")); - } -} diff --git a/firefly-nettool/src/test/resources/firefly-log.properties b/firefly-nettool/src/test/resources/firefly-log.properties deleted file mode 100644 index 06879d666..000000000 --- a/firefly-nettool/src/test/resources/firefly-log.properties +++ /dev/null @@ -1 +0,0 @@ -firefly-system=${log.level},${log.path},console \ No newline at end of file diff --git a/firefly-nettool/src/test/resources/testFile.txt b/firefly-nettool/src/test/resources/testFile.txt deleted file mode 100644 index 2d26117bd..000000000 --- a/firefly-nettool/src/test/resources/testFile.txt +++ /dev/null @@ -1 +0,0 @@ -zero copy file transfers diff --git a/firefly-serialization/pom.xml b/firefly-serialization/pom.xml new file mode 100644 index 000000000..61e1d8d6d --- /dev/null +++ b/firefly-serialization/pom.xml @@ -0,0 +1,94 @@ + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 + + firefly-serialization + jar + + firefly-serialization + http://www.fireflysource.com + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + + com.fireflysource + firefly-slf4j + test + + + + + firefly-serialization + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.jks + + + + src/main/resources + false + + **/*.xml + **/*.properties + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.ico + **/*.html + **/*.txt + + + + src/test/resources + false + + **/*.xml + **/*.properties + + + + + diff --git a/firefly-serialization/src/main/java/com/fireflysource/doc/FeignedSerializationDoc.java b/firefly-serialization/src/main/java/com/fireflysource/doc/FeignedSerializationDoc.java new file mode 100644 index 000000000..44fa031ca --- /dev/null +++ b/firefly-serialization/src/main/java/com/fireflysource/doc/FeignedSerializationDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedSerializationDoc { +} diff --git a/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/SerializationService.kt b/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/SerializationService.kt new file mode 100644 index 000000000..c8237d5b0 --- /dev/null +++ b/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/SerializationService.kt @@ -0,0 +1,13 @@ +package com.fireflysource.serialization + +import com.fasterxml.jackson.core.type.TypeReference + +interface SerializationService { + + fun read(content: String, clazz: Class): T + + fun read(content: String, ref: TypeReference): T + + fun write(obj: Any): String + +} \ No newline at end of file diff --git a/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/SerializationServiceFactory.kt b/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/SerializationServiceFactory.kt new file mode 100644 index 000000000..b2dc7bb09 --- /dev/null +++ b/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/SerializationServiceFactory.kt @@ -0,0 +1,13 @@ +package com.fireflysource.serialization + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fireflysource.serialization.impl.json.JsonSerializationService + +object SerializationServiceFactory { + + val json = json() + + @JvmOverloads + fun json(mapper: ObjectMapper = ObjectMapper()): SerializationService = JsonSerializationService(mapper) + +} \ No newline at end of file diff --git a/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/impl/json/JsonSerializationService.kt b/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/impl/json/JsonSerializationService.kt new file mode 100644 index 000000000..ed5d6ce6e --- /dev/null +++ b/firefly-serialization/src/main/kotlin/com/fireflysource/serialization/impl/json/JsonSerializationService.kt @@ -0,0 +1,25 @@ +package com.fireflysource.serialization.impl.json + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fireflysource.serialization.SerializationService + +class JsonSerializationService(val mapper: ObjectMapper = ObjectMapper()) : SerializationService { + + init { + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } + + override fun read(content: String, clazz: Class): T = mapper.readValue(content, clazz) + + override fun read(content: String, ref: TypeReference): T = mapper.readValue(content, ref) + + override fun write(obj: Any): String = mapper.writeValueAsString(obj) + +} + +inline fun SerializationService.read(content: String): T { + return this.read(content, object : TypeReference() {}) +} + diff --git a/firefly-slf4j/pom.xml b/firefly-slf4j/pom.xml new file mode 100644 index 000000000..e18f52fdd --- /dev/null +++ b/firefly-slf4j/pom.xml @@ -0,0 +1,64 @@ + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 + + firefly-slf4j + jar + + firefly-slf4j + http://www.fireflysource.com + + + + org.slf4j + slf4j-api + + + + + firefly-slf4j + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.xml + **/*.properties + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/test/resources + false + + **/*.xml + **/*.properties + + + + + diff --git a/firefly-slf4j/src/main/java/com/fireflysource/doc/FeignedLogDoc.java b/firefly-slf4j/src/main/java/com/fireflysource/doc/FeignedLogDoc.java new file mode 100644 index 000000000..7d494ef41 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/doc/FeignedLogDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedLogDoc { +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/AbstractLogConfigParser.java b/firefly-slf4j/src/main/java/com/fireflysource/log/AbstractLogConfigParser.java new file mode 100644 index 000000000..90b0252a8 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/AbstractLogConfigParser.java @@ -0,0 +1,161 @@ +package com.fireflysource.log; + +import com.fireflysource.log.internal.utils.StringUtils; + +import java.io.File; +import java.nio.charset.Charset; +import java.time.LocalDateTime; + +public abstract class AbstractLogConfigParser implements LogConfigParser { + + protected FileLog createLog(Configuration c) { + FileLog fileLog = new FileLog(); + fileLog.setLogName(c.getName()); + fileLog.setLevel(LogLevel.fromName(c.getLevel())); + fileLog.setMaxFileSize(c.getMaxFileSize()); + fileLog.setCharset(Charset.forName(c.getCharset())); + + boolean success; + if (StringUtils.hasText(c.getPath())) { + File file = new File(c.getPath()); + success = createLogDirectory(file); + if (success) { + fileLog.setPath(c.getPath()); + fileLog.setFileOutput(true); + } else { + success = createDefaultLogDir(fileLog); + } + } else { + success = createDefaultLogDir(fileLog); + } + + if (success) { + fileLog.setConsoleOutput(c.isConsole()); + } else { + fileLog.setConsoleOutput(true); + System.err.println("create log directory is failure"); + } + + if (StringUtils.hasText(c.getFormatter())) { + fileLog.setLogFormatter(new LogFormatter() { + + private LogFormatter formatter; + + @Override + public String format(LogItem logItem) { + init(); + return formatter.format(logItem); + } + + private void init() { + if (formatter == null) { + try { + Class clazz = AbstractLogConfigParser.class.getClassLoader().loadClass(c.getFormatter()); + formatter = (LogFormatter) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + formatter = DEFAULT_LOG_FORMATTER; + } + } + } + }); + } + + if (StringUtils.hasText(c.getLogNameFormatter())) { + fileLog.setLogNameFormatter(new LogNameFormatter() { + + private LogNameFormatter formatter; + + @Override + public String format(String name, LocalDateTime localDateTime) { + init(); + return formatter.format(name, localDateTime); + } + + @Override + public String formatBak(String name, LocalDateTime localDateTime, int index) { + init(); + return formatter.formatBak(name, localDateTime, index); + } + + private void init() { + if (formatter == null) { + try { + Class clazz = AbstractLogConfigParser.class.getClassLoader().loadClass(c.getLogNameFormatter()); + formatter = (LogNameFormatter) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + formatter = DEFAULT_LOG_NAME_FORMATTER; + } + } + } + }); + } + + if (StringUtils.hasText(c.getLogFilter())) { + fileLog.setLogFilter(new LogFilter() { + + private LogFilter filter; + + @Override + public void filter(LogItem logItem) { + init(); + filter.filter(logItem); + } + + private void init() { + if (filter == null) { + try { + Class clazz = AbstractLogConfigParser.class.getClassLoader().loadClass(c.getLogFilter()); + filter = (LogFilter) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + filter = DEFAULT_LOG_FILTER; + } + } + } + }); + } + + if (StringUtils.hasText(c.getMaxSplitTime())) { + fileLog.setMaxSplitTime(MaxSplitTimeEnum.from(c.getMaxSplitTime()).orElse(DEFAULT_MAX_SPLIT_TIME)); + } else { + fileLog.setMaxSplitTime(DEFAULT_MAX_SPLIT_TIME); + } + + System.out.println("initialize log " + fileLog); + return fileLog; + } + + private boolean createLogDirectory(File file) { + return file.exists() && file.isDirectory() || file.mkdirs(); + } + + private boolean createDefaultLogDir(FileLog fileLog) { + boolean success = createLogDirectory(DEFAULT_LOG_DIRECTORY); + if (success) { + fileLog.setPath(DEFAULT_LOG_DIRECTORY.getAbsolutePath()); + fileLog.setFileOutput(true); + } else { + fileLog.setFileOutput(false); + } + return success; + } + + @Override + public FileLog createDefaultLog() { + Configuration c = new Configuration(); + c.setName(DEFAULT_LOG_NAME); + c.setLevel(DEFAULT_LOG_LEVEL); + c.setPath(DEFAULT_LOG_DIRECTORY.getAbsolutePath()); + c.setConsole(false); + c.setMaxFileSize(DEFAULT_MAX_FILE_SIZE); + c.setCharset(DEFAULT_CHARSET.name()); + c.setMaxSplitTime(DEFAULT_MAX_SPLIT_TIME.getValue()); + c.setFormatter(DEFAULT_LOG_FORMATTER.getClass().getName()); + c.setLogNameFormatter(DEFAULT_LOG_NAME_FORMATTER.getClass().getName()); + c.setLogFilter(DEFAULT_LOG_FILTER.getClass().getName()); + return createLog(c); + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/ClassNameLogWrap.java b/firefly-slf4j/src/main/java/com/fireflysource/log/ClassNameLogWrap.java new file mode 100644 index 000000000..ead19987e --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/ClassNameLogWrap.java @@ -0,0 +1,131 @@ +package com.fireflysource.log; + +import java.io.IOException; + +/** + * @author Pengtao Qiu + */ +public class ClassNameLogWrap implements Log { + + public static final ThreadLocal name = new ThreadLocal<>(); + + private final Log log; + private final String className; + + public ClassNameLogWrap(Log log, String className) { + this.log = log; + this.className = className; + } + + public String getName() { + return log.getName(); + } + + public boolean isTraceEnabled() { + return log.isTraceEnabled(); + } + + public void trace(String str) { + name.set(className); + log.trace(str); + } + + public void trace(String str, Object... objs) { + name.set(className); + log.trace(str, objs); + } + + public void trace(String str, Throwable throwable, Object... objs) { + name.set(className); + log.trace(str, throwable, objs); + } + + public boolean isDebugEnabled() { + return log.isDebugEnabled(); + } + + public void debug(String str) { + name.set(className); + log.debug(str); + } + + public void debug(String str, Object... objs) { + name.set(className); + log.debug(str, objs); + } + + public void debug(String str, Throwable throwable, Object... objs) { + name.set(className); + log.debug(str, throwable, objs); + } + + public boolean isInfoEnabled() { + return log.isInfoEnabled(); + } + + public void info(String str) { + name.set(className); + log.info(str); + } + + public void info(String str, Object... objs) { + name.set(className); + log.info(str, objs); + } + + public void info(String str, Throwable throwable, Object... objs) { + name.set(className); + log.info(str, throwable, objs); + } + + public boolean isWarnEnabled() { + return log.isWarnEnabled(); + } + + public void warn(String str) { + name.set(className); + log.warn(str); + } + + public void warn(String str, Object... objs) { + name.set(className); + log.warn(str, objs); + } + + public void warn(String str, Throwable throwable, Object... objs) { + name.set(className); + log.warn(str, throwable, objs); + } + + public boolean isErrorEnabled() { + return log.isErrorEnabled(); + } + + public void error(String str) { + name.set(className); + log.error(str); + } + + public void error(String str, Object... objs) { + name.set(className); + log.error(str, objs); + } + + public void error(String str, Throwable throwable, Object... objs) { + name.set(className); + log.error(str, throwable, objs); + } + + public Log getLog() { + return log; + } + + public String getClassName() { + return className; + } + + @Override + public void close() throws IOException { + log.close(); + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/Configuration.java b/firefly-slf4j/src/main/java/com/fireflysource/log/Configuration.java new file mode 100644 index 000000000..06f35b542 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/Configuration.java @@ -0,0 +1,98 @@ +package com.fireflysource.log; + +/** + * @author Pengtao Qiu + */ +public class Configuration { + + private String name; + private String level; + private String path; + private boolean console; + private long maxFileSize; + private String charset; + private String formatter; + private String logNameFormatter; + private String logFilter; + private String maxSplitTime; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isConsole() { + return console; + } + + public void setConsole(boolean console) { + this.console = console; + } + + public long getMaxFileSize() { + return maxFileSize; + } + + public void setMaxFileSize(long maxFileSize) { + this.maxFileSize = maxFileSize; + } + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public String getFormatter() { + return formatter; + } + + public void setFormatter(String formatter) { + this.formatter = formatter; + } + + public String getLogNameFormatter() { + return logNameFormatter; + } + + public void setLogNameFormatter(String logNameFormatter) { + this.logNameFormatter = logNameFormatter; + } + + public String getLogFilter() { + return logFilter; + } + + public void setLogFilter(String logFilter) { + this.logFilter = logFilter; + } + + public String getMaxSplitTime() { + return maxSplitTime; + } + + public void setMaxSplitTime(String maxSplitTime) { + this.maxSplitTime = maxSplitTime; + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogFilter.java b/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogFilter.java new file mode 100644 index 000000000..b7c144765 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogFilter.java @@ -0,0 +1,12 @@ +package com.fireflysource.log; + +/** + * @author Pengtao Qiu + */ +public class DefaultLogFilter implements LogFilter { + + @Override + public void filter(LogItem logItem) { + + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogFormatter.java b/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogFormatter.java new file mode 100644 index 000000000..fa3ed244e --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogFormatter.java @@ -0,0 +1,13 @@ +package com.fireflysource.log; + +/** + * @author Pengtao Qiu + */ +public class DefaultLogFormatter implements LogFormatter { + + @Override + public String format(LogItem logItem) { + return logItem.toString(); + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogNameFormatter.java b/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogNameFormatter.java new file mode 100644 index 000000000..95413db04 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/DefaultLogNameFormatter.java @@ -0,0 +1,38 @@ +package com.fireflysource.log; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.temporal.ChronoField.*; + +/** + * @author Pengtao Qiu + */ +public class DefaultLogNameFormatter implements LogNameFormatter { + + public static final DateTimeFormatter DEFAULT_LOG_NAME_FORMATTER = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral('_') + .append(new DateTimeFormatterBuilder() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral('-') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral('-') + .appendValue(SECOND_OF_MINUTE, 2) + .toFormatter()) + .toFormatter(); + + @Override + public String format(String name, LocalDateTime localDateTime) { + return name + ".txt"; + } + + @Override + public String formatBak(String name, LocalDateTime localDateTime, int index) { + return name + "." + localDateTime.format(DEFAULT_LOG_NAME_FORMATTER) + "." + index + ".bak.txt"; + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/Log.java b/firefly-slf4j/src/main/java/com/fireflysource/log/Log.java new file mode 100644 index 000000000..28674577d --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/Log.java @@ -0,0 +1,50 @@ +package com.fireflysource.log; + +import java.io.Closeable; + +public interface Log extends Closeable { + + String CL = "\r\n"; + + String getName(); + + boolean isTraceEnabled(); + + void trace(String str); + + void trace(String str, Object... objs); + + void trace(String str, Throwable throwable, Object... objs); + + boolean isDebugEnabled(); + + void debug(String str); + + void debug(String str, Object... objs); + + void debug(String str, Throwable throwable, Object... objs); + + boolean isInfoEnabled(); + + void info(String str); + + void info(String str, Object... objs); + + void info(String str, Throwable throwable, Object... objs); + + boolean isWarnEnabled(); + + void warn(String str); + + void warn(String str, Object... objs); + + void warn(String str, Throwable throwable, Object... objs); + + boolean isErrorEnabled(); + + void error(String str); + + void error(String str, Object... objs); + + void error(String str, Throwable throwable, Object... objs); +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogConfigParser.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogConfigParser.java new file mode 100644 index 000000000..12b8d85f2 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogConfigParser.java @@ -0,0 +1,26 @@ +package com.fireflysource.log; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +public interface LogConfigParser { + + String DEFAULT_XML_CONFIG_FILE_NAME = "firefly-log.xml"; + String DEFAULT_LOG_NAME = "firefly-system"; + String DEFAULT_LOG_LEVEL = "INFO"; + long DEFAULT_MAX_FILE_SIZE = 209715200; + File DEFAULT_LOG_DIRECTORY = new File(System.getProperty("user.dir"), "logs"); + boolean DEFAULT_CONSOLE_ENABLED = false; + Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + MaxSplitTimeEnum DEFAULT_MAX_SPLIT_TIME = MaxSplitTimeEnum.DAY; + LogFormatter DEFAULT_LOG_FORMATTER = new DefaultLogFormatter(); + LogNameFormatter DEFAULT_LOG_NAME_FORMATTER = new DefaultLogNameFormatter(); + LogFilter DEFAULT_LOG_FILTER = new DefaultLogFilter(); + + boolean parse(Consumer consumer); + + FileLog createDefaultLog(); + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogFactory.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogFactory.java new file mode 100644 index 000000000..c231e3b79 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogFactory.java @@ -0,0 +1,86 @@ +package com.fireflysource.log; + +import com.fireflysource.log.internal.utils.collection.TreeTrie; +import com.fireflysource.log.internal.utils.collection.Trie; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class LogFactory implements Closeable { + + private final Trie logTree = new TreeTrie<>(); + private final AtomicBoolean closed = new AtomicBoolean(false); + + private LogFactory() { + LogConfigParser parser = new XmlLogConfigParser(); + boolean success = parser.parse((fileLog) -> logTree.put(fileLog.getName(), fileLog)); + + if (!success) { + System.out.println("log configuration parsing failure!"); + } + + if (logTree.get(LogConfigParser.DEFAULT_LOG_NAME) == null) { + FileLog fileLog = parser.createDefaultLog(); + logTree.put(fileLog.getName(), fileLog); + } + } + + public static LogFactory getInstance() { + return Holder.INSTANCE; + } + + public Log getLog(Class clazz) { + return getLog(clazz.getName()); + } + + public Log getLog(String name) { + Log log = logTree.getBest(name); + if (log == null) { + log = logTree.get(LogConfigParser.DEFAULT_LOG_NAME); + } + return new ClassNameLogWrap(log, name); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + logTree.keySet().forEach(k -> { + Log log = logTree.get(k); + try { + log.close(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + + + ExecutorService pool = FileLog.Companion.getExecutor(); + long timeout = 15; + TimeUnit unit = TimeUnit.SECONDS; + pool.shutdown(); // Disable new tasks from being submitted + try { + // Wait a while for existing tasks to terminate + if (!pool.awaitTermination(timeout, unit)) { + pool.shutdownNow(); // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!pool.awaitTermination(timeout, unit)) + System.err.println("Pool did not terminate"); + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted + pool.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + System.out.println("The file log thread is shutdown"); + } + } + + private static class Holder { + private static final LogFactory INSTANCE = new LogFactory(); + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogFilter.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogFilter.java new file mode 100644 index 000000000..7663074da --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogFilter.java @@ -0,0 +1,11 @@ +package com.fireflysource.log; + +/** + * @author Pengtao Qiu + */ +@FunctionalInterface +public interface LogFilter { + + void filter(LogItem logItem); + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogFormatter.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogFormatter.java new file mode 100644 index 000000000..a7b2377d4 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogFormatter.java @@ -0,0 +1,11 @@ +package com.fireflysource.log; + +/** + * @author Pengtao Qiu + */ +@FunctionalInterface +public interface LogFormatter { + + String format(LogItem logItem); + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogItem.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogItem.java new file mode 100644 index 000000000..0b3bedd3c --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogItem.java @@ -0,0 +1,148 @@ +package com.fireflysource.log; + +import com.fireflysource.log.internal.utils.StringUtils; +import com.fireflysource.log.internal.utils.TimeUtils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Date; +import java.util.Map; + +import static com.fireflysource.log.internal.utils.TimeUtils.DEFAULT_LOCAL_DATE_TIME; + +public class LogItem { + + private String name; + private String className; + private String content; + private String level; + private Object[] objs; + private Throwable throwable; + private StackTraceElement stackTraceElement; + private String logStr; + private Map mdcData; + private Date date; + private String threadName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + public Object[] getObjs() { + return objs; + } + + public void setObjs(Object[] objs) { + this.objs = objs; + } + + public Throwable getThrowable() { + return throwable; + } + + public void setThrowable(Throwable throwable) { + this.throwable = throwable; + } + + public StackTraceElement getStackTraceElement() { + return stackTraceElement; + } + + public void setStackTraceElement(StackTraceElement stackTraceElement) { + this.stackTraceElement = stackTraceElement; + } + + public Map getMdcData() { + return mdcData; + } + + public void setMdcData(Map mdcData) { + this.mdcData = mdcData; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public String getThreadName() { + return threadName; + } + + public void setThreadName(String threadName) { + this.threadName = threadName; + } + + public String renderContentTemplate() { + String ret = StringUtils.replace(content, objs); + if (throwable != null) { + StringWriter str = new StringWriter(); + try (PrintWriter out = new PrintWriter(str)) { + out.println(); + out.println("$err_start"); + throwable.printStackTrace(out); + out.println("$err_end"); + } + ret += str.toString(); + } + return ret; + } + + @Override + public String toString() { + if (logStr == null) { + logStr = level + " " + TimeUtils.format(date, DEFAULT_LOCAL_DATE_TIME); + + if (mdcData != null && !mdcData.isEmpty()) { + logStr += " " + mdcData; + } + + if (StringUtils.hasText(className)) { + logStr += " " + className; + } + + if (StringUtils.hasText(threadName)) { + logStr += " " + threadName; + } + + if (stackTraceElement != null) { + logStr += " " + stackTraceElement; + } + + logStr += "\t" + renderContentTemplate(); + } + return logStr; + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogLevel.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogLevel.java new file mode 100644 index 000000000..43b71fe74 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogLevel.java @@ -0,0 +1,64 @@ +package com.fireflysource.log; + +import java.util.HashMap; +import java.util.Map; + +public enum LogLevel { + + TRACE(0, "TRACE"), + DEBUG(1, "DEBUG"), + INFO(2, "INFO"), + WARN(3, "WARN"), + ERROR(4, "ERROR"); + + private static final LogLevel[] levels = new LogLevel[5]; + private static final Map levelNameMap = new HashMap<>(); + + static { + for (LogLevel logLevel : LogLevel.values()) { + levels[logLevel.level] = logLevel; + levelNameMap.put(logLevel.name, logLevel); + } + } + + private final int level; + private final String name; + + LogLevel(int level, String name) { + this.level = level; + this.name = name; + } + + public static LogLevel fromLevel(int level) { + if (level >= 0 && level < levels.length) { + return levels[level]; + } else { + return INFO; + } + } + + public static LogLevel fromName(String name) { + if (name == null) + return INFO; + + LogLevel logLevel = levelNameMap.get(name); + if (logLevel == null) { + return INFO; + } else { + return levelNameMap.get(name); + } + } + + public int getLevel() { + return level; + } + + public String getName() { + return name; + } + + public boolean isEnabled(LogLevel logLevel) { + return this.level <= logLevel.level; + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/LogNameFormatter.java b/firefly-slf4j/src/main/java/com/fireflysource/log/LogNameFormatter.java new file mode 100644 index 000000000..7045fb82a --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/LogNameFormatter.java @@ -0,0 +1,14 @@ +package com.fireflysource.log; + +import java.time.LocalDateTime; + +/** + * @author Pengtao Qiu + */ +public interface LogNameFormatter { + + String format(String name, LocalDateTime localDateTime); + + String formatBak(String name, LocalDateTime localDateTime, int index); + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/MappedDiagnosticContext.java b/firefly-slf4j/src/main/java/com/fireflysource/log/MappedDiagnosticContext.java new file mode 100644 index 000000000..24e0ebcbd --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/MappedDiagnosticContext.java @@ -0,0 +1,24 @@ +package com.fireflysource.log; + +import java.util.Map; +import java.util.Set; + +/** + * @author Pengtao Qiu + */ +public interface MappedDiagnosticContext { + + void put(String key, String val); + + String get(String key); + + void remove(String key); + + void clear(); + + Set getKeys(); + + Map getCopyOfContextMap(); + + void setContextMap(Map contextMap); +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/MappedDiagnosticContextFactory.java b/firefly-slf4j/src/main/java/com/fireflysource/log/MappedDiagnosticContextFactory.java new file mode 100644 index 000000000..9cd44dfcb --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/MappedDiagnosticContextFactory.java @@ -0,0 +1,24 @@ +package com.fireflysource.log; + +import com.fireflysource.log.internal.utils.ServiceUtils; + +/** + * @author Pengtao Qiu + */ +public class MappedDiagnosticContextFactory { + + private static MappedDiagnosticContextFactory ourInstance = new MappedDiagnosticContextFactory(); + private MappedDiagnosticContext mappedDiagnosticContext; + + private MappedDiagnosticContextFactory() { + mappedDiagnosticContext = ServiceUtils.loadService(MappedDiagnosticContext.class, new ThreadLocalMappedDiagnosticContext()); + } + + public static MappedDiagnosticContextFactory getInstance() { + return ourInstance; + } + + public MappedDiagnosticContext getMappedDiagnosticContext() { + return mappedDiagnosticContext; + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/MaxSplitTimeEnum.java b/firefly-slf4j/src/main/java/com/fireflysource/log/MaxSplitTimeEnum.java new file mode 100644 index 000000000..707509636 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/MaxSplitTimeEnum.java @@ -0,0 +1,25 @@ +package com.fireflysource.log; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author Pengtao Qiu + */ +public enum MaxSplitTimeEnum { + MINUTE("minute"), HOUR("hour"), DAY("day"); + + private final String value; + + MaxSplitTimeEnum(String value) { + this.value = value; + } + + public static Optional from(String value) { + return Arrays.stream(MaxSplitTimeEnum.values()).filter(e -> e.value.equals(value)).findFirst(); + } + + public String getValue() { + return value; + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/ThreadLocalMappedDiagnosticContext.java b/firefly-slf4j/src/main/java/com/fireflysource/log/ThreadLocalMappedDiagnosticContext.java new file mode 100644 index 000000000..a9078f9db --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/ThreadLocalMappedDiagnosticContext.java @@ -0,0 +1,116 @@ +package com.fireflysource.log; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author Pengtao Qiu + */ +public class ThreadLocalMappedDiagnosticContext implements MappedDiagnosticContext { + + private InheritableThreadLocal> inheritableThreadLocal = new InheritableThreadLocal>() { + @Override + protected Map childValue(Map parentValue) { + if (parentValue == null) { + return null; + } + return new HashMap<>(parentValue); + } + }; + + /** + * Put a context value (the val parameter) as identified with + * the key parameter into the current thread's context map. + * Note that contrary to log4j, the val parameter can be null. + *

+ *

+ * If the current thread does not have a context map it is created as a side + * effect of this call. + * + * @throws IllegalArgumentException in case the "key" parameter is null + */ + @Override + public void put(String key, String val) { + if (key == null) { + throw new IllegalArgumentException("key cannot be null"); + } + Map map = inheritableThreadLocal.get(); + if (map == null) { + map = new HashMap<>(); + inheritableThreadLocal.set(map); + } + map.put(key, val); + } + + /** + * Get the context identified by the key parameter. + */ + @Override + public String get(String key) { + Map map = inheritableThreadLocal.get(); + if ((map != null) && (key != null)) { + return map.get(key); + } else { + return null; + } + } + + /** + * Remove the the context identified by the key parameter. + */ + @Override + public void remove(String key) { + Map map = inheritableThreadLocal.get(); + if (map != null) { + map.remove(key); + } + } + + /** + * Clear all entries in the MDC. + */ + @Override + public void clear() { + Map map = inheritableThreadLocal.get(); + if (map != null) { + map.clear(); + inheritableThreadLocal.remove(); + } + } + + /** + * Returns the keys in the MDC as a {@link Set} of {@link String}s The + * returned value can be null. + * + * @return the keys in the MDC + */ + @Override + public Set getKeys() { + Map map = inheritableThreadLocal.get(); + if (map != null) { + return map.keySet(); + } else { + return null; + } + } + + /** + * Return a copy of the current thread's context map. + * Returned value may be null. + */ + @Override + public Map getCopyOfContextMap() { + Map oldMap = inheritableThreadLocal.get(); + if (oldMap != null) { + return new HashMap<>(oldMap); + } else { + return null; + } + } + + @Override + public void setContextMap(Map contextMap) { + inheritableThreadLocal.set(new HashMap<>(contextMap)); + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/XmlLogConfigParser.java b/firefly-slf4j/src/main/java/com/fireflysource/log/XmlLogConfigParser.java new file mode 100644 index 000000000..e619699ba --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/XmlLogConfigParser.java @@ -0,0 +1,54 @@ +package com.fireflysource.log; + +import com.fireflysource.log.internal.utils.xml.DefaultDom; +import com.fireflysource.log.internal.utils.xml.Dom; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.function.Consumer; + +public class XmlLogConfigParser extends AbstractLogConfigParser { + + @Override + public boolean parse(Consumer consumer) { + Dom dom = new DefaultDom(); + Document doc = dom.getDocument(DEFAULT_XML_CONFIG_FILE_NAME); + if (doc == null) { + return false; + } + Element root = dom.getRoot(doc); + List loggerList = dom.elements(root, "logger"); + if (loggerList == null || loggerList.isEmpty()) { + return false; + } else { + for (Element e : loggerList) { + Configuration c = new Configuration(); + c.setName(dom.getTextValueByTagName(e, "name", DEFAULT_LOG_NAME)); + c.setLevel(dom.getTextValueByTagName(e, "level", DEFAULT_LOG_LEVEL)); + c.setPath(dom.getTextValueByTagName(e, "path", DEFAULT_LOG_DIRECTORY.getAbsolutePath())); + try { + c.setConsole(Boolean.parseBoolean(dom.getTextValueByTagName(e, "enable-console"))); + } catch (Exception ex) { + c.setConsole(DEFAULT_CONSOLE_ENABLED); + } + try { + c.setMaxFileSize(Long.parseLong(dom.getTextValueByTagName(e, "max-file-size"))); + } catch (Exception ex) { + c.setMaxFileSize(DEFAULT_MAX_FILE_SIZE); + } + if (c.getMaxFileSize() < 1024 * 1024 * 10) { + System.err.println("the max log file less than 10MB, please set a larger file size"); + } + c.setCharset(dom.getTextValueByTagName(e, "charset", DEFAULT_CHARSET.name())); + c.setFormatter(dom.getTextValueByTagName(e, "formatter", DEFAULT_LOG_FORMATTER.getClass().getName())); + c.setLogNameFormatter(dom.getTextValueByTagName(e, "log-name-formatter", DEFAULT_LOG_NAME_FORMATTER.getClass().getName())); + c.setLogFilter(dom.getTextValueByTagName(e, "log-filter", DEFAULT_LOG_FILTER.getClass().getName())); + c.setMaxSplitTime(dom.getTextValueByTagName(e, "max-split-time", DEFAULT_MAX_SPLIT_TIME.getValue())); + consumer.accept(createLog(c)); + } + } + return true; + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/ServiceUtils.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/ServiceUtils.java new file mode 100644 index 000000000..b93109572 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/ServiceUtils.java @@ -0,0 +1,21 @@ +package com.fireflysource.log.internal.utils; + +import java.util.ServiceLoader; + +/** + * @author Pengtao Qiu + */ +abstract public class ServiceUtils { + + public static T loadService(Class clazz, T defaultService) { + T service = null; + ServiceLoader serviceLoader = ServiceLoader.load(clazz); + for (T t : serviceLoader) { + service = t; + } + if (service == null) { + service = defaultService; + } + return service; + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/StringUtils.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/StringUtils.java new file mode 100644 index 000000000..f76dbb6d0 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/StringUtils.java @@ -0,0 +1,371 @@ +package com.fireflysource.log.internal.utils; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Pengtao Qiu + */ +abstract public class StringUtils { + + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + public static final String EMPTY = ""; + + public static boolean hasText(String str) { + return hasText((CharSequence) str); + } + + public static boolean hasText(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + public static boolean hasLength(String str) { + return hasLength((CharSequence) str); + } + + public static String replace(String s, Object... objs) { + if (objs == null || objs.length == 0) + return s; + if (!s.contains("{}")) + return s; + + StringBuilder ret = new StringBuilder((int) (s.length() * 1.5)); + int cursor = 0; + int index = 0; + for (int start; (start = s.indexOf("{}", cursor)) != -1; ) { + ret.append(s, cursor, start); + if (index < objs.length) { + Object obj = objs[index]; + try { + if (obj != null) { + if (obj instanceof AbstractCollection) { + ret.append(Arrays.toString(((AbstractCollection) obj).toArray())); + } else { + ret.append(obj); + } + } else { + ret.append("null"); + } + } catch (Throwable ignored) { + } + } else { + ret.append("{}"); + } + cursor = start + 2; + index++; + } + ret.append(s, cursor, s.length()); + return ret.toString(); + } + + // Splitting + // ----------------------------------------------------------------------- + + /** + *

+ * Splits the provided text into an array, using whitespace as the + * separator. Whitespace is defined by {@link Character#isWhitespace(char)}. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. For more control over the split + * use the StrTokenizer class. + *

+ *

+ *

+ * A null input String returns null. + *

+ *

+ *

+     * StringUtils.split(null)       = null
+     * StringUtils.split("")         = []
+     * StringUtils.split("abc def")  = ["abc", "def"]
+     * StringUtils.split("abc  def") = ["abc", "def"]
+     * StringUtils.split(" abc ")    = ["abc"]
+     * 
+ * + * @param str the String to parse, may be null + * @return an array of parsed Strings, null if null String + * input + */ + public static String[] split(String str) { + return split(str, null, -1); + } + + /** + *

+ * Splits the provided text into an array, separators specified. This is an + * alternative to using StringTokenizer. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. For more control over the split + * use the StrTokenizer class. + *

+ *

+ *

+ * A null input String returns null. A + * null separatorChars splits on whitespace. + *

+ *

+ *

+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("abc def", null) = ["abc", "def"]
+     * StringUtils.split("abc def", " ")  = ["abc", "def"]
+     * StringUtils.split("abc  def", " ") = ["abc", "def"]
+     * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, null + * splits on whitespace + * @return an array of parsed Strings, null if null String + * input + */ + public static String[] split(String str, String separatorChars) { + return splitWorker(str, separatorChars, -1, false); + } + + /** + *

+ * Splits the provided text into an array, separator specified. This is an + * alternative to using StringTokenizer. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. For more control over the split + * use the StrTokenizer class. + *

+ *

+ *

+ * A null input String returns null. + *

+ *

+ *

+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("a.b.c", '.')    = ["a", "b", "c"]
+     * StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
+     * StringUtils.split("a:b:c", '.')    = ["a:b:c"]
+     * StringUtils.split("a b c", ' ')    = ["a", "b", "c"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChar the character used as the delimiter + * @return an array of parsed Strings, null if null String + * input + * @since 2.0 + */ + public static String[] split(String str, char separatorChar) { + return splitWorker(str, separatorChar, false); + } + + /** + *

+ * Splits the provided text into an array with a maximum length, separators + * specified. + *

+ *

+ *

+ * The separator is not included in the returned String array. Adjacent + * separators are treated as one separator. + *

+ *

+ *

+ * A null input String returns null. A + * null separatorChars splits on whitespace. + *

+ *

+ *

+ * If more than max delimited substrings are found, the last + * returned string includes all characters after the first + * max - 1 returned strings (including separator characters). + *

+ *

+ *

+     * StringUtils.split(null, *, *)            = null
+     * StringUtils.split("", *, *)              = []
+     * StringUtils.split("ab de fg", null, 0)   = ["ab", "cd", "ef"]
+     * StringUtils.split("ab   de fg", null, 0) = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, null + * splits on whitespace + * @param max the maximum number of elements to include in the array. A zero + * or negative value implies no limit + * @return an array of parsed Strings, null if null String + * input + */ + public static String[] split(String str, String separatorChars, int max) { + return splitWorker(str, separatorChars, max, false); + } + + /** + * Performs the logic for the split and + * splitPreserveAllTokens methods that return a maximum array + * length. + * + * @param str the String to parse, may be null + * @param separatorChars the separate character + * @param max the maximum number of elements to include in the array. A zero + * or negative value implies no limit. + * @param preserveAllTokens if true, adjacent separators are treated as empty + * token separators; if false, adjacent separators + * are treated as one separator. + * @return an array of parsed Strings, null if null String + * input + */ + private static String[] splitWorker(String str, String separatorChars, int max, boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + // Direct code is quicker than StringTokenizer. + // Also, StringTokenizer uses isSpace() not isWhitespace() + + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return EMPTY_STRING_ARRAY; + } + List list = new ArrayList<>(); + int sizePlus1 = 1; + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + if (separatorChars == null) { + // Null separator means use whitespace + while (i < len) { + if (Character.isWhitespace(str.charAt(i))) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else if (separatorChars.length() == 1) { + // Optimise 1 character case + char sep = separatorChars.charAt(0); + while (i < len) { + if (str.charAt(i) == sep) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else { + // standard case + while (i < len) { + if (separatorChars.indexOf(str.charAt(i)) >= 0) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } + if (match || (preserveAllTokens && lastMatch)) { + list.add(str.substring(start, i)); + } + return list.toArray(EMPTY_STRING_ARRAY); + } + + /** + * Performs the logic for the split and + * splitPreserveAllTokens methods that do not return a maximum + * array length. + * + * @param str the String to parse, may be null + * @param separatorChar the separate character + * @param preserveAllTokens if true, adjacent separators are treated as empty + * token separators; if false, adjacent separators + * are treated as one separator. + * @return an array of parsed Strings, null if null String + * input + */ + private static String[] splitWorker(String str, char separatorChar, boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return EMPTY_STRING_ARRAY; + } + List list = new ArrayList<>(); + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + while (i < len) { + if (str.charAt(i) == separatorChar) { + if (match || preserveAllTokens) { + list.add(str.substring(start, i)); + match = false; + lastMatch = true; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + if (match || (preserveAllTokens && lastMatch)) { + list.add(str.substring(start, i)); + } + return list.toArray(EMPTY_STRING_ARRAY); + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/TimeUtils.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/TimeUtils.java new file mode 100644 index 000000000..1509b8d00 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/TimeUtils.java @@ -0,0 +1,109 @@ +package com.fireflysource.log.internal.utils; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.SignStyle; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Date; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.temporal.ChronoField.*; + +/** + * @author Pengtao Qiu + */ +abstract public class TimeUtils { + + public static final DateTimeFormatter LOCAL_DATE_SLASH_SEPARATOR = new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('/') + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral('/') + .appendValue(DAY_OF_MONTH, 2) + .toFormatter(); + + public static final DateTimeFormatter DEFAULT_LOCAL_MONTH = new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(MONTH_OF_YEAR, 2) + .toFormatter(); + + public static final DateTimeFormatter DEFAULT_LOCAL_DATE = new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(DAY_OF_MONTH, 2) + .toFormatter(); + + public static final DateTimeFormatter DEFAULT_LOCAL_DATE_TIME = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral(' ') + .append(new DateTimeFormatterBuilder() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .toFormatter()) + .toFormatter(); + + public static final DateTimeFormatter ISO_LOCAL_DATE_TIME = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral('T') + .append(new DateTimeFormatterBuilder() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .toFormatter()) + .appendLiteral('Z') + .toFormatter(); + + public static Date toDate(LocalDate localDate) { + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + public static Date toDate(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + public static LocalDate toLocalDate(Date date) { + return LocalDate.from(date.toInstant().atZone(ZoneId.systemDefault())); + } + + public static LocalDateTime toLocalDateTime(Date date) { + return LocalDateTime.from(date.toInstant().atZone(ZoneId.systemDefault())); + } + + public static String format(Date date, DateTimeFormatter formatter) { + return date.toInstant().atZone(ZoneId.systemDefault()).format(formatter); + } + + public static String format(Date date, ZoneOffset offset, DateTimeFormatter formatter) { + return date.toInstant().atOffset(offset).format(formatter); + } + + public static Date parseLocalDate(String text, DateTimeFormatter formatter) { + return toDate(LocalDate.parse(text, formatter)); + } + + public static Date parseLocalDateTime(String text, DateTimeFormatter formatter) { + return toDate(LocalDateTime.parse(text, formatter)); + } + + public static LocalDate parseYearMonth(String text, DateTimeFormatter formatter) { + return YearMonth.parse(text, formatter).atDay(1); + } + + public static long between(ChronoUnit chronoUnit, Temporal temporal1Inclusive, Temporal temporal2Exclusive) { + return chronoUnit.between(temporal1Inclusive, temporal2Exclusive); + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/AbstractTrie.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/AbstractTrie.java new file mode 100644 index 000000000..e03807dae --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/AbstractTrie.java @@ -0,0 +1,50 @@ +package com.fireflysource.log.internal.utils.collection; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public abstract class AbstractTrie implements Trie { + final boolean _caseInsensitive; + + protected AbstractTrie(boolean insensitive) { + _caseInsensitive = insensitive; + } + + @Override + public boolean put(V v) { + return put(v.toString(), v); + } + + @Override + public V remove(String s) { + V o = get(s); + put(s, null); + return o; + } + + @Override + public V get(String s) { + return get(s, 0, s.length()); + } + + @Override + public V get(ByteBuffer b) { + return get(b, 0, b.remaining()); + } + + @Override + public V getBest(String s) { + return getBest(s, 0, s.length()); + } + + @Override + public V getBest(byte[] b, int offset, int len) { + return getBest(new String(b, offset, len, StandardCharsets.UTF_8)); + } + + @Override + public boolean isCaseInsensitive() { + return _caseInsensitive; + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/TreeTrie.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/TreeTrie.java new file mode 100644 index 000000000..a775fc049 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/TreeTrie.java @@ -0,0 +1,325 @@ +package com.fireflysource.log.internal.utils.collection; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; + +/** + * A Trie String lookup data structure using a tree + *

+ * This implementation is always case insensitive and is optimal for a variable + * number of fixed strings with few special characters. + *

+ *

+ * This Trie is stored in a Tree and is unlimited in capacity + *

+ *

+ *

+ * This Trie is not Threadsafe and contains no mutual exclusion or deliberate + * memory barriers. It is intended for an ArrayTrie to be built by a single + * thread and then used concurrently by multiple threads and not mutated during + * that access. If concurrent mutations of the Trie is required external locks + * need to be applied. + *

+ * + * @param the entry type + */ +public class TreeTrie extends AbstractTrie { + private static final int[] __lookup = + { // 0 1 2 3 4 5 6 7 8 9 A B C D E F + /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1, + /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1, + /*4*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*5*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + }; + private static final int INDEX = 32; + private final TreeTrie[] _nextIndex; + private final List> _nextOther = new ArrayList<>(); + private final char _c; + private String _key; + private V _value; + + @SuppressWarnings("unchecked") + public TreeTrie() { + super(true); + _nextIndex = new TreeTrie[INDEX]; + _c = 0; + } + + @SuppressWarnings("unchecked") + private TreeTrie(char c) { + super(true); + _nextIndex = new TreeTrie[INDEX]; + this._c = c; + } + + private static void toString(Appendable out, TreeTrie t) { + if (t != null) { + if (t._value != null) { + try { + out.append(','); + out.append(t._key); + out.append('='); + out.append(t._value.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + for (int i = 0; i < INDEX; i++) { + if (t._nextIndex[i] != null) + toString(out, t._nextIndex[i]); + } + for (int i = t._nextOther.size(); i-- > 0; ) + toString(out, t._nextOther.get(i)); + } + } + + private static void keySet(Set set, TreeTrie t) { + if (t != null) { + if (t._key != null) + set.add(t._key); + + for (int i = 0; i < INDEX; i++) { + if (t._nextIndex[i] != null) + keySet(set, t._nextIndex[i]); + } + for (int i = t._nextOther.size(); i-- > 0; ) + keySet(set, t._nextOther.get(i)); + } + } + + @Override + public void clear() { + Arrays.fill(_nextIndex, null); + _nextOther.clear(); + _key = null; + _value = null; + } + + @Override + public boolean put(String s, V v) { + TreeTrie t = this; + int limit = s.length(); + for (int k = 0; k < limit; k++) { + char c = s.charAt(k); + + int index = c >= 0 && c < 0x7f ? __lookup[c] : -1; + if (index >= 0) { + if (t._nextIndex[index] == null) + t._nextIndex[index] = new TreeTrie(c); + t = t._nextIndex[index]; + } else { + TreeTrie n = null; + for (int i = t._nextOther.size(); i-- > 0; ) { + n = t._nextOther.get(i); + if (n._c == c) + break; + n = null; + } + if (n == null) { + n = new TreeTrie<>(c); + t._nextOther.add(n); + } + t = n; + } + } + t._key = v == null ? null : s; + t._value = v; + return true; + } + + @Override + public V get(String s, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + char c = s.charAt(offset + i); + int index = c >= 0 && c < 0x7f ? __lookup[c] : -1; + if (index >= 0) { + if (t._nextIndex[index] == null) + return null; + t = t._nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t._nextOther.size(); j-- > 0; ) { + n = t._nextOther.get(j); + if (n._c == c) + break; + n = null; + } + if (n == null) + return null; + t = n; + } + } + return t._value; + } + + @Override + public V get(ByteBuffer b, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + byte c = b.get(offset + i); + int index = c >= 0 && c < 0x7f ? __lookup[c] : -1; + if (index >= 0) { + if (t._nextIndex[index] == null) + return null; + t = t._nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t._nextOther.size(); j-- > 0; ) { + n = t._nextOther.get(j); + if (n._c == c) + break; + n = null; + } + if (n == null) + return null; + t = n; + } + } + return t._value; + } + + @Override + public V getBest(byte[] b, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + byte c = b[offset + i]; + int index = c >= 0 && c < 0x7f ? __lookup[c] : -1; + if (index >= 0) { + if (t._nextIndex[index] == null) + break; + t = t._nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t._nextOther.size(); j-- > 0; ) { + n = t._nextOther.get(j); + if (n._c == c) + break; + n = null; + } + if (n == null) + break; + t = n; + } + + // Is the next Trie is a match + if (t._key != null) { + // Recurse so we can remember this possibility + V best = t.getBest(b, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return t._value; + } + + @Override + public V getBest(String s, int offset, int len) { + TreeTrie t = this; + for (int i = 0; i < len; i++) { + byte c = (byte) (0xff & s.charAt(offset + i)); + int index = c >= 0 && c < 0x7f ? __lookup[c] : -1; + if (index >= 0) { + if (t._nextIndex[index] == null) + break; + t = t._nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t._nextOther.size(); j-- > 0; ) { + n = t._nextOther.get(j); + if (n._c == c) + break; + n = null; + } + if (n == null) + break; + t = n; + } + + // Is the next Trie is a match + if (t._key != null) { + // Recurse so we can remember this possibility + V best = t.getBest(s, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return t._value; + } + + @Override + public V getBest(ByteBuffer b, int offset, int len) { + if (b.hasArray()) + return getBest(b.array(), b.arrayOffset() + b.position() + offset, len); + return getBestByteBuffer(b, offset, len); + } + + private V getBestByteBuffer(ByteBuffer b, int offset, int len) { + TreeTrie t = this; + int pos = b.position() + offset; + for (int i = 0; i < len; i++) { + byte c = b.get(pos++); + int index = c >= 0 && c < 0x7f ? __lookup[c] : -1; + if (index >= 0) { + if (t._nextIndex[index] == null) + break; + t = t._nextIndex[index]; + } else { + TreeTrie n = null; + for (int j = t._nextOther.size(); j-- > 0; ) { + n = t._nextOther.get(j); + if (n._c == c) + break; + n = null; + } + if (n == null) + break; + t = n; + } + + // Is the next Trie is a match + if (t._key != null) { + // Recurse so we can remember this possibility + V best = t.getBest(b, offset + i + 1, len - i - 1); + if (best != null) + return best; + break; + } + } + return t._value; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + toString(buf, this); + + if (buf.length() == 0) + return "{}"; + + buf.setCharAt(0, '{'); + buf.append('}'); + return buf.toString(); + } + + @Override + public Set keySet() { + Set keys = new HashSet<>(); + keySet(keys, this); + return keys; + } + + @Override + public boolean isFull() { + return false; + } + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/Trie.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/Trie.java new file mode 100644 index 000000000..6d4063072 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/collection/Trie.java @@ -0,0 +1,116 @@ +package com.fireflysource.log.internal.utils.collection; + +import java.nio.ByteBuffer; +import java.util.Set; + +/** + * A Trie String lookup data structure. + * + * @param the Trie entry type + */ +public interface Trie { + + /** + * Put an entry into the Trie + * + * @param s The key for the entry + * @param v The value of the entry + * @return True if the Trie had capacity to add the field. + */ + boolean put(String s, V v); + + /** + * Put a value as both a key and a value. + * + * @param v The value and key + * @return True if the Trie had capacity to add the field. + */ + boolean put(V v); + + V remove(String s); + + /** + * Get an exact match from a String key + * + * @param s The key + * @return the value for the string key + */ + V get(String s); + + /** + * Get an exact match from a String key + * + * @param s The key + * @param offset The offset within the string of the key + * @param len the length of the key + * @return the value for the string / offset / length + */ + V get(String s, int offset, int len); + + /** + * Get an exact match from a segment of a ByteBuufer as key + * + * @param b The buffer + * @return The value or null if not found + */ + V get(ByteBuffer b); + + /** + * Get an exact match from a segment of a ByteBuufer as key + * + * @param b The buffer + * @param offset The offset within the buffer of the key + * @param len the length of the key + * @return The value or null if not found + */ + V get(ByteBuffer b, int offset, int len); + + /** + * Get the best match from key in a String. + * + * @param s The string + * @return The value or null if not found + */ + V getBest(String s); + + /** + * Get the best match from key in a String. + * + * @param s The string + * @param offset The offset within the string of the key + * @param len the length of the key + * @return The value or null if not found + */ + V getBest(String s, int offset, int len); + + /** + * Get the best match from key in a byte array. + * The key is assumed to by ISO_8859_1 characters. + * + * @param b The buffer + * @param offset The offset within the array of the key + * @param len the length of the key + * @return The value or null if not found + */ + V getBest(byte[] b, int offset, int len); + + /** + * Get the best match from key in a byte buffer. + * The key is assumed to by ISO_8859_1 characters. + * + * @param b The buffer + * @param offset The offset within the buffer of the key + * @param len the length of the key + * @return The value or null if not found + */ + V getBest(ByteBuffer b, int offset, int len); + + Set keySet(); + + boolean isFull(); + + boolean isCaseInsensitive(); + + void clear(); + +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/DefaultDom.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/DefaultDom.java new file mode 100644 index 000000000..d2ff2f4a8 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/DefaultDom.java @@ -0,0 +1,113 @@ +package com.fireflysource.log.internal.utils.xml; + +import com.fireflysource.log.internal.utils.StringUtils; +import org.w3c.dom.CharacterData; +import org.w3c.dom.*; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class DefaultDom implements Dom { + + private DocumentBuilder documentBuilder; + + public DefaultDom() { + try { + documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new XmlParsingException(e); + } + } + + @Override + public Document getDocument(String file) { + try (InputStream is = DefaultDom.class.getResourceAsStream("/" + file)) { + if (is == null) { + throw new XmlParsingException("the configuration file: " + file + " is not found"); + } + Document doc = documentBuilder.parse(is); + return doc; + } catch (Exception e) { + throw new XmlParsingException(e); + } + } + + @Override + public Element getRoot(Document doc) { + return doc.getDocumentElement(); + } + + @Override + public List elements(Element e) { + return elements(e, null); + } + + @Override + public List elements(Element e, String name) { + List eList = new ArrayList(); + + NodeList nodeList = e.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); ++i) { + Node node = nodeList.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + if (name != null) { + if (node.getNodeName().equals(name)) + eList.add((Element) node); + } else { + eList.add((Element) node); + } + } + } + return eList; + } + + @Override + public Element element(Element e, String name) { + NodeList element = e.getElementsByTagName(name); + if (element != null && e.getNodeType() == Node.ELEMENT_NODE) { + return (Element) element.item(0); + } + return null; + } + + @Override + public String getTextValue(Element valueElement) { + if (valueElement != null) { + StringBuilder sb = new StringBuilder(); + NodeList nl = valueElement.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node item = nl.item(i); + if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) { + sb.append(item.getNodeValue()); + } + } + return sb.toString().trim(); + } + return null; + } + + @Override + public String getTextValueByTagName(Element e, String name) { + Element valueElement = element(e, name); + if (valueElement == null) { + return null; + } else { + String value = getTextValue(valueElement); + if (StringUtils.hasText(value)) { + return value; + } else { + return null; + } + } + } + + @Override + public String getTextValueByTagName(Element e, String name, String defaultValue) { + String value = getTextValueByTagName(e, name); + return StringUtils.hasText(value) ? value : defaultValue; + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/Dom.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/Dom.java new file mode 100644 index 000000000..78d55a65e --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/Dom.java @@ -0,0 +1,79 @@ +package com.fireflysource.log.internal.utils.xml; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; + +public interface Dom { + + /** + * Get the XML document + * + * @param file The file relative path + * @return XML document + */ + Document getDocument(String file); + + /** + * Get the root node + * + * @param doc XML document object; + * @return The root element. + */ + Element getRoot(Document doc); + + /** + * Get the children elements + * + * @param e A current XML element + * @return All children elements + */ + List elements(Element e); + + /** + * Get the children elements by element name + * + * @param e A current XML element + * @param name Element name + * @return Children elements + */ + List elements(Element e, String name); + + /** + * Get a element by name + * + * @param e A current XML element + * @param name The child node name + * @return A XML element + */ + Element element(Element e, String name); + + /** + * Get the value of a XML element + * + * @param valueElement The value node + * @return The text value + */ + String getTextValue(Element valueElement); + + /** + * Get the value of a XML node + * + * @param e Current XML element + * @param name The child node name + * @return The text value + */ + String getTextValueByTagName(Element e, String name); + + /** + * Get the value of a XML node + * + * @param e Current XML element + * @param name The child node name + * @param defaultValue Default text value, when the child node is not found or the + * child node has not text value, the method return it + * @return The text value + */ + String getTextValueByTagName(Element e, String name, String defaultValue); +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/XmlParsingException.java b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/XmlParsingException.java new file mode 100644 index 000000000..e6ae67f81 --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/internal/utils/xml/XmlParsingException.java @@ -0,0 +1,15 @@ +package com.fireflysource.log.internal.utils.xml; + +/** + * @author Pengtao Qiu + */ +public class XmlParsingException extends RuntimeException { + + public XmlParsingException(String message) { + super(message); + } + + public XmlParsingException(Throwable cause) { + super(cause); + } +} diff --git a/firefly-slf4j/src/main/java/com/fireflysource/log/loggers.xsd b/firefly-slf4j/src/main/java/com/fireflysource/log/loggers.xsd new file mode 100644 index 000000000..23a6dd9bf --- /dev/null +++ b/firefly-slf4j/src/main/java/com/fireflysource/log/loggers.xsd @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/firefly-slf4j/src/main/java/org/slf4j/impl/LogFactoryImpl.java b/firefly-slf4j/src/main/java/org/slf4j/impl/LogFactoryImpl.java new file mode 100644 index 000000000..e93fad2c3 --- /dev/null +++ b/firefly-slf4j/src/main/java/org/slf4j/impl/LogFactoryImpl.java @@ -0,0 +1,37 @@ +package org.slf4j.impl; + +import com.fireflysource.log.Log; +import com.fireflysource.log.LogFactory; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; + +import java.io.Closeable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class LogFactoryImpl implements ILoggerFactory, Closeable { + + private final Map map = new ConcurrentHashMap<>(); + + @Override + public Logger getLogger(String name) { + Logger logger = map.get(name); + if (logger != null) { + return logger; + } else { + Log log = LogFactory.getInstance().getLog(name); + if (log != null) { + Logger newInstance = new LoggerImpl(log); + Logger oldInstance = map.putIfAbsent(name, newInstance); + return oldInstance == null ? newInstance : oldInstance; + } else { + return null; + } + } + } + + @Override + public void close() { + LogFactory.getInstance().close(); + } +} diff --git a/firefly-slf4j/src/main/java/org/slf4j/impl/LoggerImpl.java b/firefly-slf4j/src/main/java/org/slf4j/impl/LoggerImpl.java new file mode 100644 index 000000000..b2ef6fe43 --- /dev/null +++ b/firefly-slf4j/src/main/java/org/slf4j/impl/LoggerImpl.java @@ -0,0 +1,179 @@ +package org.slf4j.impl; + +import com.fireflysource.log.Log; +import org.slf4j.helpers.MarkerIgnoringBase; + +public class LoggerImpl extends MarkerIgnoringBase { + + private static final long serialVersionUID = 689005039688030280L; + + private Log log; + + public LoggerImpl(Log log) { + this.log = log; + } + + @Override + public String getName() { + return log.getName(); + } + + @Override + public boolean isTraceEnabled() { + return log.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + log.trace(msg); + } + + @Override + public void trace(String format, Object arg) { + log.trace(format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + log.trace(format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) { + log.trace(format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + log.trace(msg, t); + } + + @Override + public boolean isDebugEnabled() { + return log.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + log.debug(msg); + } + + @Override + public void debug(String format, Object arg) { + log.debug(format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + log.debug(format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) { + log.debug(format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + log.debug(msg, t); + } + + @Override + public boolean isInfoEnabled() { + return log.isInfoEnabled(); + } + + @Override + public void info(String msg) { + log.info(msg); + } + + @Override + public void info(String format, Object arg) { + log.info(format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + log.info(format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + log.info(format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + log.info(msg, t); + } + + @Override + public boolean isWarnEnabled() { + return log.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + log.warn(msg); + } + + @Override + public void warn(String format, Object arg) { + log.warn(format, arg); + } + + @Override + public void warn(String format, Object... arguments) { + log.warn(format, arguments); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + if (arg1 instanceof Throwable) { + log.warn(format, (Throwable) arg1, arg2); + } else { + log.warn(format, arg1, arg2); + } + } + + @Override + public void warn(String msg, Throwable t) { + log.warn(msg, t); + } + + @Override + public boolean isErrorEnabled() { + return log.isErrorEnabled(); + } + + @Override + public void error(String msg) { + log.error(msg); + } + + @Override + public void error(String format, Object arg) { + log.error(format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + if (arg1 instanceof Throwable) { + log.error(format, (Throwable) arg1, arg2); + } else { + log.error(format, arg1, arg2); + } + } + + @Override + public void error(String format, Object... arguments) { + log.error(format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + log.error(msg, t); + } + +} diff --git a/firefly-slf4j/src/main/java/org/slf4j/impl/MDCAdapterImpl.java b/firefly-slf4j/src/main/java/org/slf4j/impl/MDCAdapterImpl.java new file mode 100644 index 000000000..19d03f83a --- /dev/null +++ b/firefly-slf4j/src/main/java/org/slf4j/impl/MDCAdapterImpl.java @@ -0,0 +1,49 @@ +package org.slf4j.impl; + +import com.fireflysource.log.MappedDiagnosticContext; +import com.fireflysource.log.MappedDiagnosticContextFactory; +import org.slf4j.spi.MDCAdapter; + +import java.util.Map; + +/** + * @author Pengtao Qiu + */ +public class MDCAdapterImpl implements MDCAdapter { + + private MappedDiagnosticContext mdc; + + public MDCAdapterImpl() { + mdc = MappedDiagnosticContextFactory.getInstance().getMappedDiagnosticContext(); + } + + @Override + public void put(String key, String val) { + mdc.put(key, val); + } + + @Override + public String get(String key) { + return mdc.get(key); + } + + @Override + public void remove(String key) { + mdc.remove(key); + } + + @Override + public void clear() { + mdc.clear(); + } + + @Override + public Map getCopyOfContextMap() { + return mdc.getCopyOfContextMap(); + } + + @Override + public void setContextMap(Map contextMap) { + mdc.setContextMap(contextMap); + } +} diff --git a/firefly-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java b/firefly-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java new file mode 100644 index 000000000..294fc5bda --- /dev/null +++ b/firefly-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java @@ -0,0 +1,27 @@ +package org.slf4j.impl; + +import org.slf4j.ILoggerFactory; +import org.slf4j.spi.LoggerFactoryBinder; + +public class StaticLoggerBinder implements LoggerFactoryBinder { + + private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); + + private static final ILoggerFactory loggerFactory = new LogFactoryImpl(); + private static final String loggerFactoryClassStr = LogFactoryImpl.class.getName(); + + public static StaticLoggerBinder getSingleton() { + return SINGLETON; + } + + @Override + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + @Override + public String getLoggerFactoryClassStr() { + return loggerFactoryClassStr; + } + +} diff --git a/firefly-slf4j/src/main/java/org/slf4j/impl/StaticMDCBinder.java b/firefly-slf4j/src/main/java/org/slf4j/impl/StaticMDCBinder.java new file mode 100644 index 000000000..64514ce6f --- /dev/null +++ b/firefly-slf4j/src/main/java/org/slf4j/impl/StaticMDCBinder.java @@ -0,0 +1,28 @@ +package org.slf4j.impl; + +import com.fireflysource.log.internal.utils.ServiceUtils; +import org.slf4j.spi.MDCAdapter; + +/** + * @author Pengtao Qiu + */ +public class StaticMDCBinder { + + /** + * The unique instance of this class. + */ + public static final StaticMDCBinder SINGLETON = new StaticMDCBinder(); + private MDCAdapter mdca; + + private StaticMDCBinder() { + mdca = ServiceUtils.loadService(MDCAdapter.class, new MDCAdapterImpl()); + } + + public MDCAdapter getMDCA() { + return mdca; + } + + public String getMDCAdapterClassStr() { + return mdca.getClass().getName(); + } +} diff --git a/firefly-slf4j/src/main/kotlin/com/fireflysource/log/FileLog.kt b/firefly-slf4j/src/main/kotlin/com/fireflysource/log/FileLog.kt new file mode 100644 index 000000000..dbf0511d0 --- /dev/null +++ b/firefly-slf4j/src/main/kotlin/com/fireflysource/log/FileLog.kt @@ -0,0 +1,404 @@ +package com.fireflysource.log + +import com.fireflysource.log.LogConfigParser.* +import com.fireflysource.log.internal.utils.TimeUtils +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import java.io.* +import java.nio.charset.Charset +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* +import java.util.concurrent.* +import java.util.concurrent.CancellationException + + +/** + * @author Pengtao Qiu + */ +class FileLog : Log { + + companion object { + private val stackTrace = java.lang.Boolean.getBoolean("com.fireflysource.log.FileLog.debugMode") + private val fileBufferSize = Integer.getInteger("com.fireflysource.log.FileLog.bufferSize", 8192) + private val fileFlushInterval = java.lang.Long.getLong("com.fireflysource.log.FileLog.flushInterval", 500) + val executor = newSingleThreadExecutor() + private val fileLogThreadScope: CoroutineScope = + CoroutineScope(executor.asCoroutineDispatcher() + CoroutineName("FireflyFileLogThread")) + + private fun newSingleThreadExecutor(): ExecutorService { + return ThreadPoolExecutor( + 1, 1, 0, TimeUnit.MILLISECONDS, LinkedTransferQueue() + ) { runnable -> Thread(runnable, "firefly-log-thread") } + } + } + + sealed interface LogMessage + + @JvmInline + value class WriteLogMessage(val logItem: LogItem) : LogMessage + object Flush : LogMessage + object Stop : LogMessage + + + var level: LogLevel = LogLevel.fromName(DEFAULT_LOG_LEVEL) + var path: String = DEFAULT_LOG_DIRECTORY.absolutePath + var logName: String = DEFAULT_LOG_NAME + var consoleOutput: Boolean = false + var fileOutput: Boolean = true + var maxFileSize: Long = DEFAULT_MAX_FILE_SIZE + var logFormatter: LogFormatter = DEFAULT_LOG_FORMATTER + var logNameFormatter: LogNameFormatter = DEFAULT_LOG_NAME_FORMATTER + var logFilter: LogFilter = DEFAULT_LOG_FILTER + var maxSplitTime: MaxSplitTimeEnum = MaxSplitTimeEnum.DAY + var charset: Charset = DEFAULT_CHARSET + + private val mdc: MappedDiagnosticContext = MappedDiagnosticContextFactory.getInstance().mappedDiagnosticContext + + private val output = LogOutputStream() + private val channel = Channel(UNLIMITED) + private val consumerJob = fileLogThreadScope.launch { + while (true) { + val message = channel.receive() + val exit = handleWriteLogMessage(message) + if (exit) break + } + println("File log $logName is closed.") + } + private val flushTickJob = fileLogThreadScope.launch { + while (true) { + delay(fileFlushInterval) + channel.trySend(Flush) + } + } + + private fun handleWriteLogMessage(message: LogMessage): Boolean { + return when (message) { + is WriteLogMessage -> { + val logItem = message.logItem + logFilter.filter(logItem) + if (consoleOutput) { + println(logFormatter.format(logItem)) + } + + if (fileOutput) { + output.write(logFormatter.format(logItem), logItem.date) + } + return false + } + is Flush -> { + output.flush() + return false + } + is Stop -> true + } + } + + + private inner class LogOutputStream { + private var fileOutputStream: BufferedOutputStream? = null + private var writtenSize: Long = 0 + private var lastWrittenTime: LocalDateTime? = null + + private fun getLogName(localDateTime: LocalDateTime): String { + return logNameFormatter.format(name, localDateTime) + } + + private fun getLogBakName(localDateTime: LocalDateTime, index: Int): String { + return logNameFormatter.formatBak(name, localDateTime, index) + } + + private fun getLogBakName(localDateTime: LocalDateTime): String { + var index = 0 + var bakName = getLogBakName(localDateTime, index) + while (Files.exists(Paths.get(path, bakName))) { + index++ + bakName = getLogBakName(localDateTime, index) + } + return bakName + } + + private fun isNotSplitByTime(newLocalDateTime: LocalDateTime): Boolean { + val lastTime = lastWrittenTime + requireNotNull(lastTime) + when (maxSplitTime) { + MaxSplitTimeEnum.DAY -> { + return (lastTime.year == newLocalDateTime.year + && lastTime.month == newLocalDateTime.month + && lastTime.dayOfMonth == newLocalDateTime.dayOfMonth) + } + MaxSplitTimeEnum.HOUR -> { + return (lastTime.year == newLocalDateTime.year + && lastTime.month == newLocalDateTime.month + && lastTime.dayOfMonth == newLocalDateTime.dayOfMonth + && lastTime.hour == newLocalDateTime.hour) + } + MaxSplitTimeEnum.MINUTE -> { + return (lastTime.year == newLocalDateTime.year + && lastTime.month == newLocalDateTime.month + && lastTime.dayOfMonth == newLocalDateTime.dayOfMonth + && lastTime.hour == newLocalDateTime.hour + && lastTime.minute == newLocalDateTime.minute) + } + } + } + + private fun getFileLastModifiedTime(logPath: Path): LocalDateTime { + val lastTime = lastWrittenTime + return if (lastTime == null) { + val fileTime = Files.getLastModifiedTime(logPath) + val time = LocalDateTime.from(fileTime.toInstant().atZone(ZoneId.systemDefault())) + lastWrittenTime = time + time + } else lastTime + } + + private fun getOutput(newDate: Date, currentLogSize: Long): OutputStream { + initializeOutputStream(newDate, currentLogSize) + val output = fileOutputStream + requireNotNull(output) + return output + } + + private fun initializeOutputStream(newDate: Date, currentLogSize: Long) { + val newLocalDateTime = TimeUtils.toLocalDateTime(newDate) + val logName = getLogName(newLocalDateTime) + val logPath = Paths.get(path, logName) + + if (Files.exists(logPath)) { + val lastModifiedTime = getFileLastModifiedTime(logPath) + + if (isNotSplitByTime(newLocalDateTime)) { + if (maxFileSize > 0) { + if (writtenSize == 0L) { + writtenSize = Files.size(logPath) + } + if (currentLogSize + writtenSize > maxFileSize) { + createLogOutputAndBackupOldLog(logName, logPath, lastModifiedTime) + } else createLogOutputIfNull(logName) + } else createLogOutputIfNull(logName) + } else createLogOutputAndBackupOldLog(logName, logPath, lastModifiedTime) + } else createLogOutputIfNull(logName) + } + + private fun createLogOutputAndBackupOldLog( + logName: String, + logPath: Path, + fileLastModifiedDateTime: LocalDateTime + ) { + close() + Files.move(logPath, Paths.get(path, getLogBakName(fileLastModifiedDateTime))) + fileOutputStream = BufferedOutputStream(FileOutputStream(File(path, logName), true), fileBufferSize) + } + + private fun createLogOutputIfNull(logName: String) { + if (fileOutputStream == null) { + fileOutputStream = BufferedOutputStream(FileOutputStream(File(path, logName), true), fileBufferSize) + } + } + + fun write(str: String, date: Date) { + val text = (str + Log.CL).toByteArray(charset) + try { + val output = getOutput(date, text.size.toLong()) + output.write(text) + writtenSize += text.size + lastWrittenTime = TimeUtils.toLocalDateTime(date) + } catch (e: IOException) { + System.err.println("write log exception. " + e.message) + } + } + + fun close() { + val output = fileOutputStream + if (output != null) { + try { + output.close() + writtenSize = 0 + } catch (e: IOException) { + System.err.println("close log writer exception. " + e.message) + } + } + } + + fun flush() { + try { + fileOutputStream?.flush() + } catch (e: IOException) { + System.err.println("flush log exception. " + e.message) + } + } + + } + + override fun getName(): String = logName + + override fun isTraceEnabled(): Boolean = level.isEnabled(LogLevel.TRACE) + + override fun trace(str: String?) { + if (isTraceEnabled) { + write(str, LogLevel.TRACE.getName(), null, null) + } + } + + override fun trace(str: String?, objs: Array?) { + if (isTraceEnabled) { + write(str, LogLevel.TRACE.getName(), null, objs) + } + } + + override fun trace(str: String?, throwable: Throwable?, objs: Array?) { + if (isTraceEnabled) { + write(str, LogLevel.TRACE.getName(), throwable, objs) + } + } + + override fun isDebugEnabled(): Boolean = level.isEnabled(LogLevel.DEBUG) + + override fun debug(str: String?) { + if (isDebugEnabled) { + write(str, LogLevel.DEBUG.getName(), null, null) + } + } + + override fun debug(str: String?, objs: Array?) { + if (isDebugEnabled) { + write(str, LogLevel.DEBUG.getName(), null, objs) + } + } + + override fun debug(str: String?, throwable: Throwable?, objs: Array?) { + if (isDebugEnabled) { + write(str, LogLevel.DEBUG.getName(), throwable, objs) + } + } + + override fun isInfoEnabled(): Boolean = level.isEnabled(LogLevel.INFO) + + override fun info(str: String?) { + if (isInfoEnabled) { + write(str, LogLevel.INFO.getName(), null, null) + } + } + + override fun info(str: String?, objs: Array?) { + if (isInfoEnabled) { + write(str, LogLevel.INFO.getName(), null, objs) + } + } + + override fun info(str: String?, throwable: Throwable?, objs: Array?) { + if (isInfoEnabled) { + write(str, LogLevel.INFO.getName(), throwable, objs) + } + } + + override fun isWarnEnabled(): Boolean = level.isEnabled(LogLevel.WARN) + + override fun warn(str: String?) { + if (isWarnEnabled) { + write(str, LogLevel.WARN.getName(), null, null) + } + } + + override fun warn(str: String?, objs: Array?) { + if (isWarnEnabled) { + write(str, LogLevel.WARN.getName(), null, objs) + } + } + + override fun warn(str: String?, throwable: Throwable?, objs: Array?) { + if (isWarnEnabled) { + write(str, LogLevel.WARN.getName(), throwable, objs) + } + } + + override fun isErrorEnabled(): Boolean = level.isEnabled(LogLevel.ERROR) + + override fun error(str: String?) { + if (isErrorEnabled) { + write(str, LogLevel.ERROR.getName(), null, null) + } + } + + override fun error(str: String?, objs: Array?) { + if (isErrorEnabled) { + write(str, LogLevel.ERROR.getName(), null, objs) + } + } + + override fun error(str: String?, throwable: Throwable?, objs: Array?) { + if (isErrorEnabled) { + write(str, LogLevel.ERROR.getName(), throwable, objs) + } + } + + override fun close() = runBlocking { + try { + channel.trySend(Stop) + consumerJob.cancel(CancellationException("Cancel file log exception.")) + consumerJob.join() + flushTickJob.cancel(CancellationException("Cancel flush file log exception.")) + } finally { + output.close() + } + } + + private fun write(content: String?, level: String, throwable: Throwable?, objs: Array?) { + val item = LogItem() + item.level = level + item.name = name + item.content = content + item.objs = objs + item.throwable = throwable + item.date = Date() + item.mdcData = mdc.copyOfContextMap + item.className = ClassNameLogWrap.name.get() + item.threadName = Thread.currentThread().name + if (stackTrace) { + item.stackTraceElement = getStackTraceElement() + } + write(item) + } + + private fun getStackTraceElement(): StackTraceElement? { + val arr = Thread.currentThread().stackTrace + val stackTraceElement = arr[4] + return if (stackTraceElement != null) { + if (stackTraceElement.className == "com.fireflysource.log.ClassNameLogWrap") + arr[6] + else stackTraceElement + } else null + } + + private fun write(logItem: LogItem) { + channel.trySend(WriteLogMessage(logItem)) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FileLog + + if (logName != other.logName) return false + + return true + } + + override fun hashCode(): Int { + return logName.hashCode() + } + + override fun toString(): String { + return "FileLog{level=$level, path='$path', name='$name', consoleOutput=$consoleOutput, " + + "fileOutput=$fileOutput, maxFileSize=$maxFileSize, " + + "fileBufferSize=$fileBufferSize, fileFlushInterval=$fileFlushInterval, " + + "charset=$charset, maxSplitTime=${maxSplitTime.value}}" + } + +} \ No newline at end of file diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/TestLog.java b/firefly-slf4j/src/test/java/com/fireflysource/log/TestLog.java new file mode 100644 index 000000000..c24880f2b --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/TestLog.java @@ -0,0 +1,277 @@ +package com.fireflysource.log; + +import com.fireflysource.log.foo.Foo; +import com.fireflysource.log.foo.bar.Bar; +import com.fireflysource.log.internal.utils.StringUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +class TestLog { + + private static final Log logTrace = LogFactory.getInstance().getLog("test-TRACE"); + private static final Log logDebug = LogFactory.getInstance().getLog("test-DEBUG"); + private static final Log logInfo = LogFactory.getInstance().getLog("test-INFO"); + private static final Log logWarn = LogFactory.getInstance().getLog("test-WARN"); + private static final Log logError = LogFactory.getInstance().getLog("test-ERROR"); + private static final Log testMdc = LogFactory.getInstance().getLog("test-request-id"); + private static final Log logFoo = LogFactory.getInstance().getLog(Foo.class); + private static final Log logBar = LogFactory.getInstance().getLog(Bar.class); + private static final Log logDefaultName = LogFactory.getInstance().getLog("test-illegal"); + private static final Log logConsole = LogFactory.getInstance().getLog("test-console"); + private static final Log logCommon = LogFactory.getInstance().getLog("firefly-common"); + private static final Log logErrorStack = LogFactory.getInstance().getLog("error-stack"); + + private static MappedDiagnosticContext mdc; + + @BeforeAll + static void init() { + mdc = MappedDiagnosticContextFactory.getInstance().getMappedDiagnosticContext(); + deleteAll(); + } + + @AfterAll + static void destroy() { + deleteAll(); + } + + private static void deleteAll() { + deleteLog(logTrace); + deleteLog(logDebug); + deleteLog(logError); + deleteLog(logWarn); + deleteLog(logInfo); + deleteLog(logFoo); + deleteLog(logBar); + deleteLog(testMdc); + deleteLog(logDefaultName); + deleteLog(logConsole); + deleteLog(logCommon); + deleteLog(logErrorStack); + } + + private static void deleteLog(Log log) { + File file = getFile(log); + if (file != null && file.exists()) { + boolean success = file.delete(); + System.out.println("delete file " + file.getAbsolutePath() + " | " + success); + } + } + + private static File getFile(Log log) { + System.out.println("getFile: " + log.getClass().getName()); + if (log instanceof ClassNameLogWrap) { + ClassNameLogWrap classNameLogWrap = (ClassNameLogWrap) log; + if (classNameLogWrap.getLog() instanceof FileLog) { + FileLog fileLog = (FileLog) classNameLogWrap.getLog(); + File file = new File(fileLog.getPath(), fileLog.getName() + ".txt"); + System.out.println("getFile: " + fileLog.getPath() + ", " + fileLog.getName()); + System.out.println("getFile: " + file.exists()); + if (file.exists()) { + return file; + } + } + } + return null; + } + + private static List readAllLines(Log log) throws IOException { + final File file = getFile(log); + if (file == null) return Collections.emptyList(); + + return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); + } + + private static void sleep() { + try { + Thread.sleep(2000L); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + + + public static void test1() { + throw new RuntimeException(); + } + + public static void test2() { + test1(); + } + + public static void test3() { + test2(); + } + + @Test + @DisplayName("should create log by class name successfully") + void testLogFactory() { + Log log = LogFactory.getInstance().getLog(Bar.class); + assertTrue(log instanceof ClassNameLogWrap); + assertFalse(log.isDebugEnabled()); + assertTrue(log.isInfoEnabled()); + } + + @Test + @DisplayName("should get log by class name successfully") + void testClassNameLog() throws IOException { + logFoo.info("testFoo"); + logBar.info("testBar"); + sleep(); + + readAllLines(logFoo).forEach(text -> { + String[] data = StringUtils.split(text, '\t'); + assertEquals("testFoo", data[1]); + }); + + readAllLines(logBar).forEach(text -> { + String[] data = StringUtils.split(text, '\t'); + assertEquals("testBar", data[1]); + }); + } + + @Test + @DisplayName("should print log the level is greater than and equals trace successfully") + void testTrace() throws IOException { + logTrace.trace("trace log"); + logTrace.debug("debug log"); + logTrace.info("info log"); + logTrace.warn("warn log"); + logTrace.error("error log"); + sleep(); + + List lines = readAllLines(logTrace); + assertTrue(lines.get(0).contains("trace log")); + assertTrue(lines.get(1).contains("debug log")); + assertTrue(lines.get(2).contains("info log")); + assertTrue(lines.get(3).contains("warn log")); + assertTrue(lines.get(4).contains("error log")); + } + + @Test + @DisplayName("should print log the level is greater than and equals debug successfully") + void testDebug() throws IOException { + logDebug.trace("trace log"); + logDebug.debug("debug log"); + logDebug.info("info log"); + logDebug.warn("warn log"); + logDebug.error("error log"); + sleep(); + + List lines = readAllLines(logDebug); + assertTrue(lines.get(0).contains("debug log")); + assertTrue(lines.get(1).contains("info log")); + assertTrue(lines.get(2).contains("warn log")); + assertTrue(lines.get(3).contains("error log")); + } + + @Test + @DisplayName("should print log the level is greater than and equals info successfully") + void testInfo() throws IOException { + logInfo.trace("trace log"); + logInfo.debug("debug log"); + logInfo.info("info log"); + logInfo.warn("warn log"); + logInfo.error("error log"); + sleep(); + + List lines = readAllLines(logInfo); + assertTrue(lines.get(0).contains("info log")); + assertTrue(lines.get(1).contains("warn log")); + assertTrue(lines.get(2).contains("error log")); + } + + @Test + @DisplayName("should print log the level is greater than and equals warn successfully") + void testWarn() throws IOException { + logWarn.trace("trace log"); + logWarn.debug("debug log"); + logWarn.info("info log"); + logWarn.warn("warn log"); + logWarn.error("error log"); + sleep(); + + List lines = readAllLines(logWarn); + assertTrue(lines.get(0).contains("warn log")); + assertTrue(lines.get(1).contains("error log")); + } + + @Test + @DisplayName("should print error log successfully") + void testError() throws IOException { + logError.trace("trace log"); + logError.debug("debug log"); + logError.info("info log"); + logError.warn("warn log"); + logError.error("error log"); + sleep(); + + List lines = readAllLines(logError); + assertTrue(lines.get(0).contains("error log")); + } + + @Test + @DisplayName("should put MDC successfully") + void testMdc() throws IOException { + mdc.put("reqId", "hello_req_id"); + testMdc.info("oooooooooo"); + testMdc.info("bbbbbbbbbb"); + sleep(); + + List lines = readAllLines(testMdc); + assertTrue(lines.get(0).contains("hello_req_id")); + assertTrue(lines.get(1).contains("hello_req_id")); + } + + @Test + @DisplayName("should create default log successfully when the log name not exists") + void testDefaultLogName() throws IOException { + logDefaultName.info("default name 0"); + sleep(); + + List lines = readAllLines(logDefaultName); + assertTrue(lines.get(0).contains("default name 0")); + } + + @Test + @DisplayName("should print log to console and file successfully") + void testConsoleAndFile() throws IOException { + logConsole.info("test console."); + logCommon.info("test common."); + sleep(); + + List lines = readAllLines(logConsole); + assertTrue(lines.get(0).contains("test console.")); + + lines = readAllLines(logCommon); + assertTrue(lines.get(0).contains("test common.")); + } + + @Test + void testErrorStack() throws IOException { + try { + test3(); + } catch (Exception e) { + logErrorStack.error("exception", e); + } + sleep(); + + List lines = readAllLines(logErrorStack); + assertTrue(lines.get(0).contains("exception")); + assertTrue(lines.size() > 1); + assertTrue(lines.get(1).contains("$err_start")); + assertTrue(lines.get(2).contains("java.lang.RuntimeException")); + } + +} diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/TestLogParser.java b/firefly-slf4j/src/test/java/com/fireflysource/log/TestLogParser.java new file mode 100644 index 000000000..4997af736 --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/TestLogParser.java @@ -0,0 +1,29 @@ +package com.fireflysource.log; + + +import com.fireflysource.log.internal.utils.collection.TreeTrie; +import com.fireflysource.log.internal.utils.collection.Trie; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TestLogParser { + + @Test + void test() { + Trie xmlLogTree = new TreeTrie<>(); + LogConfigParser parser = new XmlLogConfigParser(); + boolean success = parser.parse((fileLog) -> xmlLogTree.put(fileLog.getName(), fileLog)); + assertTrue(success); + + Log bar = xmlLogTree.getBest("com.fireflysource.log.foo.bar.Bar"); + assertEquals("com.fireflysource.log.foo.bar", bar.getName()); + + Log debug = xmlLogTree.getBest("test-DEBUG"); + assertFalse(debug.isTraceEnabled()); + assertTrue(debug.isDebugEnabled()); + assertTrue(debug.isInfoEnabled()); + + System.out.println(debug.getClass().getName()); + } +} diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/demo/CustomLogFormatter.java b/firefly-slf4j/src/test/java/com/fireflysource/log/demo/CustomLogFormatter.java new file mode 100644 index 000000000..3a0af51ad --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/demo/CustomLogFormatter.java @@ -0,0 +1,36 @@ +package com.fireflysource.log.demo; + + +import com.fireflysource.log.LogFormatter; +import com.fireflysource.log.LogItem; +import com.fireflysource.log.internal.utils.StringUtils; +import com.fireflysource.log.internal.utils.TimeUtils; + +import static com.fireflysource.log.internal.utils.TimeUtils.DEFAULT_LOCAL_DATE_TIME; + +/** + * @author Pengtao Qiu + */ +public class CustomLogFormatter implements LogFormatter { + + @Override + public String format(LogItem logItem) { + String logStr = logItem.getLevel() + " " + TimeUtils.format(logItem.getDate(), DEFAULT_LOCAL_DATE_TIME); + + if (logItem.getMdcData() != null && !logItem.getMdcData().isEmpty()) { + logStr += " " + logItem.getMdcData(); + } + + if (StringUtils.hasText(logItem.getClassName())) { + logStr += " " + logItem.getClassName(); + } + + if (StringUtils.hasText(logItem.getThreadName())) { + logStr += " " + logItem.getThreadName(); + } + + logStr += " --> " + logItem.renderContentTemplate(); + return logStr; + } + +} diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/demo/LogBenchmark.java b/firefly-slf4j/src/test/java/com/fireflysource/log/demo/LogBenchmark.java new file mode 100644 index 000000000..4f1ef9b68 --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/demo/LogBenchmark.java @@ -0,0 +1,49 @@ +package com.fireflysource.log.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CountDownLatch; + +public class LogBenchmark { + + private static final Logger log = LoggerFactory.getLogger("test-INFO"); + + public static void main(String[] args) throws InterruptedException { + test(4, 10_000_000, 20); + } + + public static void test(int threadNum, int messageNum, int messageSize) throws InterruptedException { + StringBuilder data = new StringBuilder(messageSize); + for (int i = 0; i < messageSize; i++) { + data.append("a"); + } + String str = data.toString(); + + CountDownLatch latch = new CountDownLatch(threadNum); + + Thread[] threads = new Thread[threadNum]; + int size = messageNum / threadNum; + System.out.println("size per thread: " + size); + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < size; j++) { + log.info(str); + } +// System.out.println(Thread.currentThread().getName() + " arrived"); + latch.countDown(); + }, "test-thread-" + i); + } + + long start = System.currentTimeMillis(); + for (Thread thread : threads) { + thread.start(); + } + latch.await(); + long end = System.currentTimeMillis(); + long time = (end - start) / 1000; + System.out.println("time: " + time); + System.out.println("msg/sec: " + (messageNum / time)); + } + +} diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/demo/TimeSplitDemo.java b/firefly-slf4j/src/test/java/com/fireflysource/log/demo/TimeSplitDemo.java new file mode 100644 index 000000000..6933a7e2c --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/demo/TimeSplitDemo.java @@ -0,0 +1,22 @@ +package com.fireflysource.log.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * @author Pengtao Qiu + */ +public class TimeSplitDemo { + + public static final Logger log = LoggerFactory.getLogger("time-split-minute"); + + public static void main(String[] args) throws Exception { + int i = 0; + while (true) { + log.info("test1 {}, {}", i++, System.currentTimeMillis()); + Thread.sleep(TimeUnit.SECONDS.toMillis(5)); + } + } +} diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/foo/Foo.java b/firefly-slf4j/src/test/java/com/fireflysource/log/foo/Foo.java new file mode 100644 index 000000000..36047ded0 --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/foo/Foo.java @@ -0,0 +1,7 @@ +package com.fireflysource.log.foo; + +/** + * @author Pengtao Qiu + */ +public class Foo { +} diff --git a/firefly-slf4j/src/test/java/com/fireflysource/log/foo/bar/Bar.java b/firefly-slf4j/src/test/java/com/fireflysource/log/foo/bar/Bar.java new file mode 100644 index 000000000..6a75dc48b --- /dev/null +++ b/firefly-slf4j/src/test/java/com/fireflysource/log/foo/bar/Bar.java @@ -0,0 +1,7 @@ +package com.fireflysource.log.foo.bar; + +/** + * @author Pengtao Qiu + */ +public class Bar { +} diff --git a/firefly-slf4j/src/test/resources/firefly-log.xml b/firefly-slf4j/src/test/resources/firefly-log.xml new file mode 100644 index 000000000..bc5139f60 --- /dev/null +++ b/firefly-slf4j/src/test/resources/firefly-log.xml @@ -0,0 +1,108 @@ + + + + + firefly-common + ${log.level} + ${log.path} + true + + + + test-TRACE + TRACE + ${log.path} + true + + + + test-DEBUG + DEBUG + ${log.path} + true + + + + test-ERROR + ERROR + ${log.path} + true + + + + test-WARN + WARN + ${log.path} + false + + + + test-INFO + INFO + ${log.path} + false + + + + test-console + INFO + true + + + + com.fireflysource.log.foo + INFO + ${log.path} + true + + + + com.fireflysource.log.foo.bar + INFO + ${log.path} + true + + + + test.max.size + INFO + ${log.path} + 300 + UTF-8 + + + + test.gbk + INFO + ${log.path} + GBK + + + + test-request-id + INFO + ${log.path} + + + + error-stack + INFO + ${log.path} + + + + com.fireflysource.log.demo + ${log.level} + ${log.path} + true + com.fireflysource.log.demo.CustomLogFormatter + + + + time-split-minute + INFO + ${log.path} + minute + + diff --git a/firefly-template/.gitignore b/firefly-template/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/firefly-template/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/firefly-template/pom.xml b/firefly-template/pom.xml deleted file mode 100644 index f393c9ad7..000000000 --- a/firefly-template/pom.xml +++ /dev/null @@ -1,123 +0,0 @@ - - 4.0.0 - - com.firefly - firefly-template - 1.0-SNAPSHOT - jar - - firefly-template - http://maven.apache.org - - ${project.artifactId} - install - - - src/main/resources - true - - - - - true - src/test/resources - - - false - src/test/template - - - - - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - UTF-8 - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - UTF-8 - - - - - - - - com.firefly - firefly-template - - UTF-8 - INFO - D:/log/ - - - - mac - - INFO - /Users/qiupengtao/develop/logs/ - - - - macdebug - - DEBUG - /Users/qiupengtao/develop/logs/ - - - - windebug - - DEBUG - D:/log/ - - - - - - - com.firefly - firefly-common - 1.0-SNAPSHOT - - - - junit - junit - 4.8.1 - test - - - org.hamcrest - hamcrest-all - 1.1 - test - - - org.freemarker - freemarker - 2.3.18 - test - - - - - 3rdRepo - 3rd party - http://localhost:7777/nexus-webapp/content/repositories/thirdparty - - - dev - Snapshots - http://localhost:7777/nexus-webapp/content/repositories/snapshots - - - diff --git a/firefly-template/src/main/java/com/firefly/template/Config.java b/firefly-template/src/main/java/com/firefly/template/Config.java deleted file mode 100644 index e03d29066..000000000 --- a/firefly-template/src/main/java/com/firefly/template/Config.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.firefly.template; - -import java.io.File; -import java.net.URL; - -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class Config { - public static Log LOG = LogFactory.getInstance().getLog("firefly-system"); - private String viewPath; - private String compiledPath; - private String suffix = "html"; - private String charset = "UTF-8"; - private String classPath; - public static final String COMPILED_FOLDER_NAME = "_compiled_view"; - - public Config() { - URL url = this.getClass().getResource(""); - if ("jar".equals(url.getProtocol())) { - String f = url.getPath(); - try { - this.classPath = new File(new URL(f.substring(0, - f.indexOf("!/com/firefly"))).toURI()).getAbsolutePath(); - } catch (Throwable t) { - LOG.error("template config init error", t); - } - } - } - - public String getViewPath() { - return viewPath; - } - - public void setViewPath(String viewPath) { - char ch = viewPath.charAt(viewPath.length() - 1); - this.viewPath = (ch == '/' || ch == '\\' ? viewPath : viewPath + "/") - .replace('\\', '/'); - compiledPath = this.viewPath + COMPILED_FOLDER_NAME; - } - - public String getCompiledPath() { - return compiledPath; - } - - public String getSuffix() { - return suffix; - } - - public void setSuffix(String suffix) { - this.suffix = suffix; - } - - public String getCharset() { - return charset; - } - - public void setCharset(String charset) { - this.charset = charset; - } - - public String getClassPath() { - return classPath; - } - - public void setClassPath(String classPath) { - this.classPath = classPath; - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/Function.java b/firefly-template/src/main/java/com/firefly/template/Function.java deleted file mode 100644 index b06a83767..000000000 --- a/firefly-template/src/main/java/com/firefly/template/Function.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.template; - -import java.io.OutputStream; - -public interface Function { - void render(Model model, OutputStream out, Object... obj) throws Throwable; -} diff --git a/firefly-template/src/main/java/com/firefly/template/FunctionRegistry.java b/firefly-template/src/main/java/com/firefly/template/FunctionRegistry.java deleted file mode 100644 index 57070d891..000000000 --- a/firefly-template/src/main/java/com/firefly/template/FunctionRegistry.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.firefly.template; - -import java.util.HashMap; -import java.util.Map; - -public class FunctionRegistry { - private static final Map MAP = new HashMap(); - - public static void add(String name, Function function) { - MAP.put(name, function); - } - - public static Function get(String name) { - return MAP.get(name); - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/Model.java b/firefly-template/src/main/java/com/firefly/template/Model.java deleted file mode 100644 index 00456682d..000000000 --- a/firefly-template/src/main/java/com/firefly/template/Model.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.firefly.template; - -public interface Model { - void put(String key, Object object); - - Object get(String key); - - void remove(String key); - - void clear(); -} diff --git a/firefly-template/src/main/java/com/firefly/template/TemplateFactory.java b/firefly-template/src/main/java/com/firefly/template/TemplateFactory.java deleted file mode 100644 index bfbefd036..000000000 --- a/firefly-template/src/main/java/com/firefly/template/TemplateFactory.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.firefly.template; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.firefly.template.function.DateFormatFunction; -import com.firefly.template.function.LengthFunction; -import com.firefly.template.parser.ViewFileReader; - -public class TemplateFactory { - - private Config config; - private Map map = new HashMap(); - - public TemplateFactory() { - - } - - public TemplateFactory(File file) { - this.config = new Config(); - config.setViewPath(file.getAbsolutePath()); - } - - public TemplateFactory(String path) { - this.config = new Config(); - config.setViewPath(path); - } - - public TemplateFactory(Config config) { - this.config = config; - } - - public Config getConfig() { - return config; - } - - public void setConfig(Config config) { - this.config = config; - } - - public TemplateFactory init() { - if(config == null) - throw new IllegalArgumentException("template config is null"); - - long start = System.currentTimeMillis(); - FunctionRegistry.add("dateFormat", new DateFormatFunction(config.getCharset())); - FunctionRegistry.add("len", new LengthFunction(config.getCharset())); - ViewFileReader reader = new ViewFileReader(config); - List javaFiles = reader.getJavaFiles(); - List templateFiles = reader.getTemplateFiles(); - List classNames = reader.getClassNames(); - - for (int i = 0; i < javaFiles.size(); i++) { - String c = javaFiles.get(i); - final String classFileName = c.substring(0, c.length() - 4) + "class"; - ClassLoader classLoader = new TemplateClassLoader(classFileName, TemplateFactory.class.getClassLoader()); - - try { - map.put(templateFiles.get(i), (View)classLoader.loadClass(classNames.get(i)).getConstructor(TemplateFactory.class).newInstance(this)); - } catch (Throwable e) { - Config.LOG.error("load class error", e); - } - } - long end = System.currentTimeMillis(); - Config.LOG.info("firefly-template init in {} ms", (end - start)); - return this; - } - - public View getView(String name) { - return map.get(name); - } - - private class TemplateClassLoader extends ClassLoader { - private String classFileName; - - public TemplateClassLoader(String classFileName, ClassLoader classLoader) { - super(classLoader); - this.classFileName = classFileName; - } - - @Override - public Class findClass(String name) { - BufferedInputStream bis = null; - byte[] b = null; - try { - File file = new File(classFileName); - b = new byte[(int)file.length()]; - bis = new BufferedInputStream(new FileInputStream(file)); - bis.read(b); - } catch (Throwable e) { - Config.LOG.error("read class file error", e); - } finally { - if(bis != null) - try { - bis.close(); - } catch (IOException e) { - Config.LOG.error("close error", e); - } - } - - return defineClass(name, b, 0, b.length); - } - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/View.java b/firefly-template/src/main/java/com/firefly/template/View.java deleted file mode 100644 index 0d2322bdc..000000000 --- a/firefly-template/src/main/java/com/firefly/template/View.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.template; - -import java.io.OutputStream; - -public interface View { - void render(Model model, OutputStream out); -} diff --git a/firefly-template/src/main/java/com/firefly/template/exception/ExpressionError.java b/firefly-template/src/main/java/com/firefly/template/exception/ExpressionError.java deleted file mode 100644 index a02fc67f5..000000000 --- a/firefly-template/src/main/java/com/firefly/template/exception/ExpressionError.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.template.exception; - -public class ExpressionError extends RuntimeException { - - private static final long serialVersionUID = -5607969690821083756L; - - public ExpressionError(String msg) { - super(msg); - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/exception/TemplateFileReadException.java b/firefly-template/src/main/java/com/firefly/template/exception/TemplateFileReadException.java deleted file mode 100644 index 0f0464ff3..000000000 --- a/firefly-template/src/main/java/com/firefly/template/exception/TemplateFileReadException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.template.exception; - -public class TemplateFileReadException extends RuntimeException { - - private static final long serialVersionUID = -1786019404849645091L; - - public TemplateFileReadException(String msg) { - super(msg); - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/function/DateFormatFunction.java b/firefly-template/src/main/java/com/firefly/template/function/DateFormatFunction.java deleted file mode 100644 index 99c92e567..000000000 --- a/firefly-template/src/main/java/com/firefly/template/function/DateFormatFunction.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.template.function; - -import java.io.OutputStream; -import java.util.Date; - -import com.firefly.template.Function; -import com.firefly.template.Model; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class DateFormatFunction implements Function { - - private String charset; - - public DateFormatFunction(String charset) { - this.charset = charset; - } - - @Override - public void render(Model model, OutputStream out, Object... obj) throws Throwable{ - Date date = (Date)obj[0]; - if(date != null) - out.write(SafeSimpleDateFormat.defaultDateFormat.format(date).getBytes(charset)); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/function/LengthFunction.java b/firefly-template/src/main/java/com/firefly/template/function/LengthFunction.java deleted file mode 100644 index 6735ef796..000000000 --- a/firefly-template/src/main/java/com/firefly/template/function/LengthFunction.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.firefly.template.function; - -import java.io.OutputStream; -import java.lang.reflect.Array; -import java.util.Collection; - -import com.firefly.template.Function; -import com.firefly.template.Model; - -public class LengthFunction implements Function { - - private String charset; - - public LengthFunction(String charset) { - this.charset = charset; - } - - @Override - public void render(Model model, OutputStream out, Object... obj) - throws Throwable { - Object o = obj[0]; - if(o != null) { - if(o instanceof String) - out.write(String.valueOf(((String)o).length()).getBytes(charset)); - else if(o instanceof Collection) - out.write(String.valueOf(((Collection)o).size()).getBytes(charset)); - else if(o.getClass().isArray()) - out.write(String.valueOf(Array.getLength(o)).getBytes(charset)); - } - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/JavaFileBuilder.java b/firefly-template/src/main/java/com/firefly/template/parser/JavaFileBuilder.java deleted file mode 100644 index 8c5142bf6..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/JavaFileBuilder.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.firefly.template.parser; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.Arrays; - -import com.firefly.template.Config; -import com.firefly.utils.VerifyUtils; - -public class JavaFileBuilder { - private String name; - private BufferedWriter writer; - private boolean writeHead = false; - private StringBuilder tail = new StringBuilder(); - private StringBuilder preBlank = new StringBuilder("\t\t"); - private int textCount = 0; - private Config config; - boolean outBreak; - - public JavaFileBuilder(String path, String name, Config config) { - this.name = name; - File file = new File(path, name); - try { - if (!file.exists()) - file.createNewFile(); - - writer = new BufferedWriter(new FileWriter(file)); - this.config = config; - } catch (Throwable t) { - Config.LOG.error("java file builder error", t); - } - } - - public String getName() { - return name; - } - - public JavaFileBuilder write(String str) { - try { - writeHead(); - writer.write(str); - } catch (Throwable t) { - Config.LOG.error("write error", t); - } - return this; - } - - public StringBuilder getPreBlank() { - return preBlank; - } - - public JavaFileBuilder appendTail(String str) { - tail.append(str); - return this; - } - - public JavaFileBuilder writeStringValue(String str) { - write(preBlank + "out.write(String.valueOf(" + str + ").getBytes(\"" - + config.getCharset() + "\"));\n"); - return this; - } - - public JavaFileBuilder writeText(String str) { - try { - str = Arrays.toString(str.getBytes(config.getCharset())); - str = str.substring(1, str.length() - 1); - write(preBlank + "out.write(_TEXT_" + textCount + ");\n") - .appendTail( - "\tprivate final byte[] _TEXT_" + textCount - + " = new byte[]{" + str + "};\n"); - textCount++; - } catch (UnsupportedEncodingException e) { - Config.LOG.error("write text error", e); - } - return this; - } - - public JavaFileBuilder writeObject(String el) { - write(preBlank + "out.write(objNav.getValue(model ,\"" + el - + "\").getBytes(\"" + config.getCharset() + "\"));\n"); - return this; - } - - public JavaFileBuilder writeFunction(String functionName, String[] params) { - write(preBlank + "FunctionRegistry.get(\"" + functionName + "\").render(model, out"); - for (String param : params) { - param = param.trim(); - if(param.length() > 0) { - write(", "); - if(VerifyUtils.isDouble(param) - || VerifyUtils.isLong(param) - || VerifyUtils.isFloat(param) - || VerifyUtils.isInteger(param) - || "null".equals(param) - || (param.charAt(0) == '\"' && param.charAt(param.length() - 1) == '\"' )) { - write(param); - } else { - write("objNav.find(model, \"" + param + "\")"); - } - } - } - write(");\n"); - return this; - } - - public JavaFileBuilder writeObjNav(String el) { - write("objNav.getValue(model ,\"" + el + "\")"); - return this; - } - - public JavaFileBuilder writeBooleanObj(String el) { - write("objNav.getBoolean(model ,\"" + el + "\")"); - return this; - } - - public JavaFileBuilder writeIntegerObj(String el) { - write("objNav.getInteger(model ,\"" + el + "\")"); - return this; - } - - public JavaFileBuilder writeFloatObj(String el) { - write("objNav.getFloat(model ,\"" + el + "\")"); - return this; - } - - public JavaFileBuilder writeDoubleObj(String el) { - write("objNav.getDouble(model ,\"" + el + "\")"); - return this; - } - - public JavaFileBuilder writeLongObj(String el) { - write("objNav.getLong(model ,\"" + el + "\")"); - return this; - } - - public JavaFileBuilder writeTail() { - try { - writer.write(tail.toString()); - } catch (Throwable t) { - Config.LOG.error("write error", t); - } - return this; - } - - private void writeHead() throws IOException { - if (!writeHead) { - writer.write("import java.io.OutputStream;\n"); - writer.write("import com.firefly.template.support.ObjectNavigator;\n"); - writer.write("import com.firefly.template.Model;\n"); - writer.write("import com.firefly.template.view.AbstractView;\n"); - writer.write("import com.firefly.template.TemplateFactory;\n"); - writer.write("import com.firefly.template.FunctionRegistry;\n\n"); - - String className = name.substring(0, name.length() - 5); - - writer.write("public class " + className - + " extends AbstractView {\n\n"); - writer.write("\tpublic " + className + "(TemplateFactory templateFactory){this.templateFactory = templateFactory;}\n\n"); - writer.write("\t@Override\n"); - writer.write("\tprotected void main(Model model, OutputStream out) throws Throwable {\n"); - writer.write("\t\tObjectNavigator objNav = ObjectNavigator.getInstance();\n"); - writeHead = true; - } - } - - public void close() { - try { - if (writer != null) - writer.close(); - } catch (Throwable t) { - Config.LOG.error("java file builder close error", t); - } - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StateMachine.java b/firefly-template/src/main/java/com/firefly/template/parser/StateMachine.java deleted file mode 100644 index 437d45895..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StateMachine.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.firefly.template.parser; - -import java.util.HashMap; -import java.util.Map; - -public class StateMachine { - private static final Map MAP = new HashMap(); - - static { - MAP.put("#eval", new StatementExpression()); - MAP.put("#if", new StatementIf()); - MAP.put("#elseif", new StatementElseIf()); - MAP.put("#else", new StatementElse()); - MAP.put("#for", new StatementFor()); - MAP.put("#switch", new StatementSwitch()); - MAP.put("#case", new StatementSwitchCase()); - MAP.put("#default", new StatementSwitchDefault()); - MAP.put("#end", new StatementEnd()); - MAP.put("#set", new StatementSet()); - MAP.put("#include", new StatementInclude()); - } - - public static void parse(String keyword, String content, - JavaFileBuilder javaFileBuilder) { - Statement statement = MAP.get(keyword); - if (statement != null) - statement.parse(content, javaFileBuilder); - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/Statement.java b/firefly-template/src/main/java/com/firefly/template/parser/Statement.java deleted file mode 100644 index 530dc0130..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/Statement.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.firefly.template.parser; - -public interface Statement { - void parse(String content, JavaFileBuilder javaFileBuilder); -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementElse.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementElse.java deleted file mode 100644 index 87e10e8d8..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementElse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.firefly.template.parser; - -public class StatementElse implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - javaFileBuilder.write(javaFileBuilder.getPreBlank().deleteCharAt(0).toString() + "} else {\n"); - javaFileBuilder.getPreBlank().append('\t'); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementElseIf.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementElseIf.java deleted file mode 100644 index db071e97a..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementElseIf.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.firefly.template.parser; - -public class StatementElseIf extends StatementIf { - - @Override - protected void writePrefix(JavaFileBuilder javaFileBuilder) { - javaFileBuilder.write(javaFileBuilder.getPreBlank().deleteCharAt(0).toString() + "} else if ("); - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementEnd.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementEnd.java deleted file mode 100644 index c1407ebe3..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementEnd.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.firefly.template.parser; - -public class StatementEnd implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - javaFileBuilder.write(javaFileBuilder.getPreBlank().deleteCharAt(0).toString() + "}\n"); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementExpression.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementExpression.java deleted file mode 100644 index 53f028954..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementExpression.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.firefly.template.parser; - -import static com.firefly.template.support.RPNUtils.Type.*; - -import java.util.Deque; -import java.util.LinkedList; -import java.util.List; - -import com.firefly.template.exception.ExpressionError; -import com.firefly.template.support.RPNUtils; -import com.firefly.template.support.RPNUtils.Fragment; - -public class StatementExpression implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - javaFileBuilder.writeStringValue(parse(content)); - } - - public String parse(String content) { - List list = RPNUtils.getReversePolishNotation(content); - if (list.size() == 1) { - Fragment f = list.get(0); - return f.type == VARIABLE ? getVariable(f.value, "Boolean") - : f.value; - } - Deque d = new LinkedList(); - for (Fragment f : list) { - if (isSymbol(f.type)) { - Fragment right = d.pop(); - Fragment left = d.pop(); - - Fragment ret = new Fragment(); - switch (f.type) { - case ARITHMETIC_OPERATOR: - if (left.type == STRING || right.type == STRING) { - ret.type = STRING; - if (f.value.equals("+")) { - left.value = left.type == VARIABLE ? getVariable(left.value) - : left.value; - right.value = right.type == VARIABLE ? getVariable(right.value) - : right.value; - if (left.value.charAt(0) == '"' - && left.value - .indexOf("objNav.getValue(model ,\"") < 0 - && right.value.charAt(0) == '"' - && right.value - .indexOf("objNav.getValue(model ,\"") < 0) - ret.value = "\"" - + left.value.substring(1, - left.value.length() - 1) - + right.value.substring(1, - right.value.length() - 1) - + "\""; - else - ret.value = left.value + " + " + right.value; - } else { - throw new ExpressionError( - "The operation is not supported: " - + left.type + " " + f.value + " " - + right.type); - } - } else if (left.type == DOUBLE || right.type == DOUBLE) { - ret.type = DOUBLE; - ret.value = getFloatArithmeticResult(left, right, - f.value, false); - } else if (left.type == FLOAT || right.type == FLOAT) { - ret.type = FLOAT; - ret.value = getFloatArithmeticResult(left, right, - f.value, true); - } else if (left.type == LONG || right.type == LONG) { - ret.type = LONG; - ret.value = getIntegerArithmeticResult(left, right, - f.value, false); - } else if (left.type == INTEGER || right.type == INTEGER) { - ret.type = INTEGER; - ret.value = getIntegerArithmeticResult(left, right, - f.value, true); - } else { - throw new ExpressionError(left.type + " and " - + right.type + " ​​can not do arithmetic."); - } - break; - case LOGICAL_OPERATOR: - ret.type = BOOLEAN; - ret.value = getLogicalResult(left, right, f.value); - if (ret.value == null) - throw new ExpressionError( - "The operation is not supported: " + left.type - + " " + f.value + " " + right.type); - break; - case ARITHMETIC_OR_LOGICAL_OPERATOR: - if (left.type == LONG || right.type == LONG) { - ret.type = LONG; - ret.value = getIntegerArithmeticResult(left, right, - f.value, false); - } else if (left.type == INTEGER || right.type == INTEGER) { - ret.type = INTEGER; - ret.value = getIntegerArithmeticResult(left, right, - f.value, true); - } else { - ret.type = BOOLEAN; - ret.value = getLogicalResult(left, right, f.value); - } - if (ret.value == null) - throw new ExpressionError( - "The operation is not supported: " + left.type - + " " + f.value + " " + right.type); - break; - case CONDITIONAL_OPERATOR: - ret.type = BOOLEAN; - if (f.value.equals("==") || f.value.equals("!=")) { - ret.value = getEqResult(left, right, f.value); - } else { - if (left.type == DOUBLE || right.type == DOUBLE) { - ret.value = getFloatArithmeticResult(left, right, - f.value, false); - } else if (left.type == FLOAT || right.type == FLOAT) { - ret.value = getFloatArithmeticResult(left, right, - f.value, true); - } else if (left.type == LONG || right.type == LONG) { - ret.value = getIntegerArithmeticResult(left, right, - f.value, false); - } else if (left.type == INTEGER - || right.type == INTEGER) { - ret.value = getIntegerArithmeticResult(left, right, - f.value, true); - } else { - throw new ExpressionError(left.type + " and " - + right.type + " ​​can not do arithmetic."); - } - } - break; - default: - throw new ExpressionError( - "The operation is not supported: " + left.value - + " " + f.value + " " + right.value); - } - d.push(ret); - } else { - d.push(f); - } - } - if (d.size() != 1) - throw new ExpressionError("RPN error: " + content); - return d.pop().value; - } - - private boolean isSymbol(RPNUtils.Type type) { - return type == ARITHMETIC_OPERATOR || type == LOGICAL_OPERATOR - || type == ASSIGNMENT_OPERATOR - || type == ARITHMETIC_OR_LOGICAL_OPERATOR - || type == CONDITIONAL_OPERATOR; - } - - private String getVariable(String var) { - int start = var.indexOf("${") + 2; - int end = var.indexOf('}'); - return "objNav.getValue(model ,\"" + var.substring(start, end) + "\")"; - } - - private String getVariable(String var, String t) { - StringBuilder ret = new StringBuilder(); - int start = var.indexOf("${") + 2; - int end = var.indexOf('}'); - ret.append(var.substring(0, start - 2)).append( - "objNav.get" + t + "(model ,\"" + var.substring(start, end) - + "\")"); - if (end < var.length() - 1) - throw new ExpressionError("Variable format error: " + var); - return ret.toString(); - } - - private String getVariableObj(String var) { - StringBuilder ret = new StringBuilder(); - int start = var.indexOf("${") + 2; - int end = var.indexOf('}'); - ret.append(var.substring(0, start - 2)).append( - "objNav.find(model ,\"" + var.substring(start, end) + "\")"); - if (end < var.length() - 1) - throw new ExpressionError("Variable format error: " + var); - return ret.toString(); - } - - private String getArithmeticOrLogicalResult(Fragment left, Fragment right, - String s, String type) { - char f0 = s.charAt(0); - left.value = left.type == VARIABLE ? getVariable(left.value, type) - + " " + s + " " : left.value; - right.value = right.type == VARIABLE ? " " + s + " " - + getVariable(right.value, type) : right.value; - return f0 == '*' || f0 == '/' || f0 == '%' ? (left.value + right.value) - : ("(" + left.value + right.value + ")"); - } - - private String getEqResult(Fragment left, Fragment right, String s) { - boolean eq = s.equals("=="); - String ret = null; - if (left.type == VARIABLE && right.type == VARIABLE) - ret = (eq ? "" : "!") + getVariableObj(left.value) + ".equals(" - + getVariableObj(right.value) + ")"; - else if (left.type == VARIABLE) { - if (right.type == NULL) - ret = getVariableObj(left.value) + " " + s + " " + right.value; - else - ret = (eq ? "" : "!") + "((Object)(" + right.value - + ")).equals(" + getVariableObj(left.value) + ")"; - } else if (right.type == VARIABLE) { - if (left.type == NULL) - ret = left.value + " " + s + " " + getVariableObj(right.value); - else - ret = (eq ? "" : "!") + "((Object)(" + left.value - + ")).equals(" + getVariableObj(right.value) + ")"; - } else if (left.value.indexOf("objNav") >= 0 - || right.value.indexOf("objNav") >= 0) - ret = (eq ? "" : "!") + "((Object)(" + left.value + ")).equals(" - + right.value + ")"; - else - ret = String.valueOf(eq ? left.value.equals(right.value) - : !left.value.equals(right.value)); - return ret; - } - - private String getLogicalResult(Fragment left, Fragment right, String s) { - String ret = null; - - if (left.type == VARIABLE && right.type == VARIABLE) - ret = "(" + getVariable(left.value, "Boolean") + " " + s + " " - + getVariable(right.value, "Boolean") + ")"; - else if (left.type == VARIABLE || right.type == VARIABLE) - ret = getArithmeticOrLogicalResult(left, right, s, "Boolean"); - else if (left.value.indexOf("objNav") >= 0 - || right.value.indexOf("objNav") >= 0) - ret = left.value + " " + s + " " + right.value; - else - ret = String.valueOf(s.equals("&&") ? getBooleanValue(left.value) - && getBooleanValue(right.value) - : getBooleanValue(left.value) - || getBooleanValue(right.value)); - return ret; - } - - private boolean getBooleanValue(String v) { - if (v.charAt(0) == '!') - return !new Boolean(v.substring(1).trim()); - else - return new Boolean(v.trim()); - } - - private String getFloatArithmeticResult(Fragment left, Fragment right, - String s, boolean isFloat) { - String ret = null; - char f0 = s.charAt(0); - if (left.type == VARIABLE || right.type == VARIABLE) - ret = getArithmeticOrLogicalResult(left, right, s, - isFloat ? "Float" : "Double"); - else if (left.value.indexOf("objNav") >= 0 - || right.value.indexOf("objNav") >= 0) - ret = f0 == '*' || f0 == '/' || f0 == '%' ? (left.value + " " + s - + " " + right.value) : ("(" + left.value + " " + s + " " - + right.value + ")"); - else - ret = getConstFloatArithmeticResult(left, right, s, isFloat); - return ret; - } - - private String getConstFloatArithmeticResult(Fragment lf, Fragment rf, - String s, boolean isFloat) { - float l = Float.parseFloat(lf.value), r = Float.parseFloat(rf.value); - double l0 = Double.parseDouble(lf.value), r0 = Double - .parseDouble(rf.value); - String ret = null; - char f0 = s.charAt(0); - switch (f0) { - case '+': - ret = String.valueOf(isFloat ? l + r : l0 + r0); - break; - case '-': - ret = String.valueOf(isFloat ? l - r : l0 - r0); - break; - case '*': - ret = String.valueOf(isFloat ? l * r : l0 * r0); - break; - case '/': - ret = String.valueOf(isFloat ? l / r : l0 / r0); - break; - case '%': - ret = String.valueOf(isFloat ? l % r : l0 % r0); - break; - case '<': - if (s.length() == 2 && s.charAt(1) == '=') - ret = String.valueOf(isFloat ? l <= r : l0 <= r0); - else if (s.length() == 1) - ret = String.valueOf(isFloat ? l < r : l0 < r0); - else - throw new ExpressionError("The operation is not supported: " - + lf.type + " " + s + " " + rf.type); - break; - case '>': - if (s.length() == 2 && s.charAt(1) == '=') - ret = String.valueOf(isFloat ? l >= r : l0 >= r0); - else if (s.length() == 1) - ret = String.valueOf(isFloat ? l > r : l0 > r0); - else - throw new ExpressionError("The operation is not supported: " - + lf.type + " " + s + " " + rf.type); - break; - default: - throw new ExpressionError("The operation is not supported: " - + lf.type + " " + s + " " + rf.type); - } - return ret; - } - - private String getIntegerArithmeticResult(Fragment left, Fragment right, - String s, boolean isInteger) { - String ret = null; - char f0 = s.charAt(0); - if (left.type == VARIABLE || right.type == VARIABLE) - ret = getArithmeticOrLogicalResult(left, right, s, - isInteger ? "Integer" : "Long"); - else if (left.value.indexOf("objNav") >= 0 - || right.value.indexOf("objNav") >= 0) - ret = f0 == '*' || f0 == '/' || f0 == '%' ? (left.value + " " + s - + " " + right.value) : ("(" + left.value + " " + s + " " - + right.value + ")"); - else - ret = getConstIntegerArithmeticResult(left, right, s, isInteger); - return ret; - } - - private String getConstIntegerArithmeticResult(Fragment lf, Fragment rf, - String s, boolean isInteger) { - int l = Integer.parseInt(lf.value), r = Integer.parseInt(rf.value); - long l0 = Long.parseLong(lf.value), r0 = Long.parseLong(rf.value); - String ret = null; - char f0 = s.charAt(0); - switch (f0) { - case '+': - ret = String.valueOf(isInteger ? l + r : l0 + r0); - break; - case '-': - ret = String.valueOf(isInteger ? l - r : l0 - r0); - break; - case '*': - ret = String.valueOf(isInteger ? l * r : l0 * r0); - break; - case '/': - ret = String.valueOf(isInteger ? l / r : l0 / r0); - break; - case '%': - ret = String.valueOf(isInteger ? l % r : l0 % r0); - break; - case '<': - if (s.length() == 2 && s.charAt(1) == '=') - ret = String.valueOf(isInteger ? l <= r : l0 <= r0); - else if (s.length() == 2 && s.charAt(1) == '<') - ret = String.valueOf(isInteger ? l << r : l0 << r0); - else if (s.length() == 1) - ret = String.valueOf(isInteger ? l < r : l0 < r0); - else - throw new ExpressionError("The operation is not supported: " - + lf.type + " " + s + " " + rf.type); - break; - case '>': - if (s.length() == 3 && s.charAt(1) == '>' && s.charAt(2) == '>') - ret = String.valueOf(isInteger ? l >>> r : l0 >>> r0); - else if (s.length() == 2 && s.charAt(1) == '>') - ret = String.valueOf(isInteger ? l >> r : l0 >> r0); - else if (s.length() == 2 && s.charAt(1) == '=') - ret = String.valueOf(isInteger ? l >= r : l0 >= r0); - else if (s.length() == 1) - ret = String.valueOf(isInteger ? l > r : l0 > r0); - else - throw new ExpressionError("The operation is not supported: " - + lf.type + " " + s + " " + rf.type); - break; - case '&': - ret = String.valueOf(isInteger ? l & r : l0 & r0); - break; - case '|': - ret = String.valueOf(isInteger ? l | r : l0 | r0); - break; - case '^': - ret = String.valueOf(isInteger ? l ^ r : l0 ^ r0); - break; - default: - throw new ExpressionError("The operation is not supported: " - + lf.type + " " + s + " " + rf.type); - } - return ret; - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementFor.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementFor.java deleted file mode 100644 index c0d2bfa57..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementFor.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.firefly.template.parser; - -import com.firefly.utils.StringUtils; - -public class StatementFor implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - String[] e = StringUtils.split(content, ':'); - e[0] = e[0].trim(); - e[1] = e[1].trim(); - - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "int " + e[0] + "_index = -1;\n"); - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "for(Object " - + e[0] + " : objNav.getCollection(model, \"" - + e[1].substring(e[1].indexOf("${") + 2, e[1].length() - 1) - + "\")){\n"); - javaFileBuilder.getPreBlank().append('\t'); - javaFileBuilder.write(javaFileBuilder.getPreBlank() + e[0] + "_index++;\n"); - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "model.put(\"" - + e[0] + "\", " + e[0] + ");\n"); - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "model.put(\"" - + e[0] + "_index\", " + e[0] + "_index);\n"); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementIf.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementIf.java deleted file mode 100644 index 99d006579..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementIf.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.firefly.template.parser; - -public class StatementIf extends StatementExpression { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - writePrefix(javaFileBuilder); - content = content.trim(); - javaFileBuilder.write(parse(content)); - javaFileBuilder.write("){\n"); - javaFileBuilder.getPreBlank().append('\t'); - } - - protected void writePrefix(JavaFileBuilder javaFileBuilder) { - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "if ("); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementInclude.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementInclude.java deleted file mode 100644 index b491b0508..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementInclude.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.template.parser; - -import com.firefly.utils.StringUtils; - -public class StatementInclude implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - String[] p = StringUtils.split(content, '?'); - if (p.length > 1) - StateMachine.parse("#set", p[1], javaFileBuilder); -// p[0] = p[0].replace('/', '_').substring(0, p[0].indexOf('.')); - javaFileBuilder.write(javaFileBuilder.getPreBlank() - + "templateFactory.getView(\"" + p[0] + "\").render(model, out);\n"); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementSet.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementSet.java deleted file mode 100644 index f605a9bf9..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementSet.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.firefly.template.parser; - -import com.firefly.utils.StringUtils; - -public class StatementSet implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - String[] params = StringUtils.split(content, '&'); - for (String param : params) { - String[] v = StringUtils.split(param, '='); - if (v[1].length() > 3 && v[1].charAt(0) == '$' - && v[1].charAt(1) == '{' - && v[1].charAt(v[1].length() - 1) == '}') { - javaFileBuilder.write(javaFileBuilder.getPreBlank() - + "model.put(\"" + v[0] + "\", objNav.find(model, \"" - + v[1].substring(2, v[1].length() - 1) + "\"));\n"); - } else { - javaFileBuilder.write(javaFileBuilder.getPreBlank() - + "model.put(\"" + v[0] + "\", \"" + v[1] + "\");\n"); - } - } - - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitch.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitch.java deleted file mode 100644 index 78c05cec5..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitch.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.template.parser; - -public class StatementSwitch implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - javaFileBuilder.outBreak = false; - String obj = content.substring(content.indexOf("${") + 2, - content.length() - 1); - javaFileBuilder.write(javaFileBuilder.getPreBlank() - + "switch(objNav.getInteger(model, \"" + obj + "\")) {\n"); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitchCase.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitchCase.java deleted file mode 100644 index 2244b6b83..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitchCase.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.template.parser; - -public class StatementSwitchCase implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - if (javaFileBuilder.outBreak) { - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "break;\n"); - javaFileBuilder.getPreBlank().deleteCharAt(0); - } - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "case " + content - + ":\n"); - javaFileBuilder.outBreak = true; - javaFileBuilder.getPreBlank().append('\t'); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitchDefault.java b/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitchDefault.java deleted file mode 100644 index 237031d2f..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/StatementSwitchDefault.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.firefly.template.parser; - -public class StatementSwitchDefault implements Statement { - - @Override - public void parse(String content, JavaFileBuilder javaFileBuilder) { - if (javaFileBuilder.outBreak) { - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "break;\n"); - javaFileBuilder.getPreBlank().deleteCharAt(0); - } - javaFileBuilder.write(javaFileBuilder.getPreBlank() + "default:\n"); - javaFileBuilder.outBreak = true; - javaFileBuilder.getPreBlank().append('\t'); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/parser/ViewFileReader.java b/firefly-template/src/main/java/com/firefly/template/parser/ViewFileReader.java deleted file mode 100644 index d63a39472..000000000 --- a/firefly-template/src/main/java/com/firefly/template/parser/ViewFileReader.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.firefly.template.parser; - -import java.io.Closeable; -import java.io.File; -import java.io.FileFilter; -import java.util.ArrayList; -import java.util.List; - -import com.firefly.template.Config; -import com.firefly.template.exception.TemplateFileReadException; -import com.firefly.template.support.CompileUtils; -import com.firefly.utils.StringUtils; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.io.FileUtils; -import com.firefly.utils.io.LineReaderHandler; - -public class ViewFileReader { - private Config config; - private List javaFiles = new ArrayList(); - private List templateFiles = new ArrayList(); - private List classNames = new ArrayList(); - private List javaFiles0 = new ArrayList(); - - public ViewFileReader(Config config) { - this.config = config; - if (init() != 0) - throw new TemplateFileReadException("template file parse error"); - } - - private int init() { - int ret = 0; - File file = new File(config.getCompiledPath()); - if (!file.exists()) { - file.mkdir(); - } - read0(new File(config.getViewPath())); - if(javaFiles0.size() > 0) - ret = CompileUtils.compile(config.getCompiledPath(), config.getClassPath(), config.getCharset(), javaFiles0); - return ret; - } - - public List getJavaFiles() { - return javaFiles; - } - - public List getTemplateFiles() { - return templateFiles; - } - - public List getClassNames() { - return classNames; - } - - public void setClassNames(List classNames) { - this.classNames = classNames; - } - - private void read0(File file) { - file.listFiles(new FileFilter() { - @Override - public boolean accept(File f) { - if (f.isDirectory()) { - read0(f); - } else if (f.getName().endsWith("." + config.getSuffix())) { - parse(f); - } - return false; - } - }); - } - - private void parse(File f) { - String name = f.getAbsolutePath().replace('\\', '/'); - templateFiles.add(name.substring(config.getViewPath().length() - 1)); - - name = name.substring(config.getViewPath().length() - 1, - name.length() - config.getSuffix().length()).replace('/', '_') - + "java"; - classNames.add(name.substring(0, name.length() - 5)); - - String javaFile = config.getCompiledPath() + "/" + name; - javaFiles.add(javaFile); - - String classFileName = javaFile.substring(0, javaFile.length() - 4) + "class"; - File classFile = new File(classFileName); - if(classFile.exists() && classFile.lastModified() >= f.lastModified()) { -// System.out.println(classFile.getAbsolutePath() + "|" + classFile.lastModified() + "|" + f.lastModified()); - return; - } - javaFiles0.add(javaFile); - // System.out.println("======= " + name + " ======="); - - JavaFileBuilder javaFileBuilder = new JavaFileBuilder( - config.getCompiledPath(), name, config); - TemplateFileLineReaderHandler lineReaderHandler = new TemplateFileLineReaderHandler( - javaFileBuilder); - - try { - FileUtils.read(f, lineReaderHandler, config.getCharset()); - } catch (Throwable t) { - Config.LOG.error("view file read error", t); - } finally { - lineReaderHandler.close(); - } - } - - private void parseComment(String comment, JavaFileBuilder javaFileBuilder) { - int start = comment.indexOf('#'); - int end = 0; - if (start >= 0) { - for (int i = start; i < comment.length(); i++) { - if (comment.charAt(i) == ' ' || comment.charAt(i) == '\t' - || comment.charAt(i) == '\r' - || comment.charAt(i) == '\n') { - end = i; - break; - } - } - if (end <= start) - end = comment.length(); - - String keyword = comment.substring(start, end); - String content = comment.substring(end).trim(); - // System.out.println(comment.length() + "|1|comment:\t" + keyword - // + " " + content); - StateMachine.parse(keyword, content, javaFileBuilder); - } - } - - private void parseText(String text, JavaFileBuilder javaFileBuilder) { - int cursor = 0; - String t = null; - for (int start, end; (start = text.indexOf("${", cursor)) != -1 - && (end = text.indexOf("}", start)) != -1;) { - t = text.substring(cursor, start); - if (!VerifyUtils.isEmpty(t)) - javaFileBuilder.writeText(t); - String e = text.substring(start + 2, end); - int l = e.indexOf('('); - if(l > 0) { - int r = e.indexOf(')'); - if(r > l) { // function - String functionName = e.substring(0, l); - String[] params = StringUtils.split(e.substring(l + 1, r), ','); - javaFileBuilder.writeFunction(functionName, params); - } - } else - javaFileBuilder.writeObject(e); - cursor = end + 1; - } - t = text.substring(cursor, text.length()); - if (!VerifyUtils.isEmpty(t)) - javaFileBuilder.writeText(t); - } - - private class TemplateFileLineReaderHandler implements LineReaderHandler, - Closeable { - JavaFileBuilder javaFileBuilder; - StringBuilder text = new StringBuilder(); - StringBuilder comment = new StringBuilder(); - int status = 0; - - public TemplateFileLineReaderHandler(JavaFileBuilder javaFileBuilder) { - this.javaFileBuilder = javaFileBuilder; - } - - @Override - public void close() { - if (text.length() > 0) { - parseText(text.toString(), javaFileBuilder); - } - - javaFileBuilder.write("\t}\n\n").writeTail().write("}"); - javaFileBuilder.close(); - } - - @Override - public void readline(String line, int num) { - switch (status) { - case 0: - int i = line.indexOf(""); - if (j > i + 4) { // html注释结束 - assert comment.length() == 0; - parseComment(line.substring(i + 4, j).trim(), - javaFileBuilder); - } else { - status = 1; - comment.append(line.substring(i + 4).trim() + "\n"); - } - } else { - text.append(line.trim()); - } - break; - case 1: - int j = line.indexOf("-->"); - if (j >= 0) { // html注释结束 - status = 0; - comment.append(line.substring(0, j).trim()); - parseComment(comment.toString(), javaFileBuilder); - comment = new StringBuilder(); - } else - comment.append(line.trim() + "\n"); - break; - } - - } - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/support/CompileUtils.java b/firefly-template/src/main/java/com/firefly/template/support/CompileUtils.java deleted file mode 100644 index e6415b320..000000000 --- a/firefly-template/src/main/java/com/firefly/template/support/CompileUtils.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.firefly.template.support; - -import java.util.LinkedList; -import java.util.List; - -import javax.tools.JavaCompiler; -import javax.tools.ToolProvider; - -public class CompileUtils { - public static int compile(String path, String classPath, String encoding, List files) { - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - List params = new LinkedList(); - params.add("-encoding"); - params.add(encoding); - params.add("-sourcepath"); - params.add(path); - if(classPath != null) { - params.add("-classpath"); - params.add(classPath); - } - params.addAll(files); - return compiler.run(null, null, null, params.toArray(new String[0])); - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/support/ObjectMetaInfoCache.java b/firefly-template/src/main/java/com/firefly/template/support/ObjectMetaInfoCache.java deleted file mode 100644 index 26a0bdfbd..000000000 --- a/firefly-template/src/main/java/com/firefly/template/support/ObjectMetaInfoCache.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.firefly.template.support; - -import java.lang.reflect.Method; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - - -public class ObjectMetaInfoCache { - private Map map; - - public ObjectMetaInfoCache() { - map = new ConcurrentHashMap(); - } - - public Method get(Class clazz, String propertyName) { - return map.get(clazz.getName() + "#" + propertyName); - } - - public void put(Class clazz, String propertyName, Method method) { - map.put(clazz.getName() + "#" + propertyName, method); - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/support/ObjectNavigator.java b/firefly-template/src/main/java/com/firefly/template/support/ObjectNavigator.java deleted file mode 100644 index 98b9a0a24..000000000 --- a/firefly-template/src/main/java/com/firefly/template/support/ObjectNavigator.java +++ /dev/null @@ -1,306 +0,0 @@ -package com.firefly.template.support; - -import java.lang.reflect.Method; -import java.util.AbstractCollection; -import java.util.Collection; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import com.firefly.template.Config; -import com.firefly.template.Model; -import com.firefly.template.exception.ExpressionError; -import com.firefly.utils.ReflectUtils; -import com.firefly.utils.StringUtils; - -public class ObjectNavigator { - private ObjectMetaInfoCache cache; - private IdentityHashMap, ArrayObj> map; - - private ObjectNavigator() { - this.cache = new ObjectMetaInfoCache(); - this.map = new IdentityHashMap, ArrayObj>(); - - map.put(long[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((long[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((long[]) obj).length; - } - }); - map.put(double[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((double[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((double[]) obj).length; - } - }); - map.put(int[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((int[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((int[]) obj).length; - } - }); - map.put(float[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((float[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((float[]) obj).length; - } - }); - map.put(char[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((char[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((char[]) obj).length; - } - }); - map.put(boolean[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((boolean[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((boolean[]) obj).length; - } - }); - map.put(short[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((short[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((short[]) obj).length; - } - }); - map.put(byte[].class, new ArrayObj() { - @Override - public Object get(Object obj, int index) { - return ((byte[]) obj)[index]; - } - - @Override - public int size(Object obj) { - return ((byte[]) obj).length; - } - }); - } - - interface ArrayObj { - Object get(Object obj, int index); - - int size(Object obj); - } - - private class ArrayCollection extends AbstractCollection { - - private Object obj; - private Object[] array; - private ArrayObj arrayObj; - private boolean isArrayObj; - private int size; - private Iterator iterator; - - public ArrayCollection(Object array) { - obj = array; - ArrayObj a = map.get(array.getClass()); - isArrayObj = a != null; - arrayObj = a; - if (!isArrayObj) { - this.array = (Object[]) array; - size = this.array.length; - } else - size = a.size(array); - iterator = new ArrayInterator(); - } - - private class ArrayInterator implements Iterator { - - private int i = 0; - - @Override - public boolean hasNext() { - return i < size; - } - - @SuppressWarnings("unchecked") - @Override - public E next() { - return (E) (isArrayObj ? arrayObj.get(obj, i++) : array[i++]); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - - } - - @Override - public Iterator iterator() { - return iterator; - } - - @Override - public int size() { - return size; - } - - } - - private static class Holder { - private static ObjectNavigator instance = new ObjectNavigator(); - } - - public static ObjectNavigator getInstance() { - return Holder.instance; - } - - @SuppressWarnings("rawtypes") - public Collection getCollection(Model model, String el) { - Object ret = find(model, el); - return (Collection) (ret instanceof Collection ? ret - : new ArrayCollection(ret)); - } - - public Integer getInteger(Model model, String el) { - Object ret = find(model, el); - return ret != null ? ((Number) ret).intValue() : 0; - } - - public Float getFloat(Model model, String el) { - Object ret = find(model, el); - return ret != null ? ((Number) ret).floatValue() : 0F; - } - - public Long getLong(Model model, String el) { - Object ret = find(model, el); - return ret != null ? ((Number) ret).longValue() : 0L; - } - - public Double getDouble(Model model, String el) { - Object ret = find(model, el); - return ret != null ? ((Number) ret).doubleValue() : 0.0; - } - - public Boolean getBoolean(Model model, String el) { - Object ret = find(model, el); - return ret != null ? (Boolean) ret : false; - } - - public String getValue(Model model, String el) { - Object ret = find(model, el); - return ret != null ? String.valueOf(ret) : ""; - } - - public Object find(Model model, String el) { - Object current = null; - String[] elements = StringUtils.split(el, '.'); - if ((elements != null) && (elements.length > 0)) { - current = getObject(model, elements[0]); - if (current == null) - return null; - - for (int i = 1; i < elements.length; i++) { - current = getObject(current, elements[i]); - } - } - return current; - } - - private Object getArrayObject(Object obj, int index) { - ArrayObj a = map.get(obj.getClass()); - if (a != null) - return a.get(obj, index); - else - return ((Object[]) obj)[index]; - } - - private Object getObject(Object current, String el) { - boolean root = current instanceof Model; - String element = el.trim(); - int listOrMapPrefixIndex = element.indexOf('['); - if (listOrMapPrefixIndex > 0) { // map or list or array - int listOrMapSuffixIndex = element.indexOf(']', - listOrMapPrefixIndex); - if (listOrMapSuffixIndex != element.length() - 1) - throw new ExpressionError("list or map expression error: " - + element); - - String keyEl = element.substring(listOrMapPrefixIndex + 1, - listOrMapSuffixIndex); - String p = element.substring(0, listOrMapPrefixIndex); - Object obj = root ? ((Model) current).get(p) : getObjectProperty( - current, p); - - if (isMapKey(keyEl)) { // map - if ((obj instanceof Map)) - return ((Map) obj).get(keyEl.substring(1, - keyEl.length() - 1)); - } else { // list or array - int index = Integer.parseInt(keyEl); - if ((obj instanceof List)) - return ((List) obj).get(index); - else - return getArrayObject(obj, index); - } - } else if (listOrMapPrefixIndex < 0) { // object - return root ? ((Model) current).get(element) : getObjectProperty( - current, element); - } else { - throw new ExpressionError("expression error: " + element); - } - return null; - } - - private boolean isMapKey(String el) { - char head = el.charAt(0); - char tail = el.charAt(el.length() - 1); - return ((head == '\'') && (tail == '\'')) - || ((head == '"') && (tail == '"')); - } - - private Object getObjectProperty(Object current, String propertyName) { - Class clazz = current.getClass(); - Method method = cache.get(clazz, propertyName); - if (method == null) { - method = ReflectUtils.getGetterMethod(clazz, propertyName); - cache.put(clazz, propertyName, method); - } - - Object ret = null; - try { - ret = method.invoke(current); - } catch (Throwable e) { - Config.LOG.error("getObjectProperty error", e); - } - return ret; - } - -} diff --git a/firefly-template/src/main/java/com/firefly/template/support/RPNUtils.java b/firefly-template/src/main/java/com/firefly/template/support/RPNUtils.java deleted file mode 100644 index a7c6ff6bc..000000000 --- a/firefly-template/src/main/java/com/firefly/template/support/RPNUtils.java +++ /dev/null @@ -1,488 +0,0 @@ -package com.firefly.template.support; - -import java.util.Arrays; -import java.util.Deque; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import com.firefly.template.exception.ExpressionError; -import com.firefly.utils.VerifyUtils; - -public class RPNUtils { - - private static final Set CONDITIONAL_OPERATOR = new HashSet(Arrays.asList("==", "!=", ">", "<", ">=", "<=")); - private static final Set ARITHMETIC_OR_LOGICAL_OPERATOR = new HashSet(Arrays.asList("&", "|")); - private static final Set LOGICAL_OPERATOR = new HashSet(Arrays.asList("&&", "||")); - private static final Set ASSIGNMENT_OPERATOR = new HashSet(Arrays.asList("=", "+=", "-=", "*=", "/=", "%=", "^=", "&=", "|=", "<<=", ">>=", ">>>=")); - - /** - * 生成逆波兰表达式 - * 符号优先级: - * 10: "*", "/", "%" - * 9: "+", "-" - * 8: ">>", ">>>", "<<" - * 7: ">", "<", ">=", "<=" - * 6: "==", "!=" - * 5: "&" - * 4: "|" - * 3: "^" - * 2: "&&" - * 1: "||" - * 0: "=", "+=", "-=", "*=", "/=", "%=", "^=", "&=", "|=", "<<=", ">>=", ">>>=" //0 - * @param content 需要转化的表达式 - * @return 逆波兰表示法 - */ - public static List getReversePolishNotation(String text) { - String content = preprocessing(text); - StringBuilder pre = new StringBuilder(); - Deque symbolDeque = new LinkedList(); - List list = new LinkedList(); - char c, n, n1, n2; - - for (int i = 0; i < content.length(); i++) { - switch (content.charAt(i)) { - case '\'': - case '"': - int next = content.indexOf(content.charAt(i), i + 1); - while(content.charAt(next - 1) == '\\') - next = content.indexOf(content.charAt(i), next + 1); - - pre.append(content.substring(i, next + 1)); - i += next - i; - break; - case '(': - pre.delete(0, pre.length()); - - Fragment f0 = new Fragment(); - f0.priority = -1000; - f0.value = "("; - symbolDeque.push(f0); - break; - case '*': - case '/': - case '%': - n = content.charAt(i + 1); - if(n == '=') { // *= /= %= - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + "=", 0, symbolDeque, list); - i++; - break; - } - - // * / % - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)), 10, symbolDeque, list); - break; - case '+': - case '-': - n = content.charAt(i + 1); - if(n == '=') { // += -= - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + "=", 0, symbolDeque, list); - i++; - break; - } - - if(n == content.charAt(i)) { - pre.append(content.charAt(i)).append(content.charAt(i + 1)); - i++; - break; - } - - // 正负号判断 - boolean s = false; - String left0 = "*/%+-><=&|(^"; - if(i == 0) { - s = true; - } else { - for(int j = i - 1; j >= 0; j--) { - char ch = content.charAt(j); - if(!Character.isWhitespace(ch) ) { - if(left0.indexOf(ch) >= 0) { - int _n = j - 1; - s = _n < 0 || !(ch == '+' && content.charAt(_n) == '+' || ch == '-' && content.charAt(_n) == '-'); - } - break; - } - } - } - - - // + - - if(s) { - pre.append(content.charAt(i)); - } else { - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)), 9, symbolDeque, list); - } - break; - - case '>': - case '<': - c = content.charAt(i); - n = content.charAt(i + 1); - - if(c == n) { - if(i + 2 < content.length()) { - n1 = content.charAt(i + 2); - if(n1 == '=') { // <<= >>= - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + content.charAt(i + 1) + "=", 0, symbolDeque, list); - i += 2; - break; - } - } - - // << >> - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + content.charAt(i + 1), 8, symbolDeque, list); - i++; - break; - } - - if(i + 2 < content.length()) { - n1 = content.charAt(i + 2); - if(c == '>' && n == '>' && n1 == '>') { - n2 = content.charAt(i + 3); - if(i + 3 < content.length()) { - if(n2 == '=') { // >>>= - outValue(pre, list); - outSymbol(">>>=", 0, symbolDeque, list); - i += 3; - break; - } - } - - // >>> - outValue(pre, list); - outSymbol(">>>", 8, symbolDeque, list); - i += 2; - break; - } - } - - if(n == '=') { // <= >= - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + "=", 7, symbolDeque, list); - i++; - break; - } - - // < > - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)), 7, symbolDeque, list); - break; - - case '=': - n = content.charAt(i + 1); - if(n == '=') { // == - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + "=", 6, symbolDeque, list); - i++; - break; - } - - // = - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)), 0, symbolDeque, list); - break; - - case '!': - n = content.charAt(i + 1); - if(n == '=') { // != - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + "=", 6, symbolDeque, list); - i++; - break; - } - pre.append('!'); - break; - - case '&': - n = content.charAt(i + 1); - if(n == '&') { // && - outValue(pre, list); - outSymbol("&&", 2, symbolDeque, list); - i++; - break; - } - - if(n == '=') { // &= - outValue(pre, list); - outSymbol("&=", 0, symbolDeque, list); - i++; - break; - } - - // & - outValue(pre, list); - outSymbol("&", 5, symbolDeque, list); - break; - case '|': - n = content.charAt(i + 1); - if(n == '|') { // || - outValue(pre, list); - outSymbol("||", 1, symbolDeque, list); - i++; - break; - } - - if(n == '=') { // |= - outValue(pre, list); - outSymbol("|=", 0, symbolDeque, list); - i++; - break; - } - - // | - outValue(pre, list); - outSymbol("|", 4, symbolDeque, list); - break; - case '^': - n = content.charAt(i + 1); - if(n == '=') {// ^= - outValue(pre, list); - outSymbol(String.valueOf(content.charAt(i)) + "=", 0, symbolDeque, list); - i++; - break; - } - - // ^ - outValue(pre, list); - outSymbol("^", 3, symbolDeque, list); - break; - case ')': - outValue(pre, list); - outSymbol(")", -1000, symbolDeque, list); - break; - default: - pre.append(content.charAt(i)); - break; - } - } - - outValue(pre, list); - while(!symbolDeque.isEmpty()) - list.add(symbolDeque.pop()); - - return list; - } - - /** - * 预处理单目运算符,把单目运算变成双目运算,例如:-(3-4)*4 转化为 (0-(3-4))*4 - * @param content 需要转化的表达式 - * @return 转化后的表达式 - */ - private static String preprocessing(String content) { - StringBuilder ret = new StringBuilder(); - if(preprocessing0(content, ret)) - return ret.toString(); - else - return preprocessing(ret.toString()); - } - - private static boolean preprocessing0(String content, StringBuilder ret) { - boolean t = true; - StringBuilder pre = new StringBuilder(); - int c = 0; - int start = 0; - for (int i = 0; i < content.length(); i++) { - char ch = content.charAt(i); - switch (ch) { - case '!': - case '+': - case '-': - boolean l = false; - boolean r = false; - String left0 = "*/%+-><=&|(^"; - if(i == 0 || ch == '!') { - l = true; - } else { - for(int j = i - 1; j >= 0; j--) { - char c0 = content.charAt(j); - if(!Character.isWhitespace(c0) ) { - if(left0.indexOf(c0) >= 0) { - int _n = j - 1; - l = _n < 0 || !(c0 == '+' && content.charAt(_n) == '+' || c0 == '-' && content.charAt(_n) == '-'); - } - break; - } - } - } - - if(l) { - for (int j = i + 1; j < content.length(); j++) { - char c0 = content.charAt(j); - if(!Character.isWhitespace(c0)) { - if(c0 == '(') { - start = j + 1; - r = true; - pre.append(c0); - } - break; - } - } - } - if(l && r) { - t = false; - c += 1; - while(c != 0) { - char c0 = content.charAt(start++); - if(c0 == '(') - c++; - else if(c0 == ')') - c--; - pre.append(c0); - } - if(ch == '!') - ret.append("(").append(pre).append(" == false) "); - else - ret.append("(0 ").append(ch).append(' ').append(pre).append(") "); - - pre.delete(0, pre.length()); - i = start; - } else - ret.append(ch); - break; - default: - ret.append(ch); - break; - } - } - return t; - } - - /** - * 去掉多余+,-号 - * @param v - * @return - */ - private static String getSimpleValue(String v) { - int left = 0; - boolean n = false; - for (int i = 0; i < v.length(); i++) { - char ch = v.charAt(i); - if(ch == '-') - n = true; - if(ch != '+' && ch != '-' && !Character.isWhitespace(ch)) { - left = i; - break; - } - } - String s = v.substring(left); - return n ? "-" + s : s; - } - - private static boolean isString(String v) { - int start = v.charAt(0); - int end = v.charAt(v.length() - 1); - return (start == '"' && end == '"') || (start == '\'' && end == '\''); - } - - private static boolean isBoolean(String v) { - int start = v.charAt(0) == '!' ? 1 : 0; - return v.substring(start).trim().equals("true") || v.substring(start).trim().equals("false"); - } - - private static boolean isVariable(String v) { - int start = v.indexOf("${"); - int end = v.indexOf('}'); - return start >= 0 && start < end; - } - - private static void outValue(StringBuilder pre, List list) { - String v = pre.toString().trim(); - if(v.length() > 0) { - Fragment f= new Fragment(); - f.priority = -200; - f.value = getSimpleValue(v); - - if(isVariable(f.value)) { - f.type = Type.VARIABLE; - } else if(isBoolean(f.value)) { - f.type = Type.BOOLEAN; - } else if(isString(f.value)) { - f.type = Type.STRING; - f.value = "\"" + f.value.substring(1, f.value.length() - 1) + "\""; - } else if(VerifyUtils.isFloat(f.value)) { - f.type = Type.FLOAT; - char end = f.value.charAt(f.value.length() - 1); - if(end == 'f' || end == 'F') - f.value = f.value.substring(0, f.value.length() - 1); - } else if(VerifyUtils.isDouble(f.value)) { - f.type = Type.DOUBLE; - } else if(VerifyUtils.isInteger(f.value)) { - f.type = Type.INTEGER; - } else if(VerifyUtils.isLong(f.value)) { - f.type = Type.LONG; - char end = f.value.charAt(f.value.length() - 1); - if(end == 'l' || end == 'L') - f.value = f.value.substring(0, f.value.length() - 1); - } else if(f.value.equals("null")) { - f.type = Type.NULL; - } else - throw new ExpressionError("Can not determine the type: " + f.value); - - list.add(f); - } - pre.delete(0, pre.length()); - } - - private static void outSymbol(String value, int priority, Deque symbolDeque, List list) { - Fragment f = new Fragment(); - f.value = value; - f.priority = priority; - if(ARITHMETIC_OR_LOGICAL_OPERATOR.contains(value)) { - f.type = Type.ARITHMETIC_OR_LOGICAL_OPERATOR; - } else if(LOGICAL_OPERATOR.contains(value)) { - f.type = Type.LOGICAL_OPERATOR; - } else if(ASSIGNMENT_OPERATOR.contains(value)) { - f.type = Type.ASSIGNMENT_OPERATOR; - } else if(CONDITIONAL_OPERATOR.contains(value)) { - f.type = Type.CONDITIONAL_OPERATOR; - } else - f.type = Type.ARITHMETIC_OPERATOR; - - if(f.value.equals(")")) { - for(Fragment top = null; !symbolDeque.isEmpty() - && !(top = symbolDeque.pop()).value.equals("("); ) - list.add(top); - } else { - for(Fragment top = null; !symbolDeque.isEmpty() - && (top = symbolDeque.peek()).priority >= f.priority; ) { - list.add(top); - symbolDeque.pop(); - } - symbolDeque.push(f); - } - } - - public static class Fragment { - public int priority; - public String value; - public Type type; - - public String toString() { - return value; - } - } - - public static enum Type { - VARIABLE, - INTEGER, - LONG, - FLOAT, - DOUBLE, - BOOLEAN, - STRING, - NULL, - - ARITHMETIC_OPERATOR, - LOGICAL_OPERATOR, - ASSIGNMENT_OPERATOR, - ARITHMETIC_OR_LOGICAL_OPERATOR, - CONDITIONAL_OPERATOR - } -} diff --git a/firefly-template/src/main/java/com/firefly/template/view/AbstractView.java b/firefly-template/src/main/java/com/firefly/template/view/AbstractView.java deleted file mode 100644 index 2ced274c0..000000000 --- a/firefly-template/src/main/java/com/firefly/template/view/AbstractView.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.template.view; - -import java.io.OutputStream; - -import com.firefly.template.Config; -import com.firefly.template.Model; -import com.firefly.template.TemplateFactory; -import com.firefly.template.View; - -public abstract class AbstractView implements View { - - protected TemplateFactory templateFactory; - - @Override - public void render(Model model, OutputStream out) { - try { - main(model, out); - } catch (Throwable t) { - Config.LOG.error("view render error", t); - } - } - - abstract protected void main(Model model, OutputStream out) throws Throwable; - -} diff --git a/firefly-template/src/test/java/test/Bar.java b/firefly-template/src/test/java/test/Bar.java deleted file mode 100644 index f4e4fa706..000000000 --- a/firefly-template/src/test/java/test/Bar.java +++ /dev/null @@ -1,38 +0,0 @@ -package test; - -public class Bar { - private long serialNumber; - private String info; - private double price; - - public long getSerialNumber() { - return serialNumber; - } - - public void setSerialNumber(long serialNumber) { - this.serialNumber = serialNumber; - } - - public String getInfo() { - return info; - } - - public void setInfo(String info) { - this.info = info; - } - - public double getPrice() { - return price; - } - - public void setPrice(double price) { - this.price = price; - } - - @Override - public String toString() { - return "Bar [serialNumber=" + serialNumber + ", info=" + info - + ", price=" + price + "]"; - } - -} diff --git a/firefly-template/src/test/java/test/CharsetDemo.java b/firefly-template/src/test/java/test/CharsetDemo.java deleted file mode 100644 index 3165b3be1..000000000 --- a/firefly-template/src/test/java/test/CharsetDemo.java +++ /dev/null @@ -1,28 +0,0 @@ -package test; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.util.Arrays; - -import com.firefly.template.Model; -import com.firefly.template.TemplateFactory; -import com.firefly.template.View; - -public class CharsetDemo { - - /** - * @param args - */ - public static void main(String[] args) throws Throwable { - String str = "测试一下页面"; - System.out.println(Arrays.toString(str.getBytes("UTF-8"))); - TemplateFactory t = new TemplateFactory(new File(CharsetDemo.class.getResource("/page").toURI())).init(); - View view = t.getView("/index2.html"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Model model = new ModelMock(); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - } - -} diff --git a/firefly-template/src/test/java/test/Foo.java b/firefly-template/src/test/java/test/Foo.java deleted file mode 100644 index fd8341772..000000000 --- a/firefly-template/src/test/java/test/Foo.java +++ /dev/null @@ -1,43 +0,0 @@ -package test; - -import java.util.Map; - -public class Foo { - private int[] numbers = { 3, 4, 5, 6 }; - private Integer[] bags = {1, 2, 3, 4, 5}; - private Bar bar; - private Map map; - - public Integer[] getBags() { - return bags; - } - - public void setBags(Integer[] bags) { - this.bags = bags; - } - - public int[] getNumbers() { - return numbers; - } - - public void setNumbers(int[] numbers) { - this.numbers = numbers; - } - - public Bar getBar() { - return bar; - } - - public void setBar(Bar bar) { - this.bar = bar; - } - - public Map getMap() { - return map; - } - - public void setMap(Map map) { - this.map = map; - } - -} diff --git a/firefly-template/src/test/java/test/ModelMock.java b/firefly-template/src/test/java/test/ModelMock.java deleted file mode 100644 index 3a28dabe4..000000000 --- a/firefly-template/src/test/java/test/ModelMock.java +++ /dev/null @@ -1,33 +0,0 @@ -package test; - -import java.util.HashMap; -import java.util.Map; - -import com.firefly.template.Model; - -public class ModelMock implements Model { - - private Map map = new HashMap(); - - @Override - public void put(String key, Object object) { - map.put(key, object); - } - - @Override - public Object get(String key) { - return map.get(key); - } - - @Override - public void remove(String key) { - map.remove(key); - - } - - @Override - public void clear() { - map.clear(); - } - -} diff --git a/firefly-template/src/test/java/test/TestConfig.java b/firefly-template/src/test/java/test/TestConfig.java deleted file mode 100644 index d8c5066ad..000000000 --- a/firefly-template/src/test/java/test/TestConfig.java +++ /dev/null @@ -1,139 +0,0 @@ -package test; - -import static org.hamcrest.Matchers.is; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.template.Config; -import com.firefly.template.Function; -import com.firefly.template.FunctionRegistry; -import com.firefly.template.Model; -import com.firefly.template.TemplateFactory; -import com.firefly.template.View; - -public class TestConfig { - - @Test - public void test() { - Config config = new Config(); - config.setViewPath("/page"); - Assert.assertThat(config.getCompiledPath(), is("/page/_compiled_view")); - - config.setViewPath("/page2/"); - Assert.assertThat(config.getCompiledPath(), is("/page2/_compiled_view")); - } - - public static void main(String[] args) throws IOException, URISyntaxException { - User user = new User(); - user.setName("Jim"); - user.setAge(25); - - Function function = new Function(){ - @Override - public void render(Model model, OutputStream out, Object... obj) { - Integer i = (Integer)obj[0]; - String str = (String)obj[1]; - String o = String.valueOf(obj[2]); - - try { - out.write((i + "|" + str + "|" + o).getBytes("UTF-8")); - } catch (IOException e) { - e.printStackTrace(); - } - }}; - FunctionRegistry.add("testFunction", function); - - Function function2 = new Function(){ - @Override - public void render(Model model, OutputStream out, Object... obj) { - try { - out.write("testFunction2".getBytes("UTF-8")); - } catch (IOException e) { - e.printStackTrace(); - } - }}; - FunctionRegistry.add("testFunction2", function2); - - // #if #elseif #else - TemplateFactory t = new TemplateFactory(new File(TestConfig.class.getResource("/page").toURI())).init(); -// System.out.println(t.getConfig().getViewPath()); -// System.out.println(t.getConfig().getCompiledPath()); - View view = t.getView("/testIf.html"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Model model = new ModelMock(); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - - out = new ByteArrayOutputStream(); - model.put("user", user); - model.put("login", true); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - - // #for - model = new ModelMock(); - out = new ByteArrayOutputStream(); - view = t.getView("/testFor.html"); - - List list = new ArrayList(); - user = new User(); - user.setName("Tom"); - user.setAge(20); - list.add(user); - - user = new User(); - user.setName("小明"); - user.setAge(13); - list.add(user); - - user = new User(); - user.setName("小红"); - user.setAge(20); - list.add(user); - - model.put("users", list); - model.put("intArr", new int[]{1,2,3,4,5}); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - - // #switch #case #default - model = new ModelMock(); - out = new ByteArrayOutputStream(); - view = t.getView("/testSwitch.html"); - model.put("stage", 2); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - - // #set - model = new ModelMock(); - out = new ByteArrayOutputStream(); - view = t.getView("/testSet.html"); - model.put("name", "迈克"); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - - // #include - model = new ModelMock(); - out = new ByteArrayOutputStream(); - view = t.getView("/testInclude.html"); - view.render(model, out); - out.close(); - System.out.println(out.toString()); - -// FunctionRegistry.MAP.get("").render(model, out, obj) - } -} diff --git a/firefly-template/src/test/java/test/TestObjNavigator.java b/firefly-template/src/test/java/test/TestObjNavigator.java deleted file mode 100644 index 59ae5a747..000000000 --- a/firefly-template/src/test/java/test/TestObjNavigator.java +++ /dev/null @@ -1,130 +0,0 @@ -package test; - -import static org.hamcrest.Matchers.*; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.template.Model; -import com.firefly.template.support.ObjectNavigator; - - -public class TestObjNavigator { - - @Test - public void testRoot() { - Map map2 = new HashMap(); - map2.put("ccc", "ddd"); - map2.put("eee", "fff"); - - int[] arr = {111, 222, 333}; - - List list = new ArrayList(); - list.add("list111"); - list.add("list222"); - list.add("list333"); - - Model model = new ModelMock(); - model.put("a", "fffff"); - model.put("b", map2); - model.put("arr", arr); - model.put("list", list); - - ObjectNavigator o = ObjectNavigator.getInstance(); - Assert.assertThat(o.find(model, "a").toString(), is("fffff")); - Assert.assertThat(o.find(model, "b['ccc']").toString(), is("ddd")); - Assert.assertThat(o.find(model, "b['eee']").toString(), is("fff")); - Assert.assertThat(o.find(model, "b[\"ccc\"]").toString(), is("ddd")); - Assert.assertThat((Integer)o.find(model, "arr[2]"), is(333)); - Assert.assertThat(o.find(model, "list[2]").toString(), is("list333")); - } - - @Test - public void testObject() { - Foo foo = new Foo(); - Bar bar = new Bar(); - bar.setInfo("bar1"); - bar.setSerialNumber(33L); - bar.setPrice(3.30); - foo.setBar(bar); - - - Map fooMap = new HashMap(); - bar = new Bar(); - bar.setInfo("bar2"); - bar.setSerialNumber(23L); - bar.setPrice(2.30); - fooMap.put("bar2", bar); - foo.setMap(fooMap); - - Model model = new ModelMock(); - model.put("foo", foo); - - ObjectNavigator o = ObjectNavigator.getInstance(); - Assert.assertThat(String.valueOf(o.find(model, "foo.bar.info")), is("bar1")); - Assert.assertThat(String.valueOf(o.find(model, "foo.bar.serialNumber")), is("33")); - Assert.assertThat(String.valueOf(o.find(model, "foo.bar.price")), is("3.3")); - Assert.assertThat(String.valueOf(o.find(model, "foo.numbers[2]")), is("5")); - Assert.assertThat(String.valueOf(o.find(model, "foo.map['bar2'].price")), is("2.3")); - Assert.assertThat(o.find(model, "foo.map['bar5']"), nullValue()); - Assert.assertThat(o.find(model, "ok"), nullValue()); - } - - public static void main(String[] args) { - ObjectNavigator o = ObjectNavigator.getInstance(); - - Foo foo = new Foo(); - Bar bar = new Bar(); - bar.setInfo("bar1"); - bar.setSerialNumber(33L); - bar.setPrice(3.30); - foo.setBar(bar); - - - Map fooMap = new HashMap(); - bar = new Bar(); - bar.setInfo("bar2"); - bar.setSerialNumber(23L); - bar.setPrice(2.30); - fooMap.put("bar2", bar); - foo.setMap(fooMap); - - Model model = new ModelMock(); - model.put("foo", foo); - - System.out.println(o.find(model, "foo.bar.info")); - System.out.println(o.find(model, "foo.bar.info")); - System.out.println(o.find(model, "foo.bar.serialNumber")); - System.out.println(o.find(model, "foo.bar.price")); - System.out.println(o.find(model, "foo.numbers[2]")); - System.out.println(o.find(model, "foo.bags[2]")); - System.out.println(o.find(model, "foo.map['bar2']")); - System.out.println(o.find(model, "foo.map['bar2'].price")); - System.out.println(o.find(model, "foo.map['bar4']")); - System.out.println(o.find(model, "user.name")); - -// long start = System.currentTimeMillis(); -// for (int i = 0; i < 1000000; i++) { -// o.find(model, "foo.numbers[2]"); -// } -// long end = System.currentTimeMillis() - start; -// System.out.println(o.find(model, "foo.numbers[2]") + "|" + end); -// -// start = System.currentTimeMillis(); -// for (int i = 0; i < 1000000; i++) { -// o.find(model, "foo.bags[3]"); -// } -// end = System.currentTimeMillis() - start; -// System.out.println(o.find(model, "foo.bags[3]") + "|" + end); -// List list = null; -// for(Object obj : (Collection)list) { -// -// } - - } -} diff --git a/firefly-template/src/test/java/test/TestRPN.java b/firefly-template/src/test/java/test/TestRPN.java deleted file mode 100644 index 795723448..000000000 --- a/firefly-template/src/test/java/test/TestRPN.java +++ /dev/null @@ -1,249 +0,0 @@ -package test; - -import static com.firefly.template.support.RPNUtils.*; -import static org.hamcrest.Matchers.is; - -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.template.exception.ExpressionError; -import com.firefly.template.parser.StatementExpression; -import com.firefly.template.support.RPNUtils; -import com.firefly.template.support.RPNUtils.Fragment; - - - -public class TestRPN { - - @Test - public void test() { - Assert.assertThat(getReversePolishNotation("(${i} += +-3 + + + + -${i} ++ - -+${i} --) >= 2").toString(), is("[${i}, -3, -${i} ++, +, -${i} --, -, +=, 2, >=]")); - Assert.assertThat(getReversePolishNotation("${login}").toString(), is("[${login}]")); - Assert.assertThat(getReversePolishNotation("(- ${user.age} += (-3 + - 2) * 4) > 22").toString(), is("[-${user.age}, -3, -2, +, 4, *, +=, 22, >]")); - Assert.assertThat(getReversePolishNotation("(${user.age} += 3 + 2 * 4) > 22").toString(), is("[${user.age}, 3, 2, 4, *, +, +=, 22, >]")); - Assert.assertThat(getReversePolishNotation("1*2+3").toString(), is("[1, 2, *, 3, +]")); - Assert.assertThat(getReversePolishNotation("1*2+3>>2+1").toString(), is("[1, 2, *, 3, +, 2, 1, +, >>]")); - Assert.assertThat(getReversePolishNotation("1 + ((2 + 3) * 3) * 5").toString(), is("[1, 2, 3, +, 3, *, 5, *, +]")); - Assert.assertThat(getReversePolishNotation("${user.age} > 1 + (2 + 3) * 5").toString(), is("[${user.age}, 1, 2, 3, +, 5, *, +, >]")); - Assert.assertThat(getReversePolishNotation("${user.age} + 3 > 1 + (2 + 3) * 5").toString(), is("[${user.age}, 3, +, 1, 2, 3, +, 5, *, +, >]")); - Assert.assertThat(getReversePolishNotation("${user.age} + 3 == ${user1.age} + (2 + 3) * 5").toString(), is("[${user.age}, 3, +, ${user1.age}, 2, 3, +, 5, *, +, ==]")); - Assert.assertThat(getReversePolishNotation("${apple.price} > 7f && -(${apple.price} + 2) * 0.4 + 4 <= 3").toString(), is("[${apple.price}, 7, >, 0, ${apple.price}, 2, +, -, 0.4, *, 4, +, 3, <=, &&]")); - Assert.assertThat(getReversePolishNotation("!(${apple.price} > 7f && -(${apple.price} + 2) * 0.4 + 4 <= 3)").toString(), is("[${apple.price}, 7, >, 0, ${apple.price}, 2, +, -, 0.4, *, 4, +, 3, <=, &&, false, ==]")); - - List list = getReversePolishNotation("!${login} != !false "); - Assert.assertThat(list.toString(), is("[!${login}, !false, !=]")); - Assert.assertThat(list.get(0).type, is(RPNUtils.Type.VARIABLE)); - Assert.assertThat(list.get(1).type, is(RPNUtils.Type.BOOLEAN)); - Assert.assertThat(list.get(2).type, is(RPNUtils.Type.CONDITIONAL_OPERATOR)); - - list = getReversePolishNotation("${name} != \"Pengtao Qiu\""); - Assert.assertThat(list.get(1).type, is(RPNUtils.Type.STRING)); - - list = getReversePolishNotation("${user.age} > 18"); - Assert.assertThat(list.get(1).type, is(RPNUtils.Type.INTEGER)); - - list = getReversePolishNotation("${user.id} > 18L"); - Assert.assertThat(list.get(1).type, is(RPNUtils.Type.LONG)); - - list = getReversePolishNotation("${food.price} > 3.3f"); - Assert.assertThat(list.get(1).type, is(RPNUtils.Type.FLOAT)); - - list = getReversePolishNotation("${food.price} > 3.3"); - Assert.assertThat(list.get(1).type, is(RPNUtils.Type.DOUBLE)); - } - - @Test - public void testELParse() { - StatementExpression se = new StatementExpression(); - Assert.assertThat(se.parse("3 + 3 * 5 / 2"), is("10")); - Assert.assertThat(se.parse("3L + 3L * 5L / 2L"), is("10")); - Assert.assertThat(se.parse("3.0 + 3.0 * 5.0 / 2.0"), is("10.5")); - Assert.assertThat(se.parse("3f + 3f * 5f / 2f"), is("10.5")); - Assert.assertThat(se.parse("3.0 + 3 * 5.0 / 2.0"), is("10.5")); - Assert.assertThat(se.parse("3 + 3f * 5 / 2f"), is("10.5")); - Assert.assertThat(se.parse("1L +" + Integer.MAX_VALUE), is("2147483648")); - Assert.assertThat(se.parse("1 +" + Integer.MAX_VALUE), is("-2147483648")); - - Assert.assertThat(se.parse("\"hello \" + \"firefly \\\" ***!! \""), is("\"hello firefly \\\" ***!! \"")); - Assert.assertThat(se.parse("\"hello \" + \"firefly \" + \"!\""), is("\"hello firefly !\"")); - Assert.assertThat(se.parse("'hello ' + 'firefly ' + '!'"), is("\"hello firefly !\"")); - Assert.assertThat(se.parse("'hello ' + 'firefly ' + ${i} + '!'"), is("\"hello firefly \" + objNav.getValue(model ,\"i\") + \"!\"")); - Assert.assertThat(se.parse("(3f + ${j}) / 2 + ${i} + 1.0"), is("(((3 + objNav.getFloat(model ,\"j\")) / 2 + objNav.getFloat(model ,\"i\")) + 1.0)")); - - Assert.assertThat(se.parse("true"), is("true")); - Assert.assertThat(se.parse("false"), is("false")); - Assert.assertThat(se.parse("! true"), is("! true")); - Assert.assertThat(se.parse("1|2"), is("3")); - Assert.assertThat(se.parse("!${user.pass}"), is("!objNav.getBoolean(model ,\"user.pass\")")); - Assert.assertThat(se.parse("${user.pass}"), is("objNav.getBoolean(model ,\"user.pass\")")); - Assert.assertThat(se.parse("1 | 2 & ${i}"), is("(1 | (2 & objNav.getInteger(model ,\"i\")))")); - Assert.assertThat(se.parse("!${i} || !${j} && ${k}"), is("(!objNav.getBoolean(model ,\"i\") || (!objNav.getBoolean(model ,\"j\") && objNav.getBoolean(model ,\"k\")))")); - Assert.assertThat(se.parse("${i} & ${j}"), is("(objNav.getBoolean(model ,\"i\") & objNav.getBoolean(model ,\"j\"))")); - Assert.assertThat(se.parse("${apple.price} > 7f && ${apple.price} <= 3"), is("(objNav.getFloat(model ,\"apple.price\") > 7) && (objNav.getInteger(model ,\"apple.price\") <= 3)")); - Assert.assertThat(se.parse("${i} != 'pt1 !'"), is("!((Object)(\"pt1 !\")).equals(objNav.find(model ,\"i\"))")); - Assert.assertThat(se.parse("'pt1 !'!='pt1 !'"), is("false")); - Assert.assertThat(se.parse("${i} == ${j} && ${i} != ${k}"), is("objNav.find(model ,\"i\").equals(objNav.find(model ,\"j\")) && !objNav.find(model ,\"i\").equals(objNav.find(model ,\"k\"))")); - Assert.assertThat(se.parse("${i} != null && null == ${j} && ${i} != ${k}"), is("objNav.find(model ,\"i\") != null && null == objNav.find(model ,\"j\") && !objNav.find(model ,\"i\").equals(objNav.find(model ,\"k\"))")); - Assert.assertThat(se.parse("3 + 4 +-( -(2 - 1) + 1)"), is("7")); - Assert.assertThat(se.parse("-(3 + 4) +-( -(2 - 1) + 1)"), is("-7")); - Assert.assertThat(se.parse("!(${apple.price} > 7f && -(${apple.price} + 2) * 0.4 + 4 <= 3)"), is("((Object)((objNav.getFloat(model ,\"apple.price\") > 7) && (((0 - (objNav.getInteger(model ,\"apple.price\") + 2)) * 0.4 + 4) <= 3))).equals(false)")); - Assert.assertThat(se.parse("${i} != null && ${i.size} > 0"), is("objNav.find(model ,\"i\") != null && (objNav.getInteger(model ,\"i.size\") > 0)")); - } - - @Test(expected = ExpressionError.class) - public void testELParseError() { - StatementExpression se = new StatementExpression(); - se.parse("${i} + ${j} + ${k}"); - } - - @Test(expected = ExpressionError.class) - public void testELParseError2() { - StatementExpression se = new StatementExpression(); - se.parse("${i} + ${j} + 2"); - } - - @Test(expected = ExpressionError.class) - public void testELParseError3() { - StatementExpression se = new StatementExpression(); - se.parse("${i}-- + ${j} + 2"); - } - - public static void main(String[] args) { - int i = 3 + 4 + -(-(2 - 1) + 1); - System.out.println(i); -// System.out.println(preprocessing("3 + 4 + (0 -(-(2 - 1) + 1))")); -// System.out.println(preprocessing("3 + 4 + -(-(2 - 1) + 1)")); -// System.out.println(getReversePolishNotation("3 + 4 + -(-(2 - 1) + 1)")); -// System.out.println(preprocessing("${apple.price} > 7f && -(${apple.price} + 2) * 0.4 + 4 <= 3")); - System.out.println(getReversePolishNotation("!(${apple.price} > 7f && -(${apple.price} + 2) * 0.4 + 4 <= 3)")); - - StatementExpression se = new StatementExpression(); - System.out.println(se.parse("-(3 + 4) > 8 && !(${i} > 0)")); - System.out.println(se.parse("(0-(0 - ${i})) * 4")); - System.out.println(se.parse("-(3 + 4) +-( -(2 - 1) + 1)")); - System.out.println(se.parse("!(${apple.price} > 7f && -(${apple.price} + 2) * 0.4 + 4 <= 3)")); - System.out.println(se.parse("${i} != null && ${i.size} > 0")); - } - - public static void main5(String[] args) { - System.out.println(((Object)"Bob").equals("Bob")); - StatementExpression se = new StatementExpression(); - System.out.println(se.parse("'pt1 !'!= ${i}")); - System.out.println(se.parse("${i}!= 'pt1 !'")); - System.out.println(se.parse("'pt1 !' != 'pt1 !'")); -// String str = null; -// System.out.println("".equals(null)); - List list = getReversePolishNotation("${i} != null && ${i} != ${k}"); - System.out.println(list.toString()); - for(Fragment f : list) { - System.out.print(f.type + ", "); - } - System.out.println(); - System.out.println(se.parse("${i} != null && null == ${j} && ${i} != ${k}")); - - } - - public static void main4(String[] args) { - List list = getReversePolishNotation("!(${apple.price} > 7f && ${apple.price} <= 3)"); - System.out.println(list.toString()); - for(Fragment f : list) { - System.out.print(f.type + ", "); - } - System.out.println(); - - StatementExpression se = new StatementExpression(); - - System.out.println(se.parse("!(${apple.price} > 7f && ${apple.price} <= 3)")); - System.out.println(se.parse("${apple.price} > 7f && ${apple.price} <= 3")); - System.out.println(se.parse("!${i} || !${j} && ${k}")); - System.out.println(se.parse("5 > 3 && 5 > 2")); - System.out.println(se.parse("${i} || 5 < 3 && 5 > 2")); - } - - - public static void main3(String[] args) { - System.out.println(((Object)(3)).equals(3)); - List list = getReversePolishNotation("(${i} += +-3 + + + + -${i} -- - -+${i} --) >= 2"); - System.out.println(list.toString()); - for(Fragment f : list) { - System.out.print(f.type + ", "); - } - System.out.println(); - -// list = getReversePolishNotation("!(3f + ${apple.price} > 7)"); -// System.out.println(list.toString()); -// for(Fragment f : list) { -// System.out.print(f.type + ", "); -// } -// System.out.println(); -// -// StatementExpression se = new StatementExpression(); -// System.out.println(se.parse("\"hello \" + \"firefly \\\" ***!! \"")); - - } - - public static void main2(String[] args) { - System.out.println(Long.parseLong("3")); - System.out.println(Float.parseFloat("2")); - System.out.println(Boolean.parseBoolean("!false")); - - List list = getReversePolishNotation("! ${login} != ! false"); - System.out.println(list.toString()); - for(Fragment f : list) { - System.out.print(f.type + ", "); - } - System.out.println(); - - list = getReversePolishNotation("${name} != \"Pengtao Qiu\""); - System.out.println(list.toString()); - for(Fragment f : list) { - System.out.print(f.type + ", "); - } - System.out.println(); - - list = getReversePolishNotation("1*2+3>>2+1f"); - System.out.println(list.toString()); - for(Fragment f : list) { - System.out.print(f.type + ", "); - } - System.out.println(); - - System.out.println(getReversePolishNotation("\"Pengtao Qiu\" == ${user.name}")); - System.out.println(getReversePolishNotation("(- ${user.age} += (-3 + - 2) * 4) > 22")); - System.out.println(getReversePolishNotation("(${i} += +-3 + + + + -${i} -- - -+${i} --) >= 2")); - System.out.println(getReversePolishNotation("1*2+3>>2+1")); - System.out.println(Float.parseFloat("3.5") + Long.parseLong("4")); - - System.out.println(getReversePolishNotation("3 + 3 * 5 / 2")); - System.out.println(getReversePolishNotation("3 + 3 * 5 / 2")); - System.out.println("================================================"); - StatementExpression se = new StatementExpression(); - - System.out.println(se.parse("3L + 3L * 5L / 2L")); - System.out.println(se.parse("3 + 3 * 5 / 2")); - System.out.println(se.parse("3.0 + 3 * 5.0 / 2.0")); - System.out.println(se.parse("3 + 3f * 5 / 2f")); - System.out.println(se.parse("\"hello \" + \"firefly \"")); - System.out.println(se.parse("'hello ' + 'firefly ' + '!'")); - System.out.println(se.parse("'hello ' + 'firefly ' + ${i} + '!'")); -// System.out.println(se.parse("${i} + ${j} + ${k}")); - - System.out.println(se.parse("${i} + 3 + 5 + 2 / 1.0")); - System.out.println(se.parse("(3f + ${j}) / 2 + ${i} + 1.0")); - System.out.println(se.parse("1L +" + Integer.MAX_VALUE)); - System.out.println(1 + Integer.MAX_VALUE); - System.out.println(se.parse("(3f + ${apple.price}) / 2 + ${i} + 1.0")); - System.out.println(se.parse("(3f + ${apple.price}) / 2 + ${i} + 1.0 >= 2")); - System.out.println(se.parse("!${i} || !${j} && ${k}")); - System.out.println(se.parse("1 | 2 & ${i}")); - System.out.println(se.parse("${i} & ${j}")); -// System.out.println(se.parse("${apple.price} + 1f >= 5 && ${apple.price} + 1f < 10")); -// System.out.println(se.parse("! ${user1.pass} == !true && ${user2.pass} == true ")); - - System.out.println(se.parse("!${user.pass}")); -// System.out.println(se.parse("(3f + ${j} --) / 2 + ${i}++ + 1.0")); - - } -} diff --git a/firefly-template/src/test/java/test/User.java b/firefly-template/src/test/java/test/User.java deleted file mode 100644 index 5d5a1b50a..000000000 --- a/firefly-template/src/test/java/test/User.java +++ /dev/null @@ -1,22 +0,0 @@ -package test; - -public class User { - private String name; - private int age; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } -} diff --git a/firefly-template/src/test/java/test/benchmark/Book.java b/firefly-template/src/test/java/test/benchmark/Book.java deleted file mode 100644 index b06d29cd8..000000000 --- a/firefly-template/src/test/java/test/benchmark/Book.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF 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 test.benchmark; - -import java.io.Serializable; -import java.util.Date; - -public class Book implements Serializable { - - private static final long serialVersionUID = 1L; - - private String title; - - private String author; - - private String publisher; - - private Date publication; - - private int price; - - private int discount; - - public Book() { - } - - public Book(String title, String author, String publisher, Date publication, int price, int discount){ - this.title = title; - this.author = author; - this.publisher = publisher; - this.publication = publication; - this.price = price; - this.discount = discount; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - - public String getPublisher() { - return publisher; - } - - public void setPublisher(String publisher) { - this.publisher = publisher; - } - - public Date getPublication() { - return publication; - } - - public void setPublication(Date publication) { - this.publication = publication; - } - - public int getPrice() { - return price; - } - - public void setPrice(int price) { - this.price = price; - } - - public int getDiscount() { - return discount; - } - - public void setDiscount(int discount) { - this.discount = discount; - } - -} \ No newline at end of file diff --git a/firefly-template/src/test/java/test/benchmark/PerformanceTest.java b/firefly-template/src/test/java/test/benchmark/PerformanceTest.java deleted file mode 100644 index 3939a50e9..000000000 --- a/firefly-template/src/test/java/test/benchmark/PerformanceTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package test.benchmark; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.OutputStream; -import java.io.StringWriter; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.UUID; - -import test.ModelMock; -import test.TestConfig; - -import com.firefly.template.Function; -import com.firefly.template.FunctionRegistry; -import com.firefly.template.Model; -import com.firefly.template.TemplateFactory; -import com.firefly.template.View; - -import freemarker.cache.ClassTemplateLoader; -import freemarker.template.Configuration; -import freemarker.template.Template; - -public class PerformanceTest { - - public static void main(String[] args) throws Throwable { - int size = 100; - int times = 10000 * 10; - Random random = new Random(); - Book[] books = new Book[size]; - for (int i = 0; i < size; i ++) { - books[i] = new Book(UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString(), new Date(), random.nextInt(100) + 10, random.nextInt(60) + 30); - } - Map context = new HashMap(); - context.put("user", new User("liangfei", "admin")); - context.put("books", books); - - Model model = new ModelMock(); - model.put("user", new User("liangfei", "admin")); - model.put("books", books); - - // freemark - StringWriter writer = new StringWriter(); - Configuration configuration = new Configuration(); - configuration.setTemplateLoader(new ClassTemplateLoader(PerformanceTest.class, "/")); - Template template = configuration.getTemplate("books.ftl"); - template.process(context, writer); -// byte[] ret = null; - long start = System.currentTimeMillis(); - for (int i = 0; i < times; i++) { - writer = new StringWriter(); - template.process(context, writer); - writer.toString().getBytes("UTF-8"); - } - long end = System.currentTimeMillis(); - System.out.println("freemark: " + (end - start) + "ms\t" + (int)(times / (double)(end - start) * 1000) + "tps"); -// System.out.println(new String(ret, "UTF-8")); - - // firefly - final TemplateFactory t = new TemplateFactory(new File(TestConfig.class.getResource("/").toURI())).init(); - FunctionRegistry.add("book_count", new Function() { - - @Override - public void render(Model model, OutputStream out, Object... obj) throws Throwable { - Book book = (Book)obj[0]; - out.write(String.valueOf(book.getPrice() * book.getDiscount() / 100).getBytes(t.getConfig().getCharset())); - } - - }); - View view = t.getView("/books.html"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - view.render(model, out); - out.close(); - start = System.currentTimeMillis(); - for (int i = 0; i < times; i++) { - out = new ByteArrayOutputStream(); - view.render(model, out); - out.close(); - } - end = System.currentTimeMillis(); - System.out.println("firefly-template: " + (end - start) + "ms\t" + (int)(times / (double)(end - start) * 1000) + "tps"); -// System.out.println(new String(out.toByteArray(), "UTF-8")); - - } - -} diff --git a/firefly-template/src/test/java/test/benchmark/User.java b/firefly-template/src/test/java/test/benchmark/User.java deleted file mode 100644 index 414b17a72..000000000 --- a/firefly-template/src/test/java/test/benchmark/User.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF 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 test.benchmark; - -import java.io.Serializable; - -public class User implements Serializable { - - private static final long serialVersionUID = 1L; - - private String name; - - private String role; - - public User() { - } - - public User(String name, String role) { - this.name = name; - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } - -} diff --git a/firefly-template/src/test/resources/firefly-log.properties b/firefly-template/src/test/resources/firefly-log.properties deleted file mode 100644 index 06879d666..000000000 --- a/firefly-template/src/test/resources/firefly-log.properties +++ /dev/null @@ -1 +0,0 @@ -firefly-system=${log.level},${log.path},console \ No newline at end of file diff --git a/firefly-template/src/test/template/books.ftl b/firefly-template/src/test/template/books.ftl deleted file mode 100644 index 9fc25806e..000000000 --- a/firefly-template/src/test/template/books.ftl +++ /dev/null @@ -1,38 +0,0 @@ - - - -
${user.name}/${user.role}
-<#if user.role == "admin"> - - - - - - - - - - - <#list books as book><#if book.price > 0> - - - - - - - - - -
NO.TitleAuthorPublisherPublicationDatePriceDiscountPercentDiscountPrice
${book_index + 1}${book.title}${book.author}${book.publisher}${book.publication?string("yyyy-MM-dd HH:mm:ss")}${book.price}${book.discount}%<#assign discountPrice = book.price * book.discount / 100>${discountPrice?string("0")}
-<#elseif user> - - - -
No privilege.
-<#else> - - - -
No login.
- - \ No newline at end of file diff --git a/firefly-template/src/test/template/books.html b/firefly-template/src/test/template/books.html deleted file mode 100644 index 8106f0d79..000000000 --- a/firefly-template/src/test/template/books.html +++ /dev/null @@ -1,46 +0,0 @@ - - - -
${user.name}/${user.role}
- - - - - - - - - - - - - - - - - - - - - - - - - - -
NO.TitleAuthorPublisherPublicationDatePriceDiscountPercentDiscountPrice
${book_index}${book.title}${book.author}${book.publisher}${dateFormat(book.publication)}${book.price}${book.discount}%${book_count(book)}
- - - - - -
No privilege.
- - - - - -
No login.
- - - \ No newline at end of file diff --git a/firefly-template/src/test/template/page/common/head.html b/firefly-template/src/test/template/page/common/head.html deleted file mode 100644 index bec251f01..000000000 --- a/firefly-template/src/test/template/page/common/head.html +++ /dev/null @@ -1,2 +0,0 @@ - -${title} \ No newline at end of file diff --git a/firefly-template/src/test/template/page/common/top.html b/firefly-template/src/test/template/page/common/top.html deleted file mode 100644 index 5954e6dd9..000000000 --- a/firefly-template/src/test/template/page/common/top.html +++ /dev/null @@ -1 +0,0 @@ -
${title}   >   首页
\ No newline at end of file diff --git a/firefly-template/src/test/template/page/index2.html b/firefly-template/src/test/template/page/index2.html deleted file mode 100644 index 21afa5844..000000000 --- a/firefly-template/src/test/template/page/index2.html +++ /dev/null @@ -1 +0,0 @@ -测试一下页面 \ No newline at end of file diff --git a/firefly-template/src/test/template/page/testFor.html b/firefly-template/src/test/template/page/testFor.html deleted file mode 100644 index 3efd45533..000000000 --- a/firefly-template/src/test/template/page/testFor.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -
- -${i}    - -
- -
-
${len(users)}
- - - - - - - - - -
姓名年龄
${u.name}|||${len(u.name)}${u.age}
-
- - \ No newline at end of file diff --git a/firefly-template/src/test/template/page/testIf.html b/firefly-template/src/test/template/page/testIf.html deleted file mode 100644 index 0ab7abcf5..000000000 --- a/firefly-template/src/test/template/page/testIf.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - -
- - Welcome ${user.name} - - 您无法访问 - -
- - -
年龄大于18
- - - -
年龄不小于30
- - -
${testFunction(3, "hello", user.age)}
-
${testFunction2()}
- -
- - 城主来了 - - 厨师来了 - - Jim来了 - - 小罗罗来了 - -
-
- - \ No newline at end of file diff --git a/firefly-template/src/test/template/page/testInclude.html b/firefly-template/src/test/template/page/testInclude.html deleted file mode 100644 index d7cdca4b6..000000000 --- a/firefly-template/src/test/template/page/testInclude.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/firefly-template/src/test/template/page/testSet.html b/firefly-template/src/test/template/page/testSet.html deleted file mode 100644 index b18a1c3bd..000000000 --- a/firefly-template/src/test/template/page/testSet.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - -
-${msg}  ${testName} -
-
-苹果的价格是:${price} -
- - - \ No newline at end of file diff --git a/firefly-template/src/test/template/page/testSwitch.html b/firefly-template/src/test/template/page/testSwitch.html deleted file mode 100644 index 2c59e130a..000000000 --- a/firefly-template/src/test/template/page/testSwitch.html +++ /dev/null @@ -1,16 +0,0 @@ - - - -
- - - stage1 - - stage2 - - stage-default - -
- - - \ No newline at end of file diff --git a/firefly-wechat/pom.xml b/firefly-wechat/pom.xml new file mode 100644 index 000000000..bc55ae269 --- /dev/null +++ b/firefly-wechat/pom.xml @@ -0,0 +1,75 @@ + + + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + + 4.0.0 + + firefly-wechat + jar + + firefly-wechat + http://www.fireflysource.com + + + + com.fireflysource + firefly-net + + + + com.fireflysource + firefly-serialization + + + + com.fireflysource + firefly-slf4j + test + + + + + firefly-wechat + install + + + src/main/resources + true + + **/*.xml + **/*.properties + + + + src/main/resources + false + + **/*.xml + **/*.properties + + + + + + src/test/resources + true + + **/*.xml + **/*.properties + + + + src/test/resources + false + + **/*.xml + **/*.properties + + + + + diff --git a/firefly-wechat/src/main/java/com/fireflysource/doc/FeignedWechatDoc.java b/firefly-wechat/src/main/java/com/fireflysource/doc/FeignedWechatDoc.java new file mode 100644 index 000000000..3199d0f1d --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/doc/FeignedWechatDoc.java @@ -0,0 +1,9 @@ +package com.fireflysource.doc; + +/** + * Only used to generate javadoc. + * + * @author Pengtao Qiu + */ +public class FeignedWechatDoc { +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/Article.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/Article.java new file mode 100644 index 000000000..d98e15ff9 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/Article.java @@ -0,0 +1,78 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class Article { + + private String title; + + private String description; + + private String url; + + @JsonProperty("picurl") + private String pictureUrl; + + public Article() { + } + + public Article(String title, String description, String url, String pictureUrl) { + this.title = title; + this.description = description; + this.url = url; + this.pictureUrl = pictureUrl; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getPictureUrl() { + return pictureUrl; + } + + public void setPictureUrl(String pictureUrl) { + this.pictureUrl = pictureUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Article article = (Article) o; + return Objects.equals(title, article.title) && + Objects.equals(description, article.description) && + Objects.equals(url, article.url) && + Objects.equals(pictureUrl, article.pictureUrl); + } + + @Override + public int hashCode() { + return Objects.hash(title, description, url, pictureUrl); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/GroupBotMessageResult.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/GroupBotMessageResult.java new file mode 100644 index 000000000..b720f5e94 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/GroupBotMessageResult.java @@ -0,0 +1,62 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class GroupBotMessageResult { + + @JsonProperty("errcode") + private int errorCode; + + @JsonProperty("errmsg") + private String errorMessage; + + public GroupBotMessageResult() { + } + + public GroupBotMessageResult(int errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GroupBotMessageResult that = (GroupBotMessageResult) o; + return errorCode == that.errorCode; + } + + @Override + public int hashCode() { + return Objects.hash(errorCode); + } + + @Override + public String toString() { + return "GroupBotMessageResult{" + + "errorCode=" + errorCode + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/ImageMessage.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/ImageMessage.java new file mode 100644 index 000000000..8467f6811 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/ImageMessage.java @@ -0,0 +1,37 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class ImageMessage extends Message { + + private ImageMessageContent image; + + public ImageMessage() { + setMessageType(MessageType.IMAGE); + } + + public ImageMessageContent getImage() { + return image; + } + + public void setImage(ImageMessageContent image) { + this.image = image; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ImageMessage that = (ImageMessage) o; + return image.equals(that.image); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), image); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/ImageMessageContent.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/ImageMessageContent.java new file mode 100644 index 000000000..bd5a4cae3 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/ImageMessageContent.java @@ -0,0 +1,42 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class ImageMessageContent { + + private String base64; + private String md5; + + public String getBase64() { + return base64; + } + + public void setBase64(String base64) { + this.base64 = base64; + } + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageMessageContent that = (ImageMessageContent) o; + return base64.equals(that.base64) && + md5.equals(that.md5); + } + + @Override + public int hashCode() { + return Objects.hash(base64, md5); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MarkdownMessage.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MarkdownMessage.java new file mode 100644 index 000000000..fcf93edc0 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MarkdownMessage.java @@ -0,0 +1,37 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class MarkdownMessage extends Message { + + private MarkdownMessageContent markdown; + + public MarkdownMessage() { + setMessageType(MessageType.MARKDOWN); + } + + public MarkdownMessageContent getMarkdown() { + return markdown; + } + + public void setMarkdown(MarkdownMessageContent markdown) { + this.markdown = markdown; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + MarkdownMessage that = (MarkdownMessage) o; + return markdown.equals(that.markdown); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), markdown); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MarkdownMessageContent.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MarkdownMessageContent.java new file mode 100644 index 000000000..4e7e7bef6 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MarkdownMessageContent.java @@ -0,0 +1,32 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class MarkdownMessageContent { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MarkdownMessageContent that = (MarkdownMessageContent) o; + return content.equals(that.content); + } + + @Override + public int hashCode() { + return Objects.hash(content); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/Message.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/Message.java new file mode 100644 index 000000000..32bf462e4 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/Message.java @@ -0,0 +1,35 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class Message { + + @JsonProperty("msgtype") + private MessageType messageType; + + public MessageType getMessageType() { + return messageType; + } + + public void setMessageType(MessageType messageType) { + this.messageType = messageType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Message message = (Message) o; + return messageType == message.messageType; + } + + @Override + public int hashCode() { + return Objects.hash(messageType); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MessageBuilder.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MessageBuilder.java new file mode 100644 index 000000000..9b59ba75d --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MessageBuilder.java @@ -0,0 +1,112 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author Pengtao Qiu + */ +public class MessageBuilder { + + public static TextMessageBuilder text() { + return new TextMessageBuilder(); + } + + public static MarkdownMessageBuilder markdown() { + return new MarkdownMessageBuilder(); + } + + public static ImageMessageBuilder image() { + return new ImageMessageBuilder(); + } + + public static NewsMessageBuilder news() { + return new NewsMessageBuilder(); + } + + public static class TextMessageBuilder { + + private TextMessageContent content = new TextMessageContent(); + + public TextMessageBuilder content(String content) { + this.content.setContent(content); + return this; + } + + public TextMessageBuilder mentionedList(List mentionedList) { + content.setMentionedList(mentionedList); + return this; + } + + public TextMessageBuilder mentionedMobileList(List mentionedMobileList) { + content.setMentionedMobileList(mentionedMobileList); + return this; + } + + public TextMessage end() { + TextMessage textMessage = new TextMessage(); + textMessage.setText(content); + return textMessage; + } + } + + public static class MarkdownMessageBuilder { + private MarkdownMessageContent content = new MarkdownMessageContent(); + + public MarkdownMessageBuilder content(String content) { + this.content.setContent(content); + return this; + } + + public MarkdownMessage end() { + MarkdownMessage markdownMessage = new MarkdownMessage(); + markdownMessage.setMarkdown(content); + return markdownMessage; + } + } + + public static class ImageMessageBuilder { + private ImageMessageContent content = new ImageMessageContent(); + + public ImageMessageBuilder md5(String md5) { + content.setMd5(md5); + return this; + } + + public ImageMessageBuilder base64(String base64) { + content.setBase64(base64); + return this; + } + + public ImageMessage end() { + ImageMessage imageMessage = new ImageMessage(); + imageMessage.setImage(content); + return imageMessage; + } + } + + public static class NewsMessageBuilder { + + private NewsMessageContent content = new NewsMessageContent(); + + public NewsMessageBuilder() { + content.setArticles(new LinkedList<>()); + } + + public NewsMessageBuilder addArticle(Article article) { + content.getArticles().add(article); + return this; + } + + public NewsMessageBuilder addArticles(List
articles) { + content.getArticles().addAll(articles); + return this; + } + + public NewsMessage end() { + NewsMessage newsMessage = new NewsMessage(); + newsMessage.setNews(content); + return newsMessage; + } + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MessageType.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MessageType.java new file mode 100644 index 000000000..d7a81136e --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/MessageType.java @@ -0,0 +1,22 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Pengtao Qiu + */ +public enum MessageType { + + TEXT("text"), MARKDOWN("markdown"), IMAGE("image"), NEWS("news"); + + private final String value; + + MessageType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/NewsMessage.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/NewsMessage.java new file mode 100644 index 000000000..021f9ecb2 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/NewsMessage.java @@ -0,0 +1,37 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class NewsMessage extends Message { + + private NewsMessageContent news; + + public NewsMessage() { + setMessageType(MessageType.NEWS); + } + + public NewsMessageContent getNews() { + return news; + } + + public void setNews(NewsMessageContent news) { + this.news = news; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + NewsMessage that = (NewsMessage) o; + return news.equals(that.news); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), news); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/NewsMessageContent.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/NewsMessageContent.java new file mode 100644 index 000000000..5e168919d --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/NewsMessageContent.java @@ -0,0 +1,33 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.List; +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class NewsMessageContent { + + private List
articles; + + public List
getArticles() { + return articles; + } + + public void setArticles(List
articles) { + this.articles = articles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NewsMessageContent that = (NewsMessageContent) o; + return Objects.equals(articles, that.articles); + } + + @Override + public int hashCode() { + return Objects.hash(articles); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/TextMessage.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/TextMessage.java new file mode 100644 index 000000000..224035489 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/TextMessage.java @@ -0,0 +1,37 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class TextMessage extends Message { + + private TextMessageContent text; + + public TextMessage() { + setMessageType(MessageType.TEXT); + } + + public TextMessageContent getText() { + return text; + } + + public void setText(TextMessageContent text) { + this.text = text; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + TextMessage that = (TextMessage) o; + return text.equals(that.text); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), text); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/TextMessageContent.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/TextMessageContent.java new file mode 100644 index 000000000..74132ce60 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/model/TextMessageContent.java @@ -0,0 +1,59 @@ +package com.fireflysource.wechat.enterprise.group.bot.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +/** + * @author Pengtao Qiu + */ +public class TextMessageContent { + + private String content; + + @JsonProperty("mentioned_list") + private List mentionedList; + + @JsonProperty("mentioned_mobile_list") + private List mentionedMobileList; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public List getMentionedList() { + return mentionedList; + } + + public void setMentionedList(List mentionedList) { + this.mentionedList = mentionedList; + } + + public List getMentionedMobileList() { + return mentionedMobileList; + } + + public void setMentionedMobileList(List mentionedMobileList) { + this.mentionedMobileList = mentionedMobileList; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TextMessageContent that = (TextMessageContent) o; + return content.equals(that.content) && + Objects.equals(mentionedList, that.mentionedList) && + Objects.equals(mentionedMobileList, that.mentionedMobileList); + } + + @Override + public int hashCode() { + return Objects.hash(content, mentionedList, mentionedMobileList); + } +} diff --git a/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/service/GroupBotMessageService.java b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/service/GroupBotMessageService.java new file mode 100644 index 000000000..33d88e824 --- /dev/null +++ b/firefly-wechat/src/main/java/com/fireflysource/wechat/enterprise/group/bot/service/GroupBotMessageService.java @@ -0,0 +1,15 @@ +package com.fireflysource.wechat.enterprise.group.bot.service; + +import com.fireflysource.wechat.enterprise.group.bot.model.GroupBotMessageResult; +import com.fireflysource.wechat.enterprise.group.bot.model.Message; + +import java.util.concurrent.CompletableFuture; + +/** + * @author Pengtao Qiu + */ +public interface GroupBotMessageService { + + CompletableFuture sendMessage(Message message); + +} diff --git a/firefly-wechat/src/main/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/GroupBotMessageServiceFactory.kt b/firefly-wechat/src/main/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/GroupBotMessageServiceFactory.kt new file mode 100644 index 000000000..bd7f724e3 --- /dev/null +++ b/firefly-wechat/src/main/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/GroupBotMessageServiceFactory.kt @@ -0,0 +1,16 @@ +package com.fireflysource.wechat.enterprise.group.bot.service + +import com.fireflysource.fx +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.wechat.enterprise.group.bot.service.impl.AsyncGroupBotMessageService + +/** + * @author Pengtao Qiu + */ +object GroupBotMessageServiceFactory { + + @JvmOverloads + fun create(webHookUrl: String, httpClient: HttpClient = fx.httpClient()): GroupBotMessageService { + return AsyncGroupBotMessageService(webHookUrl, httpClient) + } +} \ No newline at end of file diff --git a/firefly-wechat/src/main/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/impl/AsyncGroupBotMessageService.kt b/firefly-wechat/src/main/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/impl/AsyncGroupBotMessageService.kt new file mode 100644 index 000000000..70e9f8317 --- /dev/null +++ b/firefly-wechat/src/main/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/impl/AsyncGroupBotMessageService.kt @@ -0,0 +1,38 @@ +package com.fireflysource.wechat.enterprise.group.bot.service.impl + +import com.fireflysource.net.http.client.HttpClient +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.HttpStatus +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.serialization.SerializationServiceFactory.json +import com.fireflysource.serialization.impl.json.read +import com.fireflysource.wechat.enterprise.group.bot.model.GroupBotMessageResult +import com.fireflysource.wechat.enterprise.group.bot.model.Message +import com.fireflysource.wechat.enterprise.group.bot.service.GroupBotMessageService +import java.util.concurrent.CompletableFuture + +/** + * @author Pengtao Qiu + */ +class AsyncGroupBotMessageService( + private val webHookUrl: String, + private val httpClient: HttpClient +) : GroupBotMessageService { + + override fun sendMessage(message: Message): CompletableFuture { + return httpClient.post(webHookUrl) + .put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value) + .body(json.write(message)) + .submit() + .thenApply { response -> + if (response.status == HttpStatus.OK_200) { + json.read(response.stringBody) + } else { + GroupBotMessageResult( + -99999, + "The http request failure. status: ${response.status}, content: ${response.stringBody}" + ) + } + } + } +} \ No newline at end of file diff --git a/firefly-wechat/src/test/kotlin/com/fireflysource/wechat/enterprise/group/bot/model/TestMessageModel.kt b/firefly-wechat/src/test/kotlin/com/fireflysource/wechat/enterprise/group/bot/model/TestMessageModel.kt new file mode 100644 index 000000000..b5450b803 --- /dev/null +++ b/firefly-wechat/src/test/kotlin/com/fireflysource/wechat/enterprise/group/bot/model/TestMessageModel.kt @@ -0,0 +1,118 @@ +package com.fireflysource.wechat.enterprise.group.bot.model + +import com.fireflysource.serialization.SerializationServiceFactory.json +import com.fireflysource.serialization.impl.json.read +import com.fireflysource.wechat.enterprise.group.bot.model.MessageBuilder.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +/** + * @author Pengtao Qiu + */ +class TestMessageModel { + + companion object { + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + arguments( + """ + { + "msgtype": "text", + "text": { + "content": "hello world" + } + } + """.trimIndent(), + text().content("hello world").end() + ), + arguments( + """ + { + "msgtype": "text", + "text": { + "content": "广州今日天气:29度,大部分多云,降雨概率:60%", + "mentioned_list":["wangqing","@all"], + "mentioned_mobile_list":["13800001111","@all"] + } + } + """.trimIndent(), + text().content("广州今日天气:29度,大部分多云,降雨概率:60%") + .mentionedList(listOf("wangqing", "@all")) + .mentionedMobileList(listOf("13800001111", "@all")) + .end() + ), + arguments( + "{\"markdown\":{\"content\":\"实时新增用户反馈132例,请相关同事注意。\\n>类型:用户反馈 \\n>普通用户反馈:117例 \\n>VIP用户反馈:15例\"},\"msgtype\":\"markdown\"}", + markdown().content( + """ + |实时新增用户反馈132例,请相关同事注意。 + |>类型:用户反馈 + |>普通用户反馈:117例 + |>VIP用户反馈:15例 + """.trimMargin() + ) + .end() + ), + arguments( + """ + { + "msgtype": "image", + "image": { + "base64": "DATA", + "md5": "MD5" + } + } + """.trimIndent(), + image().base64("DATA").md5("MD5").end() + ), + arguments( + """ + { + "msgtype": "news", + "news": { + "articles" : [ + { + "title" : "中秋节礼品领取", + "description" : "今年中秋节公司有豪礼相送", + "url" : "URL", + "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png" + } + ] + } + } + """.trimIndent(), + news().addArticle( + Article( + "中秋节礼品领取", + "今年中秋节公司有豪礼相送", + "URL", + "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png" + ) + ) + .end() + ) + ) + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should read wechat enterprise group bot message successfully.") + fun test(string: String, message: Message) { + val m = when (message.messageType) { + MessageType.TEXT -> json.read(string) + MessageType.MARKDOWN -> json.read(string) + MessageType.IMAGE -> json.read(string) + MessageType.NEWS -> json.read(string) + else -> null + } + assertEquals(message, m) + } + +} \ No newline at end of file diff --git a/firefly-wechat/src/test/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/TestGroupBotMessageService.kt b/firefly-wechat/src/test/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/TestGroupBotMessageService.kt new file mode 100644 index 000000000..82b2e11d0 --- /dev/null +++ b/firefly-wechat/src/test/kotlin/com/fireflysource/wechat/enterprise/group/bot/service/TestGroupBotMessageService.kt @@ -0,0 +1,94 @@ +package com.fireflysource.wechat.enterprise.group.bot.service + +import com.fireflysource.fx +import com.fireflysource.net.http.common.model.HttpHeader +import com.fireflysource.net.http.common.model.MimeTypes +import com.fireflysource.net.http.server.HttpServer +import com.fireflysource.serialization.SerializationServiceFactory.json +import com.fireflysource.wechat.enterprise.group.bot.model.GroupBotMessageResult +import com.fireflysource.wechat.enterprise.group.bot.model.MessageBuilder +import kotlinx.coroutines.future.await +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.random.Random + +/** + * @author Pengtao Qiu + */ +class TestGroupBotMessageService { + + companion object { + + @JvmStatic + fun testParametersProvider(): Stream { + return Stream.of( + arguments( + "/cgi-bin/webhook/send", + GroupBotMessageResult( + 93000, + "invalid webhook url, hint: [1592021214_50_551fc8807c11f27ed0e6714d0c267ba4], from ip: 171.43.164.231, more info at https://open.work.weixin.qq.com/devtool/query?e=93000" + ) + ), + arguments("/cgi-bin/webhook/send?key=testKey", GroupBotMessageResult(0, "ok")), + arguments( + "/cgi-bin/webhook/send?key=xxx", GroupBotMessageResult( + 93000, + "invalid webhook url, hint: [1592021214_50_551fc8807c11f27ed0e6714d0c267ba4], from ip: 171.43.164.231, more info at https://open.work.weixin.qq.com/devtool/query?e=93000" + ) + ), + arguments( + "/cgi-bin/webhook/sendFFFFF", GroupBotMessageResult( + -99999, + "The http request failure." + ) + ) + ) + } + } + + @ParameterizedTest + @MethodSource("testParametersProvider") + @DisplayName("should send wechat enterprise group bot message successfully.") + fun test(path: String, expectedResult: GroupBotMessageResult) = runTest { + val host = "localhost" + val port = Random.nextInt(10000, 20000) + val webHookUrl = "https://$host:$port$path" + + val server = createMockServer() + val client = fx.createHttpClient() + server.listen(host, port) + + val service = GroupBotMessageServiceFactory.create(webHookUrl, client) + val message = MessageBuilder.text().content("hello world").end() + val result = service.sendMessage(message).await() + assertEquals(expectedResult, result) + + client.stop() + server.stop() + } + + private fun createMockServer(): HttpServer { + return fx.createHttpServer() + .router() + .post("/cgi-bin/webhook/send") + .handler { ctx -> + val key = ctx.getQueryString("key") + val result = when { + key.isNullOrBlank() || key != "testKey" -> GroupBotMessageResult( + 93000, + "invalid webhook url, hint: [1592021214_50_551fc8807c11f27ed0e6714d0c267ba4], from ip: 171.43.164.231, more info at https://open.work.weixin.qq.com/devtool/query?e=93000" + ) + else -> GroupBotMessageResult(0, "ok") + } + + ctx.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value).end(json.write(result)) + } + .enableSecureConnection() + } +} \ No newline at end of file diff --git a/firefly-wechat/src/test/resources/firefly-log.xml b/firefly-wechat/src/test/resources/firefly-log.xml new file mode 100644 index 000000000..d139af581 --- /dev/null +++ b/firefly-wechat/src/test/resources/firefly-log.xml @@ -0,0 +1,18 @@ + + + + + firefly-system + INFO + ${log.path} + false + + + + firefly-monitor + INFO + ${log.path} + + + diff --git a/firefly/.gitignore b/firefly/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/firefly/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/firefly/pom.xml b/firefly/pom.xml deleted file mode 100644 index f81cd8b8f..000000000 --- a/firefly/pom.xml +++ /dev/null @@ -1,166 +0,0 @@ - - 4.0.0 - com.firefly - firefly - 2.0-SNAPSHOT - jar - firefly - http://maven.apache.org - - ${project.artifactId} - install - - - src/main/resources - true - - - - - true - src/test/resources - - - false - src/test/appHome - - - - - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - UTF-8 - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - UTF-8 - - - - - - - - com.firefly - firefly-common - 1.0-SNAPSHOT - - - com.firefly - firefly-template - 1.0-SNAPSHOT - - - com.firefly - firefly-nettool - 1.0-SNAPSHOT - - - - - junit - junit - 4.8.1 - test - - - org.hamcrest - hamcrest-all - 1.1 - test - - - - - javax.servlet - servlet-api - 2.5 - provided - - - javax.servlet.jsp - jsp-api - 2.0 - provided - - - javax.servlet - jstl - ${jstl.version} - - - taglibs - standard - ${jstl.version} - - - - - - com.firefly - firefly - - - 1.1.2 - - INFO - D:/log/ - - - - mac - - INFO - /Users/qiupengtao/develop/logs/ - - - - macdebug - - DEBUG - /Users/qiupengtao/develop/logs/ - - - - windebug - - DEBUG - D:/log/ - - - - - - - - - - - - 3rdRepo - 3rd party - http://localhost:7777/nexus-webapp/content/repositories/thirdparty - - - dev - Snapshots - http://localhost:7777/nexus-webapp/content/repositories/snapshots - - - \ No newline at end of file diff --git a/firefly/src/main/java/com/firefly/annotation/Component.java b/firefly/src/main/java/com/firefly/annotation/Component.java deleted file mode 100644 index e3ff0be39..000000000 --- a/firefly/src/main/java/com/firefly/annotation/Component.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( { ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Component { - String value() default ""; -} diff --git a/firefly/src/main/java/com/firefly/annotation/Controller.java b/firefly/src/main/java/com/firefly/annotation/Controller.java deleted file mode 100644 index 815f88e68..000000000 --- a/firefly/src/main/java/com/firefly/annotation/Controller.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( { ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Controller { - String value() default ""; -} diff --git a/firefly/src/main/java/com/firefly/annotation/HttpParam.java b/firefly/src/main/java/com/firefly/annotation/HttpParam.java deleted file mode 100644 index e7f34b610..000000000 --- a/firefly/src/main/java/com/firefly/annotation/HttpParam.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( { ElementType.PARAMETER }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface HttpParam { - String value() default ""; -} diff --git a/firefly/src/main/java/com/firefly/annotation/Inject.java b/firefly/src/main/java/com/firefly/annotation/Inject.java deleted file mode 100644 index 060f421c5..000000000 --- a/firefly/src/main/java/com/firefly/annotation/Inject.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( { ElementType.FIELD, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Inject { - String value() default ""; -} diff --git a/firefly/src/main/java/com/firefly/annotation/Interceptor.java b/firefly/src/main/java/com/firefly/annotation/Interceptor.java deleted file mode 100644 index 02f8575a4..000000000 --- a/firefly/src/main/java/com/firefly/annotation/Interceptor.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( { ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Interceptor { - - String value() default ""; - - String uri(); - - int order() default 0; - -} diff --git a/firefly/src/main/java/com/firefly/annotation/PathVariable.java b/firefly/src/main/java/com/firefly/annotation/PathVariable.java deleted file mode 100644 index 380e647e3..000000000 --- a/firefly/src/main/java/com/firefly/annotation/PathVariable.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( { ElementType.PARAMETER }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface PathVariable { - -} diff --git a/firefly/src/main/java/com/firefly/annotation/RequestMapping.java b/firefly/src/main/java/com/firefly/annotation/RequestMapping.java deleted file mode 100644 index 8700bb513..000000000 --- a/firefly/src/main/java/com/firefly/annotation/RequestMapping.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import com.firefly.mvc.web.HttpMethod; - -@Target( { ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface RequestMapping { - String value() default ""; - - String method() default HttpMethod.GET; -} diff --git a/firefly/src/main/java/com/firefly/core/AbstractApplicationContext.java b/firefly/src/main/java/com/firefly/core/AbstractApplicationContext.java deleted file mode 100644 index 667879a1c..000000000 --- a/firefly/src/main/java/com/firefly/core/AbstractApplicationContext.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.firefly.core; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import com.firefly.core.support.BeanDefinition; -import com.firefly.core.support.exception.BeanDefinitionParsingException; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -abstract public class AbstractApplicationContext implements ApplicationContext { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - protected Map map = new HashMap(); - protected Set errorMemo = new HashSet(); - protected List beanDefinitions; - - public AbstractApplicationContext() { - this(null); - } - - public AbstractApplicationContext(String file) { - beanDefinitions = getBeanDefinitions(file); - check(); //冲突检测 - addObjectToContext(); - } - - private void addObjectToContext() { - for (BeanDefinition beanDefinition : beanDefinitions) { - inject(beanDefinition); - } - } - - @SuppressWarnings("unchecked") - @Override - public T getBean(Class clazz) { - return (T) map.get(clazz.getName()); - } - - @SuppressWarnings("unchecked") - @Override - public T getBean(String id) { - return (T) map.get(id); - } - - protected void check() { - // TODO 需要增加测试用例 - // 1.id相同的抛异常 - // 2.className或者interfaceName相同,但其中一个没有定义id,抛异常 - // 3.className或者interfaceName相同,且都定义的id,需要保存备忘,按类型或者接口自动注入的时候抛异常 - - for (int i = 0; i < beanDefinitions.size(); i++) { - for (int j = i + 1; j < beanDefinitions.size(); j++) { - log.debug("check bean " + i + "|" + j); - BeanDefinition b1 = beanDefinitions.get(i); - BeanDefinition b2 = beanDefinitions.get(j); - - if (VerifyUtils.isNotEmpty(b1.getId()) - && VerifyUtils.isNotEmpty(b2.getId()) - && b1.getId().equals(b2.getId())) { - error("bean " + b1.getClassName() + " and bean " - + b2.getClassName() + " have duplicate id "); - } - - if (b1.getClassName().equals(b2.getClassName())) { - if (VerifyUtils.isEmpty(b1.getId()) - || VerifyUtils.isEmpty(b2.getId())) { - error("class " + b1.getClassName() - + " duplicate definition"); - } else { - errorMemo.add(b1.getClassName()); - } - } - - for (String iname1 : b1.getInterfaceNames()) { - for (String iname2 : b2.getInterfaceNames()) { - if (iname1.equals(iname2)) { - if (VerifyUtils.isEmpty(b1.getId()) - || VerifyUtils.isEmpty(b2.getId())) { - error("class " + b1.getClassName() - + " duplicate definition"); - } else { - errorMemo.add(iname1); - } - } - } - } - - } - } - } - - protected void check(String key) { - if(errorMemo.contains(key)) { - error(key + " auto inject failure!"); - } - } - - protected void addObjectToContext(BeanDefinition beanDefinition) { - // 增加声明的组件到 ApplicationContext - Object object = beanDefinition.getObject(); - // 把id作为key - String id = beanDefinition.getId(); - if (VerifyUtils.isNotEmpty(id)) - map.put(id, object); - - // 把类名作为key - map.put(beanDefinition.getClassName(), object); - - // 把接口名作为key - String[] keys = beanDefinition.getInterfaceNames(); - for (String k : keys) { - map.put(k, object); - } - } - - protected BeanDefinition findBeanDefinition(String key) { - check(key); - BeanDefinition ret = null; - for (BeanDefinition beanDefinition : beanDefinitions) { - if (key.equals(beanDefinition.getId())) { - ret = beanDefinition; - break; - } else if (key.equals(beanDefinition.getClassName())) { - ret = beanDefinition; - break; - } else { - for (String interfaceName : beanDefinition.getInterfaceNames()) { - if (key.equals(interfaceName)) { - ret = beanDefinition; - break; - } - } - } - } - return ret; - } - - /** - * 处理异常 - * - * @param msg - * 异常信息 - */ - protected void error(String msg) { - log.error(msg); - throw new BeanDefinitionParsingException(msg); - } - - abstract protected List getBeanDefinitions(String file); - - abstract protected Object inject(BeanDefinition beanDef); - -} diff --git a/firefly/src/main/java/com/firefly/core/ApplicationContext.java b/firefly/src/main/java/com/firefly/core/ApplicationContext.java deleted file mode 100644 index ea08aeae0..000000000 --- a/firefly/src/main/java/com/firefly/core/ApplicationContext.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.firefly.core; - -public interface ApplicationContext { - - T getBean(Class clazz); - - T getBean(String id); -} diff --git a/firefly/src/main/java/com/firefly/core/XmlApplicationContext.java b/firefly/src/main/java/com/firefly/core/XmlApplicationContext.java deleted file mode 100644 index 1e4933ca0..000000000 --- a/firefly/src/main/java/com/firefly/core/XmlApplicationContext.java +++ /dev/null @@ -1,300 +0,0 @@ -package com.firefly.core; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import com.firefly.annotation.Inject; -import com.firefly.core.support.BeanDefinition; -import com.firefly.core.support.annotation.AnnotationBeanDefinition; -import com.firefly.core.support.annotation.AnnotationBeanReader; -import com.firefly.core.support.xml.ManagedArray; -import com.firefly.core.support.xml.ManagedList; -import com.firefly.core.support.xml.ManagedMap; -import com.firefly.core.support.xml.ManagedRef; -import com.firefly.core.support.xml.ManagedValue; -import com.firefly.core.support.xml.XmlBeanDefinition; -import com.firefly.core.support.xml.XmlBeanReader; -import com.firefly.utils.ConvertUtils; -import com.firefly.utils.ReflectUtils; -import com.firefly.utils.ReflectUtils.BeanMethodFilter; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.VerifyUtils; - -/** - * - * @author 须俊杰, alvinqiu - * - */ -public class XmlApplicationContext extends AbstractApplicationContext { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public XmlApplicationContext() { - this(null); - } - - public XmlApplicationContext(String file) { - super(file); - } - - @Override - protected List getBeanDefinitions(String file) { - List list1 = new AnnotationBeanReader(file) - .loadBeanDefinitions(); - List list2 = new XmlBeanReader(file) - .loadBeanDefinitions(); - if (list1 != null && list2 != null) { - log.debug("mixed bean"); - list1.addAll(list2); - return list1; - } else if (list1 != null) { - log.debug("annotation bean"); - return list1; - } else if (list2 != null) { - log.debug("xml bean"); - return list2; - } - return null; - } - - @Override - protected Object inject(BeanDefinition beanDef) { - if (beanDef instanceof XmlBeanDefinition) - return xmlInject(beanDef); - else if (beanDef instanceof AnnotationBeanDefinition) - return annotationInject(beanDef); - else - return null; - } - - /** - * xml注入方式 - * - * @param beanDef - * @return - */ - private Object xmlInject(BeanDefinition beanDef) { - XmlBeanDefinition beanDefinition = (XmlBeanDefinition) beanDef; - // 取得需要注入的对象 - final Object object = beanDefinition.getObject(); - - // 取得对象所有的属性 - final Map properties = beanDefinition.getProperties(); - - Class clazz = object.getClass(); - - // 遍历所有注册的set方法注入 - ReflectUtils.getSetterMethods(clazz, new BeanMethodFilter(){ - - @Override - public boolean accept(String propertyName, Method method) { - Object value = properties.get(propertyName); - if (value != null) { - try { - method.invoke(object, - getInjectArg(value, method)); - } catch (Throwable t) { - log.error("xml inject error", t); - } - } - return false; - }}); - - addObjectToContext(beanDefinition); - return object; - } - - /** - * - * @param value - * 属性值的元信息 - * @param method - * 该属性的set方法 - * @return - */ - private Object getInjectArg(Object value, Method method) { - if (value instanceof ManagedValue) { // value - return getValueArg(value, method); - } else if (value instanceof ManagedRef) { // ref - return getRefArg(value, method); - } else if (value instanceof ManagedList) { // list - return getListArg(value, method); - } else if (value instanceof ManagedArray) { // array - return getArrayArg(value, method); - } else if (value instanceof ManagedMap) { // map - return getMapArg(value, method); - } else - return null; - } - - private Object getValueArg(Object value, Method method) { - ManagedValue managedValue = (ManagedValue) value; - String typeName = null; - if (method == null) { - typeName = VerifyUtils.isEmpty(managedValue.getTypeName()) ? null - : managedValue.getTypeName(); - } else { - typeName = VerifyUtils.isEmpty(managedValue.getTypeName()) ? method - .getParameterTypes()[0].getName() : managedValue - .getTypeName(); - } - - log.debug("value type [{}]", typeName); - return ConvertUtils.convert(managedValue.getValue(), typeName); - } - - private Object getRefArg(Object value, Method method) { - ManagedRef ref = (ManagedRef) value; - Object instance = map.get(ref.getBeanName()); - if (instance == null) { - BeanDefinition b = findBeanDefinition(ref.getBeanName()); - if (b != null) - instance = inject(b); - } - return instance; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Object getListArg(Object value, Method method) { - Class setterParamType = null; - if (method != null) { - setterParamType = method.getParameterTypes()[0]; - } - ManagedList values = (ManagedList) value; - Collection collection = null; - - if (VerifyUtils.isNotEmpty(values.getTypeName())) { // 指定了list的类型 - try { - collection = (Collection) XmlApplicationContext.class - .getClassLoader().loadClass(values.getTypeName()) - .newInstance(); - } catch (Throwable t) { - log.error("list inject error", t); - } - } else { // 根据set方法参数类型获取list类型 - collection = (setterParamType == null ? new ArrayList() - : ConvertUtils.getCollectionObj(setterParamType)); - } - - for (Object item : values) { - Object listValue = getInjectArg(item, null); - collection.add(listValue); - } - - return collection; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Object getArrayArg(Object value, Method method) { - Class setterParamType = null; - if (method != null) { - setterParamType = method.getParameterTypes()[0]; - } - ManagedArray values = (ManagedArray) value; - Collection collection = new ArrayList(); - for (Object item : values) { - Object listValue = getInjectArg(item, null); - collection.add(listValue); - } - return ConvertUtils.convert(collection, setterParamType); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Object getMapArg(Object value, Method method) { - Class setterParamType = null; - if (method != null) { - setterParamType = method.getParameterTypes()[0]; - } - ManagedMap values = (ManagedMap) value; - Map m = null; - if (VerifyUtils.isNotEmpty(values.getTypeName())) { - try { - m = (Map) XmlApplicationContext.class.getClassLoader() - .loadClass(values.getTypeName()).newInstance(); - } catch (Throwable t) { - log.error("map inject error", t); - } - } else { // 根据set方法参数类型获取map类型 - m = (setterParamType == null ? new HashMap() : ConvertUtils - .getMapObj(setterParamType)); - log.debug("map ret [{}]", m.getClass().getName()); - } - for (Object o : values.keySet()) { - Object k = getInjectArg(o, null); - Object v = getInjectArg(values.get(o), null); - m.put(k, v); - } - return m; - } - - /** - * annotation 注入方式 - * - * @param beanDef - * @return - */ - private Object annotationInject(BeanDefinition beanDef) { - AnnotationBeanDefinition beanDefinition = (AnnotationBeanDefinition) beanDef; - fieldInject(beanDefinition); - methodInject(beanDefinition); - addObjectToContext(beanDefinition); - return beanDefinition.getObject(); - } - - private void fieldInject(AnnotationBeanDefinition beanDefinition) { - Object object = beanDefinition.getObject(); - - // 属性注入 - for (Field field : beanDefinition.getInjectFields()) { - field.setAccessible(true); - Class clazz = field.getType(); - String id = field.getAnnotation(Inject.class).value(); - String key = VerifyUtils.isNotEmpty(id) ? id : clazz.getName(); - Object instance = map.get(key); - if (instance == null) { - BeanDefinition b = findBeanDefinition(key); - if (b != null) - instance = inject(b); - } - if (instance != null) { - try { - field.set(object, instance); - } catch (Throwable t) { - log.error("field inject error", t); - } - } - } - } - - private void methodInject(AnnotationBeanDefinition beanDefinition) { - Object object = beanDefinition.getObject(); - - // 从方法注入 - for (Method method : beanDefinition.getInjectMethods()) { - method.setAccessible(true); - Class[] params = method.getParameterTypes(); - Object[] p = new Object[params.length]; - for (int i = 0; i < p.length; i++) { - String key = params[i].getName(); - Object instance = map.get(key); - if (instance != null) { - p[i] = instance; - } else { - BeanDefinition b = findBeanDefinition(key); - if (b != null) - p[i] = inject(b); - } - } - try { - method.invoke(object, p); - } catch (Throwable t) { - log.error("method inject error", t); - } - } - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/AbstractBeanReader.java b/firefly/src/main/java/com/firefly/core/support/AbstractBeanReader.java deleted file mode 100644 index e1e5bd452..000000000 --- a/firefly/src/main/java/com/firefly/core/support/AbstractBeanReader.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.firefly.core.support; - -import java.util.List; -import com.firefly.core.support.exception.BeanDefinitionParsingException; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class AbstractBeanReader implements BeanReader { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - protected List beanDefinitions; - - @Override - public List loadBeanDefinitions() { - return beanDefinitions; - } - - /** - * 处理异常 - * - * @param msg - * 异常信息 - */ - protected void error(String msg) { - log.error(msg); - throw new BeanDefinitionParsingException(msg); - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/BeanDefinition.java b/firefly/src/main/java/com/firefly/core/support/BeanDefinition.java deleted file mode 100644 index 649d9dc89..000000000 --- a/firefly/src/main/java/com/firefly/core/support/BeanDefinition.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.firefly.core.support; - -/** - * Bean信息 - * @author 杰然不同 - * @date 2010-11-29 - * @Version 1.0 - */ -public interface BeanDefinition { - // id className 以及该组件所有接口名作为 map 的key - String getId(); - - String getClassName(); - - void setId(String id); - - void setClassName(String className); - - String[] getInterfaceNames(); - - void setInterfaceNames(String[] names); - - // 该组件的对象实例 - Object getObject(); - - void setObject(Object object); -} diff --git a/firefly/src/main/java/com/firefly/core/support/BeanReader.java b/firefly/src/main/java/com/firefly/core/support/BeanReader.java deleted file mode 100644 index c3756e3a9..000000000 --- a/firefly/src/main/java/com/firefly/core/support/BeanReader.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.firefly.core.support; - -import java.util.List; - -public interface BeanReader { - List loadBeanDefinitions(); -} diff --git a/firefly/src/main/java/com/firefly/core/support/annotation/AnnotatedBeanDefinition.java b/firefly/src/main/java/com/firefly/core/support/annotation/AnnotatedBeanDefinition.java deleted file mode 100644 index 6b8d02296..000000000 --- a/firefly/src/main/java/com/firefly/core/support/annotation/AnnotatedBeanDefinition.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.firefly.core.support.annotation; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.List; - -public class AnnotatedBeanDefinition implements AnnotationBeanDefinition { - - private String id, className; - private String[] names; - private List fields; - private List methods; - private Object object; - - @Override - public Object getObject() { - return object; - } - - @Override - public void setObject(Object object) { - this.object = object; - } - - @Override - public List getInjectFields() { - return fields; - } - - @Override - public List getInjectMethods() { - return methods; - } - - @Override - public String[] getInterfaceNames() { - return names; - } - - @Override - public void setInjectFields(List fields) { - this.fields = fields; - } - - @Override - public void setInjectMethods(List methods) { - this.methods = methods; - } - - @Override - public void setInterfaceNames(String[] names) { - this.names = names; - - } - - @Override - public String getClassName() { - return className; - } - - @Override - public String getId() { - return id; - } - - @Override - public void setClassName(String className) { - this.className = className; - } - - @Override - public void setId(String id) { - this.id = id; - } - -} diff --git a/firefly/src/main/java/com/firefly/core/support/annotation/AnnotationBeanDefinition.java b/firefly/src/main/java/com/firefly/core/support/annotation/AnnotationBeanDefinition.java deleted file mode 100644 index 1cdd12e5e..000000000 --- a/firefly/src/main/java/com/firefly/core/support/annotation/AnnotationBeanDefinition.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.core.support.annotation; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.List; -import com.firefly.core.support.BeanDefinition; - -public interface AnnotationBeanDefinition extends BeanDefinition { - - List getInjectFields(); - - void setInjectFields(List fields); - - List getInjectMethods(); - - void setInjectMethods(List methods); -} diff --git a/firefly/src/main/java/com/firefly/core/support/annotation/AnnotationBeanReader.java b/firefly/src/main/java/com/firefly/core/support/annotation/AnnotationBeanReader.java deleted file mode 100644 index 72c2706df..000000000 --- a/firefly/src/main/java/com/firefly/core/support/annotation/AnnotationBeanReader.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.firefly.core.support.annotation; - -import java.io.File; -import java.io.FileFilter; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.net.JarURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.jar.JarEntry; -import com.firefly.annotation.Component; -import com.firefly.annotation.Inject; -import com.firefly.core.support.AbstractBeanReader; -import com.firefly.core.support.BeanDefinition; -import com.firefly.utils.ReflectUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -/** - * 读取Bean信息 - * - * @author AlvinQiu - * - */ -public class AnnotationBeanReader extends AbstractBeanReader { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public AnnotationBeanReader() { - this(null); - } - - public AnnotationBeanReader(String file) { - beanDefinitions = new ArrayList(); - Config config = ConfigReader.getInstance().load(file); - for (String pack : config.getPaths()) { - log.info("componentPath [{}]", pack); - scan(pack.trim()); - } - } - - private void scan(String packageName) { - String packageDirName = packageName.replace('.', '/'); - log.debug("packageDirName: " + packageDirName); - URL url = AnnotationBeanReader.class.getClassLoader().getResource( - packageDirName); - if (url == null) - error(packageName + " can not be found"); - String protocol = url.getProtocol(); - if ("file".equals(protocol)) { - parseFile(url, packageDirName); - } else if ("jar".equals(protocol)) { - parseJar(url, packageDirName); - } - } - - private void parseFile(URL url, final String packageDirName) { - File path = null; - try { - path = new File(url.toURI()); - } catch (Throwable t) { - log.error("parse file error", t); - } - path.listFiles(new FileFilter() { - public boolean accept(File file) { - String name = file.getName(); - if (name.endsWith(".class") && !name.contains("$")) - parseClass(packageDirName.replace('/', '.') + "." - + name.substring(0, file.getName().length() - 6)); - else if (file.isDirectory()) - try { - parseFile(file.toURI().toURL(), packageDirName + "/" - + name); - } catch (Throwable t) { - log.error("parse file error", t); - } - return false; - } - }); - } - - private void parseJar(URL url, String packageDirName) { - Enumeration entries = null; - try { - entries = ((JarURLConnection) url.openConnection()).getJarFile() - .entries(); - } catch (Throwable t) { - log.error("parse jar error", t); - } - while (entries.hasMoreElements()) { - String name = entries.nextElement().getName(); - if (!name.endsWith(".class") || name.contains("$") - || !name.startsWith(packageDirName + "/")) - continue; - parseClass(name.substring(0, name.length() - 6).replace('/', '.')); - } - - } - - private void parseClass(String className) { - Class c = null; - try { - c = AnnotationBeanReader.class.getClassLoader() - .loadClass(className); - } catch (Throwable t) { - log.error("parse class error", t); - } - - BeanDefinition beanDefinition = getBeanDefinition(c); - if (beanDefinition != null) - beanDefinitions.add(beanDefinition); - } - - protected BeanDefinition getBeanDefinition(Class c) { - if (c.isAnnotationPresent(Component.class)) { - log.info("classes [{}]", c.getName()); - return componentParser(c); - } else - return null; - } - - protected BeanDefinition componentParser(Class c) { - AnnotationBeanDefinition annotationBeanDefinition = new AnnotatedBeanDefinition(); - annotationBeanDefinition.setClassName(c.getName()); - - Component component = c.getAnnotation(Component.class); - String id = component.value(); - annotationBeanDefinition.setId(id); - - String[] names = ReflectUtils.getInterfaceNames(c); - annotationBeanDefinition.setInterfaceNames(names); - - List fields = getInjectField(c); - annotationBeanDefinition.setInjectFields(fields); - - List methods = getInjectMethod(c); - annotationBeanDefinition.setInjectMethods(methods); - - try { - Object object = c.newInstance(); - annotationBeanDefinition.setObject(object); - } catch (Throwable t) { - log.error("component parser error", t); - } - return annotationBeanDefinition; - } - - protected List getInjectField(Class c) { - Field[] fields = c.getDeclaredFields(); - List list = new ArrayList(); - for (Field field : fields) { - if (field.getAnnotation(Inject.class) != null) { - list.add(field); - } - } - return list; - } - - protected List getInjectMethod(Class c) { - Method[] methods = c.getDeclaredMethods(); - List list = new ArrayList(); - for (Method m : methods) { - if (m.isAnnotationPresent(Inject.class)) { - list.add(m); - } - } - return list; - } - -} diff --git a/firefly/src/main/java/com/firefly/core/support/annotation/Config.java b/firefly/src/main/java/com/firefly/core/support/annotation/Config.java deleted file mode 100644 index b7bb62057..000000000 --- a/firefly/src/main/java/com/firefly/core/support/annotation/Config.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.firefly.core.support.annotation; - -public class Config { - private String viewPath = "/WEB-INF/page", encoding = "UTF-8"; - private String[] paths; - - public String getViewPath() { - return viewPath; - } - - public void setViewPath(String viewPath) { - this.viewPath = viewPath; - } - - public String getEncoding() { - return encoding; - } - - public void setEncoding(String encoding) { - this.encoding = encoding; - } - - public String[] getPaths() { - return paths; - } - - public void setPaths(String[] paths) { - this.paths = paths; - } - -} diff --git a/firefly/src/main/java/com/firefly/core/support/annotation/ConfigReader.java b/firefly/src/main/java/com/firefly/core/support/annotation/ConfigReader.java deleted file mode 100644 index e81b43499..000000000 --- a/firefly/src/main/java/com/firefly/core/support/annotation/ConfigReader.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.firefly.core.support.annotation; - -import java.util.LinkedList; -import java.util.List; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.dom.DefaultDom; -import com.firefly.utils.dom.Dom; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class ConfigReader { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public static final String DEFAULT_CONFIG_FILE = "firefly.xml"; - public static final String SCAN_ELEMENT = "component-scan"; - public static final String MVC_ELEMENT = "mvc"; - public static final String PACKAGE_ATTRIBUTE = "base-package"; - public static final String VIEW_PATH_ATTRIBUTE = "view-path"; - public static final String VIEW_ENCODING_ATTRIBUTE = "view-encoding"; - public static final String VIEW_TYPE_ATTRIBUTE = "view-type"; - - private Config config; - - private ConfigReader() { - config = new Config(); - } - - private static class Holder { - private static ConfigReader instance = new ConfigReader(); - } - - public static ConfigReader getInstance() { - return Holder.instance; - } - - public Config load(String file) { - Dom dom = new DefaultDom(); - // 获得Xml文档对象 - Document doc = dom.getDocument(file == null ? DEFAULT_CONFIG_FILE - : file); - // 得到根节点 - Element root = dom.getRoot(doc); - load(root, dom); - return config; - } - - public Config load(Element root, Dom dom) { - // 得到所有scan节点 - List scanList = dom.elements(root, SCAN_ELEMENT); - - if (scanList != null) { - List paths = new LinkedList(); - for (int i = 0; i < scanList.size(); i++) { - Element ele = scanList.get(i); - String path = ele.getAttribute(PACKAGE_ATTRIBUTE); - if(!VerifyUtils.isEmpty(path)) - paths.add(path); - } - config.setPaths(paths.toArray(new String[0])); - } else { - config.setPaths(new String[0]); - } - - Element mvc = dom.element(root, MVC_ELEMENT); - if (mvc != null) { - String viewPath = mvc.getAttribute(VIEW_PATH_ATTRIBUTE); - String encoding = mvc.getAttribute(VIEW_ENCODING_ATTRIBUTE); - log.debug("mvc viewPath [{}] encoding [{}]", viewPath, encoding); - - if(VerifyUtils.isNotEmpty(viewPath)) - config.setViewPath(viewPath); - if(VerifyUtils.isNotEmpty(encoding)) - config.setEncoding(encoding); - } - return config; - } - - public Config getConfig() { - return config; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/exception/BeanDefinitionParsingException.java b/firefly/src/main/java/com/firefly/core/support/exception/BeanDefinitionParsingException.java deleted file mode 100644 index 85c1355c1..000000000 --- a/firefly/src/main/java/com/firefly/core/support/exception/BeanDefinitionParsingException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firefly.core.support.exception; - -/** - * 解析BeanDefinition异常 - * @author 杰然不同 - * @date 2011-3-8 - */ -@SuppressWarnings("serial") -public class BeanDefinitionParsingException extends BeansException { - - public BeanDefinitionParsingException(String msg) { - super(msg); - } - -} diff --git a/firefly/src/main/java/com/firefly/core/support/exception/BeansException.java b/firefly/src/main/java/com/firefly/core/support/exception/BeansException.java deleted file mode 100644 index d0bfef8b8..000000000 --- a/firefly/src/main/java/com/firefly/core/support/exception/BeansException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.core.support.exception; - -@SuppressWarnings("serial") -public abstract class BeansException extends RuntimeException { - - public BeansException(String msg) { - super(msg); - } - - public BeansException(String msg, Throwable cause) { - super(msg, cause); - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/ManagedArray.java b/firefly/src/main/java/com/firefly/core/support/xml/ManagedArray.java deleted file mode 100644 index 2fa940da8..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/ManagedArray.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.core.support.xml; - -import java.util.ArrayList; - -/** - * array元素 - * @author alvinqiu - */ -public class ManagedArray extends ArrayList { - - private static final long serialVersionUID = -3015988166845274665L; - -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/ManagedList.java b/firefly/src/main/java/com/firefly/core/support/xml/ManagedList.java deleted file mode 100644 index 495773619..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/ManagedList.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.firefly.core.support.xml; - -import java.util.ArrayList; - -/** - * list元素 - * @author 须俊杰 - * @date 2011-3-9 - */ -public class ManagedList extends ArrayList { - private static final long serialVersionUID = -1889497225597681323L; - private String typeName; - - public String getTypeName() { - return typeName; - } - - public void setTypeName(String typeName) { - this.typeName = typeName; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/ManagedMap.java b/firefly/src/main/java/com/firefly/core/support/xml/ManagedMap.java deleted file mode 100644 index b1ed406ed..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/ManagedMap.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.firefly.core.support.xml; - -import java.util.HashMap; - -@SuppressWarnings("serial") -public class ManagedMap extends HashMap { - - private String typeName; - - public String getTypeName() { - return typeName; - } - - public void setTypeName(String typeName) { - this.typeName = typeName; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/ManagedRef.java b/firefly/src/main/java/com/firefly/core/support/xml/ManagedRef.java deleted file mode 100644 index 37d400788..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/ManagedRef.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.firefly.core.support.xml; - -/** - * ref元素 - * @author 须俊杰 - * @date 2011-3-9 - */ -public class ManagedRef { - - private String beanName; - - public ManagedRef() { - } - - public ManagedRef(String beanName) { - this.beanName = beanName; - } - - public String getBeanName() { - return beanName; - } - - public void setBeanName(String beanName) { - this.beanName = beanName; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/ManagedValue.java b/firefly/src/main/java/com/firefly/core/support/xml/ManagedValue.java deleted file mode 100644 index 5e6567a39..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/ManagedValue.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.firefly.core.support.xml; - -/** - * 节点的属性与值 - * @author 须俊杰 - * @date 2011-3-9 - */ -public class ManagedValue { - - /** - * value值 - */ - private String value; - - /** - * 类型名称 - */ - private String typeName; - - public ManagedValue() { - } - - public ManagedValue(String value) { - this(value,null); - } - - public ManagedValue(String value,String typeName) { - this.value = value; - this.typeName = typeName; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getTypeName() { - return typeName; - } - - public void setTypeName(String typeName) { - this.typeName = typeName; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/XmlBeanDefinition.java b/firefly/src/main/java/com/firefly/core/support/xml/XmlBeanDefinition.java deleted file mode 100644 index 6779fb820..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/XmlBeanDefinition.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.core.support.xml; - -import java.util.Map; - -import com.firefly.core.support.BeanDefinition; - -/** - * Xml方式Bean定义 - * @author 杰然不同 - * @date 2011-3-5 - */ -public interface XmlBeanDefinition extends BeanDefinition { - - /** - * 取得属性集合 - * @return - */ - public abstract Map getProperties(); - - /** - * 设置属性集合 - * @param properties - */ - public abstract void setProperties(Map properties); -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/XmlBeanReader.java b/firefly/src/main/java/com/firefly/core/support/xml/XmlBeanReader.java deleted file mode 100644 index 4707dba8d..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/XmlBeanReader.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.firefly.core.support.xml; - -import static com.firefly.core.support.xml.parse.XmlNodeConstants.*; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import com.firefly.core.support.AbstractBeanReader; -import com.firefly.core.support.BeanDefinition; -import com.firefly.core.support.xml.parse.XmlNodeStateMachine; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.dom.DefaultDom; -import com.firefly.utils.dom.Dom; - -/** - * 读取Xml文件 - * - * @author 须俊杰, alvinqiu - */ -public class XmlBeanReader extends AbstractBeanReader { - - public XmlBeanReader() { - this(null); - } - - public XmlBeanReader(String file) { - beanDefinitions = new ArrayList(); - Dom dom = new DefaultDom(); - Set errorMemo = new HashSet(); // 判断循环引用 - - // 得到所有bean节点 - List beansList = new ArrayList(); - - parseXml(dom, file, beansList, errorMemo); - - // 迭代beans列表 - if (beansList != null) { - for (Element ele : beansList) { - beanDefinitions.add((BeanDefinition) XmlNodeStateMachine - .stateProcessor(ele, dom)); - } - } - } - - private void parseXml(Dom dom, String file, List beansList, - Set errorMemo) { - // 获得Xml文档对象 - Document doc = dom.getDocument(file == null ? "firefly.xml" : file); - // 得到根节点 - Element root = dom.getRoot(doc); - // 得到所有bean节点 - List list = dom.elements(root, BEAN_ELEMENT); - // 得到所有import节点 - List importList = dom.elements(root, IMPORT_ELEMENT); - if (importList != null) { - for (Element ele : importList) { - if (ele.hasAttribute("resource")) { - String resource = ele.getAttribute("resource"); - if (errorMemo.contains(resource)) { - error("disallow cyclic references between xml-file"); - return; - } else { - if (VerifyUtils.isEmpty(resource)) { - error("resource cannot be null"); - return; - } else { - errorMemo.add(resource); - parseXml(dom, resource, beansList, errorMemo); - } - } - } else { - error("has no resource attribute"); - return; - } - } - } - beansList.addAll(list); - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/XmlGenericBeanDefinition.java b/firefly/src/main/java/com/firefly/core/support/xml/XmlGenericBeanDefinition.java deleted file mode 100644 index 74c9c08f5..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/XmlGenericBeanDefinition.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.firefly.core.support.xml; - -import java.util.HashMap; -import java.util.Map; - -/** - * Xml方式Bean实现 - * - * @author 杰然不同 - * @date 2011-3-5 - */ -public class XmlGenericBeanDefinition implements XmlBeanDefinition { - - // id - private String id; - - // className - private String className; - - // 属性集合 - private Map properties = new HashMap(); - private String[] names; - private Object object; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getClassName() { - return className; - } - - public void setClassName(String className) { - this.className = className; - } - - public Map getProperties() { - return properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - - @Override - public String[] getInterfaceNames() { - return names; - } - - @Override - public Object getObject() { - return object; - } - - @Override - public void setInterfaceNames(String[] names) { - this.names = names; - } - - @Override - public void setObject(Object object) { - this.object = object; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/beans.xsd b/firefly/src/main/java/com/firefly/core/support/xml/beans.xsd deleted file mode 100644 index d30c0a47e..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/beans.xsd +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/AbstractXmlNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/AbstractXmlNodeParser.java deleted file mode 100644 index 42c8203bc..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/AbstractXmlNodeParser.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import com.firefly.core.support.exception.BeanDefinitionParsingException; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public abstract class AbstractXmlNodeParser implements XmlNodeParser { - - protected static Log log = LogFactory.getInstance().getLog("firefly-system"); - - protected void error(String msg) { - log.error(msg); - throw new BeanDefinitionParsingException(msg); - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/ArrayNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/ArrayNodeParser.java deleted file mode 100644 index 6940a1314..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/ArrayNodeParser.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import java.util.List; -import org.w3c.dom.Element; -import com.firefly.core.support.xml.ManagedArray; -import com.firefly.utils.dom.Dom; - -public class ArrayNodeParser extends AbstractXmlNodeParser implements XmlNodeParser { - - @Override - public Object parse(Element ele, Dom dom) { - ManagedArray target = new ManagedArray(); - List elements = dom.elements(ele); - for (Element e : elements) { - target.add(XmlNodeStateMachine.stateProcessor(e, dom)); - } - return target; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/BeanNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/BeanNodeParser.java deleted file mode 100644 index 52b5483cb..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/BeanNodeParser.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import static com.firefly.core.support.xml.parse.XmlNodeConstants.*; -import java.util.List; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import com.firefly.core.support.xml.ManagedRef; -import com.firefly.core.support.xml.ManagedValue; -import com.firefly.core.support.xml.XmlBeanDefinition; -import com.firefly.core.support.xml.XmlBeanReader; -import com.firefly.core.support.xml.XmlGenericBeanDefinition; -import com.firefly.utils.ReflectUtils; -import com.firefly.utils.StringUtils; -import com.firefly.utils.dom.Dom; - -public class BeanNodeParser extends AbstractXmlNodeParser implements XmlNodeParser { - - @Override - public Object parse(Element ele, Dom dom) { - // 获取基本属性 - String id = ele.getAttribute(ID_ATTRIBUTE); - String className = ele.getAttribute(CLASS_ATTRIBUTE); - XmlBeanDefinition xmlBeanDefinition = new XmlGenericBeanDefinition(); - xmlBeanDefinition.setId(id); - xmlBeanDefinition.setClassName(className); - - // 实例化对象 - Class clazz = null; - Object obj = null; - log.info("classes [{}]", className); - try { - clazz = XmlBeanReader.class.getClassLoader().loadClass(className); - obj = clazz.newInstance(); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - xmlBeanDefinition.setObject(obj); - - // 取得接口名称 - String[] names = ReflectUtils.getInterfaceNames(clazz); - xmlBeanDefinition.setInterfaceNames(names); - log.debug("class [{}] names size [{}]", className, names.length); - - // 获取所有property - List properties = dom.elements(ele, PROPERTY_ELEMENT); - - // 迭代property列表 - if (properties != null) { - for (Element property : properties) { - String name = property.getAttribute(NAME_ATTRIBUTE); - - boolean hasValueAttribute = property - .hasAttribute(VALUE_ATTRIBUTE); - boolean hasRefAttribute = property.hasAttribute(REF_ATTRIBUTE); - - // 只能有一个子元素: ref, value, list, etc. - NodeList nl = property.getChildNodes(); - Element subElement = null; - for (int i = 0; i < nl.getLength(); ++i) { - Node node = nl.item(i); - if (node instanceof Element) { - if (subElement != null) { - error(name - + " must not contain more than one sub-element"); - } else { - subElement = (Element) node; - } - } - } - - if (hasValueAttribute - && hasRefAttribute - || ((hasValueAttribute || hasRefAttribute) && subElement != null)) { - error(name - + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element"); - } - - if (hasValueAttribute) { - // 普通赋值 - String value = property.getAttribute(VALUE_ATTRIBUTE); - if (!StringUtils.hasText(value)) { - error(name + " contains empty 'value' attribute"); - } - xmlBeanDefinition.getProperties().put(name, - new ManagedValue(value)); - } else if (hasRefAttribute) { - // 依赖其他bean - String ref = property.getAttribute(REF_ATTRIBUTE); - if (!StringUtils.hasText(ref)) { - error(name + " contains empty 'ref' attribute"); - } - xmlBeanDefinition.getProperties().put(name, - new ManagedRef(ref)); - } else if (subElement != null) { - // 处理子元素 - Object subEle = XmlNodeStateMachine.stateProcessor(subElement, dom); - xmlBeanDefinition.getProperties().put(name, subEle); - } else { - error(name + " must specify a ref or value"); - return null; - } - } - } - return xmlBeanDefinition; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/ListNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/ListNodeParser.java deleted file mode 100644 index 66a6941bc..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/ListNodeParser.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import static com.firefly.core.support.xml.parse.XmlNodeConstants.*; -import java.util.List; -import org.w3c.dom.Element; -import com.firefly.core.support.xml.ManagedList; -import com.firefly.utils.dom.Dom; - -public class ListNodeParser extends AbstractXmlNodeParser implements XmlNodeParser { - - @Override - public Object parse(Element ele, Dom dom) { - String typeName = ele.getAttribute(TYPE_ATTRIBUTE); - ManagedList target = new ManagedList(); - target.setTypeName(typeName); - List elements = dom.elements(ele); - for (Element e : elements) { - target.add(XmlNodeStateMachine.stateProcessor(e, dom)); - } - return target; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/MapNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/MapNodeParser.java deleted file mode 100644 index 57786e245..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/MapNodeParser.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import java.util.List; - -import org.w3c.dom.Element; - -import com.firefly.core.support.xml.ManagedMap; -import com.firefly.core.support.xml.ManagedValue; -import com.firefly.utils.dom.Dom; - -/** - * 解析map元素 - * - * @author 须俊杰 - * @date 2011-3-17 - */ -public class MapNodeParser extends AbstractXmlNodeParser implements - XmlNodeParser { - - @Override - public Object parse(Element ele, Dom dom) { - // 获取key,value定义类型 - String typeName = ele.getAttribute(XmlNodeConstants.TYPE_ATTRIBUTE); - ManagedMap target = new ManagedMap(); - target.setTypeName(typeName); - - // 获取所有entry元素 - List elements = dom.elements(ele); - for (Element entry : elements) { - Object key = null; - Object value = null; - if (entry.hasAttribute(XmlNodeConstants.KEY_ATTRIBUTE)) { // 如果有key属性 - key = new ManagedValue(entry - .getAttribute(XmlNodeConstants.KEY_ATTRIBUTE)); - } - - if (entry.hasAttribute(XmlNodeConstants.VALUE_ATTRIBUTE)) { // 如果有value属性 - value = new ManagedValue(entry - .getAttribute(XmlNodeConstants.VALUE_ATTRIBUTE)); - } - - // 获取key元素 - List keyEle = dom.elements(entry, - XmlNodeConstants.MAP_KEY_ELEMENT); - if (keyEle.size() > 1) { - // 有且只能有一个key元素 - error("must not contain more than one key-element"); - } else if (keyEle.size() == 1) { - if (key != null) { - // key属性和key元素只能有一个 - error("only allowed to contain either 'key' attribute OR key-element"); - } else { - // 获取key子元素 - List subKey = dom.elements(keyEle.get(0)); - if (subKey.size() != 1) { - String keyText = dom.getTextValue(keyEle.get(0)); - if (keyText == null) - error("must contain one key-sub-element"); - else - key = new ManagedValue(keyText); - } else { - key = XmlNodeStateMachine.stateProcessor(subKey.get(0), - dom); - } - } - } else { - if (key == null) - error("not contain 'key' attribute OR key-element"); - } - - // 获取value元素 - List valueEle = dom.elements(entry, - XmlNodeConstants.MAP_VALUE_ELEMENT); - if (valueEle.size() > 1) { - // 有且只能有一个value元素 - error("must not contain more than one value-element"); - } else if (valueEle.size() == 1) { - if (value != null) { - // value属性和value元素只能有一个 - error("only allowed to contain either 'value' attribute OR value-element"); - } else { - // 获取value子元素 - List subValue = dom.elements(valueEle.get(0)); - if (subValue.size() != 1) { - String valueText = dom.getTextValue(valueEle.get(0)); - if (valueText == null) - error("must contain one value-sub-element"); - else - value = new ManagedValue(valueText); - } else { - value = XmlNodeStateMachine.stateProcessor(subValue - .get(0), dom); - } - } - } else { - if (value == null) - error("not contain 'value' attribute OR value-element"); - } - - target.put(key, value); - } - return target; - } - -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/RefNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/RefNodeParser.java deleted file mode 100644 index f3849fe28..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/RefNodeParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import org.w3c.dom.Element; -import static com.firefly.core.support.xml.parse.XmlNodeConstants.*; -import com.firefly.core.support.xml.ManagedRef; -import com.firefly.utils.StringUtils; -import com.firefly.utils.dom.Dom; - -public class RefNodeParser extends AbstractXmlNodeParser implements XmlNodeParser { - - @Override - public Object parse(Element ele, Dom dom) { - if (ele.hasAttribute(BEAN_REF_ATTRIBUTE)) { - String refText = ele.getAttribute(BEAN_REF_ATTRIBUTE); - if (StringUtils.hasText(refText)) { - ManagedRef ref = new ManagedRef(); - ref.setBeanName(refText); - return ref; - } else { - error(" element contains empty target attribute"); - return null; - } - } else { - error("'bean' is required for element"); - return null; - } - } - -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/ValueNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/ValueNodeParser.java deleted file mode 100644 index 1a2aededb..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/ValueNodeParser.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import org.w3c.dom.Element; -import static com.firefly.core.support.xml.parse.XmlNodeConstants.*; -import com.firefly.core.support.xml.ManagedValue; -import com.firefly.utils.dom.Dom; - -public class ValueNodeParser extends AbstractXmlNodeParser implements XmlNodeParser { - - @Override - public Object parse(Element ele, Dom dom) { - ManagedValue typedValue = new ManagedValue(); - String value = dom.getTextValue(ele); - String typeName = null; - if (ele.hasAttribute(TYPE_ATTRIBUTE)) { - // 如果有type属性 - typeName = ele.getAttribute(TYPE_ATTRIBUTE); - if (typeName == null) { - error(" element contains empty target attribute"); - return null; - } - } - - typedValue.setValue(value); - typedValue.setTypeName(typeName); - return typedValue; - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeConstants.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeConstants.java deleted file mode 100644 index cb3566fb1..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeConstants.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.firefly.core.support.xml.parse; - -public interface XmlNodeConstants { - public static final String BEAN_REF_ATTRIBUTE = "bean"; - public static final String ID_ATTRIBUTE = "id"; - public static final String CLASS_ATTRIBUTE = "class"; - public static final String NAME_ATTRIBUTE = "name"; - public static final String REF_ATTRIBUTE = "ref"; - public static final String VALUE_ATTRIBUTE = "value"; - public static final String TYPE_ATTRIBUTE = "type"; - public static final String KEY_ATTRIBUTE = "key"; - - public static final String BEAN_ELEMENT = "bean"; - public static final String IMPORT_ELEMENT = "import"; - public static final String PROPERTY_ELEMENT = "property"; - public static final String REF_ELEMENT = "ref"; - public static final String VALUE_ELEMENT = "value"; - public static final String LIST_ELEMENT = "list"; - public static final String ARRAY_ELEMENT = "array"; - public static final String MAP_ELEMENT = "map"; - public static final String MAP_ENTRY_ELEMENT = "entry"; - public static final String MAP_KEY_ELEMENT = "key"; - public static final String MAP_VALUE_ELEMENT = "value"; -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeParser.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeParser.java deleted file mode 100644 index aab9881f4..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeParser.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import org.w3c.dom.Element; - -import com.firefly.utils.dom.Dom; - -public interface XmlNodeParser { - Object parse(Element ele, Dom dom); -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeParserFactory.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeParserFactory.java deleted file mode 100644 index 2dff5669a..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeParserFactory.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import static com.firefly.core.support.xml.parse.XmlNodeConstants.*; -import java.util.HashMap; -import java.util.Map; - -public class XmlNodeParserFactory { - private static final Map map = new HashMap(); - - static { - map.put(BEAN_ELEMENT, new BeanNodeParser()); - map.put(REF_ELEMENT, new RefNodeParser()); - map.put(VALUE_ELEMENT, new ValueNodeParser()); - map.put(LIST_ELEMENT, new ListNodeParser()); - map.put(ARRAY_ELEMENT, new ArrayNodeParser()); - map.put(MAP_ELEMENT, new MapNodeParser()); - } - - public static XmlNodeParser getParser(String elementName) { - return map.get(elementName); - } -} diff --git a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeStateMachine.java b/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeStateMachine.java deleted file mode 100644 index 6a47e46b9..000000000 --- a/firefly/src/main/java/com/firefly/core/support/xml/parse/XmlNodeStateMachine.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.firefly.core.support.xml.parse; - -import org.w3c.dom.Element; -import com.firefly.core.support.exception.BeanDefinitionParsingException; -import com.firefly.utils.dom.Dom; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class XmlNodeStateMachine { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public static Object stateProcessor(Element ele, Dom dom) { - String elementName = ele.getNodeName() != null ? ele.getNodeName() - : ele.getLocalName(); - XmlNodeParser xmlNodeParser = XmlNodeParserFactory.getParser(elementName); - if(xmlNodeParser == null) - error("Unknown property sub-element: [" + ele.getNodeName() + "]"); - Object ret = xmlNodeParser.parse(ele, dom); - return ret; - } - - private static void error(String msg) { - log.error(msg); - throw new BeanDefinitionParsingException(msg); - } -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/AnnotationWebContext.java b/firefly/src/main/java/com/firefly/mvc/web/AnnotationWebContext.java deleted file mode 100644 index b0b97fdc5..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/AnnotationWebContext.java +++ /dev/null @@ -1,205 +0,0 @@ -package com.firefly.mvc.web; - -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.annotation.RequestMapping; -import com.firefly.core.XmlApplicationContext; -import com.firefly.core.support.BeanDefinition; -import com.firefly.core.support.annotation.ConfigReader; -import com.firefly.core.support.xml.XmlBeanReader; -import com.firefly.mvc.web.servlet.SystemHtmlPage; -import com.firefly.mvc.web.support.ControllerMetaInfo; -import com.firefly.mvc.web.support.ControllerBeanDefinition; -import com.firefly.mvc.web.support.InterceptorBeanDefinition; -import com.firefly.mvc.web.support.InterceptorMetaInfo; -import com.firefly.mvc.web.support.MethodParam; -import com.firefly.mvc.web.support.WebBeanReader; -import com.firefly.mvc.web.view.JsonView; -import com.firefly.mvc.web.view.JspView; -import com.firefly.mvc.web.view.TemplateView; -import com.firefly.mvc.web.view.TextView; -//import com.firefly.utils.log.Log; -//import com.firefly.utils.log.LogFactory; - -/** - * Web应用上下文默认实现 - * - * @author AlvinQiu - * - */ -public class AnnotationWebContext extends XmlApplicationContext implements WebContext { - -// private static Log log = LogFactory.getInstance().getLog("firefly-system"); - protected final Resource resource; - protected final List interceptorList = new LinkedList(); - - public AnnotationWebContext(String file) { - super(file); - resource = new Resource(getEncoding()); - initContext(); - } - - public AnnotationWebContext(String file, ServletContext servletContext) { - this(file); - - if (servletContext != null) - TemplateView.init(servletContext.getRealPath(getViewPath()), getEncoding()); - } - - private void initContext() { - for (BeanDefinition beanDef : beanDefinitions) { - if (beanDef instanceof ControllerBeanDefinition) { - ControllerBeanDefinition beanDefinition = (ControllerBeanDefinition) beanDef; - List list = beanDefinition.getReqMethods(); - if (list != null) { - for (Method m : list) { - m.setAccessible(true); - final String uri = m.getAnnotation(RequestMapping.class).value(); - ControllerMetaInfo c = new ControllerMetaInfo(beanDefinition.getObject(), m); - resource.add(uri, c); - } - } - } else if (beanDef instanceof InterceptorBeanDefinition) { - InterceptorBeanDefinition beanDefinition = (InterceptorBeanDefinition) beanDef; - if(beanDefinition.getDisposeMethod() != null) { - beanDefinition.getDisposeMethod().setAccessible(true); - InterceptorMetaInfo interceptor = new InterceptorMetaInfo(beanDefinition.getObject(), - beanDefinition.getDisposeMethod(), - beanDefinition.getUriPattern(), - beanDefinition.getOrder()); - interceptorList.add(interceptor); - } - } - } - if(interceptorList.size() > 0) - Collections.sort(interceptorList); - - TextView.setEncoding(getEncoding()); - JsonView.setEncoding(getEncoding()); - JspView.setViewPath(getViewPath()); - } - - @Override - protected List getBeanDefinitions(String file) { - List list1 = new WebBeanReader(file) - .loadBeanDefinitions(); - List list2 = new XmlBeanReader(file) - .loadBeanDefinitions(); - if (list1 != null && list2 != null) { - list1.addAll(list2); - return list1; - } else if (list1 != null) - return list1; - else if (list2 != null) - return list2; - return null; - } - - @Override - public String getEncoding() { - return ConfigReader.getInstance().getConfig().getEncoding(); - } - - @Override - public String getViewPath() { - return ConfigReader.getInstance().getConfig().getViewPath(); - } - - @Override - public HandlerChain match(String uri, String servletURI) { - final HandlerChainImpl chain = new HandlerChainImpl(); - addInterceptor(uri, servletURI, chain); - addLastHandler(uri, servletURI, chain); - chain.init(); - return chain; - } - - protected void addLastHandler(String uri, String servletURI, final HandlerChainImpl chain) { - if(servletURI == null) - return; - - WebHandler last = resource.match(servletURI); - if(last != null) - chain.add(last); - } - - protected void addInterceptor(String uri, String servletURI, final HandlerChainImpl chain) { - if(servletURI == null) - return; - - for(final InterceptorMetaInfo interceptor : interceptorList) { - if(interceptor.getPattern().match(servletURI) != null) { - chain.add(new WebHandler(){ - - @Override - public View invoke(HttpServletRequest request, HttpServletResponse response) { - return interceptor.invoke(getParams(request, response)); - } - - private Object[] getParams(HttpServletRequest request, HttpServletResponse response) { - byte[] methodParam = interceptor.getMethodParam(); - Object[] p = new Object[methodParam.length]; - - for (int i = 0; i < p.length; i++) { - switch (methodParam[i]) { - case MethodParam.REQUEST: - p[i] = request; - break; - case MethodParam.RESPONSE: - p[i] = response; - break; - case MethodParam.HANDLER_CHAIN: - p[i] = chain; - break; - } - } - return p; - } - }); - } - } - } - - protected class HandlerChainImpl implements HandlerChain { - private List list = new LinkedList(); - private Iterator iterator; - - public void add(WebHandler webHandler) { - list.add(webHandler); - } - - private void init() { - if(list.size() == 0) { // 没有找到处理器输出404响应 - list.add(new WebHandler(){ - - @Override - public View invoke(HttpServletRequest request, HttpServletResponse response) { - String msg = request.getRequestURI() + " not found"; - SystemHtmlPage.responseSystemPage(request, response, getEncoding(), HttpServletResponse.SC_NOT_FOUND, msg); - return null; - } - }); - } - - if(iterator == null) - iterator = list.iterator(); - } - - @Override - public View doNext(HttpServletRequest request, HttpServletResponse response, HandlerChain chain) { - if(iterator.hasNext()) - return iterator.next().invoke(request, response); - else - return null; - } - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/DispatcherController.java b/firefly/src/main/java/com/firefly/mvc/web/DispatcherController.java deleted file mode 100644 index 27b27a2ad..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/DispatcherController.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.mvc.web; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public interface DispatcherController { - - /** - * 前端控制器,派发http请求 - * @param request HttpServletRequest对象 - * @param response HttpServletResponse对象 - */ - void dispatcher(HttpServletRequest request, HttpServletResponse response); -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/HandlerChain.java b/firefly/src/main/java/com/firefly/mvc/web/HandlerChain.java deleted file mode 100644 index 28a34be5c..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/HandlerChain.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.firefly.mvc.web; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public interface HandlerChain { - View doNext(HttpServletRequest request, HttpServletResponse response, HandlerChain chain); -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/HttpMethod.java b/firefly/src/main/java/com/firefly/mvc/web/HttpMethod.java deleted file mode 100644 index e070972a9..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/HttpMethod.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.firefly.mvc.web; - -public interface HttpMethod { - String DELETE = "DELETE"; - String HEAD = "HEAD"; - String GET = "GET"; - String OPTIONS = "OPTIONS"; - String POST = "POST"; - String PUT = "PUT"; - String TRACE = "TRACE"; -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/Resource.java b/firefly/src/main/java/com/firefly/mvc/web/Resource.java deleted file mode 100644 index 8f86fc491..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/Resource.java +++ /dev/null @@ -1,326 +0,0 @@ -package com.firefly.mvc.web; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.servlet.SystemHtmlPage; -import com.firefly.mvc.web.support.ControllerMetaInfo; -import com.firefly.mvc.web.support.MethodParam; -import com.firefly.mvc.web.support.ParamMetaInfo; -import com.firefly.mvc.web.support.URLParser; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.pattern.Pattern; - -public class Resource { - public static final String WILDCARD = "?"; - private static final String[] EMPTY = new String[0]; - private static String ENCODING; - - private final Map CONSTANT_URI; - - private String uri; - private Pattern pattern; - private ControllerMetaInfo controller; - private ResourceSet children = new ResourceSet(); - - public Resource(String encoding) { - CONSTANT_URI = new HashMap(); - ENCODING = encoding; - } - - private Resource(boolean root) { - CONSTANT_URI = root ? new HashMap() : null; - } - - public ControllerMetaInfo getController() { - return controller; - } - - public String getEncoding() { - return ENCODING; - } - - public void add(String uri, ControllerMetaInfo c) { - if(uri.contains(WILDCARD)) { - Resource current = this; - List list = URLParser.parse(uri); - int max = list.size() - 1; - - for (int i = 0; ;i++) { - String name = list.get(i); - if (i == max) { - current = current.children.add(name, c); - return; - } - - current = current.children.add(name, null);; - } - } else { - char last = uri.charAt(uri.length() - 1); - if(last != '/') { - uri += "/"; - } - Resource resource = new Resource(false); - resource.uri = uri; - resource.controller = c; - Result result = new Result(resource, null); - CONSTANT_URI.put(uri, result); - } - } - - public Result match(String uri) { - char last = uri.charAt(uri.length() - 1); - if(last != '/') { - uri += "/"; - } - - Result ret = CONSTANT_URI.get(uri); - if(ret != null) - return ret; - - Resource current = this; - List list = URLParser.parse(uri); - List params = new ArrayList(); - - for(String i : list) { - ret = current.children.match(i); - if(ret == null) - return ret; - - if(ret.params != null) { - for(String p : ret.params) - params.add(p); - } - - current = ret.resource; - } - - if(params.size() > 0) - ret.params = params.toArray(EMPTY); - return ret; - } - - public static class Result implements WebHandler { - private final Resource resource; - private String[] params; - - public Result(Resource resource, String[] params) { - this.resource = resource; - this.params = params; - } - - public ControllerMetaInfo getController() { - return resource.controller; - } - - public String[] getParams() { - return params; - } - - @Override - public View invoke(HttpServletRequest request, HttpServletResponse response) { - if(request.getMethod().equals(resource.controller.getHttpMethod())) { - Object[] p = getParams(request, response); - return getController().invoke(p); - } - - notAllowMethodResponse(request, response); - return null; - } - - private void notAllowMethodResponse(HttpServletRequest request, HttpServletResponse response) { - response.setHeader("Allow", resource.controller.getHttpMethod()); - SystemHtmlPage.responseSystemPage(request, response, ENCODING, - HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Only support " + resource.controller.getHttpMethod() + " method"); - } - - /** - * controller方法参数注入 - * - * @param request - * @param response - * @return - */ - @SuppressWarnings("unchecked") - private Object[] getParams(HttpServletRequest request, HttpServletResponse response) { - ControllerMetaInfo info = this.getController(); - byte[] methodParam = info.getMethodParam(); - ParamMetaInfo[] paramMetaInfos = info.getParamMetaInfos(); - Object[] p = new Object[methodParam.length]; - - for (int i = 0; i < p.length; i++) { - switch (methodParam[i]) { - case MethodParam.REQUEST: - p[i] = request; - break; - case MethodParam.RESPONSE: - p[i] = response; - break; - case MethodParam.HTTP_PARAM: - // 请求参数封装到javabean - Enumeration enumeration = request.getParameterNames(); - ParamMetaInfo paramMetaInfo = paramMetaInfos[i]; - p[i] = paramMetaInfo.newParamInstance(); - - // 把http参数赋值给参数对象 - while (enumeration.hasMoreElements()) { - String httpParamName = enumeration.nextElement(); - String paramValue = request.getParameter(httpParamName); - paramMetaInfo.setParam(p[i], httpParamName, paramValue); - } - if (VerifyUtils.isNotEmpty(paramMetaInfo.getAttribute())) { - request.setAttribute(paramMetaInfo.getAttribute(), p[i]); - } - break; - case MethodParam.PATH_VARIBLE: - p[i] = this.getParams(); - break; - } - } - return p; - } - - @Override - public String toString() { - return "Result [resource=" + resource + ", params=" - + Arrays.toString(params) + "]"; - } - - } - - private class ResourceSet implements Iterable{ - private Map map = new HashMap(); - private List list = new LinkedList(); - - private boolean isVariable() { - return list.size() > 0; - } - - private Result match(String str) { - Resource ret = map.get(str); - if(ret != null) - return new Result(ret, null); - - for(Resource res : list) { - String[] p = res.pattern.match(str); - if(p != null) - return new Result(res, p); - } - - return null; - } - - private Resource add(String uri, ControllerMetaInfo c) { - Resource resource = findByURI(uri); - if(resource == null) { - resource = new Resource(false); - resource.uri = uri; - - if(uri.contains(WILDCARD)) { - resource.pattern = Pattern.compile(resource.uri, WILDCARD); - list.add(resource); - } else { - map.put(uri, resource); - } - } - if(c != null) - resource.controller = c; - return resource; - } - - private Resource findByURI(String uri) { - Resource r = map.get(uri); - if(r != null) { - return r; - } else { - for(Resource res : list) { - if(uri.equals(res.uri)) - return res; - } - } - return null; - } - - - @Override - public Iterator iterator() { - return new ResourceSetItr(); - } - - private class ResourceSetItr implements Iterator { - - private Iterator listItr = list.iterator(); - private Iterator> mapItr = map.entrySet().iterator(); - - @Override - public boolean hasNext() { - return mapItr.hasNext() || listItr.hasNext(); - } - - @Override - public Resource next() { - if(mapItr.hasNext()) - return mapItr.next().getValue(); - else - return listItr.next(); - } - - @Override - public void remove() { - throw new RuntimeException("not implements this method!"); - } - - } - - } - - @Override - public String toString() { - return toString(" ", ""); - } - - private String toString(String l, String append) { - StringBuilder s = new StringBuilder(); - s.append(append + uri + "(" + children.isVariable() + ")" + "\r\n"); - for(Resource r : children) { - s.append(l + r.toString(l + " ", "├")); - } - return s.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((uri == null) ? 0 : uri.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Resource other = (Resource) obj; - if (uri == null) { - if (other.uri != null) - return false; - } else if (!uri.equals(other.uri)) - return false; - return true; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/View.java b/firefly/src/main/java/com/firefly/mvc/web/View.java deleted file mode 100644 index 6c46127ad..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/View.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.firefly.mvc.web; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public interface View { - - void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/WebContext.java b/firefly/src/main/java/com/firefly/mvc/web/WebContext.java deleted file mode 100644 index dfc471f78..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/WebContext.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.firefly.mvc.web; - -import com.firefly.core.ApplicationContext; - -public interface WebContext extends ApplicationContext { - - String getViewPath(); - - String getEncoding(); - - HandlerChain match(String uri, String servletURI); -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/WebHandler.java b/firefly/src/main/java/com/firefly/mvc/web/WebHandler.java deleted file mode 100644 index 1e412d503..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/WebHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.firefly.mvc.web; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public interface WebHandler { - View invoke(HttpServletRequest request, HttpServletResponse response); -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/servlet/DispatcherServlet.java b/firefly/src/main/java/com/firefly/mvc/web/servlet/DispatcherServlet.java deleted file mode 100644 index 442a70f4b..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/servlet/DispatcherServlet.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.firefly.mvc.web.servlet; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.AnnotationWebContext; -import com.firefly.mvc.web.DispatcherController; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -/** - * mvc前端控制器Servlet - * - * @author alvinqiu - * - */ -public class DispatcherServlet extends HttpServlet { - - private static final long serialVersionUID = -3638120056786910984L; - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private static final String INIT_PARAM = "contextConfigLocation"; - private DispatcherController dispatcherController; - - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - processDispatcher(request, response); - } - - @Override - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - processDispatcher(request, response); - } - - @Override - public void doDelete(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - processDispatcher(request, response); - - } - - @Override - public void doPut(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - processDispatcher(request, response); - } - - protected void processDispatcher(HttpServletRequest request, - HttpServletResponse response) { - dispatcherController.dispatcher(request, response); - } - - @Override - public void init() { - String initParam = this.getInitParameter(INIT_PARAM); - log.info("initParam [{}]", initParam); - long start = System.currentTimeMillis(); - dispatcherController = new HttpServletDispatcherController(new AnnotationWebContext(initParam, getServletContext())); - long end = System.currentTimeMillis(); - log.info("firefly startup in {} ms", (end - start)); - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/servlet/HttpServletDispatcherController.java b/firefly/src/main/java/com/firefly/mvc/web/servlet/HttpServletDispatcherController.java deleted file mode 100644 index 9933b260e..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/servlet/HttpServletDispatcherController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.firefly.mvc.web.servlet; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import com.firefly.mvc.web.DispatcherController; -import com.firefly.mvc.web.HandlerChain; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.WebContext; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class HttpServletDispatcherController implements DispatcherController { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - protected WebContext webContext; - - public HttpServletDispatcherController(WebContext webContext) { - this.webContext = webContext; - } - - @Override - public void dispatcher(HttpServletRequest request, HttpServletResponse response) { - String encoding = webContext.getEncoding(); - try { - request.setCharacterEncoding(encoding); - } catch (Throwable t) { - log.error("dispatcher error", t); - } - response.setCharacterEncoding(encoding); - - StringBuilder uriBuilder = new StringBuilder(request.getRequestURI()); - uriBuilder.delete(0, request.getContextPath().length() + request.getServletPath().length()); - String servletURI = uriBuilder.length() <= 0 ? null : uriBuilder.toString(); - HandlerChain chain = webContext.match(request.getRequestURI(), servletURI); - View v = chain.doNext(request, response, chain); - - if(v == null) - return; - - try { - v.render(request, response); - } catch (Throwable t) { - log.error("dispatcher error", t); - } - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/servlet/SystemHtmlPage.java b/firefly/src/main/java/com/firefly/mvc/web/servlet/SystemHtmlPage.java deleted file mode 100644 index ff677a034..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/servlet/SystemHtmlPage.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.firefly.mvc.web.servlet; - -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.utils.StringUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class SystemHtmlPage { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public static void responseSystemPage(HttpServletRequest request, - HttpServletResponse response, String charset, int status, - String content) { - response.setStatus(status); - response.setCharacterEncoding(charset); - response.setHeader("Content-Type", "text/html; charset=" + charset); - PrintWriter writer = null; - try { - try { - writer = response.getWriter(); - } catch (Throwable t) { - log.error("responseSystemPage error", t); - } - writer.print(systemPageTemplate(status, content)); - } finally { - if (writer != null) - writer.close(); - } - } - - public static String systemPageTemplate(int status, String content) { - StringBuilder ret = new StringBuilder(); - try { - ret.append("firefly

HTTP ERROR ") - .append(status) - .append("

") - .append(StringUtils.escapeXML(URLDecoder.decode(content, "UTF-8"))) - .append("

firefly framework"); - } catch (UnsupportedEncodingException e) { - log.error("url decode error", e); - } - return ret.toString(); - } -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/ControllerAnnotatedBeanDefinition.java b/firefly/src/main/java/com/firefly/mvc/web/support/ControllerAnnotatedBeanDefinition.java deleted file mode 100644 index 9eee94888..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/ControllerAnnotatedBeanDefinition.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; -import java.util.List; - -import com.firefly.core.support.annotation.AnnotatedBeanDefinition; - -public class ControllerAnnotatedBeanDefinition extends AnnotatedBeanDefinition - implements ControllerBeanDefinition { - - private List reqMethods; - - - @Override - public List getReqMethods() { - return reqMethods; - } - - @Override - public void setReqMethods(List reqMethods) { - this.reqMethods = reqMethods; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/ControllerBeanDefinition.java b/firefly/src/main/java/com/firefly/mvc/web/support/ControllerBeanDefinition.java deleted file mode 100644 index 9c0521f4c..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/ControllerBeanDefinition.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; -import java.util.List; - -import com.firefly.core.support.annotation.AnnotationBeanDefinition; - -public interface ControllerBeanDefinition extends AnnotationBeanDefinition { - List getReqMethods(); - - void setReqMethods(List reqMethods); -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/ControllerMetaInfo.java b/firefly/src/main/java/com/firefly/mvc/web/support/ControllerMetaInfo.java deleted file mode 100644 index 46badf678..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/ControllerMetaInfo.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.annotation.HttpParam; -import com.firefly.annotation.PathVariable; -import com.firefly.annotation.RequestMapping; -import com.firefly.utils.ReflectUtils; - -public class ControllerMetaInfo extends HandlerMetaInfo { - - private final ParamMetaInfo[] paramMetaInfos; // @HttpParam标注的类的元信息 - private final String httpMethod; - - public ControllerMetaInfo(Object object, Method method) { - super(object, method); - this.httpMethod = method.getAnnotation(RequestMapping.class).method(); - Class[] paraTypes = method.getParameterTypes(); - - // 构造参数对象 - paramMetaInfos = new ParamMetaInfo[paraTypes.length]; - Annotation[][] annotations = method.getParameterAnnotations(); - for (int i = 0; i < paraTypes.length; i++) { - Annotation anno = getAnnotation(annotations[i]); - if (anno != null) { - if(anno.annotationType().equals(HttpParam.class)) { - HttpParam httpParam = (HttpParam) anno; - ParamMetaInfo paramMetaInfo = new ParamMetaInfo( - paraTypes[i], - ReflectUtils.getSetterMethods(paraTypes[i]), - httpParam.value()); - paramMetaInfos[i] = paramMetaInfo; - methodParam[i] = MethodParam.HTTP_PARAM; - } else if(anno.annotationType().equals(PathVariable.class)) { - if (paraTypes[i].equals(String[].class)) - methodParam[i] = MethodParam.PATH_VARIBLE; - } - } else { - if (paraTypes[i].equals(HttpServletRequest.class)) - methodParam[i] = MethodParam.REQUEST; - else if (paraTypes[i].equals(HttpServletResponse.class)) - methodParam[i] = MethodParam.RESPONSE; - } - } - } - - private Annotation getAnnotation(Annotation[] annotations) { - for (Annotation a : annotations) { - if (a.annotationType().equals(HttpParam.class) || a.annotationType().equals(PathVariable.class)) - return a; - } - return null; - } - - public String getHttpMethod() { - return httpMethod; - } - - public ParamMetaInfo[] getParamMetaInfos() { - return paramMetaInfos; - } - - @Override - public String toString() { - return "ControllerMetaInfo [method=" + method + ", httpMethod=" - + httpMethod + "]"; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/HandlerMetaInfo.java b/firefly/src/main/java/com/firefly/mvc/web/support/HandlerMetaInfo.java deleted file mode 100644 index 179ecc823..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/HandlerMetaInfo.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; -import java.util.Arrays; - -import com.firefly.mvc.web.View; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public abstract class HandlerMetaInfo { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - protected final Object object; // controller的实例对象 - protected final Method method; // 请求uri对应的方法 - protected final byte[] methodParam; // 请求方法参数类型 - - public HandlerMetaInfo(Object object, Method method) { - this.object = object; - this.method = method; - this.methodParam = new byte[method.getParameterTypes().length]; - } - - public byte[] getMethodParam() { - return methodParam; - } - - public View invoke(Object[] args) { - View ret = null; - try { - ret = (View)method.invoke(object, args); - } catch (Throwable t) { - log.error("controller invoke error", t); - } - return ret; - } - - @Override - public String toString() { - return "HandlerMetaInfo [method=" + method + ", methodParam=" - + Arrays.toString(methodParam) + "]"; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorAnnotatedBeanDefinition.java b/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorAnnotatedBeanDefinition.java deleted file mode 100644 index cbcd18289..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorAnnotatedBeanDefinition.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; - -import com.firefly.core.support.annotation.AnnotatedBeanDefinition; - -public class InterceptorAnnotatedBeanDefinition extends AnnotatedBeanDefinition implements InterceptorBeanDefinition{ - - private Method disposeMethod; - private String uriPattern; - private int order; - - @Override - public String getUriPattern() { - return uriPattern; - } - - @Override - public void setUriPattern(String uriPattern) { - this.uriPattern = uriPattern; - } - - @Override - public int getOrder() { - return order; - } - - @Override - public void setOrder(int order) { - this.order = order; - } - - @Override - public Method getDisposeMethod() { - return disposeMethod; - } - - @Override - public void setDisposeMethod(Method method) { - disposeMethod = method; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorBeanDefinition.java b/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorBeanDefinition.java deleted file mode 100644 index 9d3199f55..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorBeanDefinition.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; - -import com.firefly.core.support.annotation.AnnotationBeanDefinition; - -public interface InterceptorBeanDefinition extends AnnotationBeanDefinition { - Method getDisposeMethod(); - - void setDisposeMethod(Method method); - - String getUriPattern(); - - void setUriPattern(String uriPattern); - - int getOrder(); - - void setOrder(int order); -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorMetaInfo.java b/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorMetaInfo.java deleted file mode 100644 index a9b442841..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/InterceptorMetaInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.HandlerChain; -import com.firefly.utils.pattern.Pattern; - -public class InterceptorMetaInfo extends HandlerMetaInfo implements Comparable { - - private final Pattern pattern; - private final Integer order; - - public InterceptorMetaInfo(Object object, Method method, String uriPattern, int order) { - super(object, method); - Class[] paraTypes = method.getParameterTypes(); - - for (int i = 0; i < paraTypes.length; i++) { - if (paraTypes[i].equals(HttpServletRequest.class)) - methodParam[i] = MethodParam.REQUEST; - else if (paraTypes[i].equals(HttpServletResponse.class)) - methodParam[i] = MethodParam.RESPONSE; - else if (paraTypes[i].equals(HandlerChain.class)) - methodParam[i] = MethodParam.HANDLER_CHAIN; - } - - pattern = Pattern.compile(uriPattern, "*"); - this.order = order; - } - - public Pattern getPattern() { - return pattern; - } - - public Integer getOrder() { - return order; - } - - @Override - public int compareTo(InterceptorMetaInfo o) { - return order.compareTo(o.order); - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/MethodParam.java b/firefly/src/main/java/com/firefly/mvc/web/support/MethodParam.java deleted file mode 100644 index 59da8d6eb..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/MethodParam.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.firefly.mvc.web.support; - -public interface MethodParam { - byte REQUEST = 0x00; - byte RESPONSE = 0x01; - byte HTTP_PARAM = 0x02; - byte PATH_VARIBLE = 0x03; - byte HANDLER_CHAIN = 0x04; -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/ParamMetaInfo.java b/firefly/src/main/java/com/firefly/mvc/web/support/ParamMetaInfo.java deleted file mode 100644 index 53bcfd8f3..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/ParamMetaInfo.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Method; -import java.util.Map; -import com.firefly.utils.ConvertUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class ParamMetaInfo { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - private final Class paramClass; // 要注入的类型 - private final Map beanSetMethod; // 要注入的bean的set方法 - private final String attribute; // 要setAttribute的属性 - - public ParamMetaInfo(Class paramClass, Map beanSetMethod, String attribute) { - this.paramClass = paramClass; - this.beanSetMethod = beanSetMethod; - this.attribute = attribute; - } - - public String getAttribute() { - return attribute; - } - - /** - * 给参数对象的实例赋值 - * @param o 要赋值的对象 - * @param key 要赋值的属性 - * @param value 要赋的值 - */ - public void setParam(Object o, String key, String value) { - try { - Method m = beanSetMethod.get(key); - if (m != null) { - Class p = m.getParameterTypes()[0]; - m.invoke(o, ConvertUtils.convert(value, p)); - } - } catch (Throwable t) { - log.error("set param error", t); - } - } - - /** - * 新建一个参数对象实例 - * @return - */ - public Object newParamInstance() { - Object o = null; - try { - o = paramClass.newInstance(); - } catch (Throwable t) { - log.error("new param error", t); - } - return o; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/URLParser.java b/firefly/src/main/java/com/firefly/mvc/web/support/URLParser.java deleted file mode 100644 index 78c3a2d75..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/URLParser.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.util.ArrayList; -import java.util.List; - -public abstract class URLParser { - - public static List parse(String uri) { - List ret = new ArrayList(); - int start = 1; - int max = uri.length() - 1; - - for (int i = 1; i <= max; i++) { - if(uri.charAt(i) == '/') { - ret.add(uri.substring(start, i)); - start = i + 1; - } - } - - if(uri.charAt(max) != '/') - ret.add(uri.substring(start)); - return ret; - } -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/support/WebBeanReader.java b/firefly/src/main/java/com/firefly/mvc/web/support/WebBeanReader.java deleted file mode 100644 index 9ed74c3ad..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/support/WebBeanReader.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.firefly.mvc.web.support; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import com.firefly.annotation.Component; -import com.firefly.annotation.Controller; -import com.firefly.annotation.Interceptor; -import com.firefly.annotation.RequestMapping; -import com.firefly.core.support.BeanDefinition; -import com.firefly.core.support.annotation.AnnotationBeanDefinition; -import com.firefly.core.support.annotation.AnnotationBeanReader; -import com.firefly.utils.ReflectUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class WebBeanReader extends AnnotationBeanReader { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public WebBeanReader() { - this(null); - } - - public WebBeanReader(String file) { - super(file); - } - - @Override - protected BeanDefinition getBeanDefinition(Class c) { - if (c.isAnnotationPresent(Controller.class) || c.isAnnotationPresent(Component.class)) { - log.info("classes [{}]", c.getName()); - return componentParser(c); - } else if (c.isAnnotationPresent(Interceptor.class)) { - log.info("classes [{}]", c.getName()); - return interceptorParser(c); - } - else - return null; - } - - @Override - protected BeanDefinition componentParser(Class c) { - ControllerBeanDefinition beanDefinition = new ControllerAnnotatedBeanDefinition(); - setWebBeanDefinition(beanDefinition, c); - - List reqMethods = getReqMethods(c); - beanDefinition.setReqMethods(reqMethods); - return beanDefinition; - } - - private BeanDefinition interceptorParser(Class c) { - InterceptorBeanDefinition beanDefinition = new InterceptorAnnotatedBeanDefinition(); - setWebBeanDefinition(beanDefinition, c); - - beanDefinition.setDisposeMethod(getInterceptors(c)); - - String uriPattern = c.getAnnotation(Interceptor.class).uri(); - beanDefinition.setUriPattern(uriPattern); - - Integer order = c.getAnnotation(Interceptor.class).order(); - beanDefinition.setOrder(order); - return beanDefinition; - } - - private void setWebBeanDefinition(AnnotationBeanDefinition beanDefinition, - Class c) { - beanDefinition.setClassName(c.getName()); - - String id = getId(c); - beanDefinition.setId(id); - - String[] names = ReflectUtils.getInterfaceNames(c); - beanDefinition.setInterfaceNames(names); - - List fields = getInjectField(c); - beanDefinition.setInjectFields(fields); - - List methods = getInjectMethod(c); - beanDefinition.setInjectMethods(methods); - - try { - Object object = c.newInstance(); - beanDefinition.setObject(object); - } catch (Throwable t) { - log.error("set web bean error", t); - } - } - - private String getId(Class c) { - if (c.isAnnotationPresent(Controller.class)) - return c.getAnnotation(Controller.class).value(); - else if (c.isAnnotationPresent(Interceptor.class)) - return c.getAnnotation(Interceptor.class).value(); - else if (c.isAnnotationPresent(Component.class)) - return c.getAnnotation(Component.class).value(); - else - return ""; - } - - private List getReqMethods(Class c) { - Method[] methods = c.getMethods(); - List list = new ArrayList(); - for (Method m : methods) { - if (m.isAnnotationPresent(RequestMapping.class)) { - list.add(m); - } - } - return list; - } - - private Method getInterceptors(Class c) { - for (Method m : c.getMethods()) {// 验证方法名 - if (m.getName().equals("dispose")) - return m; - } - return null; - } -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/view/JsonView.java b/firefly/src/main/java/com/firefly/mvc/web/view/JsonView.java deleted file mode 100644 index beada7da4..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/view/JsonView.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.firefly.mvc.web.view; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.View; -import com.firefly.utils.json.Json; - -public class JsonView implements View { - - private static String ENCODING; - private final Object obj; - - public static void setEncoding(String encoding) { - if(ENCODING == null && encoding != null) - ENCODING = encoding; - } - - public JsonView(Object obj) { - this.obj = obj; - } - - public Object getObj() { - return obj; - } - - @Override - public void render(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - if(obj != null) { - response.setCharacterEncoding(ENCODING); - response.setHeader("Content-Type", "application/json; charset=" + ENCODING); - PrintWriter writer = response.getWriter(); - try{ - writer.print(Json.toJson(obj)); - } finally { - writer.close(); - } - } - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/view/JspView.java b/firefly/src/main/java/com/firefly/mvc/web/view/JspView.java deleted file mode 100644 index a14406e2f..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/view/JspView.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.firefly.mvc.web.view; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.View; - -public class JspView implements View { - - private static String VIEW_PATH; - private final String page; - - public JspView(String page) { - this.page = page; - } - - public static void setViewPath(String path) { - if(VIEW_PATH == null && path != null) - VIEW_PATH = path; - } - - public String getPage() { - return page; - } - - @Override - public void render(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - String ret = VIEW_PATH + page; - request.getRequestDispatcher(ret).forward(request, response); - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/view/RedirectView.java b/firefly/src/main/java/com/firefly/mvc/web/view/RedirectView.java deleted file mode 100644 index 92fd22313..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/view/RedirectView.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firefly.mvc.web.view; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.View; - -public class RedirectView implements View { - - private String uri; - - public RedirectView(String uri) { - this.uri = uri; - } - - @Override - public void render(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - response.sendRedirect(request.getContextPath() + request.getServletPath() + uri); - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/view/StaticFileView.java b/firefly/src/main/java/com/firefly/mvc/web/view/StaticFileView.java deleted file mode 100644 index 0f5e0bce9..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/view/StaticFileView.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.firefly.mvc.web.view; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.servlet.SystemHtmlPage; -import com.firefly.server.exception.HttpServerException; -import com.firefly.server.http.Config; -import com.firefly.server.http.Constants; -import com.firefly.server.http.HttpServletResponseImpl; -import com.firefly.server.io.StaticFileOutputStream; -import com.firefly.utils.RandomUtils; -import com.firefly.utils.StringUtils; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class StaticFileView implements View { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - public static final String CRLF = "\r\n"; - private static Set ALLOW_METHODS = new HashSet(Arrays.asList("GET", "POST", "HEAD")); - private static String RANGE_ERROR_HTML = SystemHtmlPage.systemPageTemplate(416, - "None of the range-specifier values in the Range request-header field overlap the current extent of the selected resource."); - private static Config CONFIG; - private static String TEMPLATE_PATH; - private final String inputPath; - - public StaticFileView(String path) { - this.inputPath = path; - } - - public static void init(Config serverConfig, String tempPath) { - if(CONFIG == null && serverConfig != null) - CONFIG = serverConfig; - - if(TEMPLATE_PATH == null && tempPath != null) - TEMPLATE_PATH = tempPath; - } - - @Override - public void render(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - if(inputPath.startsWith(TEMPLATE_PATH)) { - SystemHtmlPage.responseSystemPage(request, response, - CONFIG.getEncoding(), HttpServletResponse.SC_NOT_FOUND, - request.getRequestURI() + " not found"); - return; - } - - if(!ALLOW_METHODS.contains(request.getMethod())) { - response.setHeader("Allow", "GET,POST,HEAD"); - SystemHtmlPage.responseSystemPage(request, response, CONFIG.getEncoding(), - HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Only support GET, POST or HEAD method"); - return; - } - - String path = CONFIG.getFileAccessFilter().doFilter(request, response, inputPath); - if (VerifyUtils.isEmpty(path)) - return; - - File file = new File(CONFIG.getServerHome(), path); - if (!file.exists() || file.isDirectory()) { - SystemHtmlPage.responseSystemPage(request, response, - CONFIG.getEncoding(), HttpServletResponse.SC_NOT_FOUND, - request.getRequestURI() + " not found"); - return; - } - - String fileSuffix = getFileSuffix(file.getName()).toLowerCase(); - String contentType = Constants.MIME.get(fileSuffix); - if (contentType == null) { - response.setContentType("application/octet-stream"); - response.setHeader("Content-Disposition", "attachment; filename=" - + file.getName()); - } else { - String[] type = StringUtils.split(contentType, '/'); - if ("application".equals(type[0])) { - response.setHeader("Content-Disposition", - "attachment; filename=" + file.getName()); - } else if ("text".equals(type[0])) { - contentType += "; charset=" + CONFIG.getEncoding(); - } - response.setContentType(contentType); - } - - StaticFileOutputStream out = null; - try { - out = ((HttpServletResponseImpl) response) - .getStaticFileOutputStream(); - long fileLen = file.length(); - String range = request.getHeader("Range"); - if (range == null) { - out.write(file); - } else { - String[] rangesSpecifier = StringUtils.split(range, '='); - if (rangesSpecifier.length != 2) { - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG.getEncoding())); - return; - } - - String byteRangeSet = rangesSpecifier[1].trim(); - String[] byteRangeSets = StringUtils.split(byteRangeSet, ','); - if (byteRangeSets.length > 1) { // multipart/byteranges - String boundary = "ff10" + RandomUtils.randomString(13); - if (byteRangeSets.length > CONFIG.getMaxRangeNum()) { - log.error("multipart range more than {}", - CONFIG.getMaxRangeNum()); - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG - .getEncoding())); - return; - } - // multipart output - List tmpByteRangeSets = new ArrayList( - CONFIG.getMaxRangeNum()); - // long otherLen = 0; - for (String t : byteRangeSets) { - String tmp = t.trim(); - String[] byteRange = StringUtils.split(tmp, '-'); - if (byteRange.length == 1) { - long pos = Long.parseLong(byteRange[0].trim()); - if (pos == 0) - continue; - if (tmp.charAt(0) == '-') { - long lastBytePos = fileLen - 1; - long firstBytePos = lastBytePos - pos + 1; - if (firstBytePos > lastBytePos) - continue; - - MultipartByteranges multipartByteranges = getMultipartByteranges( - contentType, firstBytePos, lastBytePos, - fileLen, boundary); - tmpByteRangeSets.add(multipartByteranges); - } else if (tmp.charAt(tmp.length() - 1) == '-') { - long firstBytePos = pos; - long lastBytePos = fileLen - 1; - if (firstBytePos > lastBytePos) - continue; - - MultipartByteranges multipartByteranges = getMultipartByteranges( - contentType, firstBytePos, lastBytePos, - fileLen, boundary); - tmpByteRangeSets.add(multipartByteranges); - } - } else { - long firstBytePos = Long.parseLong(byteRange[0] - .trim()); - long lastBytePos = Long.parseLong(byteRange[1] - .trim()); - if (firstBytePos > fileLen - || firstBytePos >= lastBytePos) - continue; - - MultipartByteranges multipartByteranges = getMultipartByteranges( - contentType, firstBytePos, lastBytePos, - fileLen, boundary); - tmpByteRangeSets.add(multipartByteranges); - } - } - - if (tmpByteRangeSets.size() > 0) { - response.setStatus(206); - response.setHeader("Accept-Ranges", "bytes"); - response.setHeader("Content-Type", - "multipart/byteranges; boundary=" + boundary); - - for (MultipartByteranges m : tmpByteRangeSets) { - long length = m.lastBytePos - m.firstBytePos + 1; - out.write(m.head.getBytes(CONFIG.getEncoding())); - out.write(file, m.firstBytePos, length); - } - - out.write((CRLF + "--" + boundary + "--" + CRLF) - .getBytes(CONFIG.getEncoding())); - log.debug("multipart download|{}", range); - } else { - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG - .getEncoding())); - return; - } - } else { - String tmp = byteRangeSets[0].trim(); - String[] byteRange = StringUtils.split(tmp, '-'); - if (byteRange.length == 1) { - long pos = Long.parseLong(byteRange[0].trim()); - if (pos == 0) { - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG - .getEncoding())); - return; - } - - if (tmp.charAt(0) == '-') { - long lastBytePos = fileLen - 1; - long firstBytePos = lastBytePos - pos + 1; - writePartialFile(request, response, out, file, - firstBytePos, lastBytePos, fileLen); - } else if (tmp.charAt(tmp.length() - 1) == '-') { - writePartialFile(request, response, out, file, pos, - fileLen - 1, fileLen); - } else { - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG - .getEncoding())); - return; - } - } else { - long firstBytePos = Long.parseLong(byteRange[0].trim()); - long lastBytePos = Long.parseLong(byteRange[1].trim()); - if (firstBytePos > fileLen - || firstBytePos >= lastBytePos) { - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG - .getEncoding())); - return; - } - - if (lastBytePos >= fileLen) - lastBytePos = fileLen - 1; - - writePartialFile(request, response, out, file, - firstBytePos, lastBytePos, fileLen); - } - log.debug("single range download|{}", range); - } - } - } catch (Throwable e) { - throw new HttpServerException("get static file output stream error"); - } finally { - if (out != null) - try { - // System.out.println("close~~"); - out.close(); - } catch (IOException e) { - throw new HttpServerException( - "static file output stream close error"); - } - } - - } - - private void writePartialFile(HttpServletRequest request, - HttpServletResponse response, StaticFileOutputStream out, - File file, long firstBytePos, long lastBytePos, long fileLen) - throws Throwable { - - long length = lastBytePos - firstBytePos + 1; - if (length <= 0) { - response.setStatus(416); - out.write(RANGE_ERROR_HTML.getBytes(CONFIG.getEncoding())); - return; - } - response.setStatus(206); - response.setHeader("Accept-Ranges", "bytes"); - response.setHeader("Content-Range", "bytes " + firstBytePos + "-" - + lastBytePos + "/" + fileLen); - out.write(file, firstBytePos, length); - } - - public static String getFileSuffix(String name) { - if (name.charAt(name.length() - 1) == '.') - return "*"; - - for (int i = name.length() - 2; i >= 0; i--) { - if (name.charAt(i) == '.') { - return name.substring(i + 1, name.length()); - } - } - return "*"; - } - - private class MultipartByteranges { - public String head; - public long firstBytePos, lastBytePos; - } - - private MultipartByteranges getMultipartByteranges(String contentType, - long firstBytePos, long lastBytePos, long fileLen, String boundary) { - MultipartByteranges ret = new MultipartByteranges(); - ret.firstBytePos = firstBytePos; - ret.lastBytePos = lastBytePos; - ret.head = CRLF + "--" + boundary + CRLF + "Content-Type: " - + contentType + CRLF + "Content-range: bytes " + firstBytePos - + "-" + lastBytePos + "/" + fileLen + CRLF + CRLF; - return ret; - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/view/TemplateView.java b/firefly/src/main/java/com/firefly/mvc/web/view/TemplateView.java deleted file mode 100644 index f0174107a..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/view/TemplateView.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.firefly.mvc.web.view; - -import java.io.IOException; -import java.util.Enumeration; - -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.servlet.SystemHtmlPage; -import com.firefly.template.Model; -import com.firefly.template.TemplateFactory; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class TemplateView implements View { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private static TemplateFactory t; - private static boolean init = false; - - private String page; - - public static void init(String viewPath, String encoding) { - if (!init) { - log.info("template path {}", viewPath); - com.firefly.template.Config config = new com.firefly.template.Config(); - config.setViewPath(viewPath); - config.setCharset(encoding); - t = new TemplateFactory(config).init(); - init = true; - } - } - - public TemplateView(String page) { - this.page = page; - } - - @Override - public void render(final HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - com.firefly.template.View v = t.getView(page); - if (v == null) { - SystemHtmlPage.responseSystemPage(request, response, t.getConfig().getCharset(), - HttpServletResponse.SC_NOT_FOUND, "template: " + page + "not found"); - return; - } - - response.setCharacterEncoding(t.getConfig().getCharset()); - response.setHeader("Content-Type", "text/html; charset=" + t.getConfig().getCharset()); - ServletOutputStream out = response.getOutputStream(); - Model model = new Model() { - - @SuppressWarnings("unchecked") - @Override - public void clear() { - Enumeration e = request.getAttributeNames(); - while (e.hasMoreElements()) { - String name = e.nextElement(); - request.removeAttribute(name); - } - } - - @Override - public Object get(String name) { - return request.getAttribute(name); - } - - @Override - public void put(String name, Object o) { - request.setAttribute(name, o); - } - - @Override - public void remove(String name) { - request.removeAttribute(name); - } - }; - try { - v.render(model, out); - } finally { - out.close(); - } - } - -} diff --git a/firefly/src/main/java/com/firefly/mvc/web/view/TextView.java b/firefly/src/main/java/com/firefly/mvc/web/view/TextView.java deleted file mode 100644 index 3f568a462..000000000 --- a/firefly/src/main/java/com/firefly/mvc/web/view/TextView.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.firefly.mvc.web.view; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.View; - -public class TextView implements View { - - private static String ENCODING; - private final String text; - - public static void setEncoding(String encoding) { - if (ENCODING == null && encoding != null) - ENCODING = encoding; - } - - public TextView(String text) { - this.text = text; - } - - @Override - public void render(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - response.setCharacterEncoding(ENCODING); - response.setHeader("Content-Type", "text/plain; charset=" + ENCODING); - PrintWriter writer = response.getWriter(); - try { - writer.print(text); - } finally { - writer.close(); - } - } - -} diff --git a/firefly/src/main/java/com/firefly/server/ServerAnnotationWebContext.java b/firefly/src/main/java/com/firefly/server/ServerAnnotationWebContext.java deleted file mode 100644 index 82b03d10a..000000000 --- a/firefly/src/main/java/com/firefly/server/ServerAnnotationWebContext.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.firefly.server; - -import java.io.File; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.AnnotationWebContext; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.WebHandler; -import com.firefly.mvc.web.view.StaticFileView; -import com.firefly.mvc.web.view.TemplateView; -import com.firefly.server.http.Config; - -public class ServerAnnotationWebContext extends AnnotationWebContext { - - private final Config serverConfig; - /** - * 用于firefly http服务器 - * - * @param file - * firefly配置文件 - * @param serverHome - * http服务根目录 - */ - public ServerAnnotationWebContext(Config serverConfig) { - super(serverConfig.getConfigFileName()); - TemplateView.init(new File(serverConfig.getServerHome(), getViewPath()).getAbsolutePath(), getEncoding()); - StaticFileView.init(serverConfig, getViewPath()); - this.serverConfig = serverConfig; - } - - @Override - protected void addLastHandler(String uri, String servletURI, final HandlerChainImpl chain) { - WebHandler last = null; - if(servletURI != null) - last = resource.match(servletURI); - - if(last != null) { - chain.add(last); - return; - } - - final String path = uri.equals("/") ? "/index.html" : uri; - File file = new File(serverConfig.getServerHome(), path); - if (!file.exists() || file.isDirectory()) - return; - - chain.add(new WebHandler(){ - - @Override - public View invoke(HttpServletRequest request, HttpServletResponse response) { - return new StaticFileView(path); - } - - }); - } -} diff --git a/firefly/src/main/java/com/firefly/server/ServerBootstrap.java b/firefly/src/main/java/com/firefly/server/ServerBootstrap.java deleted file mode 100644 index 8dae8dbc4..000000000 --- a/firefly/src/main/java/com/firefly/server/ServerBootstrap.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.firefly.server; - -import com.firefly.mvc.web.WebContext; -import com.firefly.mvc.web.servlet.HttpServletDispatcherController; -import com.firefly.net.Server; -import com.firefly.net.tcp.TcpServer; -import com.firefly.server.http.Config; -import com.firefly.server.http.HttpDecoder; -import com.firefly.server.http.HttpEncoder; -import com.firefly.server.http.HttpHandler; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class ServerBootstrap { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public static void start(String serverHome, String host, int port) { - Config config = new Config(serverHome, host, port); - start(config); - } - - public static void start(String configFileName, String serverHome, - String host, int port) { - Config config = new Config(serverHome, host, port); - config.setConfigFileName(configFileName); - start(config); - } - - public static void start(Config config) { - log.info("server home [{}]", config.getServerHome()); - log.info("context path [{}]", config.getContextPath()); - log.info("servlet path [{}]", config.getServletPath()); - log.info("http handler num [{}]", config.getHandlerSize()); - - long start = System.currentTimeMillis(); - WebContext context = new ServerAnnotationWebContext(config); - HttpServletDispatcherController controller = new HttpServletDispatcherController(context); - config.setEncoding(context.getEncoding()); - Server server = new TcpServer(new HttpDecoder(config), new HttpEncoder(), new HttpHandler(controller, config)); - server.start(config.getHost(), config.getPort()); - long end = System.currentTimeMillis(); - log.info("firefly startup in {} ms", (end - start)); - } -} diff --git a/firefly/src/main/java/com/firefly/server/exception/HttpServerException.java b/firefly/src/main/java/com/firefly/server/exception/HttpServerException.java deleted file mode 100644 index e94271536..000000000 --- a/firefly/src/main/java/com/firefly/server/exception/HttpServerException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.firefly.server.exception; - -@SuppressWarnings("serial") -public class HttpServerException extends RuntimeException { - public HttpServerException(String msg) { - super(msg); - } -} diff --git a/firefly/src/main/java/com/firefly/server/http/Config.java b/firefly/src/main/java/com/firefly/server/http/Config.java deleted file mode 100644 index 6ba97e804..000000000 --- a/firefly/src/main/java/com/firefly/server/http/Config.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.firefly.server.http; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionBindingEvent; -import javax.servlet.http.HttpSessionEvent; -import javax.servlet.http.HttpSessionListener; - -import com.firefly.net.Session; -import com.firefly.server.session.HttpSessionManager; -import com.firefly.server.session.LocalHttpSessionManager; - -public class Config { - private String configFileName = "firefly.xml"; - private String encoding = "UTF-8"; - private int maxRequestLineLength = 8 * 1024, - maxRequestHeadLength = 16 * 1024, - maxRangeNum = 8, - writeBufferSize = 8 * 1024, - handlerSize, - maxHandlerQueueSize = 2000, - maxSessionInactiveInterval = 10 * 60; - private long maxUploadLength = 50 * 1024 * 1024; - private boolean keepAlive = true; - private String serverHome, host, servletPath = "", contextPath = "", sessionIdName = "jsessionid"; - private int port; - private HttpSessionManager httpSessionManager = new LocalHttpSessionManager(this); - private HttpSessionAttributeListener httpSessionAttributeListener = new HttpSessionAttributeListener() { - @Override - public void attributeAdded(HttpSessionBindingEvent se) { - } - - @Override - public void attributeRemoved(HttpSessionBindingEvent se) { - } - - @Override - public void attributeReplaced(HttpSessionBindingEvent se) { - } - }; - - private HttpSessionListener httpSessionListener = new HttpSessionListener() { - - @Override - public void sessionCreated(HttpSessionEvent se) { - } - - @Override - public void sessionDestroyed(HttpSessionEvent se) { - } - }; - - private HttpConnectionListener httpConnectionListener = new HttpConnectionListener() { - - @Override - public void connectionCreated(Session session) { - } - - @Override - public void connectionClosed(Session session) { - } - - }; - - private FileAccessFilter fileAccessFilter = new FileAccessFilter() { - @Override - public String doFilter(HttpServletRequest request, - HttpServletResponse response, String path) { - return path; - } - }; - - { - int p = Runtime.getRuntime().availableProcessors(); - if (p > 4) - handlerSize = p * 2; - else - handlerSize = p + 1; - } - - public Config() { - } - - public Config(String serverHome, String host, int port) { - setServerHome(serverHome); - this.host = host; - this.port = port; - } - - public HttpConnectionListener getHttpConnectionListener() { - return httpConnectionListener; - } - - public void setHttpConnectionListener( - HttpConnectionListener httpConnectionListener) { - this.httpConnectionListener = httpConnectionListener; - } - - public String getConfigFileName() { - return configFileName; - } - - public void setConfigFileName(String configFileName) { - this.configFileName = configFileName; - } - - public HttpSessionAttributeListener getHttpSessionAttributeListener() { - return httpSessionAttributeListener; - } - - public void setHttpSessionAttributeListener( - HttpSessionAttributeListener httpSessionAttributeListener) { - this.httpSessionAttributeListener = httpSessionAttributeListener; - } - - public HttpSessionListener getHttpSessionListener() { - return httpSessionListener; - } - - public void setHttpSessionListener(HttpSessionListener httpSessionListener) { - this.httpSessionListener = httpSessionListener; - } - - /** - * 获取HttpSession默认超时时间,单位秒 - * - * @return HttpSession默认超时时间 - */ - public int getMaxSessionInactiveInterval() { - return maxSessionInactiveInterval; - } - - /** - * 设置HttpSession默认超时时间,单位秒 - * - * @param maxSessionInactiveInterval - * HttpSession默认超时时间 - */ - public void setMaxSessionInactiveInterval(int maxSessionInactiveInterval) { - this.maxSessionInactiveInterval = maxSessionInactiveInterval; - } - - /** - * 获取HttpSession在cookie或者url中的名称 - * - * @return HttpSession的名称 - */ - public String getSessionIdName() { - return sessionIdName; - } - - /** - * 设置HttpSession在cookie或者url中的名称 - * - * @param sessionIdName - * HttpSession的名称 - */ - public void setSessionIdName(String sessionIdName) { - this.sessionIdName = sessionIdName; - } - - /** - * 获取HttpSession管理器,默认为本地Session存储 - * - * @return HttpSession管理器 - */ - public HttpSessionManager getHttpSessionManager() { - return httpSessionManager; - } - - /** - * 设置HttpSession管理器 - * - * @param httpSessionManager - * HttpSession管理器 - */ - public void setHttpSessionManager(HttpSessionManager httpSessionManager) { - this.httpSessionManager = httpSessionManager; - } - - /** - * 获取静态文件访问过滤器,此过滤器可以拦截本地静态文件的访问,并做出控制 - * - * @return 静态文件访问过滤器 - */ - public FileAccessFilter getFileAccessFilter() { - return fileAccessFilter; - } - - /** - * 设置静态文件访问过滤器 - * - * @param fileAccessFilter - * 静态文件访问过滤器 - */ - public void setFileAccessFilter(FileAccessFilter fileAccessFilter) { - this.fileAccessFilter = fileAccessFilter; - } - - public int getHandlerSize() { - return handlerSize; - } - - public void setHandlerSize(int handlerQueueSize) { - this.handlerSize = handlerQueueSize > 1 ? handlerQueueSize : 1; - } - - public String getContextPath() { - return contextPath; - } - - public void setContextPath(String contextPath) { - this.contextPath = removeLastSlash(contextPath); - } - - public String getServletPath() { - return servletPath; - } - - public void setServletPath(String servletPath) { - this.servletPath = removeLastSlash(servletPath); - } - - public static String removeLastSlash(String str) { - if (str.charAt(str.length() - 1) == '/') - return str.substring(0, str.length() - 1); - return str; - } - - public int getWriteBufferSize() { - return writeBufferSize; - } - - public void setWriteBufferSize(int writeBufferSize) { - this.writeBufferSize = writeBufferSize; - } - - public String getHost() { - return host; - } - - public void setHost(String host) { - this.host = host; - } - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public String getServerHome() { - return serverHome; - } - - public void setServerHome(String serverHome) { - if (serverHome.charAt(serverHome.length() - 1) == '/') - this.serverHome = serverHome.substring(0, serverHome.length() - 1); - else - this.serverHome = serverHome; - } - - public int getMaxRequestHeadLength() { - return maxRequestHeadLength; - } - - public void setMaxRequestHeadLength(int maxRequestHeadLength) { - this.maxRequestHeadLength = maxRequestHeadLength; - } - - public String getEncoding() { - return encoding; - } - - public void setEncoding(String encoding) { - this.encoding = encoding; - } - - public int getMaxRequestLineLength() { - return maxRequestLineLength; - } - - public void setMaxRequestLineLength(int maxRequestLineLength) { - this.maxRequestLineLength = maxRequestLineLength; - } - - public int getMaxRangeNum() { - return maxRangeNum; - } - - public void setMaxRangeNum(int maxRangeNum) { - this.maxRangeNum = maxRangeNum; - } - - public long getMaxUploadLength() { - return maxUploadLength; - } - - public void setMaxUploadLength(long maxUploadLength) { - this.maxUploadLength = maxUploadLength; - } - - public boolean isKeepAlive() { - return keepAlive; - } - - public void setKeepAlive(boolean keepAlive) { - this.keepAlive = keepAlive; - } - - public int getMaxHandlerQueueSize() { - return maxHandlerQueueSize; - } - - public void setMaxHandlerQueueSize(int maxHandlerQueueSize) { - this.maxHandlerQueueSize = maxHandlerQueueSize; - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/Constants.java b/firefly/src/main/java/com/firefly/server/http/Constants.java deleted file mode 100644 index 11f0bd81c..000000000 --- a/firefly/src/main/java/com/firefly/server/http/Constants.java +++ /dev/null @@ -1,1346 +0,0 @@ -package com.firefly.server.http; - -import java.util.HashMap; -import java.util.Map; - -public class Constants { - public static final Map STATUS_CODE = new HashMap(); - public static final Map MIME = new HashMap(); - - static { - // 1xx - STATUS_CODE.put(100, "Continue"); - STATUS_CODE.put(101, "Switching Protocols"); - - // 2xx - STATUS_CODE.put(200, "OK"); - STATUS_CODE.put(201, "Created"); - STATUS_CODE.put(202, "Accepted"); - STATUS_CODE.put(203, "Non-Authoritative information"); - STATUS_CODE.put(204, "No Content"); - STATUS_CODE.put(205, "Reset Content"); - STATUS_CODE.put(206, "Partial Content"); - - // 3xx - STATUS_CODE.put(300, "Multiple Choices"); - STATUS_CODE.put(301, "Moved Permanently"); - STATUS_CODE.put(302, "Found"); - STATUS_CODE.put(303, "See Other"); - STATUS_CODE.put(304, "Not Modified"); - STATUS_CODE.put(305, "User Proxy"); - STATUS_CODE.put(307, "Temporary Redirect"); - - // 4xx - STATUS_CODE.put(400, "Bad Request"); - STATUS_CODE.put(401, "Unauthorized"); - STATUS_CODE.put(403, "Forbidden"); - STATUS_CODE.put(404, "Not Found"); - STATUS_CODE.put(405, "Method Not Allowed"); - STATUS_CODE.put(406, "Not Acceptable"); - STATUS_CODE.put(407, "Proxy Authentication Required"); - STATUS_CODE.put(408, "Request Timeout"); - STATUS_CODE.put(409, "Confilict"); - STATUS_CODE.put(410, "Gone"); - STATUS_CODE.put(411, "Length Required"); - STATUS_CODE.put(412, "Precondition Failed"); - STATUS_CODE.put(413, "Request Entity Too Large"); - STATUS_CODE.put(414, "Request-URI Too Long"); - STATUS_CODE.put(415, "Unsupported Media Type"); - STATUS_CODE.put(416, "Requested Range Not Satisfiable"); - STATUS_CODE.put(417, "Expectation Failed"); - - // 5xx - STATUS_CODE.put(500, "Internal Server Error"); - STATUS_CODE.put(501, "Not Implemented"); - STATUS_CODE.put(503, "Service Unavailable"); - STATUS_CODE.put(504, "Gateway Timeout"); - STATUS_CODE.put(505, "HTTP Version Not Supported"); - - // MIME类型 - MIME.put("*", "application/octet-stream"); - MIME.put("123", "application/lotus"); - MIME.put("123", "application/vnd.lotus-1-2-3"); - MIME.put("323", "text/h323"); - MIME.put("3dm", "model/vnd.flatland.3dml"); - MIME.put("3dm", "text/vnd.in3d.3dml"); - MIME.put("3dm", "x-world/x-3dmf"); - MIME.put("3dmf", "x-world/x-3dmf"); - MIME.put("3dml", "model/vnd.flatland.3dml"); - MIME.put("3dml", "text/vnd.in3d.3dml"); - MIME.put("3gp", "video/3gpp"); - MIME.put("a", "application/octet-stream"); - MIME.put("aab", "application/x-authorware-bin"); - MIME.put("aab", "application/x-authoware-bin"); - MIME.put("aam", "application/x-authorware-map"); - MIME.put("aam", "application/x-authoware-map"); - MIME.put("aas", "application/x-authorware-seg"); - MIME.put("aas", "application/x-authoware-seg"); - MIME.put("abc", "text/vnd.abc"); - MIME.put("acc", "chemical/x-synopsys-accord"); - MIME.put("acgi", "text/html"); - MIME.put("acu", "application/vnd.acucobol"); - MIME.put("acx", "application/internet-property-stream"); - MIME.put("aep", "application/vnd.audiograph"); - MIME.put("afl", "video/animaflex"); - MIME.put("afp", "application/vnd.ibm.modcap"); - MIME.put("ai", "application/postscript"); - MIME.put("aif", "audio/aiff"); - MIME.put("aif", "audio/x-aiff"); - MIME.put("aifc", "audio/aiff"); - MIME.put("aifc", "audio/x-aiff"); - MIME.put("aiff", "audio/aiff"); - MIME.put("aiff", "audio/x-aiff"); - MIME.put("aim", "application/x-aim"); - MIME.put("aip", "text/x-audiosoft-intra"); - MIME.put("als", "audio/X-Alpha5"); - MIME.put("amc", "application/x-mpeg"); - MIME.put("ani", "application/octet-stream"); - MIME.put("ani", "application/x-navi-animation"); - MIME.put("ano", "application/x-annotator"); - MIME.put("aos", "application/x-nokia-9000-communicator-add-on-software"); - MIME.put("apm", "application/studiom"); - MIME.put("apr", "application/vnd.lotus-approach"); - MIME.put("aps", "application/mime"); - MIME.put("arc", "application/octet-stream"); - MIME.put("arj", "application/arj"); - MIME.put("arj", "application/octet-stream"); - MIME.put("art", "image/x-jg"); - MIME.put("asc", "text/plain"); - MIME.put("asd", "application/astound"); - MIME.put("asf", "application/vnd.ms-asf"); - MIME.put("asf", "video/x-ms-asf"); - MIME.put("asm", "text/x-asm"); - MIME.put("asn", "application/astound"); - MIME.put("aso", "application/vnd.accpac.simply.aso"); - MIME.put("asp", "application/x-asap"); - MIME.put("asp", "text/asp"); - MIME.put("asr", "video/x-ms-asf"); - MIME.put("asx", "application/x-mplayer2"); - MIME.put("asx", "video/x-ms-asf"); - MIME.put("asx", "video/x-ms-asf-plugin"); - MIME.put("au", "audio/basic"); - MIME.put("au", "audio/x-au"); - MIME.put("avb", "application/octet-stream"); - MIME.put("avi", "application/x-troff-msvideo"); - MIME.put("avi", "video/avi"); - MIME.put("avi", "video/msvideo"); - MIME.put("avi", "video/quicktime"); - MIME.put("avi", "video/x-msvideo"); - MIME.put("avs", "video/avs-video"); - MIME.put("avx", "video/x-rad-screenplay"); - MIME.put("awb", "audio/amr-wb"); - MIME.put("bas", "text/plain"); - MIME.put("bcpio", "application/x-bcpio"); - MIME.put("bh2", "application/vnd.fujitsu.oasysprs"); - MIME.put("bin", "application/mac-binary"); - MIME.put("bin", "application/macbinary"); - MIME.put("bin", "application/octet-stream"); - MIME.put("bin", "application/x-binary"); - MIME.put("bin", "application/x-macbinary"); - MIME.put("bld", "application/bld"); - MIME.put("bld2", "application/bld2"); - MIME.put("bm", "image/bmp"); - MIME.put("bmi", "application/vnd.bmi"); - MIME.put("bmp", "application/x-MS-bmp"); - MIME.put("bmp", "image/bitmap"); - MIME.put("bmp", "image/bmp"); - MIME.put("bmp", "image/x-bmp"); - MIME.put("bmp", "image/x-windows-bmp"); - MIME.put("boo", "application/book"); - MIME.put("book", "application/book"); - MIME.put("box", "application/vnd.previewsystems.box"); - MIME.put("boz", "application/x-bzip2"); - MIME.put("bpk", "application/octet-stream"); - MIME.put("bsh", "application/x-bsh"); - MIME.put("btf", "image/prs.btif"); - MIME.put("btif", "image/prs.btif"); - MIME.put("bz", "application/x-bzip"); - MIME.put("bz2", "application/x-bzip2"); - MIME.put("c", "text/plain"); - MIME.put("c", "text/x-c"); - MIME.put("c++", "text/plain"); - MIME.put("cal", "image/x-cals"); - MIME.put("cat", "application/vnd.ms-pki.seccat"); - MIME.put("cc", "text/plain"); - MIME.put("cc", "text/x-c"); - MIME.put("ccad", "application/clariscad"); - MIME.put("ccn", "application/x-cnc"); - MIME.put("cco", "application/x-cocoa"); - MIME.put("cdf", "application/cdf"); - MIME.put("cdf", "application/x-cdf"); - MIME.put("cdf", "application/x-netcdf"); - MIME.put("cdkey", "application/vnd.mediastation.cdkey"); - MIME.put("cdx", "chemical/x-cdx"); - MIME.put("cdx", "chemical/x-chem3d"); - MIME.put("cer", "application/pkix-cert"); - MIME.put("cer", "application/x-x509-ca-cert"); - MIME.put("cgi", "magnus-internal/cgi"); - MIME.put("cgm", "image/cgm"); - MIME.put("cha", "application/x-chat"); - MIME.put("chat", "application/x-chat"); - MIME.put("chm", "chemical/x-chemdraw"); - MIME.put("chm", "chemical/x-cs-chemdraw"); - MIME.put("cif", "chemical/x-cif"); - MIME.put("cii", "application/vnd.anser-web-certificate-issue-initiation"); - MIME.put("cla", "application/vnd.claymore"); - MIME.put("class", "application/java"); - MIME.put("clp", "application/x-msclip"); - MIME.put("cmc", "application/vnd.cosmocaller"); - MIME.put("cmdf", "chemical/x-cmdf"); - MIME.put("cml", "chemical/x-cml"); - MIME.put("cmp", "application/vnd.yellowriver-custom-menu"); - MIME.put("cmx", "application/x-cmx"); - MIME.put("cmx", "image/x-cmx"); - MIME.put("co", "application/x-cult3d-object"); - MIME.put("cod", "image/cis-cod"); - MIME.put("com", "application/octet-stream"); - MIME.put("com", "text/plain"); - MIME.put("conf", "text/plain"); - MIME.put("config", "application/x-ns-proxy-autoconfig"); - MIME.put("cpio", "application/x-cpio"); - MIME.put("cpp", "text/x-c"); - MIME.put("cpt", "application/mac-compactpro"); - MIME.put("cpt", "application/x-compactpro"); - MIME.put("cpt", "application/x-cpt"); - MIME.put("crd", "application/x-mscardfile"); - MIME.put("crl", "application/pkcs-crl"); - MIME.put("crl", "application/pkix-crl"); - MIME.put("crt", "application/pkix-cert"); - MIME.put("crt", "application/x-x509-ca-cert"); - MIME.put("crt", "application/x-x509-user-cert"); - MIME.put("csh", "application/x-csh"); - MIME.put("csh", "text/x-script.csh"); - MIME.put("csm", "chemical/x-csml"); - MIME.put("csml", "chemical/x-csml"); - MIME.put("csp", "application/vnd.commonspace"); - MIME.put("css", "application/x-pointplus"); - MIME.put("css", "text/css"); - MIME.put("cst", "application/vnd.commonspace"); - MIME.put("cub", "chemical/x-gaussian-cube"); - MIME.put("cur", "application/octet-stream"); - MIME.put("curl", "text/vnd.curl"); - MIME.put("cw", "application/prs.cww"); - MIME.put("cww", "application/prs.cww"); - MIME.put("cxx", "text/plain"); - MIME.put("daf", "application/vnd.Mobius.DAF"); - MIME.put("dcm", "x-lml/x-evm"); - MIME.put("dcr", "application/x-director"); - MIME.put("dcx", "image/x-dcx"); - MIME.put("ddd", "application/vnd.fujixerox.ddd"); - MIME.put("deepv", "application/x-deepv"); - MIME.put("def", "text/plain"); - MIME.put("der", "application/pkix-cert"); - MIME.put("der", "application/x-x509-ca-cert"); - MIME.put("dhtml", "text/html"); - MIME.put("dib", "image/bmp"); - MIME.put("dic", "text/plain"); - MIME.put("dif", "video/x-dv"); - MIME.put("dir", "application/x-director"); - MIME.put("dis", "application/vnd.Mobius.DIS"); - MIME.put("dl", "video/dl"); - MIME.put("dl", "video/x-dl"); - MIME.put("dll", "application/octet-stream"); - MIME.put("dll", "application/x-msdownload"); - MIME.put("dmg", "application/octet-stream"); - MIME.put("dms", "application/octet-stream"); - MIME.put("dna", "application/vnd.dna"); - MIME.put("doc", "application/msword"); - MIME.put("dor", "model/vnd.gdl"); - MIME.put("dor", "model/vnd.gs.gdl"); - MIME.put("dot", "application/msword"); - MIME.put("dot", "application/x-dot"); - MIME.put("dp", "application/commonground"); - MIME.put("dpg", "application/vnd.dpgraph"); - MIME.put("dpgraph", "application/vnd.dpgraph"); - MIME.put("drw", "application/drafting"); - MIME.put("dsc", "text/prs.lines.tag"); - MIME.put("dtd", "application/xml"); - MIME.put("dtd", "text/xml"); - MIME.put("dump", "application/octet-stream"); - MIME.put("dv", "video/x-dv"); - MIME.put("dvi", "application/x-dvi"); - MIME.put("dwf", "drawing/x-dwf"); - MIME.put("dwf", "drawing/x-dwf,//(old)"); - MIME.put("dwf", "model/vnd.dwf"); - MIME.put("dwg", "application/acad"); - MIME.put("dwg", "application/autocad"); - MIME.put("dwg", "application/x-autocad"); - MIME.put("dwg", "image/vnd"); - MIME.put("dwg", "image/vnd.dwg"); - MIME.put("dwg", "image/x-dwg"); - MIME.put("dx", "chemical/x-jcamp-dx"); - MIME.put("dxf", "application/dxf"); - MIME.put("dxf", "application/x-autocad"); - MIME.put("dxf", "image/vnd.dxf"); - MIME.put("dxf", "image/x-dwg"); - MIME.put("dxf", "image/x-dxf"); - MIME.put("dxr", "application/vnd.dxr"); - MIME.put("dxr", "application/x-director"); - MIME.put("ebk", "application/x-expandedbook"); - MIME.put("ecelp4800", "audio/vnd.nuera.ecelp4800"); - MIME.put("ecelp7470", "audio/vnd.nuera.ecelp7470"); - MIME.put("edm", "application/vnd.novadigm.EDM"); - MIME.put("edx", "application/vnd.novadigm.EDX"); - MIME.put("ei6", "application/vnd.pg.osasli"); - MIME.put("el", "text/x-script.elisp"); - MIME.put("elc", "application/x-bytecode.elisp"); - MIME.put("elc", "application/x-elc"); - MIME.put("emb", "chemical/x-embl-dl-nucleotide"); - MIME.put("embl", "chemical/x-embl-dl-nucleotide"); - MIME.put("eml", "message/rfc822"); - MIME.put("enc", "video/mpeg"); - MIME.put("env", "application/x-envoy"); - MIME.put("eol", "audio/vnd.digital-winds"); - MIME.put("epb", "application/x-epublisher"); - MIME.put("eps", "application/postscript"); - MIME.put("eri", "image/x-eri"); - MIME.put("es", "application/x-esrehber"); - MIME.put("es", "audio/echospeech"); - MIME.put("esl", "audio/echospeech"); - MIME.put("etc", "application/x-earthtime"); - MIME.put("etx", "text/x-setext"); - MIME.put("evm", "x-lml/x-evm"); - MIME.put("evy", "application/envoy"); - MIME.put("evy", "application/x-envoy"); - MIME.put("exc", "text/plain"); - MIME.put("exe", "application/octet-stream"); - MIME.put("ext", "application/vnd.novadigm.EXT"); - MIME.put("ez", "application/andrew-inset"); - MIME.put("f", "text/plain"); - MIME.put("f", "text/x-fortran"); - MIME.put("f77", "text/x-fortran"); - MIME.put("f90", "text/plain"); - MIME.put("f90", "text/x-fortran"); - MIME.put("faxmgr", "application/x-fax-manager"); - MIME.put("faxmgrjob", "application/x-fax-manager-job"); - MIME.put("fbs", "image/vnd.fastbidsheet"); - MIME.put("fdf", "application/vnd.fdf"); - MIME.put("fdml", "text/html"); - MIME.put("fg5", "application/vnd.fujitsu.oasysgp"); - MIME.put("fgd", "application/x-director"); - MIME.put("fh", "image/x-freehand"); - MIME.put("fh4", "image/x-freehand"); - MIME.put("fh5", "image/x-freehand"); - MIME.put("fh7", "image/x-freehand"); - MIME.put("fhc", "image/x-freehand"); - MIME.put("fif", "application/fractals"); - MIME.put("fif", "image/fif"); - MIME.put("fli", "video/fli"); - MIME.put("fli", "video/x-fli"); - MIME.put("flo", "image/florian"); - MIME.put("flr", "x-world/x-vrml"); - MIME.put("flx", "text/html"); - MIME.put("flx", "text/vnd.fmi.flexstor"); - MIME.put("fly", "text/vnd.fly"); - MIME.put("fm", "application/x-framemaker"); - MIME.put("fm", "application/x-maker"); - MIME.put("fmf", "video/x-atomic3d-feature"); - MIME.put("fml", "application/file-mirror-list"); - MIME.put("fml", "application/x-file-mirror-list"); - MIME.put("for", "text/plain"); - MIME.put("for", "text/x-fortran"); - MIME.put("fp5", "application/filemaker5"); - MIME.put("fpx", "application/vnd.netfpx"); - MIME.put("fpx", "image/vnd.fpx"); - MIME.put("fpx", "image/vnd.net-fpx"); - MIME.put("fpx", "image/x-fpx"); - MIME.put("frame", "application/x-framemaker"); - MIME.put("frl", "application/freeloader"); - MIME.put("frm", "application/vnd.ufdl"); - MIME.put("frm", "application/vnd.xfdl"); - MIME.put("frm", "application/x-framemaker"); - MIME.put("fst", "image/vnd.fst"); - MIME.put("fti", "application/vnd.anser-web-funds-transfer-initiation"); - MIME.put("funk", "audio/make"); - MIME.put("fvi", "video/isivideo"); - MIME.put("fvt", "video/vnd.fvt"); - MIME.put("g", "text/plain"); - MIME.put("g3", "image/g3fax"); - MIME.put("gac", "application/vnd.groove-account"); - MIME.put("gau", "chemical/x-gaussian-input"); - MIME.put("gca", "application/x-gca-compressed"); - MIME.put("gdb", "x-lml/x-gdb"); - MIME.put("gdl", "model/vnd.gdl"); - MIME.put("gdl", "model/vnd.gs.gdl"); - MIME.put("gif", "image/gif"); - MIME.put("gim", "application/vnd.groove-identity-message"); - MIME.put("gl", "video/gl"); - MIME.put("gl", "video/x-gl"); - MIME.put("gph", "application/vnd.FloGraphIt"); - MIME.put("gps", "application/x-gps"); - MIME.put("gqf", "application/vnd.grafeq"); - MIME.put("gqs", "application/vnd.grafeq"); - MIME.put("grv", "application/vnd.groove-injector"); - MIME.put("gsd", "audio/x-gsm"); - MIME.put("gsm", "audio/x-gsm"); - MIME.put("gsm", "model/vnd.gdl"); - MIME.put("gsm", "model/vnd.gs.gdl"); - MIME.put("gsp", "application/x-gsp"); - MIME.put("gss", "application/x-gss"); - MIME.put("gtar", "application/x-gtar"); - MIME.put("gtm", "application/vnd.froove-tool-message"); - MIME.put("gtm", "application/vnd.groove-tool-message"); - MIME.put("gtp", "application/bsi-gtp"); - MIME.put("gtw", "model/vnd.gtw"); - MIME.put("gz", "application/x-compressed"); - MIME.put("gz", "application/x-gzip"); - MIME.put("gzip", "application/x-gzip"); - MIME.put("gzip", "multipart/x-gzip"); - MIME.put("h", "text/plain"); - MIME.put("h", "text/x-h"); - MIME.put("hdf", "application/x-hdf"); - MIME.put("hdm", "text/x-hdml"); - MIME.put("hdml", "text/x-hdml"); - MIME.put("help", "application/x-helpfile"); - MIME.put("hgl", "application/vnd.hp-HPGL"); - MIME.put("hh", "text/plain"); - MIME.put("hh", "text/x-h"); - MIME.put("hlb", "text/x-script"); - MIME.put("hlp", "application/hlp"); - MIME.put("hlp", "application/winhlp"); - MIME.put("hlp", "application/x-helpfile"); - MIME.put("hlp", "application/x-winhelp"); - MIME.put("hpg", "application/vnd.hp-HPGL"); - MIME.put("hpgl", "application/vnd.hp-HPGL"); - MIME.put("hpi", "application/vnd.hp-hpid"); - MIME.put("hpid", "application/vnd.hp-hpid"); - MIME.put("hps", "application/vnd.hp-hps"); - MIME.put("hqx", "application/binhex"); - MIME.put("hqx", "application/binhex4"); - MIME.put("hqx", "application/mac-binhex"); - MIME.put("hqx", "application/mac-binhex40"); - MIME.put("hqx", "application/x-binhex40"); - MIME.put("hqx", "application/x-mac-binhex40"); - MIME.put("hta", "application/hta"); - MIME.put("htc", "text/x-component"); - MIME.put("htm", "text/html"); - MIME.put("html", "text/html"); - MIME.put("htmls", "text/html"); - MIME.put("hts", "text/html"); - MIME.put("htt", "text/webviewhtml"); - MIME.put("htx", "text/html"); - MIME.put("ic0", "application/vnd.commerce-battelle"); - MIME.put("ic1", "application/vnd.commerce-battelle"); - MIME.put("ic2", "application/vnd.commerce-battelle"); - MIME.put("ic3", "application/vnd.commerce-battelle"); - MIME.put("ic4", "application/vnd.commerce-battelle"); - MIME.put("ic5", "application/vnd.commerce-battelle"); - MIME.put("ic6", "application/vnd.commerce-battelle"); - MIME.put("ic7", "application/vnd.commerce-battelle"); - MIME.put("ic8", "application/vnd.commerce-battelle"); - MIME.put("ica", "application/vnd.commerce-battelle"); - MIME.put("icc", "application/vnd.commerce-battelle"); - MIME.put("icd", "application/vnd.commerce-battelle"); - MIME.put("ice", "x-conference/x-cooltalk"); - MIME.put("icf", "application/vnd.commerce-battelle"); - MIME.put("ico", "application/octet-stream"); - MIME.put("ico", "image/x-icon"); - MIME.put("idc", "text/plain"); - MIME.put("ief", "image/ief"); - MIME.put("iefs", "image/ief"); - MIME.put("ifm", "application/vnd.shana.informed.formdata"); - MIME.put("ifm", "image/gif"); - MIME.put("ifs", "image/ifs"); - MIME.put("iges", "application/iges"); - MIME.put("iges", "model/iges"); - MIME.put("igs", "application/iges"); - MIME.put("igs", "model/iges"); - MIME.put("iif", "application/vnd.shana.informed.interchange"); - MIME.put("iii", "application/x-iphone"); - MIME.put("ima", "application/x-ima"); - MIME.put("imap", "application/x-httpd-imap"); - MIME.put("imd", "application/immedia"); - MIME.put("imp", "application/vnd.accpac.simply.imp"); - MIME.put("ims", "application/immedia"); - MIME.put("imy", "audio/melody"); - MIME.put("inf", "application/inf"); - MIME.put("ins", "application/x-NET-Install"); - MIME.put("ins", "application/x-insight"); - MIME.put("ins", "application/x-internet-signup"); - MIME.put("ins", "application/x-internett-signup"); - MIME.put("insight", "application/x-insight"); - MIME.put("inst", "application/x-install"); - MIME.put("ip", "application/x-ip2"); - MIME.put("ipk", "application/vnd.shana.informed.package"); - MIME.put("ips", "application/x-ipscript"); - MIME.put("ipx", "application/x-ipix"); - MIME.put("ism", "model/vnd.gdl"); - MIME.put("ism", "model/vnd.gs.gdl"); - MIME.put("isp", "application/x-internet-signup"); - MIME.put("ist", "chemical/x-isostar"); - MIME.put("istr", "chemical/x-isostar"); - MIME.put("isu", "video/x-isvideo"); - MIME.put("it", "audio/it"); - MIME.put("it", "audio/x-mod"); - MIME.put("itp", "application/vnd.shana.informed.formtemp"); - MIME.put("itz", "audio/x-mod"); - MIME.put("iv", "application/x-inventor"); - MIME.put("ivf", "video/x-ivf"); - MIME.put("ivr", "i-world/I-vrml"); - MIME.put("ivr", "i-world/i-vrml"); - MIME.put("ivy", "application/x-livescreen"); - MIME.put("j2k", "image/j2k"); - MIME.put("jad", "text/vnd.sun.j2me.app-descriptor"); - MIME.put("jam", "application/x-jam"); - MIME.put("jam", "audio/x-jam"); - MIME.put("jar", "application/x-java-archive"); - MIME.put("jav", "text/x-java-source"); - MIME.put("java", "text/x-java-source"); - MIME.put("jcm", "application/x-java-commerce"); - MIME.put("jdx", "chemical/x-jcamp-dx"); - MIME.put("jfif", "image/jpeg"); - MIME.put("jfif", "image/pjpeg"); - MIME.put("jfif-tbnl", "image/jpeg"); - MIME.put("jnlp", "application/x-java-jnlp-file"); - MIME.put("jpe", "image/jpeg"); - MIME.put("jpeg", "image/jpeg"); - MIME.put("jpg", "image/jpeg"); - MIME.put("jps", "image/x-jps"); - MIME.put("jpz", "image/jpeg"); - MIME.put("js", "application/x-javascript"); - MIME.put("js", "application/x-ns-proxy-autoconfig"); - MIME.put("jut", "image/jutvision"); - MIME.put("jvs", "application/x-ns-proxy-autoconfig"); - MIME.put("jwc", "application/jwc"); - MIME.put("kar", "audio/midi"); - MIME.put("kar", "music/x-karaoke"); - MIME.put("kin", "chemical/x-kinemage"); - MIME.put("kjx", "application/x-kjx"); - MIME.put("ksh", "application/x-ksh"); - MIME.put("ksh", "text/x-script.ksh"); - MIME.put("la", "audio/nspaudio"); - MIME.put("la", "audio/x-nspaudio"); - MIME.put("lak", "x-lml/x-lak"); - MIME.put("lam", "audio/x-liveaudio"); - MIME.put("latex", "application/x-latex"); - MIME.put("lcc", "application/fastman"); - MIME.put("lcl", "application/x-digitalloca"); - MIME.put("lcr", "application/x-digitalloca"); - MIME.put("lgh", "application/lgh"); - MIME.put("lha", "application/lha"); - MIME.put("lha", "application/octet-stream"); - MIME.put("lha", "application/x-lha"); - MIME.put("lhx", "application/octet-stream"); - MIME.put("lic", "application/x-enterlicense"); - MIME.put("licmgr", "application/x-licensemgr"); - MIME.put("list", "text/plain"); - MIME.put("list3820", "application/vnd.ibm.modcap"); - MIME.put("listafp", "application/vnd.ibm.modcap"); - MIME.put("lma", "audio/nspaudio"); - MIME.put("lma", "audio/x-nspaudio"); - MIME.put("lml", "x-lml/x-lml"); - MIME.put("lmlpack", "x-lml/x-lmlpack"); - MIME.put("lmp", "model/vnd.gdl"); - MIME.put("lmp", "model/vnd.gs.gdl"); - MIME.put("log", "text/plain"); - MIME.put("lsf", "video/x-la-asf"); - MIME.put("lsf", "video/x-ms-asf"); - MIME.put("lsp", "application/x-lisp"); - MIME.put("lsp", "text/x-script.lisp"); - MIME.put("lst", "text/plain"); - MIME.put("lsx", "text/x-la-asf"); - MIME.put("lsx", "video/x-la-asf"); - MIME.put("lsx", "video/x-ms-asf"); - MIME.put("ltx", "application/x-latex"); - MIME.put("lvp", "audio/vnd.lucent.voice"); - MIME.put("lwp", "application/vnd.lotus-wordpro"); - MIME.put("lzh", "application/octet-stream"); - MIME.put("lzh", "application/x-lzh"); - MIME.put("lzx", "application/lzx"); - MIME.put("lzx", "application/octet-stream"); - MIME.put("lzx", "application/x-lzx"); - MIME.put("m", "text/plain"); - MIME.put("m", "text/x-m"); - MIME.put("m13", "application/x-msmediaview"); - MIME.put("m14", "application/x-msmediaview"); - MIME.put("m15", "audio/x-mod"); - MIME.put("m1v", "video/mpeg"); - MIME.put("m2a", "audio/mpeg"); - MIME.put("m2v", "video/mpeg"); - MIME.put("m3a", "audio/mpeg"); - MIME.put("m3u", "audio/mpegurl"); - MIME.put("m3u", "audio/x-mpegurl"); - MIME.put("m3u", "audio/x-mpequrl"); - MIME.put("m3u", "audio/x-scpls"); - MIME.put("m3u", "uadio/scpls"); - MIME.put("m3url", "audio/x-mpegurl"); - MIME.put("ma", "application/mathematica"); - MIME.put("ma", "application/mathematica-old"); - MIME.put("ma1", "audio/ma1"); - MIME.put("ma2", "audio/ma2"); - MIME.put("ma3", "audio/ma3"); - MIME.put("ma5", "audio/ma5"); - MIME.put("mag", "application/vnd.ecowin.chart"); - MIME.put("mail", "application/x-mailfolder"); - MIME.put("maker", "application/x-framemaker"); - MIME.put("man", "application/x-troff-man"); - MIME.put("map", "application/x-navimap"); - MIME.put("map", "magnus-internal/imagemap"); - MIME.put("mar", "text/plain"); - MIME.put("mb", "application/mathematica"); - MIME.put("mb", "application/mathematica-old"); - MIME.put("mbd", "application/mbedlet"); - MIME.put("mbm", "image/x-epoc-mbm"); - MIME.put("mc$", "application/x-magic-cap-package-1.0"); - MIME.put("mcd", "application/mcad"); - MIME.put("mcd", "application/vnd.mcd"); - MIME.put("mcd", "application/vnd.vectorworks"); - MIME.put("mcd", "application/x-mathcad"); - MIME.put("mcf", "image/vasa"); - MIME.put("mcf", "text/mcf"); - MIME.put("mcm", "chemical/x-macmolecule"); - MIME.put("mcp", "application/netmc"); - MIME.put("mct", "application/x-mascot"); - MIME.put("mdb", "application/msaccess"); - MIME.put("mdb", "application/x-msaccess"); - MIME.put("mdz", "audio/x-mod"); - MIME.put("me", "application/x-troff-me"); - MIME.put("med", "application/x-att-a2bmusic-pu"); - MIME.put("mel", "text/x-vmel"); - MIME.put("mes", "application/x-att-a2bmusic"); - MIME.put("mesh", "model/mesh"); - MIME.put("mht", "message/rfc822"); - MIME.put("mhtml", "message/rfc822"); - MIME.put("mi", "application/x-mif"); - MIME.put("mid", "application/x-midi"); - MIME.put("mid", "audio/mid"); - MIME.put("mid", "audio/midi"); - MIME.put("mid", "audio/x-mid"); - MIME.put("mid", "audio/x-midi"); - MIME.put("mid", "music/crescendo"); - MIME.put("mid", "x-music/x-midi"); - MIME.put("midi", "application/x-midi"); - MIME.put("midi", "audio/midi"); - MIME.put("midi", "audio/x-mid"); - MIME.put("midi", "audio/x-midi"); - MIME.put("midi", "music/crescendo"); - MIME.put("midi", "x-music/x-midi"); - MIME.put("mif", "application/vnd.mif"); - MIME.put("mif", "application/x-frame"); - MIME.put("mif", "application/x-mif"); - MIME.put("mil", "image/x-cals"); - MIME.put("mime", "message/rfc822"); - MIME.put("mime", "www/mime"); - MIME.put("mio", "audio/x-mio"); - MIME.put("mjf", "audio/x-vnd.AudioExplosion.MjuiceMediaFile"); - MIME.put("mjpg", "video/x-motion-jpeg"); - MIME.put("ml5", "application/ml5"); - MIME.put("mm", "application/base64"); - MIME.put("mm", "application/x-meme"); - MIME.put("mmd", "chemical/x-macromodel"); - MIME.put("mmd", "chemical/x-macromodel-input"); - MIME.put("mme", "application/base64"); - MIME.put("mmf", "application/x-skt-lbs"); - MIME.put("mmod", "chemical/x-macromodel-input"); - MIME.put("mmr", "image/vnd.fujixerox.edmics-mmr"); - MIME.put("mng", "video/x-mng"); - MIME.put("mny", "application/x-msmoney"); - MIME.put("moc", "application/x-mocha"); - MIME.put("mocha", "application/x-mocha"); - MIME.put("mod", "audio/mod"); - MIME.put("mod", "audio/x-mod"); - MIME.put("mof", "application/x-yumekara"); - MIME.put("mol", "chemical/x-mdl-molfile"); - MIME.put("moov", "video/quicktime"); - MIME.put("mop", "chemical/x-mopac-input"); - MIME.put("mov", "video/quicktime"); - MIME.put("movie", "video/x-sgi-movie"); - MIME.put("mp2", "video/mpeg"); - MIME.put("mp2a", "audio/x-mpeg2"); - MIME.put("mp2v", "video/x-mpeg2"); - MIME.put("mp3", "audio/mp3"); - MIME.put("mp3url", "audio/x-mpegurl"); - MIME.put("mp4", "video/mp4"); - MIME.put("mpa", "audio/mpeg"); - MIME.put("mpa", "video/mpeg"); - MIME.put("mpa2", "audio/x-mpeg2"); - MIME.put("mpc", "application/vnd.mpohun.certificate"); - MIME.put("mpc", "application/x-project"); - MIME.put("mpd", "application/vnd.ms-project"); - MIME.put("mpe", "video/mpeg"); - MIME.put("mpeg", "video/mpeg"); - MIME.put("mpf", "text/vnd-mediapackage"); - MIME.put("mpf", "text/vnd.ms-mediapackage"); - MIME.put("mpg", "audio/mpeg"); - MIME.put("mpg", "video/mpeg"); - MIME.put("mpg4", "video/mp4"); - MIME.put("mpga", "audio/mpeg"); - MIME.put("mpn", "application/vnd.mophun.application"); - MIME.put("mpp", "application/vnd.ms-project"); - MIME.put("mps", "application/x-mapserver"); - MIME.put("mps", "video/x-mpeg-system"); - MIME.put("mpt", "application/vnd.ms-project"); - MIME.put("mpt", "application/x-project"); - MIME.put("mpv", "application/x-project"); - MIME.put("mpv", "video/mpeg"); - MIME.put("mpv2", "video/mpeg"); - MIME.put("mpv2", "video/x-mpeg2"); - MIME.put("mpx", "application/x-project"); - MIME.put("mpy", "application/vnd.ibm.MiniPay"); - MIME.put("mrc", "application/marc"); - MIME.put("mrl", "text/x-mrml"); - MIME.put("mrm", "application/x-mrm"); - MIME.put("ms", "application/x-troff-ms"); - MIME.put("msf", "application/vnd.epson.msf"); - MIME.put("msh", "model/mesh"); - MIME.put("msl", "application/vnd.Mobius.MSL"); - MIME.put("msm", "model/vnd.gdl"); - MIME.put("msm", "model/vnd.gs.gdl"); - MIME.put("mss", "audio/mss"); - MIME.put("msv", "application/x-mystars-view"); - MIME.put("mts", "application/metastream"); - MIME.put("mts", "model/vnd.mts"); - MIME.put("mtx", "application/metastream"); - MIME.put("mtz", "application/metastream"); - MIME.put("mus", "application/vnd.musician"); - MIME.put("mv", "video/x-sgi-movie"); - MIME.put("mvb", "application/x-msmediaview"); - MIME.put("mwc", "application/vnd.dpgraph"); - MIME.put("mxs", "application/vnd.triscape.mxs"); - MIME.put("my", "audio/make"); - MIME.put("mzv", "application/metastream"); - MIME.put("mzz", "application/x-vnd.AudioExplosion.mzz"); - MIME.put("nap", "image/naplps"); - MIME.put("naplps", "image/naplps"); - MIME.put("nar", "application/zip"); - MIME.put("nb", "application/mathematica"); - MIME.put("nbmp", "image/nbmp"); - MIME.put("nc", "application/x-netcdf"); - MIME.put("nclk", "text/html"); - MIME.put("ncm", "application/vnd.nokia.configuration-message"); - MIME.put("ndb", "x-lml/x-ndb"); - MIME.put("ndl", "application/vnd.lotus-notes"); - MIME.put("ndwn", "application/ndwn"); - MIME.put("nif", "application/x-nif"); - MIME.put("nif", "image/x-niff"); - MIME.put("niff", "image/x-niff"); - MIME.put("nix", "application/x-mix-transfer"); - MIME.put("nls", "text/nls"); - MIME.put("nml", "application/vnd.enliven"); - MIME.put("nmz", "application/x-scream"); - MIME.put("nnd", "application/vnd.noblenet-directory"); - MIME.put("nns", "application/vnd.noblenet-sealer"); - MIME.put("nnw", "application/vnd.noblenet-web"); - MIME.put("nokia-op-logo", "image/vnd.nok-oplogo-color"); - MIME.put("npx", "application/x-netfpx"); - MIME.put("ns2", "application/vnd.lotus-notes"); - MIME.put("ns3", "application/vnd.lotus-notes"); - MIME.put("ns4", "application/vnd.lotus-notes"); - MIME.put("nsc", "application/x-conference"); - MIME.put("nsf", "application/vnd.lotus-notes"); - MIME.put("nsg", "application/vnd.lotus-notes"); - MIME.put("nsh", "application/vnd.lotus-notes"); - MIME.put("nsnd", "audio/nsnd"); - MIME.put("ntf", "application/vnd.lotus-notes"); - MIME.put("nva", "application/x-neva1"); - MIME.put("nvd", "application/x-navidoc"); - MIME.put("nvm", "application/x-navimap"); - MIME.put("nws", "message/rfc822"); - MIME.put("o", "application/octet-stream"); - MIME.put("oa2", "application/vnd.fujitsu.oasys2"); - MIME.put("oa3", "application/vnd.fujitsu.oasys3"); - MIME.put("oas", "application/vnd.fujitsu.oasys"); - MIME.put("obd", "application/x-msbinder"); - MIME.put("oda", "application/oda"); - MIME.put("omc", "application/x-omc"); - MIME.put("omcd", "application/x-omcdatamaker"); - MIME.put("omcr", "application/x-omcregerator"); - MIME.put("oom", "application/x-AtlasMate-Plugin"); - MIME.put("or2", "application/vnd.lotus-organizer"); - MIME.put("or3", "application/vnd.lotus-organizer"); - MIME.put("org", "application/vnd.lotus-organizer"); - MIME.put("orq", "application/ocsp-request"); - MIME.put("ors", "application/ocsp-response"); - MIME.put("ota", "image/x-ota-bitmap"); - MIME.put("p", "text/x-pascal"); - MIME.put("p10", "application/pkcs10"); - MIME.put("p10", "application/x-pkcs10"); - MIME.put("p12", "application/pkcs-12"); - MIME.put("p12", "application/x-pkcs12"); - MIME.put("p7a", "application/x-pkcs7-signature"); - MIME.put("p7b", "application/x-pkcs7-certificates"); - MIME.put("p7c", "application/pkcs7-mime"); - MIME.put("p7c", "application/x-pkcs7-mime"); - MIME.put("p7m", "application/pkcs7-mime"); - MIME.put("p7m", "application/x-pkcs7-mime"); - MIME.put("p7r", "application/x-pkcs7-certreqresp"); - MIME.put("p7s", "application/pkcs7-signature"); - MIME.put("pac", "application/x-ns-proxy-autoconfig"); - MIME.put("pac", "audio/x-pac"); - MIME.put("pae", "audio/x-epac"); - MIME.put("pan", "application/x-pan"); - MIME.put("part", "application/pro_eng"); - MIME.put("pas", "text/pascal"); - MIME.put("pat", "audio/x-pat"); - MIME.put("pbd", "application/vnd.powerbuilder6"); - MIME.put("pbd", "application/vnd.powerbuilder6-s"); - MIME.put("pbd", "application/vnd.powerbuilder7"); - MIME.put("pbd", "application/vnd.powerbuilder7-s"); - MIME.put("pbd", "application/vnd.powerbuilder75"); - MIME.put("pbd", "application/vnd.powerbuilder75-s"); - MIME.put("pbm", "image/x-portable-bitmap"); - MIME.put("pcd", "image/x-photo-cd"); - MIME.put("pcl", "application/vnd.hp-PCL"); - MIME.put("pcl", "application/x-pcl"); - MIME.put("pct", "image/x-pict"); - MIME.put("pcx", "image/x-pcx"); - MIME.put("pda", "image/x-pda"); - MIME.put("pdb", "chemical/x-pdb"); - MIME.put("pdf", "application/pdf"); - MIME.put("pfr", "application/font-tdpfr"); - MIME.put("pfunk", "audio/make"); - MIME.put("pfunk", "audio/make.my.funk"); - MIME.put("pfx", "application/x-pkcs12"); - MIME.put("pgm", "image/x-portable-graymap"); - MIME.put("pgm", "image/x-portable-greymap"); - MIME.put("pgn", "application/x-chess-pgn"); - MIME.put("pgp", "application/pgp-encrypted"); - MIME.put("pic", "image/pict"); - MIME.put("pict", "image/pict"); - MIME.put("pict", "image/x-pict"); - MIME.put("pkg", "application/x-newton-compatible-pkg"); - MIME.put("pki", "application/pkixcmp"); - MIME.put("pko", "application/vnd.ms-pki.pko"); - MIME.put("pl", "text/plain"); - MIME.put("pl", "text/x-script.perl"); - MIME.put("plc", "application/vnd.Mobius.PLC"); - MIME.put("plg", "text/html"); - MIME.put("plj", "audio/vnd.everad.plj"); - MIME.put("pls", "audio/scpls"); - MIME.put("plx", "application/x-PiXCLscript"); - MIME.put("pm", "application/x-perl"); - MIME.put("pm", "image/x-xpixmap"); - MIME.put("pm", "text/x-script.perl-module"); - MIME.put("pm4", "application/x-pagemaker"); - MIME.put("pm5", "application/x-pagemaker"); - MIME.put("pma", "application/x-perfmon"); - MIME.put("pmc", "application/x-perfmon"); - MIME.put("pmd", "application/x-pmd"); - MIME.put("pml", "application/vnd.ctc-posml"); - MIME.put("pml", "application/x-perfmon"); - MIME.put("pmr", "application/x-perfmon"); - MIME.put("pmw", "application/x-perfmon"); - MIME.put("png", "image/png"); - MIME.put("pnm", "application/x-portable-anymap"); - MIME.put("pnm", "image/x-portable-anymap"); - MIME.put("pnz", "image/png"); - MIME.put("pot", "application/mspowerpoint"); - MIME.put("pot", "application/vnd.ms-powerpoint"); - MIME.put("pov", "model/x-pov"); - MIME.put("ppa", "application/vnd.ms-powerpoint"); - MIME.put("ppm", "image/x-portable-pixmap"); - MIME.put("pps", "application/mspowerpoint"); - MIME.put("pps", "application/vnd.ms-powerpoint"); - MIME.put("ppt", "application/mspowerpoint"); - MIME.put("ppt", "application/powerpoint"); - MIME.put("ppt", "application/vnd.ms-powerpoint"); - MIME.put("ppt", "application/x-mspowerpoint"); - MIME.put("ppz", "application/mspowerpoint"); - MIME.put("ppz", "application/ppt"); - MIME.put("pqf", "application/x-cprplayer"); - MIME.put("pqi", "application/cprplayer"); - MIME.put("prc", "application/x-prc"); - MIME.put("pre", "application/vnd.lotus-freelance"); - MIME.put("pre", "application/x-freelance"); - MIME.put("prf", "application/pics-rules"); - MIME.put("proxy", "application/x-ns-proxy-autoconfig"); - MIME.put("prt", "application/pro_eng"); - MIME.put("prz", "application/vnd.lotus-freelance"); - MIME.put("ps", "application/postscript"); - MIME.put("psd", "application/octet-stream"); - MIME.put("pseg3820", "application/vnd.ibm.modcap"); - MIME.put("psid", "audio/prs.sid"); - MIME.put("pti", "image/prs.pti"); - MIME.put("ptlk", "application/listenup"); - MIME.put("pub", "application/x-mspublisher"); - MIME.put("puz", "application/x-crossword"); - MIME.put("pvu", "paleovu/x-pv"); - MIME.put("pvx", "video/x-pv-pvx"); - MIME.put("pwn", "application/vnd.3M.Post-it-Notes"); - MIME.put("pwz", "application/vnd.ms-powerpoint"); - MIME.put("py", "text/x-script.phyton"); - MIME.put("pyc", "applicaiton/x-bytecode.python"); - MIME.put("qam", "application/vnd.epson.quickanime"); - MIME.put("qbo", "application/vnd.intu.qbo"); - MIME.put("qca", "application/vnd.ericsson.quickcall"); - MIME.put("qcall", "application/vnd.ericsson.quickcall"); - MIME.put("qcp", "audio/vnd.qcelp"); - MIME.put("qd3", "x-world/x-3dmf"); - MIME.put("qd3d", "x-world/x-3dmf"); - MIME.put("qfx", "application/vnd.intu.qfx"); - MIME.put("qif", "image/x-quicktime"); - MIME.put("qps", "application/vnd.publishare-delta-tree"); - MIME.put("qry", "text/html"); - MIME.put("qt", "video/quicktime"); - MIME.put("qtc", "video/x-qtc"); - MIME.put("qti", "image/x-quicktime"); - MIME.put("qtif", "image/x-quicktime"); - MIME.put("qtvr", "video/quicktime"); - MIME.put("r3t", "text/vnd.rn-realtext3d"); - MIME.put("ra", "application/x-pn-realaudio"); - MIME.put("ra", "audio/vnd.rn-realaudio"); - MIME.put("ra", "audio/x-pn-realaudio"); - MIME.put("ra", "audio/x-pn-realaudio-plugin"); - MIME.put("ra", "audio/x-realaudio"); - MIME.put("ram", "application/x-pn-realaudio"); - MIME.put("ram", "audio/x-pn-realaudio"); - MIME.put("ram", "audio/x-pn-realaudio-plugin"); - MIME.put("rar", "application/rar"); - MIME.put("rar", "application/x-rar-compressed"); - MIME.put("ras", "application/x-cmu-raster"); - MIME.put("ras", "image/cmu-raster"); - MIME.put("ras", "image/x-cmu-raster"); - MIME.put("rast", "image/cmu-raster"); - MIME.put("rb", "application/x-rocketbook"); - MIME.put("rct", "application/prs.nprend"); - MIME.put("rdf", "application/rdf+xml"); - MIME.put("rep", "application/vnd.businessobjects"); - MIME.put("rexx", "text/x-script.rexx"); - MIME.put("rf", "image/vnd.rn-realflash"); - MIME.put("rgb", "image/x-rgb"); - MIME.put("rjs", "application/vnd.rn-realsystem-rjs"); - MIME.put("rlc", "image/vnd.fujixerox.edmics-rlc"); - MIME.put("rlf", "application/x-richlink"); - MIME.put("rm", "application/vnd.rn-realmedia"); - MIME.put("rm", "application/x-pn-realaudio"); - MIME.put("rm", "audio/x-pn-realaudio"); - MIME.put("rm", "audio/x-pn-realaudio-plugin"); - MIME.put("rmf", "audio/x-rmf"); - MIME.put("rmi", "audio/mid"); - MIME.put("rmm", "audio/x-pn-realaudio"); - MIME.put("rmp", "application/vnd.rn-rn_music_package"); - MIME.put("rmp", "audio/x-pn-realaudio"); - MIME.put("rmp", "audio/x-pn-realaudio-plugin"); - MIME.put("rmvb", "audio/x-pn-realaudio"); - MIME.put("rmx", "application/vnd.rn-realsystem-rmx"); - MIME.put("rnd", "application/prs.nprend"); - MIME.put("rng", "application/ringing-tones"); - MIME.put("rng", "application/vnd.nokia.ringing-tone"); - MIME.put("rnx", "application/vnd.rn-realplayer"); - MIME.put("roff", "application/x-troff"); - MIME.put("rp", "image/vnd.rn-realpix"); - MIME.put("rpm", "application/x-pn-realaudio"); - MIME.put("rpm", "audio/x-pn-RealAudio-plugin"); - MIME.put("rpm", "audio/x-pn-realaudio-plugin"); - MIME.put("rsm", "model/vnd.gdl"); - MIME.put("rsm", "model/vnd.gs.gdl"); - MIME.put("rsml", "application/vnd.rn-rsml"); - MIME.put("rt", "text/richtext"); - MIME.put("rt", "text/vnd.rn-realtext"); - MIME.put("rte", "x-lml/x-gps"); - MIME.put("rtf", "application/msword"); - MIME.put("rtf", "application/rtf"); - MIME.put("rtf", "application/x-rtf"); - MIME.put("rtf", "text/richtext"); - MIME.put("rtf", "text/rtf"); - MIME.put("rtg", "application/metastream"); - MIME.put("rts", "application/x-rtsl"); - MIME.put("rtx", "application/rtf"); - MIME.put("rtx", "text/richtext"); - MIME.put("rv", "video/vnd.rn-realvideo"); - MIME.put("rwc", "application/x-rogerwilco"); - MIME.put("rxn", "chemical/x-mdl-rxn"); - MIME.put("rxn", "chemical/x-mdl-rxnfile"); - MIME.put("s", "text/x-asm"); - MIME.put("s3m", "audio/s3m"); - MIME.put("s3m", "audio/x-mod"); - MIME.put("s3z", "audio/x-mod"); - MIME.put("sam", "application/vnd.lotus-wordpro"); - MIME.put("saveme", "application/octet-stream"); - MIME.put("sbk", "application/x-tbook"); - MIME.put("sbk", "audio/x-sbk"); - MIME.put("sc", "application/x-showcase"); - MIME.put("sca", "application/x-supercard"); - MIME.put("scd", "application/x-msschedule"); - MIME.put("scm", "application/vnd.lotus-screencam"); - MIME.put("scm", "application/x-lotusscreencam"); - MIME.put("scm", "text/x-script.guile"); - MIME.put("scm", "text/x-script.scheme"); - MIME.put("scm", "video/x-scm"); - MIME.put("scp", "text/plain"); - MIME.put("sct", "text/scriptlet"); - MIME.put("sdf", "application/e-score"); - MIME.put("sdf", "chemical/x-mdl-sdf"); - MIME.put("sdml", "text/plain"); - MIME.put("sdp", "application/sdp"); - MIME.put("sdp", "application/x-sdp"); - MIME.put("sdr", "application/sounder"); - MIME.put("sds", "application/x-onlive"); - MIME.put("sea", "application/sea"); - MIME.put("sea", "application/x-sea"); - MIME.put("sea", "application/x-stuffit"); - MIME.put("see", "application/vnd.seemail"); - MIME.put("ser", "application/x-java-serialized-object"); - MIME.put("set", "application/set"); - MIME.put("setpay", "application/set-payment-initiation"); - MIME.put("setreg", "application/set-registration-initiation"); - MIME.put("sgi-lpr", "application/x-sgi-lpr"); - MIME.put("sgm", "text/sgml"); - MIME.put("sgm", "text/x-sgml"); - MIME.put("sgml", "text/sgml"); - MIME.put("sgml", "text/x-sgml"); - MIME.put("sh", "application/x-bsh"); - MIME.put("sh", "application/x-sh"); - MIME.put("sh", "application/x-shar"); - MIME.put("sh", "text/x-script.sh"); - MIME.put("sha", "application/x-shar"); - MIME.put("shar", "application/x-bsh"); - MIME.put("shar", "application/x-shar"); - MIME.put("sho", "application/x-showcase"); - MIME.put("show", "application/x-showcase"); - MIME.put("showcase", "application/x-showcase"); - MIME.put("shtml", "magnus-internal/parsed-html"); - MIME.put("shtml", "text/html"); - MIME.put("shtml", "text/x-server-parsed-html"); - MIME.put("shw", "application/presentations"); - MIME.put("si", "text/vnd.wap.si"); - MIME.put("si6", "image/si6"); - MIME.put("si7", "image/vnd.stiwap.sis"); - MIME.put("si9", "image/vnd.lgtwap.sis"); - MIME.put("sic", "application/vnd.wap.sic"); - MIME.put("sid", "audio/prs.sid"); - MIME.put("sid", "audio/x-psid"); - MIME.put("silo", "model/mesh"); - MIME.put("sis", "application/vnd.symbian.install"); - MIME.put("sit", "application/x-sit"); - MIME.put("sit", "application/x-stuffit"); - MIME.put("skc", "chemical/x-mdl-isis"); - MIME.put("skd", "application/x-koan"); - MIME.put("skm", "application/x-koan"); - MIME.put("skp", "application/x-koan"); - MIME.put("skt", "application/x-koan"); - MIME.put("sl", "application/x-seelogo"); - MIME.put("sl", "text/vnd.wap.sl"); - MIME.put("slc", "application/vnd.wap.slc"); - MIME.put("slc", "application/x-salsa"); - MIME.put("slides", "application/x-showcase"); - MIME.put("slt", "application/vnd.epson.salt"); - MIME.put("smd", "audio/x-smd"); - MIME.put("smd", "chemical/x-smd"); - MIME.put("smi", "application/smil"); - MIME.put("smi", "chemical/x-daylight-smiles"); - MIME.put("smi", "chemical/x-x-daylight-smiles"); - MIME.put("smil", "application/smil"); - MIME.put("smp", "application/studiom"); - MIME.put("smz", "audio/x-smd"); - MIME.put("snd", "audio/basic"); - MIME.put("snd", "audio/x-adpcm"); - MIME.put("sol", "application/solids"); - MIME.put("spc", "application/x-pkcs7-certificates"); - MIME.put("spc", "chemical/x-galactic-spc"); - MIME.put("spc", "text/x-speech"); - MIME.put("spl", "application/futuresplash"); - MIME.put("spl", "application/x-futuresplash"); - MIME.put("spo", "text/vnd.in3d.spot"); - MIME.put("spot", "text/vnd.in3d.spot"); - MIME.put("spr", "application/x-sprite"); - MIME.put("sprite", "application/x-sprite"); - MIME.put("spt", "application/x-spt"); - MIME.put("src", "application/x-wais-source"); - MIME.put("ssf", "application/vnd.epson.ssf"); - MIME.put("ssi", "text/x-server-parsed-html"); - MIME.put("ssm", "application/streamingmedia"); - MIME.put("sst", "application/vnd.ms-pki.certstore"); - MIME.put("step", "application/step"); - MIME.put("stf", "application/vnd.wt.stf"); - MIME.put("stk", "application/hyperstudio"); - MIME.put("stl", "application/sla"); - MIME.put("stl", "application/vnd.ms-pki.stl"); - MIME.put("stl", "application/x-navistyle"); - MIME.put("stm", "audio/x-mod"); - MIME.put("stm", "text/html"); - MIME.put("stp", "application/step"); - MIME.put("str", "application/vnd.pg.format"); - MIME.put("str", "audio/x-str"); - MIME.put("sv4cpio", "application/x-sv4cpio"); - MIME.put("sv4crc", "application/x-sv4crc"); - MIME.put("svf", "image/vnd"); - MIME.put("svf", "image/vnd.svf"); - MIME.put("svf", "image/x-dwg"); - MIME.put("svg", "image/svg-xml"); - MIME.put("svh", "image/svh"); - MIME.put("svr", "application/x-world"); - MIME.put("svr", "x-world/x-svr"); - MIME.put("swf", "application/x-shockwave-flash"); - MIME.put("swfl", "application/x-shockwave-flash"); - MIME.put("sys", "video/x-mpeg-system"); - MIME.put("t", "application/x-troff"); - MIME.put("tad", "application/octet-stream"); - MIME.put("tag", "text/prs.lines.tag"); - MIME.put("talk", "plugin/talker"); - MIME.put("talk", "text/x-speech"); - MIME.put("talk", "x-plugin/x-talker"); - MIME.put("tar", "application/x-tar"); - MIME.put("tardist", "application/x-tardist"); - MIME.put("taz", "application/x-tar"); - MIME.put("tbk", "application/toolbook"); - MIME.put("tbk", "application/x-tbook"); - MIME.put("tbp", "application/x-timbuktu"); - MIME.put("tbt", "application/timbuktu"); - MIME.put("tbt", "application/x-timbuktu"); - MIME.put("tcl", "text/x-script.tcl"); - MIME.put("tcsh", "text/x-script.tcsh"); - MIME.put("tex", "application/x-tex"); - MIME.put("texi", "application/x-tex"); - MIME.put("texi", "application/x-texinfo"); - MIME.put("texinfo", "application/x-texinfo"); - MIME.put("text", "application/plain"); - MIME.put("text", "application/text"); - MIME.put("text", "text/plain"); - MIME.put("tgf", "chemical/x-mdl-tgf"); - MIME.put("tgz", "application/gnutar"); - MIME.put("tgz", "application/x-compressed"); - MIME.put("tgz", "application/x-tar"); - MIME.put("thm", "application/vnd.eri.thm"); - MIME.put("tif", "image/tiff"); - MIME.put("tif", "image/x-tiff"); - MIME.put("tiff", "image/tiff"); - MIME.put("tiff", "image/x-tiff"); - MIME.put("tki", "application/x-tkined"); - MIME.put("tkined", "application/x-tkined"); - MIME.put("toc", "application/toc"); - MIME.put("toy", "image/toy"); - MIME.put("tpl", "application/vnd.groove-tool-template"); - MIME.put("tr", "application/x-troff"); - MIME.put("tra", "application/vnd.trueapp"); - MIME.put("trk", "x-lml/x-gps"); - MIME.put("trm", "application/x-msterminal"); - MIME.put("tsi", "audio/tsp-audio"); - MIME.put("tsi", "audio/tsplayer"); - MIME.put("tsp", "application/dsptype"); - MIME.put("tsp", "audio/tsplayer"); - MIME.put("tsv", "text/tab-separated-values"); - MIME.put("ttf", "application/octet-stream"); - MIME.put("ttz", "application/t-time"); - MIME.put("turbot", "image/florian"); - MIME.put("tvm", "application/x-tvml"); - MIME.put("tvml", "application/x-tvml"); - MIME.put("txf", "application/vnd.Mobius.TXF"); - MIME.put("txt", "text/plain"); - MIME.put("ufdl", "application/vnd.ufdl"); - MIME.put("uil", "text/x-uil"); - MIME.put("ult", "audio/x-mod"); - MIME.put("uni", "text/uri-list"); - MIME.put("unis", "text/uri-list"); - MIME.put("unv", "application/i-deas"); - MIME.put("uri", "text/uri-list"); - MIME.put("uris", "text/uri-list"); - MIME.put("urls", "application/x-url-list"); - MIME.put("ustar", "application/x-ustar"); - MIME.put("ustar", "multipart/x-ustar"); - MIME.put("uu", "application/octet-stream"); - MIME.put("uu", "application/uue"); - MIME.put("uu", "application/x-uuencode"); - MIME.put("uu", "text/x-uuencode"); - MIME.put("uue", "application/uue"); - MIME.put("uue", "application/x-uuencode"); - MIME.put("uue", "text/x-uuencode"); - MIME.put("v5d", "application/vis5d"); - MIME.put("vbk", "audio/vnd.nortel.vbk"); - MIME.put("vbox", "application/vnd.previewsystems.box"); - MIME.put("vbs", "text/vbscript"); - MIME.put("vcd", "application/x-cdlink"); - MIME.put("vcf", "text/x-vcard"); - MIME.put("vcg", "application/vnd.groove-vcard"); - MIME.put("vcs", "text/x-vCalendar"); - MIME.put("vcx", "application/vnd.vcx"); - MIME.put("vda", "application/vda"); - MIME.put("vdo", "video/vdo"); - MIME.put("vew", "application/groupwise"); - MIME.put("vew", "application/vnd.lotus-approach"); - MIME.put("vib", "audio/vib"); - MIME.put("vis", "application/vnd.informix-visionary"); - MIME.put("viv", "video/vivo"); - MIME.put("viv", "video/vnd.vivo"); - MIME.put("vivo", "video/vivo"); - MIME.put("vivo", "video/vnd.vivo"); - MIME.put("vmd", "application/vocaltec-media-desc"); - MIME.put("vmd", "chemical/x-vmd"); - MIME.put("vmf", "application/vocaltec-media-file"); - MIME.put("vmi", "application/x-dreamcast-vms-info"); - MIME.put("vmi", "application/x-dremacast-vms-info"); - MIME.put("vms", "application/x-dreamcast-vms"); - MIME.put("vms", "application/x-dremacast-vms"); - MIME.put("voc", "audio/voc"); - MIME.put("voc", "audio/x-voc"); - MIME.put("vos", "video/vosaic"); - MIME.put("vox", "audio/voxware"); - MIME.put("vqe", "audio/x-twinvq-plugin"); - MIME.put("vqf", "audio/x-twinvq"); - MIME.put("vql", "audio/x-twinvq"); - MIME.put("vql", "audio/x-twinvq-plugin"); - MIME.put("vre", "x-world/x-vream"); - MIME.put("vrj", "x-world/x-vrt"); - MIME.put("vrml", "application/x-vrml"); - MIME.put("vrml", "model/vrml"); - MIME.put("vrml", "x-world/x-vrml"); - MIME.put("vrt", "x-world/x-vrt"); - MIME.put("vrw", "x-world/x-vream"); - MIME.put("vsd", "application/vnd.visio"); - MIME.put("vsd", "application/x-visio"); - MIME.put("vsl", "application/x-cnet-vsl"); - MIME.put("vss", "application/vnd.visio"); - MIME.put("vst", "application/vnd.visio"); - MIME.put("vst", "application/x-visio"); - MIME.put("vsw", "application/vnd.visio"); - MIME.put("vsw", "application/x-visio"); - MIME.put("vts", "workbook/formulaone"); - MIME.put("vtu", "model/vnd.vtu"); - MIME.put("w60", "application/wordperfect6.0"); - MIME.put("w61", "application/wordperfect6.1"); - MIME.put("w6w", "application/msword"); - MIME.put("wav", "application/x-wav"); - MIME.put("wav", "audio/wav"); - MIME.put("wav", "audio/x-wav"); - MIME.put("wax", "audio/x-ms-wax"); - MIME.put("wb1", "application/x-qpro"); - MIME.put("wbc", "application/x-webshots"); - MIME.put("wbmp", "image/vnd.wap.wbmp"); - MIME.put("wbxml", "application/vnd.wap.sic"); - MIME.put("wbxml", "application/vnd.wap.slc"); - MIME.put("wbxml", "application/vnd.wap.wbxml"); - MIME.put("wbxml", "application/vnd.wap.wmlc"); - MIME.put("wcm", "application/vnd.ms-works"); - MIME.put("wdb", "application/vnd.ms-works"); - MIME.put("web", "application/vnd.xara"); - MIME.put("wi", "image/wavelet"); - MIME.put("win", "model/vnd.gdl"); - MIME.put("win", "model/vnd.gs.gdl"); - MIME.put("wis", "application/x-InstallShield"); - MIME.put("wiz", "application/msword"); - MIME.put("wk1", "application/vnd.lotus-1-2-3"); - MIME.put("wk1", "application/x-123"); - MIME.put("wk3", "application/vnd.lotus-1-2-3"); - MIME.put("wk4", "application/vnd.lotus-1-2-3"); - MIME.put("wks", "application/vnd.ms-works"); - MIME.put("wkz", "application/x-wingz"); - MIME.put("wm", "video/x-ms-asf"); - MIME.put("wm", "video/x-ms-wm"); - MIME.put("wma", "audio/x-ms-wma"); - MIME.put("wmd", "application/x-ms-wmd"); - MIME.put("wmf", "application/x-msmetafile"); - MIME.put("wmf", "image/x-wmf"); - MIME.put("wmf", "windows/metafile"); - MIME.put("wml", "text/vnd.wap.wml"); - MIME.put("wmlc", "application/vnd.wap.wmlc"); - MIME.put("wmls", "text/vnd.wap.wmlscript"); - MIME.put("wmlsc", "application/vnd.wap.wmlscriptc"); - MIME.put("wmlscript", "text/vnd.wap.wmlscript"); - MIME.put("wmv", "video/x-ms-wmv"); - MIME.put("wmx", "video/x-ms-wmx"); - MIME.put("wmz", "application/x-ms-wmz"); - MIME.put("word", "application/msword"); - MIME.put("wp", "application/wordperfect"); - MIME.put("wp5", "application/wordperfect"); - MIME.put("wp5", "application/wordperfect6.0"); - MIME.put("wp6", "application/wordperfect"); - MIME.put("wpd", "application/wordperfect"); - MIME.put("wpd", "application/x-wpwin"); - MIME.put("wpng", "image/x-up-wpng"); - MIME.put("wps", "application/vnd.ms-works"); - MIME.put("wpt", "x-lml/x-gps"); - MIME.put("wq1", "application/x-lotus"); - MIME.put("wri", "application/mswrite"); - MIME.put("wri", "application/x-mswrite"); - MIME.put("wri", "application/x-wri"); - MIME.put("wrl", "application/x-world"); - MIME.put("wrl", "i-world/i-vrml"); - MIME.put("wrl", "model/vrml"); - MIME.put("wrl", "x-world/x-vrml"); - MIME.put("wrz", "model/vrml"); - MIME.put("wrz", "x-world/x-vrml"); - MIME.put("ws", "text/vnd.wap.wmlscript"); - MIME.put("wsc", "application/vnd.wap.wmlscriptc"); - MIME.put("wsc", "text/scriptlet"); - MIME.put("wsc", "text/sgml"); - MIME.put("wsrc", "application/x-wais-source"); - MIME.put("wtb", "application/vnd.webturbo"); - MIME.put("wtk", "application/x-wintalk"); - MIME.put("wtx", "text/plain"); - MIME.put("wv", "video/wavelet"); - MIME.put("wvx", "video/x-ms-wvx"); - MIME.put("wxl", "application/x-wxl"); - MIME.put("x-gzip", "application/x-gzip"); - MIME.put("x-png", "image/png"); - MIME.put("x3d", "application/vnd.hzn-3d-crossword"); - MIME.put("xaf", "x-word/x-vrml"); - MIME.put("xar", "application/vnd.xara"); - MIME.put("xbd", "application/vnd.fujixerox.docuworks.binder"); - MIME.put("xbm", "image/x-xbitmap"); - MIME.put("xbm", "image/x-xbm"); - MIME.put("xbm", "image/xbm"); - MIME.put("xdm", "application/x-xdma"); - MIME.put("xdma", "application/x-xdma"); - MIME.put("xdr", "video/x-amt-demorun"); - MIME.put("xdw", "application/vnd.fujixerox.docuworks"); - MIME.put("xfdl", "application/vnd.xfdl"); - MIME.put("xgz", "xgl/drawing"); - MIME.put("xht", "application/xhtml+xml"); - MIME.put("xhtm", "application/xhtml+xml"); - MIME.put("xhtml", "application/xhtml+xml"); - MIME.put("xif", "image/vnd.xiff"); - MIME.put("xl", "application/excel"); - MIME.put("xla", "application/excel"); - MIME.put("xla", "application/vnd.ms-excel"); - MIME.put("xla", "application/x-excel"); - MIME.put("xla", "application/x-msexcel"); - MIME.put("xlb", "application/excel"); - MIME.put("xlb", "application/vnd.ms-excel"); - MIME.put("xlb", "application/x-excel"); - MIME.put("xlc", "application/excel"); - MIME.put("xlc", "application/vnd.ms-excel"); - MIME.put("xlc", "application/x-excel"); - MIME.put("xld", "application/excel"); - MIME.put("xld", "application/x-excel"); - MIME.put("xlk", "application/excel"); - MIME.put("xlk", "application/x-excel"); - MIME.put("xll", "application/excel"); - MIME.put("xll", "application/vnd.ms-excel"); - MIME.put("xll", "application/x-excel"); - MIME.put("xlm", "application/excel"); - MIME.put("xls", "application/x-msexcel"); - MIME.put("xlt", "application/excel"); - MIME.put("xlt", "application/vnd.ms-excel"); - MIME.put("xlt", "application/x-excel"); - MIME.put("xlv", "application/excel"); - MIME.put("xlv", "application/x-excel"); - MIME.put("xlw", "application/excel"); - MIME.put("xlw", "application/vnd.ms-excel"); - MIME.put("xlw", "application/x-excel"); - MIME.put("xlw", "application/x-msexcel"); - MIME.put("xm", "audio/x-mod"); - MIME.put("xm", "audio/xm"); - MIME.put("xml", "text/xml"); - MIME.put("xmz", "audio/x-mod"); - MIME.put("xmz", "xgl/movie"); - MIME.put("xof", "x-world/x-vrml"); - MIME.put("xpi", "application/x-xpinstall"); - MIME.put("xpix", "application/x-vnd.ls-xpix"); - MIME.put("xpm", "image/x-xpixmap"); - MIME.put("xpm", "image/x-xpm"); - MIME.put("xpm", "image/xpm"); - MIME.put("xpr", "application/vnd.is-xpr"); - MIME.put("xpw", "application/vnd.intercon.formnet"); - MIME.put("xpx", "application/vnd.intercon.formnet"); - MIME.put("xsit", "text/xml"); - MIME.put("xsl", "text/xml"); - MIME.put("xsl", "text/xsl"); - MIME.put("xsr", "video/x-amt-showrun"); - MIME.put("xul", "text/xul"); - MIME.put("xwd", "image/x-xwd"); - MIME.put("xwd", "image/x-xwindowdump"); - MIME.put("xyz", "chemical/x-pdb"); - MIME.put("xyz", "chemical/x-xyz"); - MIME.put("yz1", "application/x-yz1"); - MIME.put("z", "application/x-compress"); - MIME.put("z", "application/x-compressed"); - MIME.put("zac", "application/x-zaurus-zac"); - MIME.put("zip", "application/x-compressed"); - MIME.put("zip", "application/x-zip-compressed"); - MIME.put("zip", "application/zip"); - MIME.put("zip", "multipart/x-zip"); - MIME.put("zoo", "application/octet-stream"); - MIME.put("zsh", "text/x-script.zsh"); - MIME.put("ztardist", "application/x-ztardist"); - } -} diff --git a/firefly/src/main/java/com/firefly/server/http/FileAccessFilter.java b/firefly/src/main/java/com/firefly/server/http/FileAccessFilter.java deleted file mode 100644 index 927dfb2b0..000000000 --- a/firefly/src/main/java/com/firefly/server/http/FileAccessFilter.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firefly.server.http; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public interface FileAccessFilter { - /** - * 静态文件访问过滤器 - * @param request - * @param response - * @param path 程序输入路径 - * @return 需要输出的静态文件名,返回null则跳过后续处理,此时需要在函数内做出http响应。 - */ - String doFilter(HttpServletRequest request, HttpServletResponse response, String path); -} diff --git a/firefly/src/main/java/com/firefly/server/http/HttpConnectionListener.java b/firefly/src/main/java/com/firefly/server/http/HttpConnectionListener.java deleted file mode 100644 index 89d6ec8b1..000000000 --- a/firefly/src/main/java/com/firefly/server/http/HttpConnectionListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.firefly.server.http; - -import java.util.EventListener; - -import com.firefly.net.Session; - -public interface HttpConnectionListener extends EventListener { - void connectionCreated(Session session); - - void connectionClosed(Session session); -} diff --git a/firefly/src/main/java/com/firefly/server/http/HttpDecoder.java b/firefly/src/main/java/com/firefly/server/http/HttpDecoder.java deleted file mode 100644 index 349d5da34..000000000 --- a/firefly/src/main/java/com/firefly/server/http/HttpDecoder.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.firefly.server.http; - -import java.io.IOException; -import java.io.PipedOutputStream; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import com.firefly.net.Decoder; -import com.firefly.net.Session; -import com.firefly.utils.StringUtils; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class HttpDecoder implements Decoder { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private Config config; - private AbstractHttpDecoder[] httpDecode = new AbstractHttpDecoder[] { - new RequestLineDecoder(), new HeadDecoder(), new BodyDecoder() }; - public static final String HTTP_REQUEST = "http_req"; - public static final String REMAIN_DATA = "remain_data"; - private static final byte LINE_LIMITOR = '\n'; - - public HttpDecoder(Config config) { - this.config = config; - } - - @Override - public void decode(ByteBuffer buf, Session session) throws Throwable { - ByteBuffer now = getBuffer(buf, session); - HttpServletRequestImpl req = getHttpServletRequestImpl(session); - httpDecode[req.status].decode0(now, session, req); - } - - private ByteBuffer getBuffer(ByteBuffer buf, Session session) { - ByteBuffer now = buf; - ByteBuffer prev = (ByteBuffer) session.getAttribute(REMAIN_DATA); - - if (prev != null) { - session.removeAttribute(REMAIN_DATA); - now = (ByteBuffer) ByteBuffer - .allocate(prev.remaining() + buf.remaining()).put(prev) - .put(buf).flip(); - } - return now; - } - - private HttpServletRequestImpl getHttpServletRequestImpl(Session session) { - HttpServletRequestImpl req = (HttpServletRequestImpl) session - .getAttribute(HTTP_REQUEST); - if (req == null) { - req = new HttpServletRequestImpl(session, config); - session.setAttribute(HTTP_REQUEST, req); - } - return req; - } - - abstract private class AbstractHttpDecoder { - private void decode0(ByteBuffer now, Session session, - HttpServletRequestImpl req) throws Throwable { - boolean next = decode(now, session, req); - if (next) - next(now.slice(), session, req); - else - save(now, session); - } - - private void save(ByteBuffer buf, Session session) { - if (buf.hasRemaining()) - session.setAttribute(REMAIN_DATA, buf); - } - - private void next(ByteBuffer buf, Session session, - HttpServletRequestImpl req) throws Throwable { - req.status++; - if (req.status < httpDecode.length) { - req.offset = 0; - httpDecode[req.status].decode0(buf, session, req); - } - } - - protected void finish(Session session, HttpServletRequestImpl req) { - session.removeAttribute(REMAIN_DATA); - session.removeAttribute(HTTP_REQUEST); - req.status = httpDecode.length; - } - - protected void responseError(Session session, - HttpServletRequestImpl req, int httpStatus, String content) { - finish(session, req); - req.response.scheduleSendError(httpStatus, content); - req.commitAndAllowDuplicate(); - } - - protected void response(Session session, HttpServletRequestImpl req) { - finish(session, req); - req.commitAndAllowDuplicate(); - } - - abstract protected boolean decode(ByteBuffer buf, Session session, - HttpServletRequestImpl req) throws Throwable; - } - - private class RequestLineDecoder extends AbstractHttpDecoder { - - @Override - public boolean decode(ByteBuffer buf, Session session, - HttpServletRequestImpl req) throws Throwable { - if (req.offset >= config.getMaxRequestLineLength()) { - String msg = "request line length is " + req.offset - + ", it more than " + config.getMaxRequestLineLength() - + "|" + session.getRemoteAddress(); - log.error(msg); - responseError(session, req, 414, msg); - return true; - } - - int len = buf.remaining(); - for (; req.offset < len; req.offset++) { - - if (buf.get(req.offset) == LINE_LIMITOR) { - byte[] data = new byte[req.offset + 1]; - buf.get(data); - String requestLine = new String(data, config.getEncoding()) - .trim(); - if (VerifyUtils.isEmpty(requestLine)) { - String msg = "request line length is 0|" - + session.getRemoteAddress(); - log.error(msg); - responseError(session, req, 400, msg); - return true; - } - - String[] reqLine = StringUtils.split(requestLine, ' '); - if (reqLine.length != 3) { - String msg = "request line format error: " - + requestLine + "|" - + session.getRemoteAddress(); - log.error(msg); - responseError(session, req, 400, msg); - return true; - } - - int s = reqLine[1].indexOf('?'); - req.method = reqLine[0].toUpperCase(); - if (s > 0) { - req.requestURI = reqLine[1].substring(0, s); - req.queryString = reqLine[1].substring(s + 1, reqLine[1].length()); - } else { - req.requestURI = reqLine[1]; - } - req.protocol = reqLine[2]; - return true; - } - } - return false; - } - - } - - private class HeadDecoder extends AbstractHttpDecoder { - - @Override - public boolean decode(ByteBuffer buf, Session session, - HttpServletRequestImpl req) throws Throwable { - int len = buf.remaining(); - - for (int i = req.offset, p = 0; i < len; i++) { - if (buf.get(i) == LINE_LIMITOR) { - int parseLen = i - p + 1; - req.headLength += parseLen; - - if (req.headLength >= config.getMaxRequestHeadLength()) { - String msg = "request head length is " + req.headLength - + ", it more than " - + config.getMaxRequestHeadLength() + "|" - + session.getRemoteAddress() + "|" - + req.getRequestURI(); - log.error(msg); - responseError(session, req, 400, msg); - return true; - } - - byte[] data = new byte[parseLen]; - buf.get(data); - String headLine = new String(data, config.getEncoding()) - .trim(); - p = i + 1; - - if (VerifyUtils.isEmpty(headLine)) { - if (!req.getMethod().equals("POST") && !req.getMethod().equals("PUT")) - response(session, req); - return true; - } else { - int h = headLine.indexOf(':'); - if (h <= 0) { - String msg = "head line format error: " + headLine - + "|" + session.getRemoteAddress() + "|" - + req.getRequestURI(); - log.error(msg); - responseError(session, req, 400, msg); - return true; - } - - String name = headLine.substring(0, h).toLowerCase().trim(); - String value = headLine.substring(h + 1).trim(); - req.headMap.put(name, value); - req.offset = len - i - 1; - - if (name.equals("expect") && value.startsWith("100-") && req.getProtocol().equals("HTTP/1.1")) - response100Continue(session); - } - } - } - return false; - } - - private void response100Continue(Session session) throws UnsupportedEncodingException { - session.write(ByteBuffer.wrap("HTTP/1.1 100 Continue\r\n\r\n".getBytes(config.getEncoding()))); - } - } - - private class BodyDecoder extends AbstractHttpDecoder { - - @Override - public boolean decode(ByteBuffer buf, Session session, - HttpServletRequestImpl req) throws Throwable { - int contentLength = req.getContentLength(); - if (contentLength > 0) { - if (contentLength > config.getMaxUploadLength()) { - String msg = "body length is " + contentLength - + " , it more than " + config.getMaxUploadLength() - + "|" + session.getRemoteAddress() + "|" - + req.getRequestURI(); - log.error(msg); - responseError(session, req, 400, msg); - return true; - } - - if (req.pipedOutputStream == null) - req.pipedOutputStream = new PipedOutputStream(req.pipedInputStream); - - req.commit(); - - req.offset += buf.remaining(); - byte[] data = new byte[buf.remaining()]; - buf.get(data); - try { - req.pipedOutputStream.write(data); - } catch(IOException e) { - log.error("receive body data error", e); - req.pipedOutputStream.close(); - } - - if (req.offset >= contentLength) { - req.pipedOutputStream.close(); - finish(session, req); - return true; - } - } else { - response(session, req); - return true; - } - return false; - } - - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/HttpEncoder.java b/firefly/src/main/java/com/firefly/server/http/HttpEncoder.java deleted file mode 100644 index 03c7ecba5..000000000 --- a/firefly/src/main/java/com/firefly/server/http/HttpEncoder.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.firefly.server.http; - -import com.firefly.net.Encoder; -import com.firefly.net.Session; - -public class HttpEncoder implements Encoder { - - @Override - public void encode(Object message, Session session) throws Throwable { - // TODO Auto-generated method stub - - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/HttpHandler.java b/firefly/src/main/java/com/firefly/server/http/HttpHandler.java deleted file mode 100644 index ba153c758..000000000 --- a/firefly/src/main/java/com/firefly/server/http/HttpHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.firefly.server.http; - -import com.firefly.mvc.web.servlet.HttpServletDispatcherController; -import com.firefly.net.Handler; -import com.firefly.net.Session; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class HttpHandler implements Handler { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private RequestHandler requestHandler; - private HttpConnectionListener httpConnectionListener; - - public HttpHandler(HttpServletDispatcherController servletController, Config config) { - httpConnectionListener = config.getHttpConnectionListener(); - requestHandler = new QueueRequestHandler(servletController, config); - } - - @Override - public void sessionOpened(Session session) throws Throwable { - httpConnectionListener.connectionCreated(session); - } - - @Override - public void sessionClosed(Session session) throws Throwable { - httpConnectionListener.connectionClosed(session); - } - - @Override - public void messageRecieved(Session session, Object message) - throws Throwable { - HttpServletRequestImpl request = (HttpServletRequestImpl) message; - requestHandler.doRequest(session, request); - } - - @Override - public void exceptionCaught(Session session, Throwable t) throws Throwable { - log.error("server error", t); - session.close(true); - } - - public void shutdown() { - requestHandler.shutdown(); - } -} diff --git a/firefly/src/main/java/com/firefly/server/http/HttpServletRequestImpl.java b/firefly/src/main/java/com/firefly/server/http/HttpServletRequestImpl.java deleted file mode 100644 index f2cd4941f..000000000 --- a/firefly/src/main/java/com/firefly/server/http/HttpServletRequestImpl.java +++ /dev/null @@ -1,826 +0,0 @@ -package com.firefly.server.http; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.security.Principal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletInputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import com.firefly.net.Session; -import com.firefly.server.exception.HttpServerException; -import com.firefly.server.utils.StringParser; -import com.firefly.utils.StringUtils; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class HttpServletRequestImpl implements HttpServletRequest { - int status, headLength, offset; - String method, requestURI, queryString; - String protocol = "HTTP/1.1"; - - PipedInputStream pipedInputStream = new PipedInputStream(); - PipedOutputStream pipedOutputStream; - Cookie[] cookies; - Map headMap = new HashMap(); - HttpServletResponseImpl response; - Config config; - Session session; - boolean systemReq = false; - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private static final Set IDEMPOTENT_METHODS = new HashSet(Arrays.asList("GET", "HEAD", "OPTIONS", "TRACE", "DELETE")); - private StringParser parser = new StringParser(); - private static final String[] EMPTY_STR_ARR = new String[0]; - private static final Cookie[] EMPTY_COOKIE_ARR = new Cookie[0]; - private String characterEncoding, requestedSessionId; - private boolean requestedSessionIdFromCookie, requestedSessionIdFromURL; - private boolean commit = false; - private HttpSession httpSession; - private Map> parameterMap = new HashMap>(); - private Map attributeMap = new HashMap(); - private BufferedReader bufferedReader; - private ServletInputStream servletInputStream = new ServletInputStream() { - - @Override - public int read() throws IOException { - return pipedInputStream.read(); - } - - @Override - public int available() throws IOException { - return pipedInputStream.available(); - } - - @Override - public void close() throws IOException { - pipedInputStream.close(); - } - - public int read(byte[] b, int off, int len) throws IOException { - return pipedInputStream.read(b, off, len); - } - }; - private RequestDispatcherImpl requestDispatcher = new RequestDispatcherImpl(); - - protected static Locale DEFAULT_LOCALE = Locale.getDefault(); - protected ArrayList locales = new ArrayList(); - private boolean loadParam, localesParsed; - - public HttpServletRequestImpl(Session session, Config config) { - this.characterEncoding = config.getEncoding(); - this.session = session; - this.config = config; - response = new HttpServletResponseImpl(session, this, - characterEncoding, config.getWriteBufferSize()); - } - - private void loadParam() { - if (!loadParam) { - try { - loadParam(queryString); - if (method.equals("POST") - && "application/x-www-form-urlencoded" - .equals(getContentType())) { - int contentLength = getContentLength(); - byte[] data = new byte[contentLength]; - byte[] buf = new byte[1024]; - ServletInputStream input = getInputStream(); - try { - int readBytes = 0; - for (int len = 0; (len = input.read(buf)) != -1;) { - System.arraycopy(buf, 0, data, readBytes, len); - readBytes += len; - if (readBytes >= contentLength) - break; - } - loadParam(new String(data, characterEncoding)); - } finally { - input.close(); - } - } - } catch (Throwable t) { - log.error("load param error", t); - } - loadParam = true; - } - } - - private void loadParam(String str) throws UnsupportedEncodingException { - if (VerifyUtils.isNotEmpty(str)) { - String[] p = StringUtils.split(str, '&'); - for (String kv : p) { - int i = kv.indexOf('='); - if (i > 0) { - String name = kv.substring(0, i); - String value = kv.substring(i + 1); - - List list = parameterMap.get(name); - if (list == null) { - list = new ArrayList(); - parameterMap.put(name, list); - - } - list.add(URLDecoder.decode(value, characterEncoding)); - } - - } - } - } - - public boolean isKeepAlive() { - return !systemReq - && config.isKeepAlive() - && ( - "Keep-Alive".equalsIgnoreCase(getHeader("Connection")) || - ( !getProtocol().equals("HTTP/1.0") && !"close".equalsIgnoreCase(getHeader("Connection")) ) - ); - } - - public boolean isSupportPipeline() { - return config.isKeepAlive() && IDEMPOTENT_METHODS.contains(getMethod()) && - ( "Keep-Alive".equalsIgnoreCase(getHeader("Connection")) || !getProtocol().equals("HTTP/1.0") ); - } - - public boolean isChunked() { - return !systemReq && !getProtocol().equals("HTTP/1.0"); - } - - @Override - public Object getAttribute(String name) { - return attributeMap.get(name); - } - - @Override - public Enumeration getAttributeNames() { - return new Enumeration() { - private Iterator iterator = attributeMap.keySet() - .iterator(); - - @Override - public boolean hasMoreElements() { - return iterator.hasNext(); - } - - @Override - public String nextElement() { - return iterator.next(); - } - }; - } - - @Override - public String getCharacterEncoding() { - return characterEncoding; - } - - @Override - public void setCharacterEncoding(String characterEncoding) - throws UnsupportedEncodingException { - this.characterEncoding = characterEncoding; - } - - @Override - public int getContentLength() { - return getIntHeader("Content-Length"); - } - - @Override - public String getContentType() { - return getHeader("Content-Type"); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - return servletInputStream; - } - - @Override - public String getParameter(String name) { - loadParam(); - List list = parameterMap.get(name); - return list != null ? list.get(0) : null; - } - - @Override - public Enumeration getParameterNames() { - loadParam(); - return new Enumeration() { - private Iterator iterator = parameterMap.keySet() - .iterator(); - - @Override - public boolean hasMoreElements() { - return iterator.hasNext(); - } - - @Override - public String nextElement() { - return iterator.next(); - } - }; - } - - @Override - public String[] getParameterValues(String name) { - loadParam(); - return parameterMap.get(name).toArray(EMPTY_STR_ARR); - } - - @Override - public Map> getParameterMap() { - loadParam(); - return parameterMap; - } - - @Override - public String getProtocol() { - return protocol; - } - - @Override - public String getScheme() { - return "http"; - } - - /** - * @return 服务器绑定的ip或者域名 - */ - @Override - public String getServerName() { - return session.getLocalAddress().getHostName(); - } - - /** - * @return 服务器监听的端口 - */ - @Override - public int getServerPort() { - return session.getLocalAddress().getPort(); - } - - @Override - public BufferedReader getReader() throws IOException { - if (bufferedReader == null) - bufferedReader = new BufferedReader(new InputStreamReader( - pipedInputStream, characterEncoding)); - return bufferedReader; - } - - @Override - public String getRemoteAddr() { - return session.getRemoteAddress().toString(); - } - - @Override - public String getRemoteHost() { - return session.getRemoteAddress().getHostName(); - } - - @Override - public void setAttribute(String name, Object o) { - attributeMap.put(name, o); - } - - @Override - public void removeAttribute(String name) { - attributeMap.remove(name); - } - - @Override - public Locale getLocale() { - if (!localesParsed) - parseLocales(); - - if (locales.size() > 0) { - return ((Locale) locales.get(0)); - } else { - return (DEFAULT_LOCALE); - } - } - - @Override - public Enumeration getLocales() { - if (!localesParsed) - parseLocales(); - - if (locales.size() == 0) - locales.add(DEFAULT_LOCALE); - - return new Enumeration() { - private int i = 0; - - @Override - public boolean hasMoreElements() { - return i < locales.size(); - } - - @Override - public Locale nextElement() { - return locales.get(i++); - } - }; - } - - @Override - public RequestDispatcher getRequestDispatcher(String path) { - requestDispatcher.path = path; - return requestDispatcher; - } - - @Override - public int getRemotePort() { - return session.getRemoteAddress().getPort(); - } - - /** - * @return 接收请求的ip或域名 - */ - @Override - public String getLocalName() { - return session.getLocalAddress().getHostName(); - } - - /** - * @return 接收请求的ip - */ - @Override - public String getLocalAddr() { - return session.getLocalAddress().toString(); - } - - /** - * @return 接收请求的端口 - */ - @Override - public int getLocalPort() { - return session.getLocalAddress().getPort(); - } - - @Override - public Cookie[] getCookies() { - if (cookies == null) { - List list = new ArrayList(); - String cookieStr = getHeader("Cookie"); - if (VerifyUtils.isEmpty(cookieStr)) { - cookies = EMPTY_COOKIE_ARR; - } else { - String[] c = StringUtils.split(cookieStr, ';'); - for (String t : c) { - int j = 0; - for (int i = 0; i < t.length(); i++) { - if (t.charAt(i) == '=') { - j = i; - break; - } - } - if (j > 1) { - String name = t.substring(0, j).trim(); - String value = t.substring(j + 1).trim(); - Cookie cookie = new Cookie(name, value); - list.add(cookie); - } else - continue; - } - cookies = list.toArray(EMPTY_COOKIE_ARR); - } - } - return cookies; - } - - @Override - public long getDateHeader(String name) { - String v = getHeader(name); - return v != null ? Long.parseLong(v) : 0; - } - - @Override - public String getHeader(String name) { - return headMap.get(name.toLowerCase()); - } - - @Override - public Enumeration getHeaders(String name) { - String value = getHeader(name); - final String[] values = StringUtils.split(value, ','); - return new Enumeration() { - private int i = 0; - - @Override - public boolean hasMoreElements() { - return i < values.length; - } - - @Override - public String nextElement() { - return values[i++]; - } - }; - } - - @Override - public Enumeration getHeaderNames() { - return new Enumeration() { - private Iterator iterator = headMap.keySet().iterator(); - - @Override - public boolean hasMoreElements() { - return iterator.hasNext(); - } - - @Override - public String nextElement() { - return iterator.next(); - } - }; - } - - @Override - public int getIntHeader(String name) { - String v = getHeader(name); - return v != null ? Integer.parseInt(v) : 0; - } - - @Override - public String getMethod() { - return method; - } - - @Override - public String getContextPath() { - return config.getContextPath(); - } - - @Override - public String getQueryString() { - return queryString; - } - - @Override - public String getRequestURI() { - return requestURI; - } - - @Override - public StringBuffer getRequestURL() { - StringBuffer url = new StringBuffer(); - String scheme = getScheme(); - int port = getServerPort(); - if (port < 0) - port = 80; // Work around java.net.URL bug - - url.append(scheme); - url.append("://"); - url.append(getServerName()); - if ((scheme.equals("http") && (port != 80)) - || (scheme.equals("https") && (port != 443))) { - url.append(':'); - url.append(port); - } - url.append(getRequestURI()); - return url; - } - - @Override - public String getServletPath() { - return config.getServletPath(); - } - - @Override - public String getRemoteUser() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public boolean isUserInRole(String role) { - throw new HttpServerException("no implements this method!"); - } - - @Override - public Principal getUserPrincipal() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public String getAuthType() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public boolean isSecure() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public String getRealPath(String path) { - throw new HttpServerException("no implements this method!"); - } - - @Override - public String getPathInfo() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public String getPathTranslated() { - return new File(config.getServerHome(), getRequestURI()) - .getAbsolutePath(); - } - - @Override - public String getRequestedSessionId() { - return requestedSessionId; - } - - @Override - public HttpSession getSession(boolean create) { - if (create) { - httpSession = config.getHttpSessionManager().create(); - requestedSessionId = httpSession.getId(); - response.addCookie(new Cookie(config.getSessionIdName(), - httpSession.getId())); - } else { - if (isRequestedSessionIdFromCookie() - || isRequestedSessionIdFromURL()) { - httpSession = config.getHttpSessionManager().get( - requestedSessionId); - } - } - - return httpSession; - } - - @Override - public HttpSession getSession() { - if (httpSession == null) { - if (isRequestedSessionIdFromCookie() - || isRequestedSessionIdFromURL()) - httpSession = config.getHttpSessionManager().get( - requestedSessionId); - - if (httpSession == null) - getSession(true); - } - return httpSession; - } - - @Override - public boolean isRequestedSessionIdValid() { - return requestedSessionId != null ? config.getHttpSessionManager() - .containsKey(requestedSessionId) : false; - } - - @Override - public boolean isRequestedSessionIdFromCookie() { - if (requestedSessionId != null) - return requestedSessionIdFromCookie; - - for (Cookie cookie : getCookies()) { - if (cookie.getName().equals(config.getSessionIdName())) { - requestedSessionId = cookie.getValue(); - requestedSessionIdFromCookie = true; - return true; - } - } - return false; - } - - @Override - public boolean isRequestedSessionIdFromURL() { - if (requestedSessionId != null) - return requestedSessionIdFromURL; - - String sessionId = getSessionId(requestURI, config.getSessionIdName()); - if (VerifyUtils.isNotEmpty(sessionId)) { - requestedSessionId = sessionId; - requestedSessionIdFromURL = true; - return true; - } - return false; - } - - public static String getSessionId(String uri, String sessionIdName) { - String sessionId = null; - int i = uri.indexOf(';'); - int j = uri.indexOf('#'); - if (i > 0) { - String tmp = j > i ? uri.substring(i + 1, j) : uri.substring(i + 1); - int m = 0; - for (int k = 0; k < tmp.length(); k++) { - if (tmp.charAt(k) == '=') { - m = k; - break; - } - } - if (m > 0) { - String name = tmp.substring(0, m); - String value = tmp.substring(m + 1); - if (name.equals(sessionIdName)) { - sessionId = value; - } - } - } - return sessionId; - } - - @Override - public boolean isRequestedSessionIdFromUrl() { - return isRequestedSessionIdFromURL(); - } - - @Override - public String toString() { - return method + " " + requestURI + queryString + " " + protocol - + "\r\n" + headMap.toString(); - } - - protected void parseLocales() { - localesParsed = true; - Enumeration values = getHeaders("accept-language"); - while (values.hasMoreElements()) { - String value = values.nextElement().toString(); - parseLocalesHeader(value); - } - } - - /** - * Parse accept-language header value. - */ - protected void parseLocalesHeader(String value) { - - // Store the accumulated languages that have been requested in - // a local collection, sorted by the quality value (so we can - // add Locales in descending order). The values will be ArrayLists - // containing the corresponding Locales to be added - TreeMap> locales = new TreeMap>(); - - // Preprocess the value to remove all whitespace - int white = value.indexOf(' '); - if (white < 0) - white = value.indexOf('\t'); - if (white >= 0) { - StringBuilder sb = new StringBuilder(); - int len = value.length(); - for (int i = 0; i < len; i++) { - char ch = value.charAt(i); - if ((ch != ' ') && (ch != '\t')) - sb.append(ch); - } - value = sb.toString(); - } - - // Process each comma-delimited language specification - parser.setString(value); // ASSERT: parser is available to us - int length = parser.getLength(); - while (true) { - - // Extract the next comma-delimited entry - int start = parser.getIndex(); - if (start >= length) - break; - int end = parser.findChar(','); - String entry = parser.extract(start, end).trim(); - parser.advance(); // For the following entry - - // Extract the quality factor for this entry - double quality = 1.0; - int semi = entry.indexOf(";q="); - if (semi >= 0) { - try { - String strQuality = entry.substring(semi + 3); - if (strQuality.length() <= 5) { - quality = Double.parseDouble(strQuality); - } else { - quality = 0.0; - } - } catch (NumberFormatException e) { - quality = 0.0; - } - entry = entry.substring(0, semi); - } - - // Skip entries we are not going to keep track of - if (quality < 0.00005) - continue; // Zero (or effectively zero) quality factors - if ("*".equals(entry)) - continue; // FIXME - "*" entries are not handled - - // Extract the language and country for this entry - String language = null; - String country = null; - String variant = null; - int dash = entry.indexOf('-'); - if (dash < 0) { - language = entry; - country = ""; - variant = ""; - } else { - language = entry.substring(0, dash); - country = entry.substring(dash + 1); - int vDash = country.indexOf('-'); - if (vDash > 0) { - String cTemp = country.substring(0, vDash); - variant = country.substring(vDash + 1); - country = cTemp; - } else { - variant = ""; - } - } - if (!isAlpha(language) || !isAlpha(country) || !isAlpha(variant)) { - continue; - } - - // Add a new Locale to the list of Locales for this quality level - Locale locale = new Locale(language, country, variant); - Double key = new Double(-quality); // Reverse the order - ArrayList values = locales.get(key); - if (values == null) { - values = new ArrayList(); - locales.put(key, values); - } - values.add(locale); - - } - - // Process the quality values in highest->lowest order (due to - // negating the Double value when creating the key) - Iterator keys = locales.keySet().iterator(); - while (keys.hasNext()) { - Double key = keys.next(); - ArrayList list = locales.get(key); - Iterator values = list.iterator(); - while (values.hasNext()) { - Locale locale = (Locale) values.next(); - addLocale(locale); - } - } - - } - - protected static final boolean isAlpha(String value) { - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) { - return false; - } - } - return true; - } - - protected void addLocale(Locale locale) { - locales.add(locale); - } - - /** - * 提交到handler处理 - */ - void commit() { - if(!commit) { - session.fireReceiveMessage(this); - commit = true; - } - } - - void commitAndAllowDuplicate() { - session.fireReceiveMessage(this); - } - - void releaseInputStreamData() throws IOException { - try { - if(getContentLength() > 0) { - ServletInputStream input = getInputStream(); - if(input.available() > 0) { - log.warn("release input stream data"); - byte[] buf = new byte[1024]; - for (; input.read(buf) != -1;) { - - } - } - } - } finally { - getInputStream().close(); - } - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/HttpServletResponseImpl.java b/firefly/src/main/java/com/firefly/server/http/HttpServletResponseImpl.java deleted file mode 100644 index 635330cca..000000000 --- a/firefly/src/main/java/com/firefly/server/http/HttpServletResponseImpl.java +++ /dev/null @@ -1,508 +0,0 @@ -package com.firefly.server.http; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.servlet.SystemHtmlPage; -import com.firefly.net.Session; -import com.firefly.server.exception.HttpServerException; -import com.firefly.server.io.ChunkedOutputStream; -import com.firefly.server.io.HttpServerOutpuStream; -import com.firefly.server.io.NetBufferedOutputStream; -import com.firefly.server.io.StaticFileOutputStream; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.time.SafeSimpleDateFormat; - -public class HttpServletResponseImpl implements HttpServletResponse { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - public static SafeSimpleDateFormat GMT_FORMAT; - private boolean committed; - private HttpServletRequestImpl request; - private int status, bufferSize; - private String characterEncoding, shortMessage, contentLanguage; - private Locale locale; - private Map headMap = new HashMap(); - private List cookies = new LinkedList(); - private boolean usingWriter, usingOutputStream, usingFileOutputStream; - private HttpServerOutpuStream out; - private StaticFileOutputStream fileOut; - private PrintWriter writer; - private NetBufferedOutputStream bufferedOutput; - - boolean system; - String systemResponseContent; - - static { - SimpleDateFormat sdf = new SimpleDateFormat( - "EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH); - sdf.setTimeZone(TimeZone.getTimeZone("GMT")); - GMT_FORMAT = new SafeSimpleDateFormat(sdf); - } - - public HttpServletResponseImpl(Session session, - HttpServletRequestImpl request, String characterEncoding, - int bufferSize) { - this.request = request; - this.characterEncoding = characterEncoding; - this.bufferSize = bufferSize; - - locale = HttpServletRequestImpl.DEFAULT_LOCALE; - setStatus(200); - setHeader("Server", "firefly-server/1.0"); - } - - private void createOutput() { - if (bufferedOutput == null) { - setHeader("Date", GMT_FORMAT.format(new Date())); - setHeader("Connection", request.isKeepAlive() ? "keep-alive" : "close"); - - bufferedOutput = new NetBufferedOutputStream(request.session, bufferSize, request.isKeepAlive()); - - if(request.isChunked() && VerifyUtils.isEmpty(headMap.get("Content-Length"))) - out = new ChunkedOutputStream(bufferSize, bufferedOutput, request, this); - else - out = new HttpServerOutpuStream(bufferSize, bufferedOutput, request, this); - - fileOut = new StaticFileOutputStream(bufferSize, bufferedOutput, request, this); - writer = new PrintWriter(out); - } - } - - public byte[] getHeadData() { - StringBuilder sb = new StringBuilder(); - sb.append(request.getProtocol()).append(' ').append(status).append(' ') - .append(shortMessage).append("\r\n"); - - for (String name : headMap.keySet()) - sb.append(name).append(": ").append(headMap.get(name)) - .append("\r\n"); - - if (contentLanguage != null) - sb.append("Content-Language: ").append(contentLanguage) - .append("\r\n"); - - for (Cookie cookie : cookies) { - sb.append("Set-Cookie: ").append(cookie.getName()).append('=') - .append(cookie.getValue()); - - if (VerifyUtils.isNotEmpty(cookie.getComment())) - sb.append(";Comment=").append(cookie.getComment()); - - if (VerifyUtils.isNotEmpty(cookie.getDomain())) - sb.append(";Domain=").append(cookie.getDomain()); - - if (cookie.getMaxAge() >= 0) - sb.append(";Max-Age=").append(cookie.getMaxAge()); - - String path = VerifyUtils.isEmpty(cookie.getPath()) ? "/" : cookie - .getPath(); - sb.append(";Path=").append(path); - - if (cookie.getSecure()) - sb.append(";Secure"); - - sb.append(";Version=").append(cookie.getVersion()).append("\r\n"); - } - - sb.append("\r\n"); - String head = sb.toString(); - // System.out.println(head); - return stringToByte(head); - } - - public byte[] getChunkedSize(int length) { - return stringToByte(Integer.toHexString(length) + "\r\n"); - } - - public byte[] stringToByte(String str) { - byte[] ret = null; - try { - ret = str.getBytes(characterEncoding); - } catch (UnsupportedEncodingException e) { - log.error("string to bytes", e); - } - return ret; - } - - @Override - public String getCharacterEncoding() { - return characterEncoding; - } - - @Override - public String getContentType() { - return headMap.get("Content-Type"); - } - - public StaticFileOutputStream getStaticFileOutputStream() - throws IOException { - if (usingWriter) - throw new HttpServerException( - "getWriter has already been called for this response"); - if (usingOutputStream) - throw new HttpServerException( - "getOutputStream has already been called for this response"); - - createOutput(); - usingFileOutputStream = true; - return fileOut; - } - - @Override - public ServletOutputStream getOutputStream() throws IOException { - if (usingWriter) - throw new HttpServerException( - "getWriter has already been called for this response"); - if (usingFileOutputStream) - throw new HttpServerException( - "getStaticFileOutputStream has already been called for this response"); - - createOutput(); - - usingOutputStream = true; - return out; - } - - @Override - public PrintWriter getWriter() throws IOException { - if (usingOutputStream) - throw new HttpServerException( - "getOutputStream has already been called for this response"); - if (usingFileOutputStream) - throw new HttpServerException( - "getStaticFileOutputStream has already been called for this response"); - - createOutput(); - - usingWriter = true; - return writer; - } - - @Override - public void setCharacterEncoding(String charset) { - characterEncoding = charset; - } - - @Override - public void setContentLength(int len) { - headMap.put("Content-Length", String.valueOf(len)); - } - - @Override - public void setContentType(String type) { - headMap.put("Content-Type", type); - } - - @Override - public void setBufferSize(int size) { - bufferSize = size; - } - - @Override - public int getBufferSize() { - return bufferSize; - } - - @Override - public void flushBuffer() throws IOException { - out.flush(); - } - - @Override - public void resetBuffer() { - out.resetBuffer(); - } - - @Override - public boolean isCommitted() { - return committed; - } - - public void setCommitted(boolean committed) { - this.committed = committed; - } - - @Override - public void reset() { - usingWriter = false; - usingOutputStream = false; - committed = false; - bufferedOutput = null; - out = null; - writer = null; - } - - @Override - public void setLocale(Locale locale) { - if (locale == null) { - return; - } - - this.locale = locale; - - // Set the contentLanguage for header output - contentLanguage = locale.getLanguage(); - if ((contentLanguage != null) && (contentLanguage.length() > 0)) { - String country = locale.getCountry(); - StringBuilder value = new StringBuilder(contentLanguage); - if (country != null && country.length() > 0) { - value.append('-'); - value.append(country); - } - contentLanguage = value.toString(); - } - } - - @Override - public Locale getLocale() { - return locale; - } - - @Override - public void addCookie(Cookie cookie) { - if (cookie == null) - throw new HttpServerException("cookie is null"); - - if (VerifyUtils.isNotEmpty(cookie.getName()) - && VerifyUtils.isNotEmpty(cookie.getValue())) { - cookies.add(cookie); - } else { - throw new HttpServerException( - "cookie name or value or domain is null"); - } - } - - @Override - public boolean containsHeader(String name) { - return headMap.containsKey(name); - } - - @Override - public String encodeURL(String url) { - if (VerifyUtils.isEmpty(url)) - return null; - - if (url.contains(";" + request.config.getSessionIdName() + "=")) - return url; - - String absoluteURL = toAbsolute(url); - - if (request.isRequestedSessionIdFromCookie() - || request.isRequestedSessionIdFromURL()) - return toEncoded(absoluteURL, request.getRequestedSessionId(), - request.config.getSessionIdName()); - - return null; - } - - @Override - public String encodeRedirectURL(String url) { - return encodeURL(url); - } - - @Override - public String encodeUrl(String url) { - return encodeURL(url); - } - - @Override - public String encodeRedirectUrl(String url) { - return encodeRedirectURL(url); - } - - @Override - public void sendError(int sc, String msg) throws IOException { - setStatus(sc, msg); - systemResponseContent = shortMessage; - outSystemData(); - } - - @Override - public void sendError(int sc) throws IOException { - setStatus(sc); - systemResponseContent = shortMessage; - outSystemData(); - } - - public void outSystemData() throws IOException { - if (isCommitted()) - throw new IllegalStateException("response is committed"); - - createOutput(); - if (status >= 400) { - try { - boolean hasContent = VerifyUtils - .isNotEmpty(systemResponseContent); - byte[] b = null; - - if (hasContent) { - b = SystemHtmlPage.systemPageTemplate(status, - systemResponseContent).getBytes(characterEncoding); - setHeader("Content-Length", String.valueOf(b.length)); - } else { - setHeader("Content-Length", "0"); - } - - bufferedOutput.write(getHeadData()); - if (hasContent) - bufferedOutput.write(b); - } finally { - bufferedOutput.close(); - } - setCommitted(true); - } - } - - public void scheduleSendError(int sc, String content) { - setStatus(sc); - systemResponseContent = content; - system = true; - request.systemReq = true; - } - - @Override - public void sendRedirect(String location) throws IOException { - String absolute = toAbsolute(location); - setStatus(SC_FOUND); - setHeader("Location", absolute); - setHeader("Content-Length", "0"); - outHeadData(); - } - - public void outHeadData() throws IOException { - if (isCommitted()) - throw new IllegalStateException("response is committed"); - - createOutput(); - try { - bufferedOutput.write(getHeadData()); - } finally { - bufferedOutput.close(); - } - setCommitted(true); - } - - public String toAbsolute(String location) { - if (location.startsWith("http")) - return location; - - StringBuilder sb = new StringBuilder(); - sb.append(request.getScheme()).append("://") - .append(request.getServerName()).append(":") - .append(request.getServerPort()); - - if (location.charAt(0) == '/') { - sb.append(location); - } else { - String URI = request.getRequestURI(); - int last = 0; - for (int i = URI.length() - 1; i >= 0; i--) { - if (URI.charAt(i) == '/') { - last = i + 1; - break; - } - } - sb.append(URI.substring(0, last)).append(location); - } - return sb.toString(); - } - - @Override - public void setDateHeader(String name, long date) { - setHeader(name, GMT_FORMAT.format(new Date(date))); - } - - @Override - public void addDateHeader(String name, long date) { - addHeader(name, GMT_FORMAT.format(new Date(date))); - } - - @Override - public void setHeader(String name, String value) { - headMap.put(name, value); - } - - @Override - public void addHeader(String name, String value) { - String v = headMap.get(name); - if (v != null) { - v += "," + value; - setHeader(name, v); - } else - setHeader(name, value); - } - - @Override - public void setIntHeader(String name, int value) { - setHeader(name, String.valueOf(value)); - } - - @Override - public void addIntHeader(String name, int value) { - addHeader(name, String.valueOf(value)); - } - - @Override - public void setStatus(int status) { - this.status = status; - this.shortMessage = Constants.STATUS_CODE.get(status); - } - - @Override - public void setStatus(int status, String shortMessage) { - this.status = status; - this.shortMessage = shortMessage; - } - - public int getStatus() { - return status; - } - - public static String toEncoded(String url, String sessionId, - String sessionIdName) { - if (url == null || sessionId == null) - return url; - - String path = url; - String query = ""; - String anchor = ""; - int question = url.indexOf('?'); - if (question >= 0) { - path = url.substring(0, question); - query = url.substring(question); - } - int pound = path.indexOf('#'); - if (pound >= 0) { - anchor = path.substring(pound); - path = path.substring(0, pound); - } - StringBuilder sb = new StringBuilder(path); - if (sb.length() > 0) { // jsessionid can't be first. - sb.append(";"); - sb.append(sessionIdName); - sb.append("="); - sb.append(sessionId); - } - sb.append(anchor); - sb.append(query); - return sb.toString(); - - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/QueueRequestHandler.java b/firefly/src/main/java/com/firefly/server/http/QueueRequestHandler.java deleted file mode 100644 index 83bca9c98..000000000 --- a/firefly/src/main/java/com/firefly/server/http/QueueRequestHandler.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.firefly.server.http; - -import java.io.IOException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import com.firefly.mvc.web.servlet.HttpServletDispatcherController; -import com.firefly.mvc.web.servlet.SystemHtmlPage; -import com.firefly.net.Session; -import com.firefly.utils.collection.LinkedTransferQueue; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class QueueRequestHandler extends RequestHandler { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private ExecutorService executor = Executors.newCachedThreadPool(); - private HttpQueueHandler[] queues; - private Config config; - private AtomicInteger currentQueueSize = new AtomicInteger(); - - public QueueRequestHandler(HttpServletDispatcherController servletController, Config config) { - super(servletController); - this.config = config; - queues = new HttpQueueHandler[config.getHandlerSize()]; - for (int i = 0; i < queues.length; i++) { - queues[i] = new HttpQueueHandler(i); - } - } - - private class HttpQueueHandler { - private int id; - private boolean start = true; - private LinkedTransferQueue queue = new LinkedTransferQueue(); - private Thread thread = new Thread(new Runnable() { - - @Override - public void run() { - while (start) { - try { - for (HttpServletRequestImpl request = null; (request = queue.poll( - 1000, TimeUnit.MILLISECONDS)) != null;) { - currentQueueSize.decrementAndGet(); - doRequest(request, id); - } - } catch (Throwable e) { - log.error("http queue error", e); - } - } - - } - }, "http queue " + id); - - public HttpQueueHandler(int id) { - this.id = id; - thread.start(); - } - - public void add(HttpServletRequestImpl request) { - queue.offer(request); - currentQueueSize.incrementAndGet(); - } - - public void shutdown() { - start = false; - } - - } - - @Override - public void shutdown() { - for (HttpQueueHandler h : queues) - h.shutdown(); - - executor.shutdown(); - } - - @Override - public void doRequest(Session session, final HttpServletRequestImpl request) - throws IOException { - if (request.response.system) { // 系统错误在当前线程响应 - request.response.outSystemData(); - } else { - int s = currentQueueSize.get(); - if(s >= config.getMaxHandlerQueueSize()) { // 队列过载保护 - log.warn("http request queue size is {}, more than {}", s, config.getMaxHandlerQueueSize()); - request.response.setHeader("Retry-After", "30"); - SystemHtmlPage.responseSystemPage(request, request.response, config.getEncoding(), 503, "Service unavailable, please try again later."); - } else { - if(request.isSupportPipeline()) { // pipeline请求使用队列线程池 - int sessionId = session.getSessionId(); - int handlerIndex = Math.abs(sessionId) % queues.length; - queues[handlerIndex].add(request); - } else { // 使用cache线程池 - executor.submit(new Runnable(){ - @Override - public void run() { - try { - doRequest(request, -1); - } catch (IOException e) { - log.error("http handle thread error", e); - } - } - }); - } - } - } - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/RequestDispatcherImpl.java b/firefly/src/main/java/com/firefly/server/http/RequestDispatcherImpl.java deleted file mode 100644 index 92d966d13..000000000 --- a/firefly/src/main/java/com/firefly/server/http/RequestDispatcherImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.firefly.server.http; - -import java.io.IOException; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.mvc.web.view.TemplateView; - -public class RequestDispatcherImpl implements RequestDispatcher { - - private boolean forward = false; - String path; - - @Override - public void forward(ServletRequest request, ServletResponse response) - throws ServletException, IOException { - if (!forward) { - new TemplateView(path).render((HttpServletRequest) request, (HttpServletResponse) response); - forward = true; - } - - } - - @Override - public void include(ServletRequest request, ServletResponse response) - throws ServletException, IOException { - new TemplateView(path).render((HttpServletRequest) request, (HttpServletResponse) response); - } - -} diff --git a/firefly/src/main/java/com/firefly/server/http/RequestHandler.java b/firefly/src/main/java/com/firefly/server/http/RequestHandler.java deleted file mode 100644 index c3f221ed2..000000000 --- a/firefly/src/main/java/com/firefly/server/http/RequestHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.firefly.server.http; - -import java.io.IOException; - -import com.firefly.mvc.web.DispatcherController; -import com.firefly.mvc.web.servlet.HttpServletDispatcherController; -import com.firefly.net.Session; -import com.firefly.utils.VerifyUtils; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; -import com.firefly.utils.time.Millisecond100Clock; - -public abstract class RequestHandler { - - private static Log access = LogFactory.getInstance().getLog("firefly-access"); - private DispatcherController servletController; - - public RequestHandler(HttpServletDispatcherController servletController) { - this.servletController = servletController; - } - - protected void doRequest(HttpServletRequestImpl request, int id) throws IOException { - long start = Millisecond100Clock.currentTimeMillis(); - servletController.dispatcher(request, request.response); - request.releaseInputStreamData(); - long end = Millisecond100Clock.currentTimeMillis(); - access.info("{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}", - request.session.getSessionId(), - id, - getClientAddress(request), - request.response.getStatus(), - request.getProtocol(), - request.getMethod(), - request.getRequestURI(), - request.getQueryString(), - request.session.getReadBytes(), - request.session.getWrittenBytes(), - (end - start)); - } - - private final String getClientAddress(HttpServletRequestImpl request) { - String address = request.getHeader("X-Forwarded-For"); - if(VerifyUtils.isNotEmpty(address)) - return address; - - return request.getRemoteAddr(); - } - - abstract public void doRequest(Session session, HttpServletRequestImpl request) throws IOException; - abstract public void shutdown(); -} diff --git a/firefly/src/main/java/com/firefly/server/io/ChunkedOutputStream.java b/firefly/src/main/java/com/firefly/server/io/ChunkedOutputStream.java deleted file mode 100644 index 524311987..000000000 --- a/firefly/src/main/java/com/firefly/server/io/ChunkedOutputStream.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.firefly.server.io; - -import java.io.IOException; - -import com.firefly.server.http.HttpServletRequestImpl; -import com.firefly.server.http.HttpServletResponseImpl; - -public class ChunkedOutputStream extends HttpServerOutpuStream { - - private byte[] crlf, endFlag; - private boolean chunked; - - public ChunkedOutputStream(int bufferSize, - NetBufferedOutputStream bufferedOutput, - HttpServletRequestImpl request, HttpServletResponseImpl response) { - super(bufferSize, bufferedOutput, request, response); - crlf = response.stringToByte("\r\n"); - endFlag = response.stringToByte("0\r\n\r\n"); - } - - @Override - public void write(int b) throws IOException { - super.write(b); - if (size > bufferSize) - flush(); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - super.write(b, off, len); - if (size > bufferSize) - flush(); - } - - @Override - public void flush() throws IOException { - if (!response.isCommitted()) { - chunked = true; - response.setHeader("Transfer-Encoding", "chunked"); - bufferedOutput.write(response.getHeadData()); - response.setCommitted(true); - } - - if (size > 0) { - bufferedOutput.write(response.getChunkedSize(size)); - for (ChunkedData d = null; (d = queue.poll()) != null;) - d.write(); - bufferedOutput.write(crlf); - size = 0; - } - } - - @Override - public void close() throws IOException { - if (!response.isCommitted()) { - response.setHeader("Content-Length", String.valueOf(size)); - bufferedOutput.write(response.getHeadData()); - response.setCommitted(true); - } - - if (size > 0) { - if (chunked) - bufferedOutput.write(response.getChunkedSize(size)); - for (ChunkedData d = null; (d = queue.poll()) != null;) - d.write(); - if (chunked) - bufferedOutput.write(crlf); - size = 0; - } - - if (chunked) - bufferedOutput.write(endFlag); - bufferedOutput.close(); - } - -} diff --git a/firefly/src/main/java/com/firefly/server/io/HttpServerOutpuStream.java b/firefly/src/main/java/com/firefly/server/io/HttpServerOutpuStream.java deleted file mode 100644 index de888400b..000000000 --- a/firefly/src/main/java/com/firefly/server/io/HttpServerOutpuStream.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.firefly.server.io; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.Queue; - -import javax.servlet.ServletOutputStream; - -import com.firefly.server.http.HttpServletRequestImpl; -import com.firefly.server.http.HttpServletResponseImpl; - -public class HttpServerOutpuStream extends ServletOutputStream { - protected Queue queue = new LinkedList(); - protected int size, bufferSize; - protected NetBufferedOutputStream bufferedOutput; - protected HttpServletResponseImpl response; - protected HttpServletRequestImpl request; - - public HttpServerOutpuStream(int bufferSize, - NetBufferedOutputStream bufferedOutput, - HttpServletRequestImpl request, HttpServletResponseImpl response) { - this.bufferSize = bufferSize; - this.bufferedOutput = bufferedOutput; - this.request = request; - this.response = response; - } - - @Override - public void write(int b) throws IOException { - queue.offer(new ByteChunkedData((byte) b)); - size++; - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - queue.offer(new ByteArrayChunkedData(b, off, len)); - size += len; - } - - @Override - public void print(String s) throws IOException { - if (s == null) - s = "null"; - if (s.length() > 0) - write(response.stringToByte(s)); - } - - @Override - public void flush() throws IOException { - - } - - @Override - public void close() throws IOException { - if (!response.isCommitted()) { - response.setHeader("Content-Length", String.valueOf(size)); - byte[] head = response.getHeadData(); - // System.out.print(new String(head, "UTF-8")); - bufferedOutput.write(head); - response.setCommitted(true); - } - - if (size > 0) { - if (request.getMethod().equals("HEAD")) - queue.clear(); - else { - for (ChunkedData d = null; (d = queue.poll()) != null;) - d.write(); - } - - size = 0; - } - bufferedOutput.close(); - } - - public void resetBuffer() { - bufferedOutput.resetBuffer(); - size = 0; - queue.clear(); - } - - private class ByteChunkedData extends ChunkedData { - private byte b; - - public ByteChunkedData(byte b) { - this.b = b; - } - - @Override - public void write() throws IOException { - bufferedOutput.write(b); - } - } - - private class ByteArrayChunkedData extends ChunkedData { - private byte[] b; - private int off, len; - - public ByteArrayChunkedData(byte[] b, int off, int len) { - this.b = b; - this.off = off; - this.len = len; - } - - @Override - public void write() throws IOException { - bufferedOutput.write(b, off, len); - } - } - - abstract protected class ChunkedData { - abstract void write() throws IOException; - } -} diff --git a/firefly/src/main/java/com/firefly/server/io/NetBufferedOutputStream.java b/firefly/src/main/java/com/firefly/server/io/NetBufferedOutputStream.java deleted file mode 100644 index 193ae4488..000000000 --- a/firefly/src/main/java/com/firefly/server/io/NetBufferedOutputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.firefly.server.io; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; - -import com.firefly.net.Session; -import com.firefly.net.buffer.FileRegion; - -public class NetBufferedOutputStream extends OutputStream { - - protected byte[] buf; - protected int count; - protected Session session; - protected int bufferSize; - protected boolean keepAlive; - - public NetBufferedOutputStream(Session session, int bufferSize, - boolean keepAlive) { - this.session = session; - this.bufferSize = bufferSize; - this.keepAlive = keepAlive; - buf = new byte[bufferSize]; - } - - @Override - public void write(int b) throws IOException { - if (count >= buf.length) - flush(); - - buf[count++] = (byte) b; - } - - @Override - public void write(byte b[], int off, int len) throws IOException { - if (len >= buf.length) { - flush(); - session.write(ByteBuffer.wrap(b, off, len)); - return; - } - if (len > buf.length - count) - flush(); - - System.arraycopy(b, off, buf, count, len); - count += len; - } - - public void write(File file, long off, long len) throws IOException { - flush(); - FileRegion fileRegion = new FileRegion(new RandomAccessFile(file, "r"), off, len); - session.write(fileRegion); - } - - public void write(File file) throws IOException { - write(file, 0, file.length()); - } - - @Override - public void flush() throws IOException { - if (count > 0) { - session.write(ByteBuffer.wrap(buf, 0, count)); - resetBuffer(); - } - } - - @Override - public void close() throws IOException { - flush(); - if (!keepAlive) - session.close(false); - } - - public void resetBuffer() { - buf = new byte[bufferSize]; - count = 0; - } - -} diff --git a/firefly/src/main/java/com/firefly/server/io/StaticFileOutputStream.java b/firefly/src/main/java/com/firefly/server/io/StaticFileOutputStream.java deleted file mode 100644 index 12c95bbb1..000000000 --- a/firefly/src/main/java/com/firefly/server/io/StaticFileOutputStream.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.firefly.server.io; - -import java.io.File; -import java.io.IOException; - -import com.firefly.server.http.HttpServletRequestImpl; -import com.firefly.server.http.HttpServletResponseImpl; - -public class StaticFileOutputStream extends HttpServerOutpuStream { - - public StaticFileOutputStream(int bufferSize, - NetBufferedOutputStream bufferedOutput, - HttpServletRequestImpl request, HttpServletResponseImpl response) { - super(bufferSize, bufferedOutput, request, response); - } - - public void write(File file, long off, long len) throws IOException { - queue.offer(new FileChunkedData(file, off, len)); - size += len; - } - - public void write(File file) throws IOException { - write(file, 0, file.length()); - } - - private class FileChunkedData extends ChunkedData { - - private long off, len; - private File file; - - public FileChunkedData(File file, long off, long len) { - super(); - this.off = off; - this.len = len; - this.file = file; - } - - @Override - void write() throws IOException { - bufferedOutput.write(file, off, len); - } - - } - -} diff --git a/firefly/src/main/java/com/firefly/server/session/HttpSessionImpl.java b/firefly/src/main/java/com/firefly/server/session/HttpSessionImpl.java deleted file mode 100644 index f6ea5ddb1..000000000 --- a/firefly/src/main/java/com/firefly/server/session/HttpSessionImpl.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.firefly.server.session; - -import java.util.Enumeration; -import java.util.concurrent.ConcurrentHashMap; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionBindingEvent; -import javax.servlet.http.HttpSessionContext; - -import com.firefly.server.exception.HttpServerException; -import com.firefly.utils.time.Millisecond100Clock; - -@SuppressWarnings("deprecation") -public class HttpSessionImpl implements HttpSession { - private static final String[] EMPTY_ARR = new String[0]; - private final HttpSessionManager sessionManager; - private final String id; - private final long creationTime; - private volatile long lastAccessedTime; - private volatile int maxInactiveInterval; - private ConcurrentHashMap map = new ConcurrentHashMap(); - - public HttpSessionImpl(HttpSessionManager sessionManager, String id, - long creationTime, int maxInactiveInterval) { - super(); - this.sessionManager = sessionManager; - this.id = id; - this.creationTime = creationTime; - this.maxInactiveInterval = maxInactiveInterval; - } - - @Override - public long getCreationTime() { - return creationTime; - } - - @Override - public String getId() { - return id; - } - - @Override - public long getLastAccessedTime() { - return lastAccessedTime; - } - - @Override - public ServletContext getServletContext() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public void setMaxInactiveInterval(int interval) { - maxInactiveInterval = interval; - } - - @Override - public int getMaxInactiveInterval() { - return maxInactiveInterval; - } - - @Override - public HttpSessionContext getSessionContext() { - throw new HttpServerException("no implements this method!"); - } - - @Override - public Object getAttribute(String name) { - lastAccessedTime = Millisecond100Clock.currentTimeMillis(); - return map.get(name); - } - - @Override - public Object getValue(String name) { - return getAttribute(name); - } - - @Override - public Enumeration getAttributeNames() { - lastAccessedTime = Millisecond100Clock.currentTimeMillis(); - return map.keys(); - } - - @Override - public String[] getValueNames() { - lastAccessedTime = Millisecond100Clock.currentTimeMillis(); - return map.keySet().toArray(EMPTY_ARR); - } - - @Override - public void setAttribute(String name, Object value) { - lastAccessedTime = Millisecond100Clock.currentTimeMillis(); - Object v = map.put(name, value); - if(v == null) - sessionManager.getConfig().getHttpSessionAttributeListener().attributeAdded(new HttpSessionBindingEvent(this, name, value)); - else - sessionManager.getConfig().getHttpSessionAttributeListener().attributeReplaced(new HttpSessionBindingEvent(this, name, value)); - } - - @Override - public void putValue(String name, Object value) { - setAttribute(name, value); - } - - @Override - public void removeAttribute(String name) { - lastAccessedTime = Millisecond100Clock.currentTimeMillis(); - Object value = map.remove(name); - if(value != null) - sessionManager.getConfig().getHttpSessionAttributeListener().attributeRemoved(new HttpSessionBindingEvent(this, name, value)); - } - - @Override - public void removeValue(String name) { - removeAttribute(name); - } - - @Override - public void invalidate() { - sessionManager.remove(id); - } - - @Override - public boolean isNew() { - return lastAccessedTime > 0; - } -} diff --git a/firefly/src/main/java/com/firefly/server/session/HttpSessionManager.java b/firefly/src/main/java/com/firefly/server/session/HttpSessionManager.java deleted file mode 100644 index 38c630b20..000000000 --- a/firefly/src/main/java/com/firefly/server/session/HttpSessionManager.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.firefly.server.session; - -import javax.servlet.http.HttpSession; - -import com.firefly.server.http.Config; - -public interface HttpSessionManager { - boolean containsKey(String id); - - HttpSession remove(String id); - - HttpSession get(String id); - - HttpSession create(); - - int size(); - - Config getConfig(); -} diff --git a/firefly/src/main/java/com/firefly/server/session/LocalHttpSessionManager.java b/firefly/src/main/java/com/firefly/server/session/LocalHttpSessionManager.java deleted file mode 100644 index 90ad13d6c..000000000 --- a/firefly/src/main/java/com/firefly/server/session/LocalHttpSessionManager.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.firefly.server.session; - -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionEvent; - -import com.firefly.server.http.Config; -import com.firefly.utils.time.HashTimeWheel; -import com.firefly.utils.time.Millisecond100Clock; - -public class LocalHttpSessionManager implements HttpSessionManager { - - private ConcurrentHashMap map = new ConcurrentHashMap(); - private Config config; - private HashTimeWheel timeWheel = new HashTimeWheel(); - - public LocalHttpSessionManager(Config config) { - this.config = config; - timeWheel.start(); - } - - @Override - public boolean containsKey(String id) { - return map.containsKey(id); - } - - @Override - public HttpSession remove(String id) { - HttpSession session = map.remove(id); - if (session != null) - config.getHttpSessionListener().sessionDestroyed( - new HttpSessionEvent(session)); - - return session; - } - - @Override - public HttpSession get(String id) { - return map.get(id); - } - - @Override - public HttpSession create() { - String id = UUID.randomUUID().toString().replace("-", ""); - long timeout = config.getMaxSessionInactiveInterval() * 1000; - HttpSessionImpl session = new HttpSessionImpl(this, id, - Millisecond100Clock.currentTimeMillis(), - config.getMaxSessionInactiveInterval()); - timeWheel.add(timeout, new TimeoutTask(session)); - map.put(id, session); - config.getHttpSessionListener().sessionCreated( - new HttpSessionEvent(session)); - return session; - } - - @Override - public int size() { - return map.size(); - } - - private class TimeoutTask implements Runnable { - private HttpSessionImpl session; - - public TimeoutTask(HttpSessionImpl session) { - this.session = session; - } - - @Override - public void run() { - long t = Millisecond100Clock.currentTimeMillis() - - session.getLastAccessedTime(); - long timeout = session.getMaxInactiveInterval() * 1000; - // System.out.println(timeout + "|" + t); - if (t > timeout) { - // System.out.println("removie session id: " + session.getId()); - remove(session.getId()); - } else { - if (timeout > 0) { - long nextCheckTime = timeout - t; - timeWheel.add(nextCheckTime, TimeoutTask.this); - } else if (timeout == 0) { - remove(session.getId()); - } - } - } - - } - - @Override - public Config getConfig() { - return config; - } - -} diff --git a/firefly/src/main/java/com/firefly/server/utils/StringParser.java b/firefly/src/main/java/com/firefly/server/utils/StringParser.java deleted file mode 100644 index 7803db93e..000000000 --- a/firefly/src/main/java/com/firefly/server/utils/StringParser.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF 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 com.firefly.server.utils; - - -/** - * Utility class for string parsing that is higher performance than - * StringParser for simple delimited text cases. Parsing is performed - * by setting the string, and then using the findXxxx() and - * skipXxxx() families of methods to remember significant - * offsets. To retrieve the parsed substrings, call the extract() - * method with the appropriate saved offset values. - * - * @author Craig R. McClanahan - * @version $Id: StringParser.java 939353 2010-04-29 15:50:43Z kkolinko $ - */ - -public final class StringParser { - - - // ----------------------------------------------------------- Constructors - - - /** - * Construct a string parser with no preset string to be parsed. - */ - public StringParser() { - - this(null); - - } - - - /** - * Construct a string parser that is initialized to parse the specified - * string. - * - * @param string The string to be parsed - */ - public StringParser(String string) { - - super(); - setString(string); - - } - - - // ----------------------------------------------------- Instance Variables - - - /** - * The characters of the current string, as a character array. Stored - * when the string is first specified to speed up access to characters - * being compared during parsing. - */ - private char chars[] = null; - - - /** - * The zero-relative index of the current point at which we are - * positioned within the string being parsed. NOTE: - * the value of this index can be one larger than the index of the last - * character of the string (i.e. equal to the string length) if you - * parse off the end of the string. This value is useful for extracting - * substrings that include the end of the string. - */ - private int index = 0; - - - /** - * The length of the String we are currently parsing. Stored when the - * string is first specified to avoid repeated recalculations. - */ - private int length = 0; - - - /** - * The String we are currently parsing. - */ - private String string = null; - - - // ------------------------------------------------------------- Properties - - - /** - * Return the zero-relative index of our current parsing position - * within the string being parsed. - */ - public int getIndex() { - - return (this.index); - - } - - - /** - * Return the length of the string we are parsing. - */ - public int getLength() { - - return (this.length); - - } - - - /** - * Return the String we are currently parsing. - */ - public String getString() { - - return (this.string); - - } - - - /** - * Set the String we are currently parsing. The parser state is also reset - * to begin at the start of this string. - * - * @param string The string to be parsed. - */ - public void setString(String string) { - - this.string = string; - if (string != null) { - this.length = string.length(); - chars = this.string.toCharArray(); - } else { - this.length = 0; - chars = new char[0]; - } - reset(); - - } - - - // --------------------------------------------------------- Public Methods - - - /** - * Advance the current parsing position by one, if we are not already - * past the end of the string. - */ - public void advance() { - - if (index < length) - index++; - - } - - - /** - * Extract and return a substring that starts at the specified position, - * and extends to the end of the string being parsed. If this is not - * possible, a zero-length string is returned. - * - * @param start Starting index, zero relative, inclusive - */ - public String extract(int start) { - - if ((start < 0) || (start >= length)) - return (""); - else - return (string.substring(start)); - - } - - - /** - * Extract and return a substring that starts at the specified position, - * and ends at the character before the specified position. If this is - * not possible, a zero-length string is returned. - * - * @param start Starting index, zero relative, inclusive - * @param end Ending index, zero relative, exclusive - */ - public String extract(int start, int end) { - - if ((start < 0) || (start >= end) || (end > length)) - return (""); - else - return (string.substring(start, end)); - - } - - - /** - * Return the index of the next occurrence of the specified character, - * or the index of the character after the last position of the string - * if no more occurrences of this character are found. The current - * parsing position is updated to the returned value. - * - * @param ch Character to be found - */ - public int findChar(char ch) { - - while ((index < length) && (ch != chars[index])) - index++; - return (index); - - } - - - /** - * Return the index of the next occurrence of a non-whitespace character, - * or the index of the character after the last position of the string - * if no more non-whitespace characters are found. The current - * parsing position is updated to the returned value. - */ - public int findText() { - - while ((index < length) && isWhite(chars[index])) - index++; - return (index); - - } - - - /** - * Return the index of the next occurrence of a whitespace character, - * or the index of the character after the last position of the string - * if no more whitespace characters are found. The current parsing - * position is updated to the returned value. - */ - public int findWhite() { - - while ((index < length) && !isWhite(chars[index])) - index++; - return (index); - - } - - - /** - * Reset the current state of the parser to the beginning of the - * current string being parsed. - */ - public void reset() { - - index = 0; - - } - - - /** - * Advance the current parsing position while it is pointing at the - * specified character, or until it moves past the end of the string. - * Return the final value. - * - * @param ch Character to be skipped - */ - public int skipChar(char ch) { - - while ((index < length) && (ch == chars[index])) - index++; - return (index); - - } - - - /** - * Advance the current parsing position while it is pointing at a - * non-whitespace character, or until it moves past the end of the string. - * Return the final value. - */ - public int skipText() { - - while ((index < length) && !isWhite(chars[index])) - index++; - return (index); - - } - - - /** - * Advance the current parsing position while it is pointing at a - * whitespace character, or until it moves past the end of the string. - * Return the final value. - */ - public int skipWhite() { - - while ((index < length) && isWhite(chars[index])) - index++; - return (index); - - } - - - // ------------------------------------------------------ Protected Methods - - - /** - * Is the specified character considered to be whitespace? - * - * @param ch Character to be checked - */ - protected boolean isWhite(char ch) { - - if ((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n')) - return (true); - else - return (false); - - } - - -} diff --git a/firefly/src/test/appHome/page/favicon.ico b/firefly/src/test/appHome/page/favicon.ico deleted file mode 100755 index a51a76e33..000000000 Binary files a/firefly/src/test/appHome/page/favicon.ico and /dev/null differ diff --git a/firefly/src/test/appHome/page/index.html b/firefly/src/test/appHome/page/index.html deleted file mode 100644 index 761e06594..000000000 --- a/firefly/src/test/appHome/page/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - -Welcome firefly! -测试页面 - - diff --git a/firefly/src/test/appHome/page/template/index.html b/firefly/src/test/appHome/page/template/index.html deleted file mode 100644 index 9b99b4242..000000000 --- a/firefly/src/test/appHome/page/template/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - -${hello} 测试一下页面 - - - -${hello} -
-
-
-
- 书名: -
-
- 价格: -
-
- 内容: -
-
- -
-
-
- -
-
-
- 书名2: -
-
- 价格2: -
-
- 内容2: -
-
- -
-
-
- -
- - - - - diff --git a/firefly/src/test/appHome/page/template/index_1.html b/firefly/src/test/appHome/page/template/index_1.html deleted file mode 100644 index d6023ec7f..000000000 --- a/firefly/src/test/appHome/page/template/index_1.html +++ /dev/null @@ -1,10 +0,0 @@ -

闲暇时的遐想

-
-文/〔俄罗斯〕弗·伊·克里别里 -  译/陈淑贤 -我一直企盼幸福,却感到这非常遥远,它又如此特殊,可望而不可即,几乎无法获得。殊不知幸福就在我身边,在我背后,它悄然无声,不事张扬。原来,我做的工作,度过的时日,与周围人的和谐共处——幸福就在其中。日复一日,年复一年,流年似水,只有到了回首时才顿悟:这就是幸福,幸福本来就一直与我相伴相随! -  人,实质上分为两类:第一类感觉自己是债权人,第二类则感觉自己是债务人。债权人杞人忧天,总是怨天尤人,认为所有的人——儿女、双亲、同事乃至人民——都在某些方面对不起他、亏欠他,造成他生活的不幸,葬送他的个人前程。债务人则经受着另外一种更高的境界,令人羡慕的痛苦:无法偿还亏欠生活、亏欠人民的深情厚谊。我更像债务人。我既不求功名,也不盼利禄,我感到幸福的是能够做一点有益处的事情。 -  人在很多方面依赖大自然,依赖天气状况。寒冷、阴雨,人对此无能为力,只有等待大自然状态的好转,继续生活。雅库特老人说得好:“寒冷、阴雨——很好!这些过后将出现太阳,将会暖洋洋!” -  寻找幸福——微妙而离奇。生活中一味追求功成名就,结果徒劳无益。应该老老实实地生活,接受大自然本身的馈赠。我们常常在遥远的某个地方寻找幸福,我们匆忙追逐转瞬即逝的光束。其实幸福就在身旁,在日常生活之中——达到力所能及的目标,称心的工作,家庭的和谐,亲人的安全无恙。不过,永久的、一成不变的幸福是没有的,幸福有时上升,过一段时间又会下降。下降时或平稳或急骤,甚至会严重跌伤。对这种升降只需耐心度过,好像忍耐恶劣天气或命中注定的其他不测一样。最后,临终时你有权说:“我是幸福的。” -  当有人问萧伯纳是否幸福时,他回答说,他幸福是因为没有时间考虑这一点。他的幸福在于工作,在于创作。幸福各式各样,也可划分阶段,犹如昼夜时间一样。如果一切都按大自然规定的那样:适时、适度、无忧、无虑,那么,可以认为,人幸福地度过了一生。 -
\ No newline at end of file diff --git a/firefly/src/test/appHome/page/template/index_2.html b/firefly/src/test/appHome/page/template/index_2.html deleted file mode 100644 index 05dbcc65b..000000000 --- a/firefly/src/test/appHome/page/template/index_2.html +++ /dev/null @@ -1,33 +0,0 @@ -

那看不见的崩溃

-
- 文/南桥 -马克·吐温曾称,真相往往比虚构作品更荒谬,因为虚构的文学作品里,你好歹得自圆其说。这就是我发觉社会新闻很好看的一个原因,因为它们有趣,且包含着人生本身的荒谬和悖论。例如近日我看到,纽约的Poughkeepsie 一高中两个女老师,平时好好的,说打就打了起来,其中一个英文老师,试图用螺丝刀要把对方扎死,被一个学生救了下来。此事成了雅虎头条新闻。由于是一黑人老师试图扎死白人老师,又被黑人学生救了下来,读者中有不少关于种族问题的争论。 -但此新闻更让我关注的,是一个平时好好的老师,怎么突然说崩溃就崩溃了。人心真是个深不可测的黑洞,不知道内里都发生着什么。 -我们上大学的时候,老师曾给我们看过一首小诗: -Richard Cory -Edwin Arlington Robinson - -Whenever Richard Cory went down town, -We people on the pavement looked at him: -He was a gentleman from sole to crown, -Clean favored, and imperially slim. - -And he was always quietly arrayed, -And he was always human when he talked; -But still he fluttered pulses when he said, -"Good-morning," and he glittered when he walked. - -And he was rich—yes, richer than a king— -And admirably schooled in every grace: -In fine, we thought that he was everything -To make us wish that we were in his place. - -So on we worked, and waited for the light, -And went without the meat, and cursed the bread; -And Richard Cory, one calm summer night, · -Went home and put a bullet through his head. -保罗·西蒙有首歌曲Richard Cory,唱出了这个故事。原诗是一叙事诗,大意是:理查德·科瑞走在小镇上,所有人都把他看作一位绅士,身材好,气质好,说话温文尔雅,跟人打声招呼,大家都激动得脉搏狂跳,他还富得像一国王。镇上其余的人都恨不得和这位天之骄子位置对调,大家看着他,都诅咒自己的工薪阶层的苦命,不能吃肉,只能骂娘。可是一个静悄悄的夏夜,科瑞回到家,一枪把自己脑袋爆掉了。 -时隔多年,我还记得这小诗,因为如果写成小说,恐怕得交代他这自杀的前因后果,得有情节铺垫,最为典型的是福克纳笔下的昆丁。如果是戏剧,那得把戏剧的冲突交代出来,引向最后这个结果。只有诗歌可以这么霸道,出其不意在前面这个幸福成功人士的人生交代之后,突然一下子告诉我们他回家让一颗子弹穿过自己的头颅。这中间到底是怎么回事,没有人知道,但是这寂静,却如同中国画的留白处一样,充满叙述,让人发挥无尽想象。更厉害的是,它如同一个在熟悉场景拍摄出来的恐怖片,时常让人产生联想和关联。 -比如今日,我还看到一个朋友在网上叙述自己人生的苦闷,我便想起了这首小诗来。很多时候,我们觉得周围的同事朋友和同学,怎么都过得那么顺利,我们恨不能和他们位置对调,好像就我们充满苦难,他人把事情都摆得平平的。其实你周围的人,或许心里有更大的挣扎,而你无法看见,突然有一天,整个人山崩地裂,你才幡然醒悟过来。所以如果大家有什么不开心的事情,不如说出来,或是换个方式写出来,是让大家开心开心吗?不是,是让你自己可以卸掉一些担子,好继续往前走。 -(出自:南桥的博客) -
\ No newline at end of file diff --git a/firefly/src/test/appHome/page/template/index_3.html b/firefly/src/test/appHome/page/template/index_3.html deleted file mode 100644 index 3c88a9316..000000000 --- a/firefly/src/test/appHome/page/template/index_3.html +++ /dev/null @@ -1,35 +0,0 @@ -

开溜,从拥挤的数字房间

-
-文/〔美〕威廉·鲍尔斯 -  译/陈盟 - - “比尔,你已经联系过不止10亿人啦,我很好奇接下来会发生什么呢?是青蛙发电邮、金鱼编网页,还是变形虫也开始更新博客了?” - -  这是微软公司推出的新广告中,喜剧演员杰瑞·宋飞和微软主席比尔·盖茨的对话。随后屏幕一片空白,只剩下4个字:永久上网。 - -  回到现实中,电邮、MSN、微博、视频、搜索等网络任务已经挤占了我们的大部分时间,由电脑、手机、MP4等构成的“屏幕世界”,也让生活变得越来越忙碌。身处“屏幕世界”越久,我们似乎越顺从,越安于思维在虚拟社区里安营扎寨。 - -  “在劳碌的网络生活中,我们早已失去了深度。”威廉·鲍尔斯在其新作《哈姆雷特的黑莓》一书中,提出了这样的质疑。 - -  他认为,我们向“屏幕世界”妥协太多,也背负了过多的信息,失去了思维、情感、关系还有工作及生活方方面面的深度,陷入了“数字极端主义”的时代。 - -  有调查显示,2008年全球就业人口中16%的人符合资深网迷的标准,而不久以后,这一数字将增加到40%。与此同时,著名调查公司Basex公布的一项调查结果说,很多企业陷入了“创新思维枯竭”的困境,该公司估计,由信息过载导致的损失高达9000亿元人民币。 - -  为此,一个名叫“信息过载研究小组”的机构宣称,要帮助人们限制上网时间,解决人们联系过度的问题。谷歌董事会主席兼公司总裁埃里克·施密特也劝说人们:“只有关了手机,关了电脑,你才能欣赏周围的每个人。”就连主打技术的《连线》杂志也专门用大标题提醒大家:“信息过载,小心大脑受煎熬。” - -  不过,单纯地限制时间、杜绝技术似乎并不能解决问题。鲍尔斯表示,每个人都有潜力过上有深度的生活,关键“取决于灵魂有多沉醉于当时的情境,是不是紧紧盯住手上的事情,没有神游天外”。 - -  他建议人们向同样身处技术变革时代的先哲们学习。比如,柏拉图就认为,要挣脱群体的桎梏,最原始的办法就是拉开地理距离,从繁忙的现实生活中抽身而出。正如《柏拉图对话集》中描述的,苏格拉底与友人斐德若一次出城到乡间散步,讨论一位智者吕西亚斯的演讲,简单的时空距离,两人就把城市的喧嚣抛到脑后,碰撞出了许多享誉后世的哲学观点。 - -  罗马人建立专门的法律和行政体系,依靠成文法、布告、政府公报的方式维系着帝国统治,书面语言深刻地改变着人们的生活。与此同时,各种信息也水涨船高,几乎达到波涛汹涌之势。对此,哲学家塞内加不管外界怎样天翻地覆,每天只选择一个想法来仔细思考,千万人中只选择一个人进行书信交流,使自己不受外界打扰。 - -  19世纪中期铁路和电报的发明,促使人类的联系日趋频繁,而此时的梭罗,在离人群不远的瓦尔登湖畔,搭起一座小木屋,创造了一片自我天地,以此享受平和、简单的心境。 - -  而身处电子时代的哲学家麦克卢汉也认为,我们可以尝试保持内心的沉静,主动出击,靠思想来抵御外界的侵扰,来享受幸福的温度。 - -  的确,大人物通过自我实践,试图引导我们走向数字时代的线下乌托邦。鲍尔斯自己在每周五关掉路由器,周末断网两天,享受家人间的沟通和交流,他将这称为“网络安息日”。 - -  看来,无论是改变时空距离,还是喝口心灵鸡汤,我们都需要内心强大的自我约束,懂得在喧嚣的数字时代构建避风港,以此触摸生活里的点滴幸福。网络房间到处都是,我们想怎么利用它,它就会变成什么样。抬起右手,摸摸自己的心,问问自己:“你想要的是什么?” - -  如果觉得数字房间太过拥挤,那就让我们一起开溜吧! -
\ No newline at end of file diff --git a/firefly/src/test/appHome/page/template/test.html b/firefly/src/test/appHome/page/template/test.html deleted file mode 100644 index d712575ad..000000000 --- a/firefly/src/test/appHome/page/template/test.html +++ /dev/null @@ -1,20 +0,0 @@ - - - -Welcome firefly - - - -Welcome ${name} - - diff --git a/firefly/src/test/appHome/page/test/index.html b/firefly/src/test/appHome/page/test/index.html deleted file mode 100644 index 1fc0cfdc8..000000000 --- a/firefly/src/test/appHome/page/test/index.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - -Welcome firefly! -测试页面 - -
-
-
- 姓名: -
-
- 编号: -
-
- 头像: -
-
- -
-
-
- - -
-
-
- 书名2: -
-
- 价格: -
-
- 内容: -
-
- -
-
-
- -
======================================
-
-
-
- 姓名: -
-
- 编号: -
-
- 头像: -
-
- 头像2: -
-
- -
-
-
- -
-
-
- 书名: -
-
- 价格: -
-
- 内容: -
-
- -
-
-
-
测试环境logintest
-
-
-
- 用户名: -
-
- 密码: -
-
- -
-
-
-
测试环境login
-
-
-
- 用户名: -
-
- 密码: -
-
- -
-
-
-
正式环境
-
-
-
- 用户名: -
-
- 密码: -
-
- -
-
-
- - \ No newline at end of file diff --git a/firefly/src/test/java/test/component/AddService.java b/firefly/src/test/java/test/component/AddService.java deleted file mode 100644 index 744fcde0f..000000000 --- a/firefly/src/test/java/test/component/AddService.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.component; - -public interface AddService { - int add(int x, int y); - - int getI(); -} diff --git a/firefly/src/test/java/test/component/FieldInject.java b/firefly/src/test/java/test/component/FieldInject.java deleted file mode 100644 index e4ea3f523..000000000 --- a/firefly/src/test/java/test/component/FieldInject.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.component; - -public interface FieldInject { - int add(int x, int y); - - int add2(int x, int y); -} diff --git a/firefly/src/test/java/test/component/MethodInject.java b/firefly/src/test/java/test/component/MethodInject.java deleted file mode 100644 index bdfc29dfb..000000000 --- a/firefly/src/test/java/test/component/MethodInject.java +++ /dev/null @@ -1,5 +0,0 @@ -package test.component; - -public interface MethodInject { - int add(int x, int y); -} diff --git a/firefly/src/test/java/test/component/impl/AddServiceImpl.java b/firefly/src/test/java/test/component/impl/AddServiceImpl.java deleted file mode 100644 index 779259de6..000000000 --- a/firefly/src/test/java/test/component/impl/AddServiceImpl.java +++ /dev/null @@ -1,20 +0,0 @@ -package test.component.impl; - -import test.component.AddService; -import com.firefly.annotation.Component; - -@Component("addService") -public class AddServiceImpl implements AddService { - private int i = 0; - - @Override - public int add(int x, int y) { - return x + y; - } - - @Override - public int getI() { - return i++; - } - -} diff --git a/firefly/src/test/java/test/component/impl/FieldInjectImpl.java b/firefly/src/test/java/test/component/impl/FieldInjectImpl.java deleted file mode 100644 index 48acab2b9..000000000 --- a/firefly/src/test/java/test/component/impl/FieldInjectImpl.java +++ /dev/null @@ -1,26 +0,0 @@ -package test.component.impl; - -import test.component.FieldInject; -import test.component.AddService; -import com.firefly.annotation.Component; -import com.firefly.annotation.Inject; - -@Component("fieldInject") -public class FieldInjectImpl implements FieldInject { - - @Inject - private AddService addService; - @Inject("addService") - private AddService addService2; - - @Override - public int add(int x, int y) { - return addService.add(x, y); - } - - @Override - public int add2(int x, int y) { - return addService2.add(x, y); - } - -} diff --git a/firefly/src/test/java/test/component/impl/MethodInjectImpl.java b/firefly/src/test/java/test/component/impl/MethodInjectImpl.java deleted file mode 100644 index 87d932a12..000000000 --- a/firefly/src/test/java/test/component/impl/MethodInjectImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package test.component.impl; - -import com.firefly.annotation.Component; -import com.firefly.annotation.Inject; - -import test.component.AddService; -import test.component.MethodInject; - -@Component("methodInject") -public class MethodInjectImpl implements MethodInject { - - private AddService addService; - - @Inject - public void init(AddService addService) { - this.addService = addService; - } - - @Override - public int add(int x, int y) { - return addService.add(x, y); - } - -} diff --git a/firefly/src/test/java/test/component2/MethodInject2.java b/firefly/src/test/java/test/component2/MethodInject2.java deleted file mode 100644 index 499b70afe..000000000 --- a/firefly/src/test/java/test/component2/MethodInject2.java +++ /dev/null @@ -1,7 +0,0 @@ -package test.component2; - -public interface MethodInject2 { - int add(int x, int y); - - Integer getNum(); -} diff --git a/firefly/src/test/java/test/component2/impl/MethodInjectImpl2.java b/firefly/src/test/java/test/component2/impl/MethodInjectImpl2.java deleted file mode 100644 index 68b0abebe..000000000 --- a/firefly/src/test/java/test/component2/impl/MethodInjectImpl2.java +++ /dev/null @@ -1,36 +0,0 @@ -package test.component2.impl; - -import test.component.AddService; -import test.component.FieldInject; -import test.component2.MethodInject2; - -import com.firefly.annotation.Component; -import com.firefly.annotation.Inject; - -@Component("methodInject2") -public class MethodInjectImpl2 implements MethodInject2 { - - @Inject - private Integer num = 3; - @Inject - public AddService addService; - protected FieldInject fieldInject; - - @Inject - public void init(AddService addService, FieldInject fieldInject, String str) { - this.addService = addService; - this.fieldInject = fieldInject; - //TODO 此处测试注入对象图是否完整 - fieldInject.add(3, 4); - } - - @Override - public int add(int x, int y) { - return fieldInject.add(x, y); - } - - public Integer getNum() { - return num; - } - -} diff --git a/firefly/src/test/java/test/component3/ArrayService.java b/firefly/src/test/java/test/component3/ArrayService.java deleted file mode 100644 index 70443651b..000000000 --- a/firefly/src/test/java/test/component3/ArrayService.java +++ /dev/null @@ -1,32 +0,0 @@ -package test.component3; - -public class ArrayService { - private String[] strArray; - private int[] intArray; - private Object[] objArray; - - public Object[] getObjArray() { - return objArray; - } - - public void setObjArray(Object[] objArray) { - this.objArray = objArray; - } - - public String[] getStrArray() { - return strArray; - } - - public void setStrArray(String[] strArray) { - this.strArray = strArray; - } - - public int[] getIntArray() { - return intArray; - } - - public void setIntArray(int[] intArray) { - this.intArray = intArray; - } - -} diff --git a/firefly/src/test/java/test/component3/CollectionService.java b/firefly/src/test/java/test/component3/CollectionService.java deleted file mode 100644 index be5d136df..000000000 --- a/firefly/src/test/java/test/component3/CollectionService.java +++ /dev/null @@ -1,26 +0,0 @@ -package test.component3; - -import java.util.LinkedList; -import java.util.Set; - -public class CollectionService extends ArrayService { - private LinkedList list; - private Set set; - - public LinkedList getList() { - return list; - } - - public void setList(LinkedList list) { - this.list = list; - } - - public Set getSet() { - return set; - } - - public void setSet(Set set) { - this.set = set; - } - -} diff --git a/firefly/src/test/java/test/component3/MapService.java b/firefly/src/test/java/test/component3/MapService.java deleted file mode 100644 index 3ceaf4a4e..000000000 --- a/firefly/src/test/java/test/component3/MapService.java +++ /dev/null @@ -1,16 +0,0 @@ -package test.component3; - -import java.util.Map; - -public class MapService { - private Map map; - - public Map getMap() { - return map; - } - - public void setMap(Map map) { - this.map = map; - } - -} diff --git a/firefly/src/test/java/test/component3/Person.java b/firefly/src/test/java/test/component3/Person.java deleted file mode 100644 index 13371af75..000000000 --- a/firefly/src/test/java/test/component3/Person.java +++ /dev/null @@ -1,18 +0,0 @@ -package test.component3; - -public class Person { - private String name; - private Integer age; - public String getName() { - return name; - } - public void setName(String name) { - this.name = name; - } - public Integer getAge() { - return age; - } - public void setAge(Integer age) { - this.age = age; - } -} diff --git a/firefly/src/test/java/test/component3/PersonService.java b/firefly/src/test/java/test/component3/PersonService.java deleted file mode 100644 index e6d80e9df..000000000 --- a/firefly/src/test/java/test/component3/PersonService.java +++ /dev/null @@ -1,37 +0,0 @@ -package test.component3; - -import java.util.List; - -public class PersonService { - private Person person, person2; - - private List testList; - - public void setPerson(Person person) { - this.person = person; - } - - public void setPersion2(Person person2) { - this.person2 = person2; - } - - public void setTestList(List testList) { - this.testList = testList; - } - - public List getTestList() { - return testList; - } - - public Person getPerson2() { - return person2; - } - - public void setPerson2(Person person2) { - this.person2 = person2; - } - - public Person getPerson() { - return person; - } -} diff --git a/firefly/src/test/java/test/controller/Book.java b/firefly/src/test/java/test/controller/Book.java deleted file mode 100644 index bf9895300..000000000 --- a/firefly/src/test/java/test/controller/Book.java +++ /dev/null @@ -1,49 +0,0 @@ -package test.controller; - -public class Book { - private String text, title; - private Integer id; - private Double price; - private Boolean sell; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Boolean getSell() { - return sell; - } - - public void setSell(Boolean sell) { - this.sell = sell; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public Double getPrice() { - return price; - } - - public void setPrice(Double price) { - this.price = price; - } - -} diff --git a/firefly/src/test/java/test/controller/FoodController.java b/firefly/src/test/java/test/controller/FoodController.java deleted file mode 100644 index c1589d135..000000000 --- a/firefly/src/test/java/test/controller/FoodController.java +++ /dev/null @@ -1,29 +0,0 @@ -package test.controller; - -import javax.servlet.http.HttpServletRequest; - -import test.mixed.Food; - -import com.firefly.annotation.Controller; -import com.firefly.annotation.RequestMapping; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.view.JspView; - -@Controller -public class FoodController { - - @RequestMapping(value = "/food") - public View getFood(HttpServletRequest request) { - Food food = new Food(); - food.setName("orange"); - food.setPrice(3.5); - request.setAttribute("fruit", food); - return new JspView("/food.jsp"); - } - - @RequestMapping(value = "/food/view1") - public View getFoodView(HttpServletRequest request) { - return new JspView("/foodView1.jsp"); - } - -} diff --git a/firefly/src/test/java/test/controller/HelloController.java b/firefly/src/test/java/test/controller/HelloController.java deleted file mode 100644 index d14d638bd..000000000 --- a/firefly/src/test/java/test/controller/HelloController.java +++ /dev/null @@ -1,66 +0,0 @@ -package test.controller; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.firefly.annotation.Controller; -import com.firefly.annotation.HttpParam; -import com.firefly.annotation.PathVariable; -import com.firefly.annotation.RequestMapping; -import com.firefly.mvc.web.HttpMethod; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.view.JsonView; -import com.firefly.mvc.web.view.JspView; -import com.firefly.mvc.web.view.RedirectView; -import com.firefly.mvc.web.view.TextView; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -@Controller -public class HelloController { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - @RequestMapping(value = "/hello") - public View index(HttpServletRequest request) { - request.setAttribute("hello", "你好 firefly!"); - return new JspView("/index.jsp"); - } - - @RequestMapping(value = "/hello/text") - public View text(HttpServletRequest request) { - log.info("into text output >>>>>>>>>>>>>>>>>"); - return new TextView("文本输出"); - } - - @RequestMapping(value = "/hello/text-?/?-?") - public View text2(HttpServletRequest request, @PathVariable String[] args) { - return new TextView("text-" + args[0] + "-" + args[1] + "-" + args[2]); - } - - @RequestMapping(value = "/hello?") - public View text3(HttpServletRequest request, @PathVariable String[] args) { - return new TextView("text-" + args[0]); - } - - @RequestMapping(value = "/hello/redirect") - public View hello5(HttpServletRequest request, - HttpServletResponse response) { - return new RedirectView("/hello"); - } - - @RequestMapping(value = "/book/value") - public View bookValue(HttpServletRequest request, @HttpParam Book book) { - request.setAttribute("book", book); - return new JspView("/book.jsp"); - } - - @RequestMapping(value = "/book/create", method = HttpMethod.POST) - public View createBook(@HttpParam("book") Book book) { - return new JspView("/book.jsp"); - } - - @RequestMapping(value = "/book/json", method = HttpMethod.POST) - public View getBook(@HttpParam("book") Book book) { - return new JsonView(book); - } -} diff --git a/firefly/src/test/java/test/http/MockSession.java b/firefly/src/test/java/test/http/MockSession.java deleted file mode 100644 index ef55cec2d..000000000 --- a/firefly/src/test/java/test/http/MockSession.java +++ /dev/null @@ -1,129 +0,0 @@ -package test.http; - -import java.net.InetSocketAddress; -import java.util.HashMap; -import java.util.Map; - -import com.firefly.net.Session; -import com.firefly.server.http.HttpServletRequestImpl; - -public class MockSession implements Session { - - Map map = new HashMap(); - HttpServletRequestImpl request = null; - - @Override - public void setAttribute(String key, Object value) { - map.put(key, value); - } - - @Override - public Object getAttribute(String key) { - return map.get(key); - } - - @Override - public void removeAttribute(String key) { - map.remove(key); - } - - @Override - public void clearAttributes() { - map.clear(); - } - - @Override - public void fireReceiveMessage(Object message) { - request = (HttpServletRequestImpl)message; - - } - - @Override - public void encode(Object message) { - // TODO Auto-generated method stub - - } - - @Override - public void write(Object object) { - // TODO Auto-generated method stub - - } - - @Override - public int getInterestOps() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int getSessionId() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long getOpenTime() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long getLastReadTime() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long getLastWrittenTime() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long getLastActiveTime() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long getReadBytes() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public long getWrittenBytes() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void close(boolean immediately) { - // TODO Auto-generated method stub - - } - - @Override - public int getState() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public boolean isOpen() { - // TODO Auto-generated method stub - return false; - } - - @Override - public InetSocketAddress getLocalAddress() { - return new InetSocketAddress("localhost", 80); - } - - @Override - public InetSocketAddress getRemoteAddress() { - return new InetSocketAddress("localhost", 9999); - } - -} diff --git a/firefly/src/test/java/test/http/ServerDebug.java b/firefly/src/test/java/test/http/ServerDebug.java deleted file mode 100644 index f30d6e254..000000000 --- a/firefly/src/test/java/test/http/ServerDebug.java +++ /dev/null @@ -1,50 +0,0 @@ -package test.http; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -public class ServerDebug { - - public static void main(String[] args) throws Throwable{ - String msg = - "GET /test/index.html HTTP/1.1\r\n" + - "Host: 127.0.0.1\r\n" + - "Range: bytes=3-10,-7\r\n\r\n"; -// String msg = -// "GET /app/index HTTP/1.1\r\n" + -// "Host: 127.0.0.1\r\n\r\n"; -// String msg = -// "POST /index.html HTTP/1.1\r\n" + -// "Host: 127.0.0.1\r\n" + -// "Expect: 100-continue\r\n\r\n"; - test(msg); - } - - public static void test(String msg) throws Throwable{ - Socket socket = new Socket("localhost", 6655); - OutputStream out = socket.getOutputStream(); - out.write(msg.getBytes("UTF-8")); - out.flush(); - InputStream in = socket.getInputStream(); - byte[] ret = new byte[32 * 1024]; - in.read(ret); - System.out.print(new String(ret, "UTF-8")); - - ret = new byte[32 * 1024]; - in.read(ret); - System.out.print(new String(ret, "UTF-8")); - - ret = new byte[32 * 1024]; - in.read(ret); - System.out.print(new String(ret, "UTF-8")); - - ret = new byte[32 * 1024]; - in.read(ret); - System.out.print(new String(ret, "UTF-8")); - out.close(); - in.close(); - socket.close(); - } - -} diff --git a/firefly/src/test/java/test/http/ServerDemo.java b/firefly/src/test/java/test/http/ServerDemo.java deleted file mode 100644 index 360bbb4b4..000000000 --- a/firefly/src/test/java/test/http/ServerDemo.java +++ /dev/null @@ -1,14 +0,0 @@ -package test.http; - -import java.io.File; - -import com.firefly.server.ServerBootstrap; - -public class ServerDemo { - - public static void main(String[] args) throws Throwable { - String serverHome = new File(ServerBootstrap.class.getResource("/page").toURI()).getAbsolutePath(); - ServerBootstrap.start("firefly-server.xml", serverHome, "localhost", 6655); - } - -} diff --git a/firefly/src/test/java/test/http/TestHttpDecoder.java b/firefly/src/test/java/test/http/TestHttpDecoder.java deleted file mode 100644 index 2e0ecf0e4..000000000 --- a/firefly/src/test/java/test/http/TestHttpDecoder.java +++ /dev/null @@ -1,421 +0,0 @@ -package test.http; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; - -import java.io.BufferedReader; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.Enumeration; - -import javax.servlet.ServletInputStream; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.server.http.Config; -import com.firefly.server.http.HttpDecoder; -import com.firefly.server.http.HttpServletRequestImpl; -import com.firefly.server.http.HttpServletResponseImpl; - -public class TestHttpDecoder { - private static final Config config = new Config(); - private static final HttpDecoder httpDecoder = new HttpDecoder(config); - - @Test - public void testRequestLine() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hel" - .getBytes(config.getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n".getBytes(config - .getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getMethod(), is("GET")); - Assert.assertThat(req.getRequestURI(), is("/firefly-demo/app/hello")); - Assert.assertThat(req.getProtocol(), is("HTTP/1.1")); - } - - @Test - public void testRequestLine2() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hello HTTP/1.1\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getMethod(), is("GET")); - Assert.assertThat(req.getRequestURI(), is("/firefly-demo/app/hello")); - Assert.assertThat(req.getProtocol(), is("HTTP/1.1")); - } - - @Test - public void testRequestLine3() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hello?query=3.3&test=4 HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getMethod(), is("GET")); - Assert.assertThat(req.getRequestURI(), is("/firefly-demo/app/hello")); - Assert.assertThat(req.getProtocol(), is("HTTP/1.1")); - Assert.assertThat(req.getQueryString(), is("query=3.3&test=4")); - } - - @Test - public void testHead() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hel" - .getBytes(config.getEncoding()); - byte[] buf2 = "lo?query=3.3&test=4 HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getHeader("host"), is("127.0.0.1")); - Assert.assertThat(req.getHeader("Host"), is("127.0.0.1")); - } - - @Test - public void testHead2() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hel" - .getBytes(config.getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getHeader("host"), is("127.0.0.1")); - Assert.assertThat(req.getHeader("connection"), is("keep-alive")); - Assert.assertThat(req.getHeader("Accept-Language"), - is("zh-CN,zh;q=0.8")); - } - - @Test - public void testHead3() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hello HTTP/1.1\r\n" - .getBytes(config.getEncoding()); - byte[] buf2 = "Host:127.0.0.1\r\n".getBytes(config.getEncoding()); - byte[] buf3 = "Accept-Language:zh-CN,zh;q=0.8\r\nConnection:keep-alive\r\n" - .getBytes(config.getEncoding()); - byte[] buf4 = "Accept-Encoding: gzip,deflate,sdch\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getHeader("host"), is("127.0.0.1")); - Assert.assertThat(req.getHeader("connection"), is("keep-alive")); - Assert.assertThat(req.getHeader("Accept-Language"), - is("zh-CN,zh;q=0.8")); - Assert.assertThat(req.getHeader("Accept-Encoding"), - is("gzip,deflate,sdch")); - } - - @Test - public void testHead4() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hello HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN,zh;q=0.8\r\nConnection:keep-alive\r\nAccept-Encoding: gzip,deflate,sdch\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getMethod(), is("GET")); - Assert.assertThat(req.getRequestURI(), is("/firefly-demo/app/hello")); - Assert.assertThat(req.getProtocol(), is("HTTP/1.1")); - Assert.assertThat(req.getHeader("host"), is("127.0.0.1")); - Assert.assertThat(req.getHeader("connection"), is("keep-alive")); - Assert.assertThat(req.getHeader("Accept-Language"), - is("zh-CN,zh;q=0.8")); - Assert.assertThat(req.getHeader("Accept-Encoding"), - is("gzip,deflate,sdch")); - } - - @Test - public void testBody() throws Throwable { - byte[] buf1 = "POST /firefly-demo/app/hel".getBytes(config - .getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n".getBytes(config - .getEncoding()); - byte[] buf4 = "Accept-Encoding:gzip,deflate,sdch\r\nContent-Type:app" - .getBytes(config.getEncoding()); - byte[] buf5 = "lication/x-www-form-urlencoded\r\nContent-Length:34\r\n\r\n" - .getBytes(config.getEncoding()); - byte[] buf6 = "title=%E6%B5%8B%E8%AF%95&price=3.3".getBytes(config - .getEncoding()); - - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4), ByteBuffer.wrap(buf5), - ByteBuffer.wrap(buf6) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getParameter("title"), is("测试")); - Assert.assertThat(req.getParameter("price"), is("3.3")); - } - - @Test - public void testBody2() throws Throwable { - byte[] buf1 = "POST /firefly-demo/app/hel".getBytes(config - .getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n".getBytes(config - .getEncoding()); - byte[] buf4 = "Accept-Encoding:gzip,deflate,sdch\r\nContent-Type:app" - .getBytes(config.getEncoding()); - byte[] buf5 = "lication/x-www-form-urlencoded\r\nContent-Length:31\r\n\r\n" - .getBytes(config.getEncoding()); - byte[] buf6 = "title=%E6%B5%8B%E8%AF%95&price=".getBytes(config - .getEncoding()); - - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4), ByteBuffer.wrap(buf5), - ByteBuffer.wrap(buf6) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - Assert.assertThat(req.getParameter("title"), is("测试")); - Assert.assertThat(req.getParameter("price"), is("")); - } - - @Test - public void testBody3() throws Throwable { - byte[] buf1 = "POST /firefly-demo/app/hel".getBytes(config - .getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n".getBytes(config - .getEncoding()); - byte[] buf4 = "Accept-Encoding:gzip,deflate,sdch\r\nContent-Type:app" - .getBytes(config.getEncoding()); - byte[] buf5 = "lication/x-www-form-urlencoded\r\nContent-Length:24\r\n\r\ntit" - .getBytes(config.getEncoding()); - byte[] buf6 = "le=%E6%B5%8B%E8%AF%95".getBytes(config.getEncoding()); - - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4), ByteBuffer.wrap(buf5), - ByteBuffer.wrap(buf6) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - System.out.println(req.getParameter("title")); - System.out.println(req.getLocale().toString()); - System.out.println(req.getRequestURL().toString()); - Assert.assertThat(req.getRequestURL().toString(), is("http://localhost/firefly-demo/app/hello")); - Assert.assertThat(req.getLocale().toString(), is("zh_CN")); - Assert.assertThat(req.getParameter("title"), is("测试")); - Assert.assertThat(req.getParameter("price"), nullValue()); - Assert.assertThat(req.getContentLength(), is(24)); - Assert.assertThat(req.getContentType(), - is("application/x-www-form-urlencoded")); - } - - @Test - public void testBody4() throws Throwable { - byte[] buf1 = "POST /firefly-demo/app/hel".getBytes(config - .getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n".getBytes(config - .getEncoding()); - byte[] buf4 = "Accept-Encoding:gzip,deflate,sdch".getBytes(config - .getEncoding()); - byte[] buf5 = "\r\nContent-Length:47\r\n\r\n".getBytes(config - .getEncoding()); - byte[] buf6 = "| 90 | 测试 | 测试当前book | 3.3 | true |".getBytes(config - .getEncoding()); - - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4), ByteBuffer.wrap(buf5), - ByteBuffer.wrap(buf6) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - ServletInputStream input = req.getInputStream(); - byte[] temp = new byte[30]; - byte[] data = null; - for (int len = 0; (len = input.read(temp)) != -1;) { - if (data == null) { - data = new byte[len]; - System.arraycopy(temp, 0, data, 0, len); - } else { - byte[] pre = data; - data = new byte[pre.length + len]; - System.arraycopy(pre, 0, data, 0, pre.length); - System.arraycopy(temp, 0, data, pre.length, len); - } - } - input.close(); - - Assert.assertThat(new String(data, config.getEncoding()), is("| 90 | 测试 | 测试当前book | 3.3 | true |")); - Assert.assertThat(data, is(buf6)); - } - - @Test - public void testBody5() throws Throwable { - byte[] buf1 = "POST /firefly-demo/app/hel".getBytes(config - .getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n".getBytes(config - .getEncoding()); - byte[] buf4 = "Accept-Encoding:gzip,deflate,sdch".getBytes(config - .getEncoding()); - byte[] buf5 = "\r\nContent-Length:47\r\n\r\n".getBytes(config - .getEncoding()); - byte[] buf6 = "| 90 | 测试 | 测试当前book | 3.3 | true |".getBytes(config - .getEncoding()); - - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4), ByteBuffer.wrap(buf5), - ByteBuffer.wrap(buf6) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - - HttpServletRequestImpl req = session.request; - BufferedReader reader = req.getReader(); - StringBuilder sb = new StringBuilder(); - for (String line = null; (line = reader.readLine()) != null;) { - sb.append(line); - } - reader.close(); - - Assert.assertThat(sb.toString(), is("| 90 | 测试 | 测试当前book | 3.3 | true |")); - } - - @Test - public void testGetSessionId() { - String sessionIdName = "jsessionid"; - String uri = "/app/hello;jsessionid=33342424jkl#apple"; - Assert.assertThat(HttpServletRequestImpl.getSessionId(uri, sessionIdName), is("33342424jkl")); - uri = "/app/hello;jsessionid=33342424jkl"; - Assert.assertThat(HttpServletRequestImpl.getSessionId(uri, sessionIdName), is("33342424jkl")); - - uri = "http://www.firefly.com/app/hello?q=333"; - Assert.assertThat(HttpServletResponseImpl.toEncoded(uri, "ccccccccccccc", sessionIdName), is("http://www.firefly.com/app/hello;jsessionid=ccccccccccccc?q=333")); - uri = "http://www.firefly.com/app/hello#ddddc?q=333"; - Assert.assertThat(HttpServletResponseImpl.toEncoded(uri, "ccccccccccccc", sessionIdName), is("http://www.firefly.com/app/hello;jsessionid=ccccccccccccc#ddddc?q=333")); - } - - public static void main(String[] args) throws Throwable { -// TestHttpDecoder t = new TestHttpDecoder(); -// t.testBody3(); - String sessionIdName = "jsessionid"; - String uri = "/app/hello;jsessionid=33342424jkl#apple"; - System.out.println(uri.contains(";jsessionid=")); - System.out.println(HttpServletRequestImpl.getSessionId(uri, sessionIdName)); - uri = "/app/hello;jsessionid=33342424jkl"; - System.out.println(HttpServletRequestImpl.getSessionId(uri, sessionIdName)); - - uri = "http://www.firefly.com/app/hello?q=333"; - System.out.println(uri.contains(";jsessionid=")); - System.out.println(HttpServletResponseImpl.toEncoded(uri, "ccccccccccccc", sessionIdName)); - uri = "http://www.firefly.com/app/hello#ddddc?q=333"; - System.out.println(HttpServletResponseImpl.toEncoded(uri, "ccccccccccccc", sessionIdName)); - - - } - - public static void test1() throws Throwable { - byte[] buf1 = "GET /firefly-demo/app/hel" - .getBytes(config.getEncoding()); - byte[] buf2 = "lo HTTP/1.1\r\nHost:127.0.0.1\r\nAccept-Language:zh-CN," - .getBytes(config.getEncoding()); - byte[] buf3 = "zh;q=0.8\r\nConnection:keep-alive\r\n".getBytes(config - .getEncoding()); - byte[] buf4 = "Accept-Encoding:gzip,deflate,sdch\r\n\r\n" - .getBytes(config.getEncoding()); - ByteBuffer[] buf = new ByteBuffer[] { ByteBuffer.wrap(buf1), - ByteBuffer.wrap(buf2), ByteBuffer.wrap(buf3), - ByteBuffer.wrap(buf4) }; - MockSession session = new MockSession(); - - for (int i = 0; i < buf.length; i++) { - httpDecoder.decode(buf[i], session); - } - HttpServletRequestImpl req = session.request; - System.out.println(req.getMethod()); - System.out.println(req.getRequestURI()); - System.out.println(req.getProtocol()); - System.out.println(req.getHeader("Host")); - System.out.println(req.getHeader("Accept-Language")); - System.out.println(req.getHeader("Connection")); - System.out.println(req.getHeader("Accept-Encoding")); - System.out.println(req.toString()); - System.out.println((req.getContextPath() + req.getServletPath()).length()); - - Enumeration enumeration = req.getHeaders("Accept-Encoding"); - while (enumeration.hasMoreElements()) { - System.out.println(">>" + enumeration.nextElement()); - } - - System.out.println(HttpServletResponseImpl.GMT_FORMAT - .format(new Date())); - } - -} diff --git a/firefly/src/test/java/test/interceptor/FoodInterceptor.java b/firefly/src/test/java/test/interceptor/FoodInterceptor.java deleted file mode 100644 index da9c35b9d..000000000 --- a/firefly/src/test/java/test/interceptor/FoodInterceptor.java +++ /dev/null @@ -1,35 +0,0 @@ -package test.interceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import test.mixed.Food; -import test.mixed.FoodService; - -import com.firefly.annotation.Inject; -import com.firefly.annotation.Interceptor; -import com.firefly.mvc.web.HandlerChain; -import com.firefly.mvc.web.View; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -@Interceptor(uri = "/food*") -public class FoodInterceptor { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - @Inject - private FoodService foodService; - - public View dispose(HandlerChain chain, HttpServletRequest request, HttpServletResponse response) { - Food food = new Food(); - food.setName("apple"); - food.setPrice(8.0); - request.setAttribute("fruit0", food); - - food = foodService.getFood("strawberry"); - request.setAttribute("strawberry", food); - log.info("food interceptor 0 : {}", food); - - return chain.doNext(request, response, chain); - } -} diff --git a/firefly/src/test/java/test/interceptor/FoodInterceptor2.java b/firefly/src/test/java/test/interceptor/FoodInterceptor2.java deleted file mode 100644 index a29aa4473..000000000 --- a/firefly/src/test/java/test/interceptor/FoodInterceptor2.java +++ /dev/null @@ -1,31 +0,0 @@ -package test.interceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import test.mixed.Food; -import test.mixed.FoodService; -import com.firefly.annotation.Inject; -import com.firefly.annotation.Interceptor; -import com.firefly.mvc.web.HandlerChain; -import com.firefly.mvc.web.View; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -@Interceptor(uri = "/food/view*", order = 1) -public class FoodInterceptor2 { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - @Inject - private FoodService foodService; - - public View dispose(HttpServletResponse response, HttpServletRequest request, HandlerChain chain) { - Food food = new Food(); - food.setName("ananas"); - food.setPrice(4.99); - request.setAttribute("fruit1", food); - log.info("start food interceptor 1"); - View view = chain.doNext(request, response, chain); - log.info("end food interceptor 1 : {}", food); - return view; - } -} diff --git a/firefly/src/test/java/test/interceptor/FoodInterceptor3.java b/firefly/src/test/java/test/interceptor/FoodInterceptor3.java deleted file mode 100644 index 98da141e0..000000000 --- a/firefly/src/test/java/test/interceptor/FoodInterceptor3.java +++ /dev/null @@ -1,25 +0,0 @@ -package test.interceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import test.mixed.Food; - -import com.firefly.annotation.Interceptor; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.view.JsonView; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -@Interceptor(uri = "/*/view*", order = 2) -public class FoodInterceptor3 { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public View dispose(HttpServletRequest request, HttpServletResponse response) { - Food food = new Food(); - food.setName("banana"); - food.setPrice(3.99); - log.info("food interceptor 2 : {}", food); - return new JsonView(food); - } -} diff --git a/firefly/src/test/java/test/ioc/TestAnnotationIoc.java b/firefly/src/test/java/test/ioc/TestAnnotationIoc.java deleted file mode 100644 index c46e50844..000000000 --- a/firefly/src/test/java/test/ioc/TestAnnotationIoc.java +++ /dev/null @@ -1,54 +0,0 @@ -package test.ioc; - -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; - -import org.junit.Assert; -import org.junit.Test; - -import test.component.AddService; -import test.component.FieldInject; -import test.component.MethodInject; -import test.component2.MethodInject2; - -import com.firefly.core.ApplicationContext; -import com.firefly.core.XmlApplicationContext; - - -public class TestAnnotationIoc { -// private static Logger log = LoggerFactory.getLogger(TestAnnotationIoc.class); - public static ApplicationContext applicationContext = new XmlApplicationContext("annotation-config.xml"); - - @Test - public void testFieldInject() { - FieldInject fieldInject = applicationContext.getBean("fieldInject"); - Assert.assertThat(fieldInject.add(5, 4), is(9)); - Assert.assertThat(fieldInject.add2(5, 4), is(9)); - - fieldInject = applicationContext.getBean(FieldInject.class); - Assert.assertThat(fieldInject.add(5, 4), is(9)); - Assert.assertThat(fieldInject.add2(5, 4), is(9)); - } - - @Test - public void testMethodInject() { - MethodInject m = applicationContext.getBean("methodInject"); - Assert.assertThat(m.add(5, 4), is(9)); - } - - @Test - public void testMethodInject2() { - MethodInject2 m = applicationContext.getBean("methodInject2"); - Assert.assertThat(m.add(5, 5), is(10)); - Assert.assertThat(m.getNum(), is(3)); - } - - @Test - public void testSingle() { - AddService t = applicationContext.getBean("addService"); - t.getI(); - t.getI(); - Assert.assertThat(t.getI(), greaterThan(0)); - } - -} diff --git a/firefly/src/test/java/test/ioc/TestMixIoc.java b/firefly/src/test/java/test/ioc/TestMixIoc.java deleted file mode 100644 index 9580b42e3..000000000 --- a/firefly/src/test/java/test/ioc/TestMixIoc.java +++ /dev/null @@ -1,58 +0,0 @@ -package test.ioc; - -import static org.hamcrest.Matchers.is; - -import org.junit.Assert; -import org.junit.Test; -import test.mixed.Food; -import test.mixed.FoodService; -import test.mixed.FoodService2; -import com.firefly.core.ApplicationContext; -import com.firefly.core.XmlApplicationContext; -import com.firefly.core.support.exception.BeanDefinitionParsingException; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class TestMixIoc { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - public static ApplicationContext applicationContext = new XmlApplicationContext( - "mixed-config.xml"); - - @Test - public void testInject() { - FoodService2 foodService2 = applicationContext.getBean("foodService2"); - Food food = foodService2.getFood("apple"); - log.debug(food.getName()); - Assert.assertThat(food.getPrice(), is(5.3)); - - FoodService foodService = applicationContext.getBean(FoodService.class); - food = foodService.getFood("strawberry"); - log.debug(food.getName()); - Assert.assertThat(food.getPrice(), is(10.00)); - } - - @Test(expected = BeanDefinitionParsingException.class) - public void testErrorConfig1() { - new XmlApplicationContext("error-config1.xml"); - } - - @Test(expected = BeanDefinitionParsingException.class) - public void testErrorConfig2() { - new XmlApplicationContext("error-config2.xml"); - } - - @Test(expected = BeanDefinitionParsingException.class) - public void testErrorConfig3() { - new XmlApplicationContext("error-config3.xml"); - } - - @Test(expected = BeanDefinitionParsingException.class) - public void testErrorConfig4() { - new XmlApplicationContext("error-config4.xml"); - } - - @Test(expected = BeanDefinitionParsingException.class) - public void testErrorConfig5() { - new XmlApplicationContext("error-config5.xml"); - } -} diff --git a/firefly/src/test/java/test/ioc/TestXmlIoc.java b/firefly/src/test/java/test/ioc/TestXmlIoc.java deleted file mode 100644 index afd7f8298..000000000 --- a/firefly/src/test/java/test/ioc/TestXmlIoc.java +++ /dev/null @@ -1,126 +0,0 @@ -package test.ioc; - -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; -import org.junit.Assert; -import org.junit.Test; -import test.component3.CollectionService; -import test.component3.MapService; -import test.component3.Person; -import test.component3.PersonService; -import com.firefly.core.ApplicationContext; -import com.firefly.core.XmlApplicationContext; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class TestXmlIoc { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - public static ApplicationContext xmlApplicationContext = new XmlApplicationContext(); - - @Test - public void testXmlInject() { - Person person = xmlApplicationContext.getBean("person"); - Assert.assertThat(person.getName(), is("Jack")); - PersonService personService = xmlApplicationContext - .getBean("personService"); - List l = personService.getTestList(); - Assert.assertThat(l.size(), greaterThan(0)); - int i = 0; - for (Object p : l) { - if (p instanceof Person) { - person = (Person) p; - i++; - log.debug(person.getName()); - } else if (p instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map)p; - log.info(map.toString()); - Assert.assertThat(map.entrySet().size(), greaterThan(0)); - Assert.assertThat((Double)map.get(2.2), is(3.3)); - } else { - log.debug(p.toString()); - } - } - Assert.assertThat(i, greaterThan(1)); - } - - @Test - public void testXmlLinkedListInject() { - // 注入的不仅仅是List - CollectionService collectionService = xmlApplicationContext - .getBean("collectionService"); - List list = collectionService.getList(); - Assert.assertThat(list.size(), greaterThan(0)); - log.debug(list.toString()); - } - - @SuppressWarnings("unchecked") - @Test - public void testListInject() { - // list的值也是list - CollectionService collectionService = xmlApplicationContext - .getBean("collectionService2"); - List list = collectionService.getList(); - Assert.assertThat(list.size(), greaterThan(2)); - Set set = (Set) list.get(2); - Assert.assertThat(set.size(), is(2)); - log.debug(set.toString()); - - // set赋值 - Set set1 = collectionService.getSet(); - Assert.assertThat(set1.size(), is(2)); - log.debug(set1.toString()); - } - - @Test - public void testArrayInject() { - CollectionService collectionService = xmlApplicationContext - .getBean("collectionService3"); - String[] strArray = collectionService.getStrArray(); - Assert.assertThat(strArray.length, greaterThan(0)); - log.debug(Arrays.toString(strArray)); - - collectionService = xmlApplicationContext.getBean("collectionService4"); - int[] intArray = collectionService.getIntArray(); - Assert.assertThat(intArray.length, greaterThan(0)); - log.debug(Arrays.toString(intArray)); - - collectionService = xmlApplicationContext.getBean("collectionService5"); - Object[] obj = collectionService.getObjArray(); - Assert.assertThat(obj.length, greaterThan(0)); - Object[] obj2 = (Object[]) obj[3]; - Assert.assertThat(obj2.length, greaterThan(0)); - Assert.assertThat((Long) obj2[1], is(10000000000L)); - } - - @Test(expected = ClassCastException.class) - public void testIdTypeError() { - ApplicationContext context = new XmlApplicationContext("firefly2.xml"); - CollectionService collectionService = context - .getBean("collectionService"); - for (Integer i : collectionService.getSet()) - i++; - - } - - @Test - public void testMapInject() { - MapService mapService = xmlApplicationContext.getBean("mapService"); - Map map = mapService.getMap(); - // System.out.println("size ================================ "+map.size()); - for (Entry entry : map.entrySet()) { - log.info(entry.getKey() + "\t" + entry.getValue()); - if(entry.getKey().getClass().isArray()) { - Object[] objects = (Object[])entry.getKey(); - log.info("array key [{}]", Arrays.toString(objects)); - Assert.assertThat(objects.length, greaterThan(0)); - } - } - Assert.assertThat(map.get(1).toString(), is("www")); - } -} diff --git a/firefly/src/test/java/test/mixed/Food.java b/firefly/src/test/java/test/mixed/Food.java deleted file mode 100644 index 2dfcecbdb..000000000 --- a/firefly/src/test/java/test/mixed/Food.java +++ /dev/null @@ -1,28 +0,0 @@ -package test.mixed; - -public class Food { - private String name; - private double price; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public double getPrice() { - return price; - } - - public void setPrice(double price) { - this.price = price; - } - - @Override - public String toString() { - return "Food [name=" + name + ", price=" + price + "]"; - } - -} diff --git a/firefly/src/test/java/test/mixed/FoodRepository.java b/firefly/src/test/java/test/mixed/FoodRepository.java deleted file mode 100644 index 9261b6912..000000000 --- a/firefly/src/test/java/test/mixed/FoodRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package test.mixed; - -import java.util.List; - - -public interface FoodRepository { - List getFood(); -} diff --git a/firefly/src/test/java/test/mixed/FoodService.java b/firefly/src/test/java/test/mixed/FoodService.java deleted file mode 100644 index 0698288de..000000000 --- a/firefly/src/test/java/test/mixed/FoodService.java +++ /dev/null @@ -1,5 +0,0 @@ -package test.mixed; - -public interface FoodService { - Food getFood(String name); -} diff --git a/firefly/src/test/java/test/mixed/FoodService2.java b/firefly/src/test/java/test/mixed/FoodService2.java deleted file mode 100644 index 4300bed03..000000000 --- a/firefly/src/test/java/test/mixed/FoodService2.java +++ /dev/null @@ -1,5 +0,0 @@ -package test.mixed; - -public interface FoodService2 { - Food getFood(String name); -} diff --git a/firefly/src/test/java/test/mixed/impl/FoodRepositoryImpl.java b/firefly/src/test/java/test/mixed/impl/FoodRepositoryImpl.java deleted file mode 100644 index 6f2c65f16..000000000 --- a/firefly/src/test/java/test/mixed/impl/FoodRepositoryImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package test.mixed.impl; - -import java.util.List; - -import test.mixed.Food; -import test.mixed.FoodRepository; - -public class FoodRepositoryImpl implements FoodRepository { - - private List food; - - @Override - public List getFood() { - return food; - } - - public void setFood(List food) { - this.food = food; - } - -} diff --git a/firefly/src/test/java/test/mixed/impl/FoodService2Impl.java b/firefly/src/test/java/test/mixed/impl/FoodService2Impl.java deleted file mode 100644 index 987d22158..000000000 --- a/firefly/src/test/java/test/mixed/impl/FoodService2Impl.java +++ /dev/null @@ -1,20 +0,0 @@ -package test.mixed.impl; - -import test.mixed.Food; -import test.mixed.FoodService; -import test.mixed.FoodService2; - -public class FoodService2Impl implements FoodService2 { - - private FoodService foodService; - - public void setFoodService(FoodService foodService) { - this.foodService = foodService; - } - - @Override - public Food getFood(String name) { - return foodService.getFood(name); - } - -} diff --git a/firefly/src/test/java/test/mixed/impl/FoodServiceImpl.java b/firefly/src/test/java/test/mixed/impl/FoodServiceImpl.java deleted file mode 100644 index 525e2f48f..000000000 --- a/firefly/src/test/java/test/mixed/impl/FoodServiceImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package test.mixed.impl; - -import com.firefly.annotation.Component; -import com.firefly.annotation.Inject; -import test.mixed.Food; -import test.mixed.FoodRepository; -import test.mixed.FoodService; - -@Component("foodService") -public class FoodServiceImpl implements FoodService { - - @Inject - private FoodRepository foodRepository; - - @Override - public Food getFood(String name) { - for (Food f : foodRepository.getFood()) { - if (f.getName().equals(name)) - return f; - } - return null; - } - -} diff --git a/firefly/src/test/java/test/mock/servlet/MockFilterConfig.java b/firefly/src/test/java/test/mock/servlet/MockFilterConfig.java deleted file mode 100644 index ce2fc85dd..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockFilterConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package test.mock.servlet; - -import javax.servlet.FilterConfig; - -public class MockFilterConfig extends MockServletObject implements FilterConfig { - - private String filterName; - - public String getFilterName() { - return filterName; - } - - public void setFilterName(String filterName) { - this.filterName = filterName; - } -} diff --git a/firefly/src/test/java/test/mock/servlet/MockHttpServletRequest.java b/firefly/src/test/java/test/mock/servlet/MockHttpServletRequest.java deleted file mode 100644 index bf7c5ba22..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockHttpServletRequest.java +++ /dev/null @@ -1,399 +0,0 @@ -package test.mock.servlet; - -import java.io.BufferedReader; -import java.io.IOException; -import java.security.Principal; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Vector; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletInputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import com.firefly.utils.ConvertUtils; - -public class MockHttpServletRequest implements HttpServletRequest { - - protected HttpSession session; - - protected String contextPath; - - protected String[] dispatcherTarget; - - public MockHttpServletRequest() { - this.headers = new HashMap(); - this.dispatcherTarget = new String[1]; - } - - public String getDispatcherTarget() { - return this.dispatcherTarget[0]; - } - - public String getAuthType() { - throw new NoImplException(); - } - - public String getContextPath() { - return contextPath; - } - - public void setContextPath(String contextPath) { - this.contextPath = contextPath; - } - - public Cookie[] getCookies() { - throw new NoImplException(); - } - - public long getDateHeader(String arg0) { - throw new NoImplException(); - } - - protected Map headers; - - public String getHeader(String name) { - return headers.get(name); - } - - public void setHeader(String name, Object value) { - headers.put(name, value.toString()); - } - - public Enumeration getHeaderNames() { - return ConvertUtils.enumeration(headers.keySet()); - } - - public Enumeration getHeaders(String name) { - throw new NoImplException(); - } - - public int getIntHeader(String arg0) { - throw new NoImplException(); - } - - protected String method; - - public String getMethod() { - return method; - } - - public void setMethod(String method) { - this.method = method; - } - - protected String pathInfo; - - public String getPathInfo() { - return pathInfo; - } - - public void setPathInfo(String pathInfo) { - this.pathInfo = pathInfo; - } - - protected String pathTranslated; - - public String getPathTranslated() { - return pathTranslated; - } - - public void setPathTranslated(String pathTranslated) { - this.pathTranslated = pathTranslated; - } - - // protected String queryString; - - public String getQueryString() { - if (params.size() == 0) - return null; - StringBuilder sb = new StringBuilder(); - for (Entry entry : params.entrySet()) { - if (entry.getValue() == null) - sb.append(entry.getKey()).append("=&"); - else - for (String str : entry.getValue()) { - sb.append(entry.getKey()).append("=").append(str) - .append("&"); - } - } - return sb.toString(); - } - - // public void setQueryString(String queryString) { - // this.queryString = queryString; - // } - - public String remoteUser; - - public String getRemoteUser() { - return remoteUser; - } - - public void setRemoteUser(String remoteUser) { - this.remoteUser = remoteUser; - } - - protected String requestURI; - - public String getRequestURI() { - return requestURI; - } - - public void setRequestURI(String requestURI) { - this.requestURI = requestURI; - } - - protected StringBuffer requestURL; - - public StringBuffer getRequestURL() { - return requestURL; - } - - public void setRequestURL(StringBuffer requestURL) { - this.requestURL = requestURL; - } - - public String getRequestedSessionId() { - if (session != null) - return session.getId(); - return null; - } - - protected String servletPath; - - public String getServletPath() { - return servletPath; - } - - public void setServletPath(String servletPath) { - this.servletPath = servletPath; - } - - public HttpSession getSession() { - return getSession(true); - } - - public HttpSession getSession(boolean flag) { - return session; - } - - public MockHttpServletRequest setSession(HttpSession session) { - this.session = session; - return this; - } - - protected Principal userPrincipal; - - public Principal getUserPrincipal() { - return userPrincipal; - } - - public void setUserPrincipal(Principal userPrincipal) { - this.userPrincipal = userPrincipal; - } - - public boolean isRequestedSessionIdFromCookie() { - throw new NoImplException(); - } - - public boolean isRequestedSessionIdFromURL() { - throw new NoImplException(); - } - - public boolean isRequestedSessionIdFromUrl() { - throw new NoImplException(); - } - - public boolean isRequestedSessionIdValid() { - throw new NoImplException(); - } - - public boolean isUserInRole(String arg0) { - throw new NoImplException(); - } - - protected Map attributeMap = new HashMap(); - - public Object getAttribute(String key) { - return attributeMap.get(key); - } - - public Enumeration getAttributeNames() { - return new Vector(attributeMap.keySet()).elements(); - } - - protected String characterEncoding; - - public String getCharacterEncoding() { - return characterEncoding; - } - - public int getContentLength() { - String cl = this.getHeader("content-length"); - try { - return Integer.parseInt(cl); - } catch (NumberFormatException e) { - return 0; - } - } - - public String getContentType() { - return this.getHeader("content-type"); - } - - protected ServletInputStream inputStream; - - public ServletInputStream getInputStream() throws IOException { - return inputStream; - } - - public MockHttpServletRequest setInputStream(ServletInputStream ins) { - this.inputStream = ins; - return this; - } - - public MockHttpServletRequest init() { - // if (null != inputStream) - // if (inputStream instanceof MultipartInputStream) { - // ((MultipartInputStream) inputStream).init(); - // this.setCharacterEncoding(((MultipartInputStream) - // inputStream).getCharset()); - // try { - // this.setHeader("content-length", inputStream.available()); - // this.setHeader( "content-type", - // ((MultipartInputStream) inputStream).getContentType()); - // } - // catch (IOException e) { - // - // } - // } - return this; - } - - public String getLocalAddr() { - throw new NoImplException(); - } - - public String getLocalName() { - throw new NoImplException(); - } - - public int getLocalPort() { - throw new NoImplException(); - } - - public Locale getLocale() { - throw new NoImplException(); - } - - public Enumeration getLocales() { - throw new NoImplException(); - } - - protected Map params = new HashMap(); - - public String getParameter(String key) { - if (params.containsKey(key)) { - return params.get(key)[0]; - } - return null; - } - - public void setParameter(String key, String value) { - params.put(key, new String[] { value }); - } - - public void setParameter(String key, Number num) { - setParameter(key, num.toString()); - } - - public void setParameterValues(String key, String[] values) { - params.put(key, values); - } - - public void addParameter(String key, String value) { - params.put(key, new String[] { value }); - } - - public Map getParameterMap() { - return params; - } - - public Enumeration getParameterNames() { - return new Vector(params.keySet()).elements(); - } - - public String[] getParameterValues(String name) { - String[] param = params.get(name); - return param; - } - - protected String protocol; - - public String getProtocol() { - return protocol; - } - - public void setProtocol(String protocol) { - this.protocol = protocol; - } - - public BufferedReader getReader() throws IOException { - throw new NoImplException(); - } - - public String getRealPath(String arg0) { - throw new NoImplException(); - } - - public String getRemoteAddr() { - throw new NoImplException(); - } - - public String getRemoteHost() { - throw new NoImplException(); - } - - public int getRemotePort() { - throw new NoImplException(); - } - - public RequestDispatcher getRequestDispatcher(String dest) { - return new MockRequestDispatcher(dispatcherTarget, dest); - } - - public String getScheme() { - throw new NoImplException(); - } - - public String getServerName() { - throw new NoImplException(); - } - - public int getServerPort() { - throw new NoImplException(); - } - - public boolean isSecure() { - throw new NoImplException(); - } - - public void removeAttribute(String key) { - attributeMap.remove(key); - } - - public void setAttribute(String key, Object value) { - attributeMap.put(key, value); - } - - public void setCharacterEncoding(String characterEncoding) { - this.characterEncoding = characterEncoding; - } - -} diff --git a/firefly/src/test/java/test/mock/servlet/MockHttpServletResponse.java b/firefly/src/test/java/test/mock/servlet/MockHttpServletResponse.java deleted file mode 100644 index 24ebae56a..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockHttpServletResponse.java +++ /dev/null @@ -1,214 +0,0 @@ -package test.mock.servlet; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; - -public class MockHttpServletResponse implements HttpServletResponse { - - protected ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - protected PrintWriter writer; - - protected Map headers; - - protected Set cookies; - - protected int status; - - protected String statusMessage; - - protected Locale locale; - - protected String contentType; - - public MockHttpServletResponse() { - headers = new HashMap(); - cookies = new HashSet(); - status = 200; - statusMessage = "OK"; - } - - public void addCookie(Cookie cookie) { - cookies.add(cookie); - } - - public void addDateHeader(String key, long value) { - headers.put(key, "" + value); - } - - public void addHeader(String key, String value) { - headers.put(key, value); - } - - public void addIntHeader(String key, int value) { - headers.put(key, "" + value); - } - - public boolean containsHeader(String key) { - return headers.containsKey(key); - } - - public String encodeRedirectURL(String arg0) { - throw new NoImplException(); - } - - public String encodeRedirectUrl(String arg0) { - throw new NoImplException(); - } - - public String encodeURL(String arg0) { - throw new NoImplException(); - } - - public String encodeUrl(String arg0) { - throw new NoImplException(); - } - - public void sendError(int error) throws IOException { - throw new NoImplException(); - } - - public void sendError(int arg0, String arg1) throws IOException { - throw new NoImplException(); - } - - public void sendRedirect(String value) throws IOException { - headers.put("Location", "" + value); - } - - public void setDateHeader(String key, long value) { - headers.put(key, "" + value); - } - - public void setHeader(String key, String value) { - headers.put(key, value); - } - - public void setIntHeader(String key, int value) { - headers.put(key, "" + value); - } - - public void setStatus(int status) { - this.status = status; - } - - public void setStatus(int status, String statusMessage) { - this.status = status; - this.statusMessage = statusMessage; - } - - public void flushBuffer() throws IOException { - getWriter().flush(); - } - - public int getBufferSize() { - return stream.size(); - } - - public String getCharacterEncoding() { - return characterEncoding; - } - - public String getContentType() { - return contentType; - } - - public Locale getLocale() { - return locale; - } - - public ServletOutputStream getOutputStream() throws IOException { - throw new NoImplException(); - } - - public PrintWriter getWriter() throws IOException { - if (writer == null) { - writer = new PrintWriter(new OutputStreamWriter(stream, - characterEncoding)); - } - return writer; - } - - public boolean isCommitted() { - throw new NoImplException(); - } - - public void reset() { - stream.reset(); - } - - public void resetBuffer() { - stream.reset(); - } - - public void setBufferSize(int arg0) { - - } - - protected String characterEncoding = "UTF-8"; - - public void setCharacterEncoding(String characterEncoding) { - this.characterEncoding = characterEncoding; - } - - public void setContentLength(int arg0) { - - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public void setLocale(Locale locale) { - this.locale = locale; - } - - public int getStatus() { - return status; - } - - public String getStatusMessage() { - return statusMessage; - } - - public String getAsString() { - String ret = null; - try { - getWriter().flush(); - ret = stream.toString(characterEncoding); - } catch (UnsupportedEncodingException e) { - - } catch (IOException e) { - - } - return ret; - } - - public int getAsInt() { - return Integer.parseInt(getAsString()); - } - - public long getAsLong() { - return Long.parseLong(getAsString()); - } - - // public T getAs(Class type) { - // - // } - - public String getHeader(String key) { - return headers.get(key); - } -} diff --git a/firefly/src/test/java/test/mock/servlet/MockHttpSession.java b/firefly/src/test/java/test/mock/servlet/MockHttpSession.java deleted file mode 100644 index 9484d0038..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockHttpSession.java +++ /dev/null @@ -1,93 +0,0 @@ -package test.mock.servlet; - -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Vector; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionContext; - -@SuppressWarnings("deprecation") -public class MockHttpSession implements HttpSession { - - protected ServletContext servletContext; - - public MockHttpSession(MockServletContext servletContext) { - this.servletContext = servletContext; - } - - protected Map attributeMap = new HashMap(); - - public void removeAttribute(String key) { - attributeMap.remove(key); - } - - public void setAttribute(String key, Object value) { - attributeMap.put(key, value); - } - - public Object getAttribute(String key) { - return attributeMap.get(key); - } - - public long getCreationTime() { - throw new NoImplException(); - } - - public String getId() { - throw new NoImplException(); - } - - public long getLastAccessedTime() { - throw new NoImplException(); - } - - public int getMaxInactiveInterval() { - throw new NoImplException(); - } - - public ServletContext getServletContext() { - return servletContext; - } - - public Object getValue(String arg0) { - throw new NoImplException(); - } - - public String[] getValueNames() { - throw new NoImplException(); - } - - public void invalidate() { - - } - - public boolean isNew() { - throw new NoImplException(); - } - - public void putValue(String arg0, Object arg1) { - - } - - public void removeValue(String arg0) { - - } - - public void setMaxInactiveInterval(int arg0) { - - } - - public Enumeration getAttributeNames() { - return new Vector(attributeMap.keySet()).elements(); - } - - /** - * @deprecated - */ - public HttpSessionContext getSessionContext() { - return null; - } - -} diff --git a/firefly/src/test/java/test/mock/servlet/MockInputStream.java b/firefly/src/test/java/test/mock/servlet/MockInputStream.java deleted file mode 100644 index 6439950cb..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockInputStream.java +++ /dev/null @@ -1,11 +0,0 @@ -package test.mock.servlet; - -import javax.servlet.ServletInputStream; - -public abstract class MockInputStream extends ServletInputStream { - - public abstract void init(); - - public abstract void append(String name, String value); - -} diff --git a/firefly/src/test/java/test/mock/servlet/MockRequestDispatcher.java b/firefly/src/test/java/test/mock/servlet/MockRequestDispatcher.java deleted file mode 100644 index ae497d1c9..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockRequestDispatcher.java +++ /dev/null @@ -1,24 +0,0 @@ -package test.mock.servlet; - -import java.io.IOException; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -public class MockRequestDispatcher implements RequestDispatcher { - - public MockRequestDispatcher(String[] target, String dest) { - target[0] = dest; - } - - public void forward(ServletRequest arg0, ServletResponse arg1) - throws ServletException, IOException { - } - - public void include(ServletRequest arg0, ServletResponse arg1) - throws ServletException, IOException { - } - -} diff --git a/firefly/src/test/java/test/mock/servlet/MockServletConfig.java b/firefly/src/test/java/test/mock/servlet/MockServletConfig.java deleted file mode 100644 index f67b4e2bc..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockServletConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -package test.mock.servlet; - -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Vector; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; - -public class MockServletConfig implements ServletConfig { - - private Map initParameterMap = new HashMap(); - - private ServletContext servletContext; - - private String servletName; - - public MockServletConfig(MockServletContext servletContext, String string) { - this.servletContext = servletContext; - this.servletName = string; - } - - public String getInitParameter(String key) { - return initParameterMap.get(key); - } - - public Enumeration getInitParameterNames() { - return new Vector(initParameterMap.keySet()).elements(); - } - - public ServletContext getServletContext() { - return servletContext; - } - - public String getServletName() { - return servletName; - } - - public void addInitParameter(String key, String value) { - initParameterMap.put(key, value); - } - - public void setServletContext(ServletContext servletContext) { - this.servletContext = servletContext; - } - - public void setServletName(String servletName) { - this.servletName = servletName; - } -} diff --git a/firefly/src/test/java/test/mock/servlet/MockServletContext.java b/firefly/src/test/java/test/mock/servlet/MockServletContext.java deleted file mode 100644 index eb5c197c0..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockServletContext.java +++ /dev/null @@ -1,148 +0,0 @@ -package test.mock.servlet; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.Vector; -import javax.servlet.RequestDispatcher; -import javax.servlet.Servlet; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class MockServletContext extends MockServletObject implements - ServletContext { - - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - - public int getMajorVersion() { - throw new NoImplException(); - } - - public String getMimeType(String arg0) { - throw new NoImplException(); - } - - public int getMinorVersion() { - throw new NoImplException(); - } - - public RequestDispatcher getNamedDispatcher(String arg0) { - throw new NoImplException(); - } - - public String getRealPath(String path) { - if (path.startsWith("/WEB-INF/lib/")) - return new File(path.substring("/WEB-INF/lib/".length())) - .getAbsolutePath(); - if (path.startsWith("/WEB-INF/classes/")) - return new File(path.substring("/WEB-INF/classes/".length())) - .getAbsolutePath(); - if (path.startsWith("/")) - return new File("." + path).getAbsolutePath(); - return new File(path).getAbsolutePath(); - } - - public RequestDispatcher getRequestDispatcher(String arg0) { - throw new NoImplException(); - } - - public URL getResource(String name) throws MalformedURLException { - return getClass().getResource(name); - } - - public InputStream getResourceAsStream(String name) { - return getClass().getResourceAsStream(name); - } - - public Set getResourcePaths(String name) { - try { - HashSet hashSet = new HashSet(); - Enumeration enumeration; - enumeration = getClass().getClassLoader().getResources(name); - while (enumeration.hasMoreElements()) { - URL url = (URL) enumeration.nextElement(); - hashSet.add(url.toString()); - } - return hashSet; - } catch (IOException e) { - return null; - } - } - - public String getServerInfo() { - throw new NoImplException(); - } - - public Servlet getServlet(String name) throws ServletException { - throw new NoImplException(); - } - - private String servletContextName; - - public String getServletContextName() { - return servletContextName; - } - - public void setServletContextName(String servletContextName) { - this.servletContextName = servletContextName; - } - - public Enumeration getServletNames() { - throw new NoImplException(); - } - - public Enumeration getServlets() { - throw new NoImplException(); - } - - protected Map attributeMap = new HashMap(); - - public void removeAttribute(String key) { - attributeMap.remove(key); - } - - public void setAttribute(String key, Object value) { - attributeMap.put(key, value); - } - - public Object getAttribute(String key) { - return attributeMap.get(key); - } - - public Enumeration getAttributeNames() { - return new Vector(attributeMap.keySet()).elements(); - } - - public ServletContext getContext(String arg0) { - throw new NoImplException(); - } - - public String getContextPath() { - throw new NoImplException(); - } - - @Override - public void log(String msg) { - log.debug(msg); - } - - @Override - public void log(Exception exception, String msg) { - log.debug(msg); - } - - @Override - public void log(String message, Throwable throwable) { - log.debug(message); - } - -} diff --git a/firefly/src/test/java/test/mock/servlet/MockServletObject.java b/firefly/src/test/java/test/mock/servlet/MockServletObject.java deleted file mode 100644 index 9f8ae1540..000000000 --- a/firefly/src/test/java/test/mock/servlet/MockServletObject.java +++ /dev/null @@ -1,35 +0,0 @@ -package test.mock.servlet; - -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Vector; - -import javax.servlet.ServletContext; - -public class MockServletObject { - - private Map initParameterMap = new HashMap(); - - protected ServletContext servletContext; - - public String getInitParameter(String key) { - return initParameterMap.get(key); - } - - public Enumeration getInitParameterNames() { - return new Vector(initParameterMap.keySet()).elements(); - } - - public ServletContext getServletContext() { - return servletContext; - } - - public void addInitParameter(String key, String value) { - initParameterMap.put(key, value); - } - - public void setServletContext(ServletContext servletContext) { - this.servletContext = servletContext; - } -} diff --git a/firefly/src/test/java/test/mock/servlet/NoImplException.java b/firefly/src/test/java/test/mock/servlet/NoImplException.java deleted file mode 100644 index 427ebbe35..000000000 --- a/firefly/src/test/java/test/mock/servlet/NoImplException.java +++ /dev/null @@ -1,15 +0,0 @@ -package test.mock.servlet; - -public class NoImplException extends RuntimeException { - - private static final long serialVersionUID = -530610412864911395L; - - public NoImplException() { - super(); - } - - public NoImplException(String msg) { - super(msg); - } - -} diff --git a/firefly/src/test/java/test/mvc/TestMvc.java b/firefly/src/test/java/test/mvc/TestMvc.java deleted file mode 100644 index 30f48f84d..000000000 --- a/firefly/src/test/java/test/mvc/TestMvc.java +++ /dev/null @@ -1,243 +0,0 @@ -package test.mvc; - -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; - -import org.junit.Assert; -import org.junit.Test; - -import test.controller.Book; -import test.mixed.Food; -import test.mock.servlet.MockHttpServletRequest; -import test.mock.servlet.MockHttpServletResponse; - -import com.firefly.mvc.web.AnnotationWebContext; -import com.firefly.mvc.web.DispatcherController; -import com.firefly.mvc.web.HttpMethod; -import com.firefly.mvc.web.servlet.HttpServletDispatcherController; -import com.firefly.utils.json.Json; -import com.firefly.utils.log.Log; -import com.firefly.utils.log.LogFactory; - -public class TestMvc { - private static Log log = LogFactory.getInstance().getLog("firefly-system"); - private static DispatcherController dispatcherController = new HttpServletDispatcherController(new AnnotationWebContext("firefly-mvc.xml")); - - public static void main(String[] args) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/book/create/"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod(HttpMethod.GET); - request.setParameter("title", "good book"); - request.setParameter("text", "一本好书"); - request.setParameter("id", "330"); - request.setParameter("price", "79.9"); - request.setParameter("sell", "true"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - - System.out.println(response.getAsString()); - System.out.println(response.getHeader("Allow")); - } - - @Test - public void testInterceptorChain() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/food/view1"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - - Food food = (Food)request.getAttribute("fruit0"); - Assert.assertThat(food.getName(), is("apple")); - Assert.assertThat(food.getPrice(), is(8.0)); - - food = (Food)request.getAttribute("fruit1"); - Assert.assertThat(food.getName(), is("ananas")); - Assert.assertThat(food.getPrice(), is(4.99)); - - food = Json.toObject(response.getAsString(), Food.class); - Assert.assertThat(food.getName(), is("banana")); - Assert.assertThat(food.getPrice(), is(3.99)); - } - - @Test - public void testInterceptor() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/food"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - - Food food = (Food)request.getAttribute("fruit"); - Assert.assertThat(food.getName(), is("orange")); - Assert.assertThat(food.getPrice(), is(3.5)); - - food = (Food)request.getAttribute("fruit0"); - Assert.assertThat(food.getName(), is("apple")); - Assert.assertThat(food.getPrice(), is(8.0)); - - food = (Food)request.getAttribute("strawberry"); - Assert.assertThat(food.getName(), is("strawberry")); - Assert.assertThat(food.getPrice(), is(10.0)); - } - - @Test - public void testNotFoundPage() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/book/create00/"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod(HttpMethod.GET); - request.setParameter("title", "good book"); - request.setParameter("text", "一本好书"); - request.setParameter("id", "330"); - request.setParameter("price", "79.9"); - request.setParameter("sell", "true"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - - Assert.assertThat(response.getStatus(), is(404)); - } - - @Test - public void testNotAllowMethod() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/book/create/"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod(HttpMethod.GET); - request.setParameter("title", "good book"); - request.setParameter("text", "一本好书"); - request.setParameter("id", "330"); - request.setParameter("price", "79.9"); - request.setParameter("sell", "true"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - - Assert.assertThat(response.getHeader("Allow"), is("POST")); - Assert.assertThat(request.getAttribute("book"), nullValue()); - } - - @Test - public void testControllerHello() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/hello"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Assert.assertThat(request.getAttribute("hello").toString(), - is("你好 firefly!")); - } - - @Test - public void testBeanParamInject() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/book/value"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - request.setParameter("text", "ddd"); - request.setParameter("id", "345"); - request.setParameter("price", "23.3"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Book book = (Book) request.getAttribute("book"); - Assert.assertThat(book.getText(), is("ddd")); - Assert.assertThat(book.getPrice(), is(23.3)); - Assert.assertThat(book.getId(), is(345)); - } - - @Test - public void testPostBeanParamInject() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/book/create/"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod(HttpMethod.POST); - request.setParameter("title", "good book"); - request.setParameter("text", "一本好书"); - request.setParameter("id", "330"); - request.setParameter("price", "79.9"); - request.setParameter("sell", "true"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Book book = (Book) request.getAttribute("book"); - Assert.assertThat(book.getText(), is("一本好书")); - Assert.assertThat(book.getPrice(), is(79.9)); - Assert.assertThat(book.getId(), is(330)); - Assert.assertThat(book.getSell(), is(true)); - } - - @Test - public void testResponseOutput() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/hello/text"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Assert.assertThat(response.getAsString(), is("文本输出")); - - request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/hello/text-xo/333-444"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Assert.assertThat(response.getAsString(), is("text-xo-333-444")); - - request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/hello55555/"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Assert.assertThat(response.getAsString(), is("text-55555")); - } - - @Test - public void testJsonOutput() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/book/json/"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod(HttpMethod.POST); - request.setParameter("title", "good book"); - request.setParameter("text", "very good"); - request.setParameter("id", "331"); - request.setParameter("price", "10.0"); - request.setParameter("sell", "false"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - log.info(response.getAsString()); - Assert.assertThat(response.getAsString().length(), greaterThan(10)); - Book book = Json.toObject(response.getAsString(), Book.class); - Assert.assertThat(book.getId(), is(331)); - Assert.assertThat(book.getSell(), is(false)); - Assert.assertThat(book.getTitle(), is("good book")); - } - - @Test - public void testRedirect() { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/firefly/app/hello/redirect"); - request.setServletPath("/app"); - request.setContextPath("/firefly"); - request.setMethod("GET"); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcherController.dispatcher(request, response); - Assert.assertThat(response.getHeader("Location"), is("/firefly/app/hello")); - } -} diff --git a/firefly/src/test/java/test/mvc/TestResource.java b/firefly/src/test/java/test/mvc/TestResource.java deleted file mode 100644 index 77e15973d..000000000 --- a/firefly/src/test/java/test/mvc/TestResource.java +++ /dev/null @@ -1,42 +0,0 @@ -package test.mvc; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.notNullValue; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.mvc.web.Resource; -import com.firefly.mvc.web.Resource.Result; - -public class TestResource { - - @Test - public void testResource() throws NoSuchMethodException, SecurityException { - Resource resource = new Resource("utf-8"); - resource.add("/user/id-?-?", null); - resource.add("/user/id-?-?/?", null); - resource.add("/user/add", null); - - resource.add("/shop/fruit/apple/?", null); - resource.add("/shop/fruit/banana", null); - resource.add("/file/info.txt", null); - - Result ret = resource.match("/user/id-3344-2222/55555"); - Assert.assertThat(ret.getParams().length, is(3)); - Assert.assertThat(ret.getParams()[1], is("2222")); - Assert.assertThat(ret.getParams()[2], is("55555")); - - ret = resource.match("/shop/fruit/banana"); - Assert.assertThat(ret.getParams(), nullValue()); - - ret = resource.match("/hello"); - Assert.assertThat(ret, nullValue()); - - ret = resource.match("/file/info.txt"); - Assert.assertThat(ret, notNullValue()); - - Assert.assertThat(resource.getEncoding(), is("utf-8")); - } -} diff --git a/firefly/src/test/java/test/server/AddInterceptor.java b/firefly/src/test/java/test/server/AddInterceptor.java deleted file mode 100644 index 1b56b96df..000000000 --- a/firefly/src/test/java/test/server/AddInterceptor.java +++ /dev/null @@ -1,23 +0,0 @@ -package test.server; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import com.firefly.annotation.Interceptor; -import com.firefly.mvc.web.HandlerChain; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.view.StaticFileView; - -@Interceptor(uri = "/add*") -public class AddInterceptor { - public View dispose(HttpServletRequest request, HttpServletResponse response, HandlerChain chain) { - HttpSession session = request.getSession(); - String name = (String)session.getAttribute("name"); - if(name == null) { - return new StaticFileView("/index.html"); - } - - return chain.doNext(request, response, chain); - } -} diff --git a/firefly/src/test/java/test/server/IndexController.java b/firefly/src/test/java/test/server/IndexController.java deleted file mode 100644 index ae974a51e..000000000 --- a/firefly/src/test/java/test/server/IndexController.java +++ /dev/null @@ -1,103 +0,0 @@ -package test.server; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import com.firefly.annotation.Controller; -import com.firefly.annotation.RequestMapping; -import com.firefly.mvc.web.HttpMethod; -import com.firefly.mvc.web.View; -import com.firefly.mvc.web.view.RedirectView; -import com.firefly.mvc.web.view.TemplateView; -import com.firefly.mvc.web.view.TextView; - -@Controller -public class IndexController { - - @RequestMapping(value = "/index") - public View index(HttpServletRequest request, HttpServletResponse response) { - System.out.println("into /index"); - HttpSession session = request.getSession(); - request.setAttribute("hello", session.getAttribute("name")); - response.addCookie(new Cookie("test", "cookie_value")); - Cookie cookie = new Cookie("myname", "xiaoqiu"); - cookie.setMaxAge(5 * 60); - response.addCookie(cookie); - return new TemplateView("/index.html"); - } - - @RequestMapping(value = "/add", method = HttpMethod.POST) - public View add(HttpServletRequest request, HttpServletResponse response) { - System.out.println("into /add"); - return new TextView(request.getParameter("content")); - } - - @RequestMapping(value = "/add2", method = HttpMethod.POST) - public View add2(HttpServletRequest request, HttpServletResponse response) { - System.out.println("into /add2"); - return new TextView("test add 2"); - } - - @RequestMapping(value = "/login") - public View test(HttpServletRequest request, HttpServletResponse response) { - HttpSession session = request.getSession(); - session.setMaxInactiveInterval(15); - String name = (String)session.getAttribute("name"); - if(name == null) { - System.out.println("name is null"); - name = "Qiu Pengtao"; - session.setAttribute("name", name); - } - request.setAttribute("name", name); - return new TemplateView("/test.html"); - } - - @RequestMapping(value = "/exit") - public View exit(HttpServletRequest request, HttpServletResponse response) { - request.getSession().invalidate(); - request.setAttribute("name", "exit"); - return new TemplateView("/test.html"); - } - - @RequestMapping(value = "/index2") - public View index2(HttpServletRequest request, - HttpServletResponse response) throws IOException { - response.sendRedirect("index"); - return null; - } - - @RequestMapping(value = "/index3") - public View index3(HttpServletRequest request, - HttpServletResponse response) throws IOException { - response.sendRedirect(request.getContextPath() - + request.getServletPath() + "/index"); - return null; - } - - @RequestMapping(value = "/testc") - public View testOutContentLength(HttpServletRequest request, HttpServletResponse response) throws IOException { - String msg = "test Content-Length output"; - response.setCharacterEncoding("UTF-8"); - response.setHeader("Content-Type", "text/html; charset=UTF-8"); - response.setHeader("Content-Length", String.valueOf(msg.getBytes("UTF-8").length)); - PrintWriter writer = response.getWriter(); - try { - writer.print(msg); - } finally { - writer.close(); - } - return null; - } - - @RequestMapping(value = "/index4") - public View index4(HttpServletRequest request, - HttpServletResponse response) throws IOException { - return new RedirectView("/index"); - } - -} diff --git a/firefly/src/test/java/test/utils/TestURLParser.java b/firefly/src/test/java/test/utils/TestURLParser.java deleted file mode 100644 index 83ae465cf..000000000 --- a/firefly/src/test/java/test/utils/TestURLParser.java +++ /dev/null @@ -1,31 +0,0 @@ -package test.utils; - -import static org.hamcrest.Matchers.*; - -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import com.firefly.mvc.web.support.URLParser; - -public class TestURLParser { - - @Test - public void testParser() { - List uri = URLParser.parse("/app/index/"); - System.out.println(uri); - Assert.assertThat(uri.get(0), is("app")); - Assert.assertThat(uri.get(1), is("index")); - - uri = URLParser.parse("/app/q_{}_{}.html"); - System.out.println(uri); - Assert.assertThat(uri.get(0), is("app")); - Assert.assertThat(uri.get(1), is("q_{}_{}.html")); - - uri = URLParser.parse("/apple"); - Assert.assertThat(uri.get(0), is("apple")); - } - - -} diff --git a/firefly/src/test/resources/annotation-config.xml b/firefly/src/test/resources/annotation-config.xml deleted file mode 100644 index 8607ae976..000000000 --- a/firefly/src/test/resources/annotation-config.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/firefly/src/test/resources/error-config1.xml b/firefly/src/test/resources/error-config1.xml deleted file mode 100644 index 2bff5258b..000000000 --- a/firefly/src/test/resources/error-config1.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/error-config2.xml b/firefly/src/test/resources/error-config2.xml deleted file mode 100644 index b90849a27..000000000 --- a/firefly/src/test/resources/error-config2.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/error-config3.xml b/firefly/src/test/resources/error-config3.xml deleted file mode 100644 index 16a25f7e5..000000000 --- a/firefly/src/test/resources/error-config3.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/error-config4.xml b/firefly/src/test/resources/error-config4.xml deleted file mode 100644 index 23465c0ee..000000000 --- a/firefly/src/test/resources/error-config4.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/error-config5.xml b/firefly/src/test/resources/error-config5.xml deleted file mode 100644 index ad33fe62d..000000000 --- a/firefly/src/test/resources/error-config5.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/error-config6.xml b/firefly/src/test/resources/error-config6.xml deleted file mode 100644 index 0a3553f5c..000000000 --- a/firefly/src/test/resources/error-config6.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/firefly/src/test/resources/error-config7.xml b/firefly/src/test/resources/error-config7.xml deleted file mode 100644 index 25c205531..000000000 --- a/firefly/src/test/resources/error-config7.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/firefly-log.properties b/firefly/src/test/resources/firefly-log.properties deleted file mode 100644 index 257af3835..000000000 --- a/firefly/src/test/resources/firefly-log.properties +++ /dev/null @@ -1,2 +0,0 @@ -firefly-system=${log.level},${log.path},console -firefly-access=${log.level},${log.path},console \ No newline at end of file diff --git a/firefly/src/test/resources/firefly-mvc.xml b/firefly/src/test/resources/firefly-mvc.xml deleted file mode 100644 index 52b480964..000000000 --- a/firefly/src/test/resources/firefly-mvc.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/firefly/src/test/resources/firefly-server.xml b/firefly/src/test/resources/firefly-server.xml deleted file mode 100644 index 86427deab..000000000 --- a/firefly/src/test/resources/firefly-server.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/firefly/src/test/resources/firefly.xml b/firefly/src/test/resources/firefly.xml deleted file mode 100644 index 5c14d7aa3..000000000 --- a/firefly/src/test/resources/firefly.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - 123 - - dfdf - - - - - - dddd - - - - 11 - 12 - 11.5 - - - - - - - - - a1 - a2 - - - - - - - - str1 - str2 - 3 - - - - - - - - 1022 - 1032 - 1042 - - - - - - - - 1022 - 1032 - dddddd - - - 10000000000 - - - - - - - - - - - ddd - sfsfsfsf - - - - - - ssss - - - - - sssss - - - 234444444 - - - - - sssss - sssss3 - sssss4 - - - - - - ssss - - - 12345 - 67890 - - - - - - - - - - - - diff --git a/firefly/src/test/resources/firefly2.xml b/firefly/src/test/resources/firefly2.xml deleted file mode 100644 index 5acb49e44..000000000 --- a/firefly/src/test/resources/firefly2.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - s1 - s2 - - - - - 3 - 3 - 4sss - - - - - - - diff --git a/firefly/src/test/resources/logback.xml b/firefly/src/test/resources/logback.xml deleted file mode 100644 index 5103b1d52..000000000 --- a/firefly/src/test/resources/logback.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - ${LOG_PATTERN} - - - - - - ${log.path} - - ${LOG_PATTERN} - - - ${log.level} - - - ${log.path}.%d{yyyy-MM-dd} - - - - - - - \ No newline at end of file diff --git a/firefly/src/test/resources/mixed-config.xml b/firefly/src/test/resources/mixed-config.xml deleted file mode 100644 index ba851658d..000000000 --- a/firefly/src/test/resources/mixed-config.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firefly/src/test/resources/mixed-config2.xml b/firefly/src/test/resources/mixed-config2.xml deleted file mode 100644 index bb6d61598..000000000 --- a/firefly/src/test/resources/mixed-config2.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/firefly/src/test/resources/mixed-config3.xml b/firefly/src/test/resources/mixed-config3.xml deleted file mode 100644 index 3bc9ec93b..000000000 --- a/firefly/src/test/resources/mixed-config3.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/firefly/src/test/resources/mixed-config4.xml b/firefly/src/test/resources/mixed-config4.xml deleted file mode 100644 index 370c24b56..000000000 --- a/firefly/src/test/resources/mixed-config4.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/firefly/src/test/resources/testImport.xml b/firefly/src/test/resources/testImport.xml deleted file mode 100644 index 4824f1ca0..000000000 --- a/firefly/src/test/resources/testImport.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - Bob - - - - - diff --git a/firefly/src/test/resources/testImport1.xml b/firefly/src/test/resources/testImport1.xml deleted file mode 100644 index 84938af80..000000000 --- a/firefly/src/test/resources/testImport1.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - s1 - s2 - - - - - - - - s1 - s2 - - s3 - s4 - s4 - - - - - - 3 - 3 - 4 - - - - - diff --git a/local_install.ps1 b/local_install.ps1 new file mode 100644 index 000000000..9cb190334 --- /dev/null +++ b/local_install.ps1 @@ -0,0 +1 @@ +mvn clean install --% -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=7890 -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=7890 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..cfeddfeb3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,1557 @@ + + + 4.0.0 + + com.fireflysource + firefly-framework + 5.0.3-SNAPSHOT + pom + + + Firefly Framework + + Firefly framework helps you create a java web application easy and quickly. It provides MVC framework with HTTP + Server and many other useful components for developing web applications. It means you can easy deploy your web + without any other java web containers, in short , it's containerless. It taps into the fullest potential of + hardware using SEDA architecture, a highly customizable thread model. + + http://www.fireflysource.com + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + Alvin Qiu + qptkk@163.com + Fireflysource + http://www.fireflysource.com + + + + scm:git@github.com:hypercube1024/firefly.git + scm:git@github.com:hypercube1024/firefly.git + git@github.com:hypercube1024/firefly.git + + + + firefly-common + firefly-slf4j + firefly-net + firefly-wechat + firefly-serialization + firefly-example + firefly-jni-helper + + + + UTF-8 + + INFO + ../logs + + ${project.version} + https://github.com/hypercube1024/firefly + + 8 + + + 1.9.20 + 1.8.0 + false + 1.8 + 1.9 + 1.9 + official + + + 1.9.20 + true + 8 + + + 2.15.4 + 2.5.2 + 2.1.4.Final + 3.30.2-GA + 1.7.36 + 2023.0.3 + 4.0.3 + + + 5.10.2 + + + + + + + com.fireflysource + firefly-net + ${project.version} + + + com.fireflysource + firefly-common + ${project.version} + + + com.fireflysource + firefly-slf4j + ${project.version} + + + com.fireflysource + firefly-wechat + ${project.version} + + + com.fireflysource + firefly-serialization + ${project.version} + + + + + org.slf4j + slf4j-api + ${slf4j.api.version} + + + org.javassist + javassist + ${javassist.version} + + + + + io.projectreactor + reactor-bom + ${io.reactor.version} + pom + import + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib-common + ${kotlin.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-core-jvm + ${kotlin.coroutine.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-jdk8 + ${kotlin.coroutine.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlin.coroutine.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-test + ${kotlin.coroutine.version} + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + + org.conscrypt + conscrypt-openjdk-uber + ${conscrypt.version} + + + org.wildfly.openssl + wildfly-openssl + ${wildfly-openssl.version} + + + org.openjsse + openjsse + 1.1.12 + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + org.jctools + jctools-core + ${jctools.version} + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlinx + kotlinx-coroutines-core-jvm + + + org.jetbrains.kotlinx + kotlinx-coroutines-jdk8 + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.jetbrains.kotlinx + kotlinx-coroutines-test + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + + org.jetbrains.dokka + dokka-maven-plugin + ${dokka.version} + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + org.apache.maven.reporting + maven-reporting-api + 3.0 + + + org.apache.maven.reporting + maven-reporting-impl + 3.0.0 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + + + + + org.jacoco + jacoco-maven-plugin + + + + prepare-agent + + + + report + test + + report + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.ow2.asm + asm + 6.2.1 + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + UTF-8 + + + + + + + + + + jdk_8_222 + + 1.8.0_222 + + + + + org.openjsse + openjsse + 1.1.0 + + + + + + + jdk_8_231 + + 1.8.0_231 + + + + + org.openjsse + openjsse + 1.1.1 + + + + + + jdk_8_232 + + 1.8.0_232 + + + + + org.openjsse + openjsse + 1.1.1 + + + + + + + jdk_8_241 + + 1.8.0_241 + + + + + org.openjsse + openjsse + 1.1.2 + + + + + + jdk_8_242 + + 1.8.0_242 + + + + + org.openjsse + openjsse + 1.1.2 + + + + + + + jdk_8_251 + + 1.8.0_251 + + + + + org.openjsse + openjsse + 1.1.2 + + + + + + jdk_8_252 + + 1.8.0_252 + + + + + org.openjsse + openjsse + 1.1.3 + + + + + + + jdk_8_261 + + 1.8.0_261 + + + + + org.openjsse + openjsse + 1.1.4 + + + + + + jdk_8_262 + + 1.8.0_262 + + + + + org.openjsse + openjsse + 1.1.4 + + + + + + + jdk_8_271 + + 1.8.0_271 + + + + + org.openjsse + openjsse + 1.1.5 + + + + + + jdk_8_272 + + 1.8.0_272 + + + + + org.openjsse + openjsse + 1.1.5 + + + + + + + jdk_8_281 + + 1.8.0_281 + + + + + org.openjsse + openjsse + 1.1.5 + + + + + + jdk_8_282 + + 1.8.0_282 + + + + + org.openjsse + openjsse + 1.1.5 + + + + + + + jdk_8_291 + + 1.8.0_291 + + + + + org.openjsse + openjsse + 1.1.6 + + + + + + jdk_8_292 + + 1.8.0_292 + + + + + org.openjsse + openjsse + 1.1.6 + + + + + + + + jdk_8_301 + + 1.8.0_301 + + + + + org.openjsse + openjsse + 1.1.7 + + + + + + jdk_8_302 + + 1.8.0_302 + + + + + org.openjsse + openjsse + 1.1.7 + + + + + + + jdk_8_311 + + 1.8.0_311 + + + + + org.openjsse + openjsse + 1.1.8 + + + + + + jdk_8_312 + + 1.8.0_312 + + + + + org.openjsse + openjsse + 1.1.8 + + + + + + + jdk_8_321 + + 1.8.0_321 + + + + + org.openjsse + openjsse + 1.1.9 + + + + + + jdk_8_322 + + 1.8.0_322 + + + + + org.openjsse + openjsse + 1.1.9 + + + + + + + jdk_8_331 + + 1.8.0_331 + + + + + org.openjsse + openjsse + 1.1.10 + + + + + + jdk_8_332 + + 1.8.0_332 + + + + + org.openjsse + openjsse + 1.1.10 + + + + + + + jdk_8_341 + + 1.8.0_341 + + + + + org.openjsse + openjsse + 1.1.10 + + + + + + jdk_8_342 + + 1.8.0_342 + + + + + org.openjsse + openjsse + 1.1.10 + + + + + + + jdk_8_351 + + 1.8.0_351 + + + + + org.openjsse + openjsse + 1.1.10 + + + + + + jdk_8_352 + + 1.8.0_352 + + + + + org.openjsse + openjsse + 1.1.10 + + + + + + + jdk_8_361 + + 1.8.0_361 + + + + + org.openjsse + openjsse + 1.1.11 + + + + + + jdk_8_362 + + 1.8.0_362 + + + + + org.openjsse + openjsse + 1.1.11 + + + + + + + jdk_8_371 + + 1.8.0_371 + + + + + org.openjsse + openjsse + 1.1.12 + + + + + + jdk_8_372 + + 1.8.0_372 + + + + + org.openjsse + openjsse + 1.1.12 + + + + + + + jdk_8_381 + + 1.8.0_381 + + + + + org.openjsse + openjsse + 1.1.13 + + + + + + jdk_8_382 + + 1.8.0_382 + + + + + org.openjsse + openjsse + 1.1.13 + + + + + + + jdk_8_391 + + 1.8.0_391 + + + + + org.openjsse + openjsse + 1.1.14 + + + + + + jdk_8_392 + + 1.8.0_392 + + + + + org.openjsse + openjsse + 1.1.14 + + + + + + + jdk_8 + + 1.8 + + + + + org.mockito + mockito-core + 4.11.0 + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + + no-arg + all-open + + + + + + + + + + + + + + compile + + compile + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + + + + org.jetbrains.kotlin + kotlin-maven-noarg + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -parameters + -h + target/headers + + ${java.release.version} + ${java.release.version} + UTF-8 + + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + **/*Doc.java + true + UTF-8 + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdk_11_plus + + [11,16) + + + + + org.mockito + mockito-core + 5.10.0 + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + + no-arg + all-open + + + + + + + + + + + + + + compile + + compile + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + + + + org.jetbrains.kotlin + kotlin-maven-noarg + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -parameters + -h + target/headers + + ${java.release.version} + ${java.release.version} + ${java.release.version} + UTF-8 + + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + ${java.release.version} + ${java.release.version} + **/*Doc.java + true + UTF-8 + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdk_16_plus + + [16,) + + + + + org.mockito + mockito-core + 5.10.0 + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + + no-arg + all-open + + + + + + + + + + + + + + compile + + compile + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + + + + org.jetbrains.kotlin + kotlin-maven-noarg + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -parameters + -h + target/headers + + ${java.release.version} + ${java.release.version} + ${java.release.version} + UTF-8 + + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + ${java.release.version} + ${java.release.version} + **/*Doc.java + true + UTF-8 + UTF-8 + + + + + + + + release + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + 30 + + + + + + + + sonatype_snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + + central + CentralRepository + https://repo1.maven.org/maven2/ + + + jcentral + JCenter + https://jcenter.bintray.com/ + + + + + + central + CentralRepository + https://repo1.maven.org/maven2/ + + + jcentral + JCenter + https://jcenter.bintray.com/ + + +