initial commit
This commit is contained in:
commit
a8d46c86f7
490 changed files with 56530 additions and 0 deletions
80
.github/workflows/codeql.yml
vendored
Normal file
80
.github/workflows/codeql.yml
vendored
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '27 21 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'java' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Java JDK
|
||||||
|
uses: actions/setup-java@v3.5.1
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/.settings
|
||||||
|
/.classpath
|
||||||
|
/.project
|
||||||
|
/.gradle
|
||||||
|
**/data
|
||||||
|
**/work
|
||||||
|
**/logs
|
||||||
|
**/.idea
|
||||||
|
**/target
|
||||||
|
**/out
|
||||||
|
**/build
|
||||||
|
.DS_Store
|
||||||
|
*.iml
|
||||||
|
*~
|
||||||
|
*.key
|
||||||
|
*.crt
|
202
LICENSE.txt
Normal file
202
LICENSE.txt
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
68
README.md
Normal file
68
README.md
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# Java Net API for servers and clients
|
||||||
|
|
||||||
|
## A consolidated Uniform Resource Locator implementation for Java
|
||||||
|
|
||||||
|
A Uniform Resource Locator (URL) is a compact representation of the
|
||||||
|
location and access method for a resource available via the Internet.
|
||||||
|
|
||||||
|
Historically, there are many different forms of internet resource representations, for example,
|
||||||
|
the URL (RFC 1738 as of 1994), the URI (RFC 2396 as of 1998), and IRI (RFC 3987 as of 2005),
|
||||||
|
and most of them have updated specifications.
|
||||||
|
|
||||||
|
This Java implementation serves as a universal point of handling all
|
||||||
|
different forms. It follows the syntax of the Uniform Resource Identifier (RFC 3986)
|
||||||
|
in accordance with the https://url.spec.whatwg.org/[WHATWG URL standard].
|
||||||
|
|
||||||
|
This alternative implementation of Uniform Resource Locator combines the features of the vanilla URI/URL Java SDK implementations
|
||||||
|
but removes it peculiarities and deficiencies, such as `java.lang.IllegalArgumentException: Illegal character in path at ... at java.net.URI.create()`
|
||||||
|
|
||||||
|
Normalization, NIO charset encoding/decoding, IPv6, an extensive set of schemes, and path matching have been added.
|
||||||
|
|
||||||
|
Fast building and parsing URLs, improved percent decoding/encoding, and URI templating features are included, to make
|
||||||
|
this library also useful in URI and IRI contexts.
|
||||||
|
|
||||||
|
While parsing and building, you have better control about address resolving. Only explicit `resolveFromhost` methods
|
||||||
|
will execute host lookup queries against DNS resolvers, otherwise, no resolving will occur under the hood.
|
||||||
|
|
||||||
|
You can build URLs with a fluent API, for example
|
||||||
|
|
||||||
|
```
|
||||||
|
URL.http().host("foo.com").toUrlString()
|
||||||
|
```
|
||||||
|
|
||||||
|
And you can parse URLs with a fluent API, for exmaple
|
||||||
|
|
||||||
|
```
|
||||||
|
URL url = URL.parser().parse("file:///foo/bar?foo=bar#fragment");
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no external dependency. The size of the jar library is ~118k. The only dependency on `java.net` are the classes
|
||||||
|
|
||||||
|
```
|
||||||
|
java.net.IDN
|
||||||
|
java.net.Inet4Address
|
||||||
|
java.net.Inet6Address
|
||||||
|
java.net.InetAddress
|
||||||
|
```
|
||||||
|
|
||||||
|
which might get re-implemented in another library at a later time, in a project like Netty DNS resolver.
|
||||||
|
|
||||||
|
## A simple HTTP server
|
||||||
|
|
||||||
|
## A netty-based HTTP server
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
Copyright (C) 2018 Jörg Prante
|
||||||
|
|
||||||
|
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.
|
14
benchmark/build.gradle
Normal file
14
benchmark/build.gradle
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
plugins {
|
||||||
|
id "io.morethan.jmhreport" version "0.9.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
jmhReport {
|
||||||
|
jmhResultPath = project.file('build/reports/jmh/result.json')
|
||||||
|
jmhReportOutput = project.file('build/reports/jmh')
|
||||||
|
}
|
||||||
|
apply from: rootProject.file('gradle/test/jmh.gradle')
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':net')
|
||||||
|
implementation project(':net-path')
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
package org.xbib.net.benchmark;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Fork;
|
||||||
|
import org.openjdk.jmh.annotations.Level;
|
||||||
|
import org.openjdk.jmh.annotations.Measurement;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.annotations.Scope;
|
||||||
|
import org.openjdk.jmh.annotations.Setup;
|
||||||
|
import org.openjdk.jmh.annotations.State;
|
||||||
|
import org.openjdk.jmh.annotations.Threads;
|
||||||
|
import org.openjdk.jmh.annotations.Timeout;
|
||||||
|
import org.openjdk.jmh.annotations.Warmup;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
import org.xbib.net.path.simple.Path;
|
||||||
|
import org.xbib.net.path.simple.PathComparator;
|
||||||
|
import org.xbib.net.path.simple.PathMatcher;
|
||||||
|
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
@Warmup(iterations = 3)
|
||||||
|
@Measurement(iterations = 3)
|
||||||
|
@Fork(value = 1, jvmArgsAppend = "-Xmx1024m")
|
||||||
|
@Threads(4)
|
||||||
|
@Timeout(time = 10, timeUnit = TimeUnit.MINUTES)
|
||||||
|
public class SimplePathBenchmark {
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class AllRoutesPatternParser extends PatternParserData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.allRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class StaticRoutesPatternParser extends PatternParserData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.staticRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class AllRoutesAntPathMatcher extends PathMatcherData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.allRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
bh.consume(data.matcher.match(pattern, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchAndSortAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
List<Path> matches = new ArrayList<>();
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
if (data.matcher.match(pattern, path)) {
|
||||||
|
matches.add(new Path(pattern));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches.sort(new PathComparator(path));
|
||||||
|
bh.consume(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class StaticRoutesAntPathMatcher extends PathMatcherData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.staticRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchStaticRoutesWithAntPathMatcher(StaticRoutesAntPathMatcher data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
bh.consume(data.matcher.match(pattern, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PatternParserData {
|
||||||
|
|
||||||
|
List<String> patterns = new ArrayList<>();
|
||||||
|
|
||||||
|
List<String> requestPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
void parseRoutes(List<Route> routes) {
|
||||||
|
routes.forEach(route -> {
|
||||||
|
this.patterns.add(route.pattern);
|
||||||
|
this.requestPaths.addAll(route.matchingPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PathMatcherData {
|
||||||
|
|
||||||
|
PathMatcher matcher = new PathMatcher();
|
||||||
|
|
||||||
|
List<String> patterns = new ArrayList<>();
|
||||||
|
|
||||||
|
List<String> requestPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
void parseRoutes(List<Route> routes) {
|
||||||
|
routes.forEach(route -> {
|
||||||
|
this.patterns.add(route.pattern);
|
||||||
|
this.requestPaths.addAll(route.matchingPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route in the web application.
|
||||||
|
* Each route has a path pattern and can generate sets of matching request paths for that pattern.
|
||||||
|
*/
|
||||||
|
static class Route {
|
||||||
|
|
||||||
|
private final String pattern;
|
||||||
|
|
||||||
|
private final List<String> matchingPaths;
|
||||||
|
|
||||||
|
public Route(String pattern, String... matchingPaths) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
if (matchingPaths.length > 0) {
|
||||||
|
this.matchingPaths = Arrays.asList(matchingPaths);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.matchingPaths = Collections.singletonList(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String pattern() {
|
||||||
|
return this.pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<String> matchingPaths() {
|
||||||
|
return this.matchingPaths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RouteGenerator {
|
||||||
|
|
||||||
|
static List<Route> staticRoutes() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/"),
|
||||||
|
new Route("/why-spring"),
|
||||||
|
new Route("/microservices"),
|
||||||
|
new Route("/reactive"),
|
||||||
|
new Route("/event-driven"),
|
||||||
|
new Route("/cloud"),
|
||||||
|
new Route("/web-applications"),
|
||||||
|
new Route("/serverless"),
|
||||||
|
new Route("/batch"),
|
||||||
|
new Route("/community/overview"),
|
||||||
|
new Route("/community/team"),
|
||||||
|
new Route("/community/events"),
|
||||||
|
new Route("/community/support"),
|
||||||
|
new Route("/some/other/section"),
|
||||||
|
new Route("/blog.atom")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> captureRoutes() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/guides"),
|
||||||
|
new Route("/guides/gs/{repositoryName}",
|
||||||
|
"/guides/gs/rest-service", "/guides/gs/scheduling-tasks",
|
||||||
|
"/guides/gs/consuming-rest", "/guides/gs/relational-data-access"),
|
||||||
|
new Route("/projects"),
|
||||||
|
new Route("/projects/{name}",
|
||||||
|
"/projects/spring-boot", "/projects/spring-framework",
|
||||||
|
"/projects/spring-data", "/projects/spring-security", "/projects/spring-cloud"),
|
||||||
|
new Route("/blog/category/{category}.atom",
|
||||||
|
"/blog/category/releases.atom", "/blog/category/engineering.atom",
|
||||||
|
"/blog/category/news.atom"),
|
||||||
|
new Route("/tools/{name}", "/tools/eclipse", "/tools/vscode"),
|
||||||
|
new Route("/team/{username}",
|
||||||
|
"/team/jhoeller", "/team/bclozel", "/team/snicoll", "/team/sdeleuze", "/team/rstoyanchev"),
|
||||||
|
new Route("/api/projects/{projectId}",
|
||||||
|
"/api/projects/spring-boot", "/api/projects/spring-framework",
|
||||||
|
"/api/projects/reactor", "/api/projects/spring-data",
|
||||||
|
"/api/projects/spring-restdocs", "/api/projects/spring-batch"),
|
||||||
|
new Route("/api/projects/{projectId}/releases/{version}",
|
||||||
|
"/api/projects/spring-boot/releases/2.3.0", "/api/projects/spring-framework/releases/5.3.0",
|
||||||
|
"/api/projects/spring-boot/releases/2.2.0", "/api/projects/spring-framework/releases/5.2.0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> regexRoute() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/blog/{year:\\\\d+}/{month:\\\\d+}/{day:\\\\d+}/{slug}",
|
||||||
|
"/blog/2020/01/01/spring-boot-released", "/blog/2020/02/10/this-week-in-spring",
|
||||||
|
"/blog/2020/03/12/spring-one-conference-2020", "/blog/2020/05/17/spring-io-barcelona-2020",
|
||||||
|
"/blog/2020/05/17/spring-io-barcelona-2020", "/blog/2020/06/06/spring-cloud-release"),
|
||||||
|
new Route("/user/{name:[a-z]+}",
|
||||||
|
"/user/emily", "/user/example", "/user/spring")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> allRoutes() {
|
||||||
|
List<Route> routes = new ArrayList<>();
|
||||||
|
routes.addAll(staticRoutes());
|
||||||
|
routes.addAll(captureRoutes());
|
||||||
|
routes.addAll(regexRoute());
|
||||||
|
routes.add(new Route("/static/**", "/static/image.png", "/static/style.css"));
|
||||||
|
routes.add(new Route("/**", "/notfound", "/favicon.ico"));
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
package org.xbib.net.benchmark;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Fork;
|
||||||
|
import org.openjdk.jmh.annotations.Level;
|
||||||
|
import org.openjdk.jmh.annotations.Measurement;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.annotations.Scope;
|
||||||
|
import org.openjdk.jmh.annotations.Setup;
|
||||||
|
import org.openjdk.jmh.annotations.State;
|
||||||
|
import org.openjdk.jmh.annotations.Threads;
|
||||||
|
import org.openjdk.jmh.annotations.Timeout;
|
||||||
|
import org.openjdk.jmh.annotations.Warmup;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
import org.xbib.net.path.spring.PathContainer;
|
||||||
|
import org.xbib.net.path.spring.PathPattern;
|
||||||
|
import org.xbib.net.path.spring.PathPatternParser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
@Warmup(iterations = 3)
|
||||||
|
@Measurement(iterations = 3)
|
||||||
|
@Fork(value = 1, jvmArgsAppend = "-Xmx1024m")
|
||||||
|
@Threads(4)
|
||||||
|
@Timeout(time = 10, timeUnit = TimeUnit.MINUTES)
|
||||||
|
public class SpringPathBenchmark {
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class AllRoutesPatternParser extends PatternParserData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.allRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchAllRoutesWithPathPatternParser(AllRoutesPatternParser data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
PathContainer pathContainer = PathContainer.parsePath(path);
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
PathPattern pathPattern = data.parser.parse(pattern);
|
||||||
|
bh.consume(pathPattern.matches(pathContainer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchAndSortAllRoutesWithPathPatternParser(AllRoutesPatternParser data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
PathContainer pathContainer = PathContainer.parsePath(path);
|
||||||
|
List<PathPattern> matches = new ArrayList<>();
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
PathPattern pathPattern = data.parser.parse(pattern);
|
||||||
|
if (pathPattern.matches(pathContainer)) {
|
||||||
|
matches.add(pathPattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(matches);
|
||||||
|
bh.consume(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class StaticRoutesPatternParser extends PatternParserData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.staticRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchStaticRoutesWithPathPatternParser(StaticRoutesPatternParser data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
PathContainer pathContainer = PathContainer.parsePath(path);
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
PathPattern pathPattern = data.parser.parse(pattern);
|
||||||
|
bh.consume(pathPattern.matches(pathContainer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PatternParserData {
|
||||||
|
|
||||||
|
PathPatternParser parser = new PathPatternParser();
|
||||||
|
|
||||||
|
List<String> patterns = new ArrayList<>();
|
||||||
|
|
||||||
|
List<String> requestPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
void parseRoutes(List<Route> routes) {
|
||||||
|
routes.forEach(route -> {
|
||||||
|
this.patterns.add(route.pattern);
|
||||||
|
this.requestPaths.addAll(route.matchingPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route in the web application.
|
||||||
|
* Each route has a path pattern and can generate sets of matching request paths for that pattern.
|
||||||
|
*/
|
||||||
|
static class Route {
|
||||||
|
|
||||||
|
private final String pattern;
|
||||||
|
|
||||||
|
private final List<String> matchingPaths;
|
||||||
|
|
||||||
|
public Route(String pattern, String... matchingPaths) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
if (matchingPaths.length > 0) {
|
||||||
|
this.matchingPaths = Arrays.asList(matchingPaths);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.matchingPaths = Collections.singletonList(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String pattern() {
|
||||||
|
return this.pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<String> matchingPaths() {
|
||||||
|
return this.matchingPaths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RouteGenerator {
|
||||||
|
|
||||||
|
static List<Route> staticRoutes() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/"),
|
||||||
|
new Route("/why-spring"),
|
||||||
|
new Route("/microservices"),
|
||||||
|
new Route("/reactive"),
|
||||||
|
new Route("/event-driven"),
|
||||||
|
new Route("/cloud"),
|
||||||
|
new Route("/web-applications"),
|
||||||
|
new Route("/serverless"),
|
||||||
|
new Route("/batch"),
|
||||||
|
new Route("/community/overview"),
|
||||||
|
new Route("/community/team"),
|
||||||
|
new Route("/community/events"),
|
||||||
|
new Route("/community/support"),
|
||||||
|
new Route("/some/other/section"),
|
||||||
|
new Route("/blog.atom")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> captureRoutes() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/guides"),
|
||||||
|
new Route("/guides/gs/{repositoryName}",
|
||||||
|
"/guides/gs/rest-service", "/guides/gs/scheduling-tasks",
|
||||||
|
"/guides/gs/consuming-rest", "/guides/gs/relational-data-access"),
|
||||||
|
new Route("/projects"),
|
||||||
|
new Route("/projects/{name}",
|
||||||
|
"/projects/spring-boot", "/projects/spring-framework",
|
||||||
|
"/projects/spring-data", "/projects/spring-security", "/projects/spring-cloud"),
|
||||||
|
new Route("/blog/category/{category}.atom",
|
||||||
|
"/blog/category/releases.atom", "/blog/category/engineering.atom",
|
||||||
|
"/blog/category/news.atom"),
|
||||||
|
new Route("/tools/{name}", "/tools/eclipse", "/tools/vscode"),
|
||||||
|
new Route("/team/{username}",
|
||||||
|
"/team/jhoeller", "/team/bclozel", "/team/snicoll", "/team/sdeleuze", "/team/rstoyanchev"),
|
||||||
|
new Route("/api/projects/{projectId}",
|
||||||
|
"/api/projects/spring-boot", "/api/projects/spring-framework",
|
||||||
|
"/api/projects/reactor", "/api/projects/spring-data",
|
||||||
|
"/api/projects/spring-restdocs", "/api/projects/spring-batch"),
|
||||||
|
new Route("/api/projects/{projectId}/releases/{version}",
|
||||||
|
"/api/projects/spring-boot/releases/2.3.0", "/api/projects/spring-framework/releases/5.3.0",
|
||||||
|
"/api/projects/spring-boot/releases/2.2.0", "/api/projects/spring-framework/releases/5.2.0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> regexRoute() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/blog/{year:\\\\d+}/{month:\\\\d+}/{day:\\\\d+}/{slug}",
|
||||||
|
"/blog/2020/01/01/spring-boot-released", "/blog/2020/02/10/this-week-in-spring",
|
||||||
|
"/blog/2020/03/12/spring-one-conference-2020", "/blog/2020/05/17/spring-io-barcelona-2020",
|
||||||
|
"/blog/2020/05/17/spring-io-barcelona-2020", "/blog/2020/06/06/spring-cloud-release"),
|
||||||
|
new Route("/user/{name:[a-z]+}",
|
||||||
|
"/user/emily", "/user/example", "/user/spring")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> allRoutes() {
|
||||||
|
List<Route> routes = new ArrayList<>();
|
||||||
|
routes.addAll(staticRoutes());
|
||||||
|
routes.addAll(captureRoutes());
|
||||||
|
routes.addAll(regexRoute());
|
||||||
|
routes.add(new Route("/static/**", "/static/image.png", "/static/style.css"));
|
||||||
|
routes.add(new Route("/**", "/notfound", "/favicon.ico"));
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
package org.xbib.net.benchmark;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Fork;
|
||||||
|
import org.openjdk.jmh.annotations.Level;
|
||||||
|
import org.openjdk.jmh.annotations.Measurement;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.annotations.Scope;
|
||||||
|
import org.openjdk.jmh.annotations.Setup;
|
||||||
|
import org.openjdk.jmh.annotations.State;
|
||||||
|
import org.openjdk.jmh.annotations.Threads;
|
||||||
|
import org.openjdk.jmh.annotations.Timeout;
|
||||||
|
import org.openjdk.jmh.annotations.Warmup;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
import org.xbib.net.path.structure.Path;
|
||||||
|
import org.xbib.net.path.structure.PathComparator;
|
||||||
|
import org.xbib.net.path.structure.PathMatcher;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
@Warmup(iterations = 3)
|
||||||
|
@Measurement(iterations = 3)
|
||||||
|
@Fork(value = 1, jvmArgsAppend = "-Xmx1024m")
|
||||||
|
@Threads(4)
|
||||||
|
@Timeout(time = 10, timeUnit = TimeUnit.MINUTES)
|
||||||
|
public class StructurePathBenchmark {
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class AllRoutesPatternParser extends PatternParserData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.allRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class StaticRoutesPatternParser extends PatternParserData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.staticRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class AllRoutesAntPathMatcher extends PathMatcherData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.allRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
bh.consume(data.matcher.match(pattern, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchAndSortAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
List<Path> matches = new ArrayList<>();
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
if (data.matcher.match(pattern, path)) {
|
||||||
|
matches.add(new Path(pattern));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches.sort(new PathComparator(path));
|
||||||
|
bh.consume(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class StaticRoutesAntPathMatcher extends PathMatcherData {
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void registerPatterns() {
|
||||||
|
parseRoutes(RouteGenerator.staticRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void matchStaticRoutesWithAntPathMatcher(StaticRoutesAntPathMatcher data, Blackhole bh) {
|
||||||
|
for (String path : data.requestPaths) {
|
||||||
|
for (String pattern : data.patterns) {
|
||||||
|
bh.consume(data.matcher.match(pattern, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PatternParserData {
|
||||||
|
|
||||||
|
List<String> patterns = new ArrayList<>();
|
||||||
|
|
||||||
|
List<String> requestPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
void parseRoutes(List<Route> routes) {
|
||||||
|
routes.forEach(route -> {
|
||||||
|
this.patterns.add(route.pattern);
|
||||||
|
this.requestPaths.addAll(route.matchingPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PathMatcherData {
|
||||||
|
|
||||||
|
PathMatcher matcher = new PathMatcher();
|
||||||
|
|
||||||
|
List<String> patterns = new ArrayList<>();
|
||||||
|
|
||||||
|
List<String> requestPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
void parseRoutes(List<Route> routes) {
|
||||||
|
routes.forEach(route -> {
|
||||||
|
this.patterns.add(route.pattern);
|
||||||
|
this.requestPaths.addAll(route.matchingPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route in the web application.
|
||||||
|
* Each route has a path pattern and can generate sets of matching request paths for that pattern.
|
||||||
|
*/
|
||||||
|
static class Route {
|
||||||
|
|
||||||
|
private final String pattern;
|
||||||
|
|
||||||
|
private final List<String> matchingPaths;
|
||||||
|
|
||||||
|
public Route(String pattern, String... matchingPaths) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
if (matchingPaths.length > 0) {
|
||||||
|
this.matchingPaths = Arrays.asList(matchingPaths);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.matchingPaths = Collections.singletonList(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String pattern() {
|
||||||
|
return this.pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<String> matchingPaths() {
|
||||||
|
return this.matchingPaths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RouteGenerator {
|
||||||
|
|
||||||
|
static List<Route> staticRoutes() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/"),
|
||||||
|
new Route("/why-spring"),
|
||||||
|
new Route("/microservices"),
|
||||||
|
new Route("/reactive"),
|
||||||
|
new Route("/event-driven"),
|
||||||
|
new Route("/cloud"),
|
||||||
|
new Route("/web-applications"),
|
||||||
|
new Route("/serverless"),
|
||||||
|
new Route("/batch"),
|
||||||
|
new Route("/community/overview"),
|
||||||
|
new Route("/community/team"),
|
||||||
|
new Route("/community/events"),
|
||||||
|
new Route("/community/support"),
|
||||||
|
new Route("/some/other/section"),
|
||||||
|
new Route("/blog.atom")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> captureRoutes() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/guides"),
|
||||||
|
new Route("/guides/gs/{repositoryName}",
|
||||||
|
"/guides/gs/rest-service", "/guides/gs/scheduling-tasks",
|
||||||
|
"/guides/gs/consuming-rest", "/guides/gs/relational-data-access"),
|
||||||
|
new Route("/projects"),
|
||||||
|
new Route("/projects/{name}",
|
||||||
|
"/projects/spring-boot", "/projects/spring-framework",
|
||||||
|
"/projects/spring-data", "/projects/spring-security", "/projects/spring-cloud"),
|
||||||
|
new Route("/blog/category/{category}.atom",
|
||||||
|
"/blog/category/releases.atom", "/blog/category/engineering.atom",
|
||||||
|
"/blog/category/news.atom"),
|
||||||
|
new Route("/tools/{name}", "/tools/eclipse", "/tools/vscode"),
|
||||||
|
new Route("/team/{username}",
|
||||||
|
"/team/jhoeller", "/team/bclozel", "/team/snicoll", "/team/sdeleuze", "/team/rstoyanchev"),
|
||||||
|
new Route("/api/projects/{projectId}",
|
||||||
|
"/api/projects/spring-boot", "/api/projects/spring-framework",
|
||||||
|
"/api/projects/reactor", "/api/projects/spring-data",
|
||||||
|
"/api/projects/spring-restdocs", "/api/projects/spring-batch"),
|
||||||
|
new Route("/api/projects/{projectId}/releases/{version}",
|
||||||
|
"/api/projects/spring-boot/releases/2.3.0", "/api/projects/spring-framework/releases/5.3.0",
|
||||||
|
"/api/projects/spring-boot/releases/2.2.0", "/api/projects/spring-framework/releases/5.2.0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> regexRoute() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Route("/blog/{year:\\\\d+}/{month:\\\\d+}/{day:\\\\d+}/{slug}",
|
||||||
|
"/blog/2020/01/01/spring-boot-released", "/blog/2020/02/10/this-week-in-spring",
|
||||||
|
"/blog/2020/03/12/spring-one-conference-2020", "/blog/2020/05/17/spring-io-barcelona-2020",
|
||||||
|
"/blog/2020/05/17/spring-io-barcelona-2020", "/blog/2020/06/06/spring-cloud-release"),
|
||||||
|
new Route("/user/{name:[a-z]+}",
|
||||||
|
"/user/emily", "/user/example", "/user/spring")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Route> allRoutes() {
|
||||||
|
List<Route> routes = new ArrayList<>();
|
||||||
|
routes.addAll(staticRoutes());
|
||||||
|
routes.addAll(captureRoutes());
|
||||||
|
routes.addAll(regexRoute());
|
||||||
|
routes.add(new Route("/static/**", "/static/image.png", "/static/style.css"));
|
||||||
|
routes.add(new Route("/**", "/notfound", "/favicon.ico"));
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
502
benchmark/src/jmh/reports/result-20220213.json
Normal file
502
benchmark/src/jmh/reports/result-20220213.json
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"jmhVersion" : "1.34",
|
||||||
|
"benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAllRoutesWithAntPathMatcher",
|
||||||
|
"mode" : "thrpt",
|
||||||
|
"threads" : 1,
|
||||||
|
"forks" : 5,
|
||||||
|
"jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java",
|
||||||
|
"jvmArgs" : [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Duser.country=DE",
|
||||||
|
"-Duser.language=de",
|
||||||
|
"-Duser.variant"
|
||||||
|
],
|
||||||
|
"jdkVersion" : "17.0.2",
|
||||||
|
"vmName" : "OpenJDK 64-Bit Server VM",
|
||||||
|
"vmVersion" : "17.0.2+8-LTS",
|
||||||
|
"warmupIterations" : 5,
|
||||||
|
"warmupTime" : "10 s",
|
||||||
|
"warmupBatchSize" : 1,
|
||||||
|
"measurementIterations" : 5,
|
||||||
|
"measurementTime" : "10 s",
|
||||||
|
"measurementBatchSize" : 1,
|
||||||
|
"primaryMetric" : {
|
||||||
|
"score" : 979.0629333513093,
|
||||||
|
"scoreError" : 27.642716728605016,
|
||||||
|
"scoreConfidence" : [
|
||||||
|
951.4202166227043,
|
||||||
|
1006.7056500799143
|
||||||
|
],
|
||||||
|
"scorePercentiles" : {
|
||||||
|
"0.0" : 879.6177196347368,
|
||||||
|
"50.0" : 996.1943219563238,
|
||||||
|
"90.0" : 1010.1573254891413,
|
||||||
|
"95.0" : 1011.419317810492,
|
||||||
|
"99.0" : 1011.8817472575878,
|
||||||
|
"99.9" : 1011.8817472575878,
|
||||||
|
"99.99" : 1011.8817472575878,
|
||||||
|
"99.999" : 1011.8817472575878,
|
||||||
|
"99.9999" : 1011.8817472575878,
|
||||||
|
"100.0" : 1011.8817472575878
|
||||||
|
},
|
||||||
|
"scoreUnit" : "ops/s",
|
||||||
|
"rawData" : [
|
||||||
|
[
|
||||||
|
1009.2169360206196,
|
||||||
|
1007.6289077910706,
|
||||||
|
1010.3403157672683,
|
||||||
|
979.6478990918882,
|
||||||
|
927.4713639881832
|
||||||
|
],
|
||||||
|
[
|
||||||
|
954.1692363754581,
|
||||||
|
1006.7576171864503,
|
||||||
|
1008.6971779542434,
|
||||||
|
987.6616019498011,
|
||||||
|
1006.9774361173152
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1003.6756349360693,
|
||||||
|
1008.777218930438,
|
||||||
|
996.8131753845925,
|
||||||
|
1010.0353319703898,
|
||||||
|
1011.8817472575878
|
||||||
|
],
|
||||||
|
[
|
||||||
|
996.1943219563238,
|
||||||
|
987.4294465450744,
|
||||||
|
980.0787394199706,
|
||||||
|
924.6048449498232,
|
||||||
|
955.5666395887631
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1005.7277746036237,
|
||||||
|
936.5475165296377,
|
||||||
|
879.6177196347368,
|
||||||
|
917.0897875515193,
|
||||||
|
963.9649422818879
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secondaryMetrics" : {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jmhVersion" : "1.34",
|
||||||
|
"benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAllRoutesWithPathPatternParser",
|
||||||
|
"mode" : "thrpt",
|
||||||
|
"threads" : 1,
|
||||||
|
"forks" : 5,
|
||||||
|
"jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java",
|
||||||
|
"jvmArgs" : [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Duser.country=DE",
|
||||||
|
"-Duser.language=de",
|
||||||
|
"-Duser.variant"
|
||||||
|
],
|
||||||
|
"jdkVersion" : "17.0.2",
|
||||||
|
"vmName" : "OpenJDK 64-Bit Server VM",
|
||||||
|
"vmVersion" : "17.0.2+8-LTS",
|
||||||
|
"warmupIterations" : 5,
|
||||||
|
"warmupTime" : "10 s",
|
||||||
|
"warmupBatchSize" : 1,
|
||||||
|
"measurementIterations" : 5,
|
||||||
|
"measurementTime" : "10 s",
|
||||||
|
"measurementBatchSize" : 1,
|
||||||
|
"primaryMetric" : {
|
||||||
|
"score" : 44254.50565969629,
|
||||||
|
"scoreError" : 1524.9519049748324,
|
||||||
|
"scoreConfidence" : [
|
||||||
|
42729.553754721455,
|
||||||
|
45779.457564671124
|
||||||
|
],
|
||||||
|
"scorePercentiles" : {
|
||||||
|
"0.0" : 41271.26175680116,
|
||||||
|
"50.0" : 43804.78755944657,
|
||||||
|
"90.0" : 48137.73981257094,
|
||||||
|
"95.0" : 48652.8016902135,
|
||||||
|
"99.0" : 48702.20603834606,
|
||||||
|
"99.9" : 48702.20603834606,
|
||||||
|
"99.99" : 48702.20603834606,
|
||||||
|
"99.999" : 48702.20603834606,
|
||||||
|
"99.9999" : 48702.20603834606,
|
||||||
|
"100.0" : 48702.20603834606
|
||||||
|
},
|
||||||
|
"scoreUnit" : "ops/s",
|
||||||
|
"rawData" : [
|
||||||
|
[
|
||||||
|
43954.01874148045,
|
||||||
|
43804.78755944657,
|
||||||
|
43280.17445633301,
|
||||||
|
44021.35079059644,
|
||||||
|
43077.16644786855
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41715.626689627716,
|
||||||
|
43111.08050914323,
|
||||||
|
42619.336800500045,
|
||||||
|
41271.26175680116,
|
||||||
|
43714.16736786474
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43084.22399673246,
|
||||||
|
41654.897785714944,
|
||||||
|
43662.041021929304,
|
||||||
|
44422.07541071771,
|
||||||
|
45034.34913614275
|
||||||
|
],
|
||||||
|
[
|
||||||
|
44618.34940975135,
|
||||||
|
44743.65546839248,
|
||||||
|
42666.36782811372,
|
||||||
|
44101.84625525753,
|
||||||
|
43335.04048928811
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48702.20603834606,
|
||||||
|
47490.34864222843,
|
||||||
|
45869.52757654424,
|
||||||
|
47871.216435682116,
|
||||||
|
48537.52487790418
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secondaryMetrics" : {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jmhVersion" : "1.34",
|
||||||
|
"benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAndSortAllRoutesWithAntPathMatcher",
|
||||||
|
"mode" : "thrpt",
|
||||||
|
"threads" : 1,
|
||||||
|
"forks" : 5,
|
||||||
|
"jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java",
|
||||||
|
"jvmArgs" : [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Duser.country=DE",
|
||||||
|
"-Duser.language=de",
|
||||||
|
"-Duser.variant"
|
||||||
|
],
|
||||||
|
"jdkVersion" : "17.0.2",
|
||||||
|
"vmName" : "OpenJDK 64-Bit Server VM",
|
||||||
|
"vmVersion" : "17.0.2+8-LTS",
|
||||||
|
"warmupIterations" : 5,
|
||||||
|
"warmupTime" : "10 s",
|
||||||
|
"warmupBatchSize" : 1,
|
||||||
|
"measurementIterations" : 5,
|
||||||
|
"measurementTime" : "10 s",
|
||||||
|
"measurementBatchSize" : 1,
|
||||||
|
"primaryMetric" : {
|
||||||
|
"score" : 950.1275103274515,
|
||||||
|
"scoreError" : 15.190556194806026,
|
||||||
|
"scoreConfidence" : [
|
||||||
|
934.9369541326455,
|
||||||
|
965.3180665222576
|
||||||
|
],
|
||||||
|
"scorePercentiles" : {
|
||||||
|
"0.0" : 911.3943124100621,
|
||||||
|
"50.0" : 952.2968617887568,
|
||||||
|
"90.0" : 977.7307762395892,
|
||||||
|
"95.0" : 981.1789404332043,
|
||||||
|
"99.0" : 982.0691544544186,
|
||||||
|
"99.9" : 982.0691544544186,
|
||||||
|
"99.99" : 982.0691544544186,
|
||||||
|
"99.999" : 982.0691544544186,
|
||||||
|
"99.9999" : 982.0691544544186,
|
||||||
|
"100.0" : 982.0691544544186
|
||||||
|
},
|
||||||
|
"scoreUnit" : "ops/s",
|
||||||
|
"rawData" : [
|
||||||
|
[
|
||||||
|
982.0691544544186,
|
||||||
|
955.0374171216056,
|
||||||
|
979.1017743837043,
|
||||||
|
964.6624852400518,
|
||||||
|
925.0276825554569
|
||||||
|
],
|
||||||
|
[
|
||||||
|
956.1755582451195,
|
||||||
|
911.3943124100621,
|
||||||
|
929.0652340073174,
|
||||||
|
967.4455956409554,
|
||||||
|
952.9508945384788
|
||||||
|
],
|
||||||
|
[
|
||||||
|
973.057696831325,
|
||||||
|
966.6347503815825,
|
||||||
|
936.0666443887872,
|
||||||
|
976.8167774768458,
|
||||||
|
951.504251121885
|
||||||
|
],
|
||||||
|
[
|
||||||
|
952.2968617887568,
|
||||||
|
921.966200024008,
|
||||||
|
916.1240790196013,
|
||||||
|
933.0207219756703,
|
||||||
|
945.3684132560395
|
||||||
|
],
|
||||||
|
[
|
||||||
|
939.5733852782138,
|
||||||
|
975.007311710178,
|
||||||
|
956.226440100797,
|
||||||
|
940.4884972825927,
|
||||||
|
946.1056189528312
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secondaryMetrics" : {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jmhVersion" : "1.34",
|
||||||
|
"benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAndSortAllRoutesWithPathPatternParser",
|
||||||
|
"mode" : "thrpt",
|
||||||
|
"threads" : 1,
|
||||||
|
"forks" : 5,
|
||||||
|
"jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java",
|
||||||
|
"jvmArgs" : [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Duser.country=DE",
|
||||||
|
"-Duser.language=de",
|
||||||
|
"-Duser.variant"
|
||||||
|
],
|
||||||
|
"jdkVersion" : "17.0.2",
|
||||||
|
"vmName" : "OpenJDK 64-Bit Server VM",
|
||||||
|
"vmVersion" : "17.0.2+8-LTS",
|
||||||
|
"warmupIterations" : 5,
|
||||||
|
"warmupTime" : "10 s",
|
||||||
|
"warmupBatchSize" : 1,
|
||||||
|
"measurementIterations" : 5,
|
||||||
|
"measurementTime" : "10 s",
|
||||||
|
"measurementBatchSize" : 1,
|
||||||
|
"primaryMetric" : {
|
||||||
|
"score" : 40031.37477838221,
|
||||||
|
"scoreError" : 1996.2389298981366,
|
||||||
|
"scoreConfidence" : [
|
||||||
|
38035.135848484075,
|
||||||
|
42027.61370828035
|
||||||
|
],
|
||||||
|
"scorePercentiles" : {
|
||||||
|
"0.0" : 36433.00109114869,
|
||||||
|
"50.0" : 39320.61428369096,
|
||||||
|
"90.0" : 45155.11092333606,
|
||||||
|
"95.0" : 45599.71727569494,
|
||||||
|
"99.0" : 45693.3908403424,
|
||||||
|
"99.9" : 45693.3908403424,
|
||||||
|
"99.99" : 45693.3908403424,
|
||||||
|
"99.999" : 45693.3908403424,
|
||||||
|
"99.9999" : 45693.3908403424,
|
||||||
|
"100.0" : 45693.3908403424
|
||||||
|
},
|
||||||
|
"scoreUnit" : "ops/s",
|
||||||
|
"rawData" : [
|
||||||
|
[
|
||||||
|
39599.68477777575,
|
||||||
|
37973.60369831034,
|
||||||
|
38396.27633481238,
|
||||||
|
39253.010054103164,
|
||||||
|
37429.51267783532
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38067.106506328586,
|
||||||
|
39124.57632651807,
|
||||||
|
38262.53596403596,
|
||||||
|
39303.94121151709,
|
||||||
|
39504.46473802137
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39794.17254168896,
|
||||||
|
39705.71096962702,
|
||||||
|
39652.38632950062,
|
||||||
|
39579.80206238178,
|
||||||
|
39512.01803650355
|
||||||
|
],
|
||||||
|
[
|
||||||
|
44115.77467607626,
|
||||||
|
45004.42112232619,
|
||||||
|
45381.14562485087,
|
||||||
|
44725.72512753276,
|
||||||
|
45693.3908403424
|
||||||
|
],
|
||||||
|
[
|
||||||
|
36433.00109114869,
|
||||||
|
39320.61428369096,
|
||||||
|
38484.66091818098,
|
||||||
|
38495.814342280995,
|
||||||
|
37971.019204165306
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secondaryMetrics" : {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jmhVersion" : "1.34",
|
||||||
|
"benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchStaticRoutesWithAntPathMatcher",
|
||||||
|
"mode" : "thrpt",
|
||||||
|
"threads" : 1,
|
||||||
|
"forks" : 5,
|
||||||
|
"jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java",
|
||||||
|
"jvmArgs" : [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Duser.country=DE",
|
||||||
|
"-Duser.language=de",
|
||||||
|
"-Duser.variant"
|
||||||
|
],
|
||||||
|
"jdkVersion" : "17.0.2",
|
||||||
|
"vmName" : "OpenJDK 64-Bit Server VM",
|
||||||
|
"vmVersion" : "17.0.2+8-LTS",
|
||||||
|
"warmupIterations" : 5,
|
||||||
|
"warmupTime" : "10 s",
|
||||||
|
"warmupBatchSize" : 1,
|
||||||
|
"measurementIterations" : 5,
|
||||||
|
"measurementTime" : "10 s",
|
||||||
|
"measurementBatchSize" : 1,
|
||||||
|
"primaryMetric" : {
|
||||||
|
"score" : 8205.295027193253,
|
||||||
|
"scoreError" : 253.93643368708845,
|
||||||
|
"scoreConfidence" : [
|
||||||
|
7951.358593506165,
|
||||||
|
8459.23146088034
|
||||||
|
],
|
||||||
|
"scorePercentiles" : {
|
||||||
|
"0.0" : 7494.992406948476,
|
||||||
|
"50.0" : 8250.043607694692,
|
||||||
|
"90.0" : 8682.981256843032,
|
||||||
|
"95.0" : 8702.07708088446,
|
||||||
|
"99.0" : 8703.611244566133,
|
||||||
|
"99.9" : 8703.611244566133,
|
||||||
|
"99.99" : 8703.611244566133,
|
||||||
|
"99.999" : 8703.611244566133,
|
||||||
|
"99.9999" : 8703.611244566133,
|
||||||
|
"100.0" : 8703.611244566133
|
||||||
|
},
|
||||||
|
"scoreUnit" : "ops/s",
|
||||||
|
"rawData" : [
|
||||||
|
[
|
||||||
|
7776.33432977133,
|
||||||
|
7796.534803257248,
|
||||||
|
7976.205818850183,
|
||||||
|
7750.053487676609,
|
||||||
|
8023.12289153653
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7849.010784866861,
|
||||||
|
7494.992406948476,
|
||||||
|
7888.447757517585,
|
||||||
|
7982.353388245457,
|
||||||
|
8308.311812674916
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8046.8394027785125,
|
||||||
|
8184.99855687572,
|
||||||
|
8209.36631211148,
|
||||||
|
8363.572625644612,
|
||||||
|
8460.14147216783
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8658.496160722716,
|
||||||
|
8654.928791158243,
|
||||||
|
8703.611244566133,
|
||||||
|
8698.497365627227,
|
||||||
|
8672.637184320234
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8296.063727961213,
|
||||||
|
8396.558144577311,
|
||||||
|
8250.043607694692,
|
||||||
|
8381.721189986161,
|
||||||
|
8309.5324122941
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secondaryMetrics" : {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jmhVersion" : "1.34",
|
||||||
|
"benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchStaticRoutesWithPathPatternParser",
|
||||||
|
"mode" : "thrpt",
|
||||||
|
"threads" : 1,
|
||||||
|
"forks" : 5,
|
||||||
|
"jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java",
|
||||||
|
"jvmArgs" : [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Duser.country=DE",
|
||||||
|
"-Duser.language=de",
|
||||||
|
"-Duser.variant"
|
||||||
|
],
|
||||||
|
"jdkVersion" : "17.0.2",
|
||||||
|
"vmName" : "OpenJDK 64-Bit Server VM",
|
||||||
|
"vmVersion" : "17.0.2+8-LTS",
|
||||||
|
"warmupIterations" : 5,
|
||||||
|
"warmupTime" : "10 s",
|
||||||
|
"warmupBatchSize" : 1,
|
||||||
|
"measurementIterations" : 5,
|
||||||
|
"measurementTime" : "10 s",
|
||||||
|
"measurementBatchSize" : 1,
|
||||||
|
"primaryMetric" : {
|
||||||
|
"score" : 365562.56886055216,
|
||||||
|
"scoreError" : 18569.95078039208,
|
||||||
|
"scoreConfidence" : [
|
||||||
|
346992.61808016006,
|
||||||
|
384132.51964094426
|
||||||
|
],
|
||||||
|
"scorePercentiles" : {
|
||||||
|
"0.0" : 325787.95505597815,
|
||||||
|
"50.0" : 374718.5719088665,
|
||||||
|
"90.0" : 392889.71549621335,
|
||||||
|
"95.0" : 398253.89062451763,
|
||||||
|
"99.0" : 399176.92717245553,
|
||||||
|
"99.9" : 399176.92717245553,
|
||||||
|
"99.99" : 399176.92717245553,
|
||||||
|
"99.999" : 399176.92717245553,
|
||||||
|
"99.9999" : 399176.92717245553,
|
||||||
|
"100.0" : 399176.92717245553
|
||||||
|
},
|
||||||
|
"scoreUnit" : "ops/s",
|
||||||
|
"rawData" : [
|
||||||
|
[
|
||||||
|
388662.8727349311,
|
||||||
|
390749.43337413616,
|
||||||
|
387249.9287947881,
|
||||||
|
386499.16250676767,
|
||||||
|
374425.2685252388
|
||||||
|
],
|
||||||
|
[
|
||||||
|
352727.7248253849,
|
||||||
|
349601.7502852739,
|
||||||
|
351124.6614028987,
|
||||||
|
333424.47001959843,
|
||||||
|
333542.1583066851
|
||||||
|
],
|
||||||
|
[
|
||||||
|
327552.12555473513,
|
||||||
|
325787.95505597815,
|
||||||
|
335012.9126031102,
|
||||||
|
331770.3252415796,
|
||||||
|
335493.0603334665
|
||||||
|
],
|
||||||
|
[
|
||||||
|
399176.92717245553,
|
||||||
|
396100.1386793292,
|
||||||
|
381384.6900987156,
|
||||||
|
372390.3028315935,
|
||||||
|
376181.92271467706
|
||||||
|
],
|
||||||
|
[
|
||||||
|
374718.5719088665,
|
||||||
|
382122.1544260723,
|
||||||
|
385231.46982420154,
|
||||||
|
384140.7704616707,
|
||||||
|
383993.4638316473
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secondaryMetrics" : {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
46
build.gradle
Normal file
46
build.gradle
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url 'https://xbib.org/repository'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "org.xbib.gradle.plugin:gradle-plugin-shadow:1.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "de.marcphilipp.nexus-publish" version "0.4.0"
|
||||||
|
id "io.codearte.nexus-staging" version "0.21.1"
|
||||||
|
id "org.xbib.gradle.plugin.asciidoctor" version "2.5.2.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper {
|
||||||
|
gradleVersion = libs.versions.gradle.get()
|
||||||
|
distributionType = Wrapper.DistributionType.ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
user = 'xbib'
|
||||||
|
name = 'net'
|
||||||
|
description = 'Network classes for Java'
|
||||||
|
inceptionYear = '2016'
|
||||||
|
url = 'https://github.com/' + user + '/' + name
|
||||||
|
scmUrl = 'https://github.com/' + user + '/' + name
|
||||||
|
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
|
||||||
|
scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git'
|
||||||
|
issueManagementSystem = 'Github'
|
||||||
|
issueManagementUrl = ext.scmUrl + '/issues'
|
||||||
|
licenseName = 'The Apache License, Version 2.0'
|
||||||
|
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply from: rootProject.file('gradle/ide/idea.gradle')
|
||||||
|
apply from: rootProject.file('gradle/repositories/maven.gradle')
|
||||||
|
apply from: rootProject.file('gradle/compile/java.gradle')
|
||||||
|
apply from: rootProject.file('gradle/test/junit5.gradle')
|
||||||
|
apply from: rootProject.file('gradle/documentation/asciidoc.gradle')
|
||||||
|
apply from: rootProject.file('gradle/publish/maven.gradle')
|
||||||
|
}
|
||||||
|
apply from: rootProject.file('gradle/publish/sonatype.gradle')
|
321
config/checkstyle/checkstyle.xml
Normal file
321
config/checkstyle/checkstyle.xml
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
||||||
|
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
|
||||||
|
|
||||||
|
<!-- This is a checkstyle configuration file. For descriptions of
|
||||||
|
what the following rules do, please see the checkstyle configuration
|
||||||
|
page at http://checkstyle.sourceforge.net/config.html -->
|
||||||
|
|
||||||
|
<module name="Checker">
|
||||||
|
|
||||||
|
<module name="FileTabCharacter">
|
||||||
|
<!-- Checks that there are no tab characters in the file.
|
||||||
|
-->
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="NewlineAtEndOfFile">
|
||||||
|
<property name="lineSeparator" value="lf"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<!-- Checks that FIXME is not used in comments. TODO is preferred.
|
||||||
|
-->
|
||||||
|
<property name="format" value="((//.*)|(\*.*))FIXME" />
|
||||||
|
<property name="message" value='TODO is preferred to FIXME. e.g. "TODO(johndoe): Refactor when v2 is released."' />
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<!-- Checks that TODOs are named. (Actually, just that they are followed
|
||||||
|
by an open paren.)
|
||||||
|
-->
|
||||||
|
<property name="format" value="((//.*)|(\*.*))TODO[^(]" />
|
||||||
|
<property name="message" value='All TODOs should be named. e.g. "TODO(johndoe): Refactor when v2 is released."' />
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="JavadocPackage">
|
||||||
|
<!-- Checks that each Java package has a Javadoc file used for commenting.
|
||||||
|
Only allows a package-info.java, not package.html. -->
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- All Java AST specific tests live under TreeWalker module. -->
|
||||||
|
<module name="TreeWalker">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
IMPORT CHECKS
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<module name="RedundantImport">
|
||||||
|
<!-- Checks for redundant import statements. -->
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ImportOrder">
|
||||||
|
<property name="separated" value="true"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
<property name="groups" value="*,javax,java"/>
|
||||||
|
<property name="option" value="bottom"/>
|
||||||
|
<property name="elements" value="IMPORT, STATIC_IMPORT"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
JAVADOC CHECKS
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Checks for Javadoc comments. -->
|
||||||
|
<!-- See http://checkstyle.sf.net/config_javadoc.html -->
|
||||||
|
<module name="JavadocMethod">
|
||||||
|
<property name="scope" value="protected"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
<property name="allowMissingJavadoc" value="true"/>
|
||||||
|
<property name="allowMissingParamTags" value="true"/>
|
||||||
|
<property name="allowMissingReturnTag" value="true"/>
|
||||||
|
<property name="allowMissingThrowsTags" value="true"/>
|
||||||
|
<property name="allowThrowsTagsForSubclasses" value="true"/>
|
||||||
|
<property name="allowUndeclaredRTE" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="JavadocType">
|
||||||
|
<property name="scope" value="protected"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="JavadocStyle">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
NAMING CHECKS
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Item 38 - Adhere to generally accepted naming conventions -->
|
||||||
|
|
||||||
|
<module name="PackageName">
|
||||||
|
<!-- Validates identifiers for package names against the
|
||||||
|
supplied expression. -->
|
||||||
|
<!-- Here the default checkstyle rule restricts package name parts to
|
||||||
|
seven characters, this is not in line with common practice at Google.
|
||||||
|
-->
|
||||||
|
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]{1,})*$"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="TypeNameCheck">
|
||||||
|
<!-- Validates static, final fields against the
|
||||||
|
expression "^[A-Z][a-zA-Z0-9]*$". -->
|
||||||
|
<metadata name="altname" value="TypeName"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ConstantNameCheck">
|
||||||
|
<!-- Validates non-private, static, final fields against the supplied
|
||||||
|
public/package final fields "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$". -->
|
||||||
|
<metadata name="altname" value="ConstantName"/>
|
||||||
|
<property name="applyToPublic" value="true"/>
|
||||||
|
<property name="applyToProtected" value="true"/>
|
||||||
|
<property name="applyToPackage" value="true"/>
|
||||||
|
<property name="applyToPrivate" value="false"/>
|
||||||
|
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|FLAG_.*)$"/>
|
||||||
|
<message key="name.invalidPattern"
|
||||||
|
value="Variable ''{0}'' should be in ALL_CAPS (if it is a constant) or be private (otherwise)."/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="StaticVariableNameCheck">
|
||||||
|
<!-- Validates static, non-final fields against the supplied
|
||||||
|
expression "^[a-z][a-zA-Z0-9]*_?$". -->
|
||||||
|
<metadata name="altname" value="StaticVariableName"/>
|
||||||
|
<property name="applyToPublic" value="true"/>
|
||||||
|
<property name="applyToProtected" value="true"/>
|
||||||
|
<property name="applyToPackage" value="true"/>
|
||||||
|
<property name="applyToPrivate" value="true"/>
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*_?$"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="MemberNameCheck">
|
||||||
|
<!-- Validates non-static members against the supplied expression. -->
|
||||||
|
<metadata name="altname" value="MemberName"/>
|
||||||
|
<property name="applyToPublic" value="true"/>
|
||||||
|
<property name="applyToProtected" value="true"/>
|
||||||
|
<property name="applyToPackage" value="true"/>
|
||||||
|
<property name="applyToPrivate" value="true"/>
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="MethodNameCheck">
|
||||||
|
<!-- Validates identifiers for method names. -->
|
||||||
|
<metadata name="altname" value="MethodName"/>
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ParameterName">
|
||||||
|
<!-- Validates identifiers for method parameters against the
|
||||||
|
expression "^[a-z][a-zA-Z0-9]*$". -->
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LocalFinalVariableName">
|
||||||
|
<!-- Validates identifiers for local final variables against the
|
||||||
|
expression "^[a-z][a-zA-Z0-9]*$". -->
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LocalVariableName">
|
||||||
|
<!-- Validates identifiers for local variables against the
|
||||||
|
expression "^[a-z][a-zA-Z0-9]*$". -->
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
LENGTH and CODING CHECKS
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<module name="LineLength">
|
||||||
|
<!-- Checks if a line is too long. -->
|
||||||
|
<property name="max" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}" default="128"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The default ignore pattern exempts the following elements:
|
||||||
|
- import statements
|
||||||
|
- long URLs inside comments
|
||||||
|
-->
|
||||||
|
|
||||||
|
<property name="ignorePattern"
|
||||||
|
value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}"
|
||||||
|
default="^(package .*;\s*)|(import .*;\s*)|( *(\*|//).*https?://.*)$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="LeftCurly">
|
||||||
|
<!-- Checks for placement of the left curly brace ('{'). -->
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="RightCurly">
|
||||||
|
<!-- Checks right curlies on CATCH, ELSE, and TRY blocks are on
|
||||||
|
the same line. e.g., the following example is fine:
|
||||||
|
<pre>
|
||||||
|
if {
|
||||||
|
...
|
||||||
|
} else
|
||||||
|
</pre>
|
||||||
|
-->
|
||||||
|
<!-- This next example is not fine:
|
||||||
|
<pre>
|
||||||
|
if {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
else
|
||||||
|
</pre>
|
||||||
|
-->
|
||||||
|
<property name="option" value="same"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Checks for braces around if and else blocks -->
|
||||||
|
<module name="NeedBraces">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
<property name="elements" value="LITERAL_IF, LITERAL_ELSE, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="UpperEll">
|
||||||
|
<!-- Checks that long constants are defined with an upper ell.-->
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="FallThrough">
|
||||||
|
<!-- Warn about falling through to the next case statement. Similar to
|
||||||
|
javac -Xlint:fallthrough, but the check is suppressed if a single-line comment
|
||||||
|
on the last non-blank line preceding the fallen-into case contains 'fall through' (or
|
||||||
|
some other variants which we don't publicized to promote consistency).
|
||||||
|
-->
|
||||||
|
<property name="reliefPattern"
|
||||||
|
value="fall through|Fall through|fallthru|Fallthru|falls through|Falls through|fallthrough|Fallthrough|No break|NO break|no break|continue on"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
MODIFIERS CHECKS
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<module name="ModifierOrder">
|
||||||
|
<!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and
|
||||||
|
8.4.3. The prescribed order is:
|
||||||
|
public, protected, private, abstract, static, final, transient, volatile,
|
||||||
|
synchronized, native, strictfp
|
||||||
|
-->
|
||||||
|
</module>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
WHITESPACE CHECKS
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<module name="WhitespaceAround">
|
||||||
|
<!-- Checks that various elements are surrounded by whitespace.
|
||||||
|
This includes most binary operators and keywords followed
|
||||||
|
by regular or curly braces.
|
||||||
|
-->
|
||||||
|
<property name="elements" value="ASSIGN, BAND, BAND_ASSIGN, BOR,
|
||||||
|
BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,
|
||||||
|
EQUAL, GE, GT, LAND, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
|
||||||
|
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
|
||||||
|
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
|
||||||
|
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,
|
||||||
|
SL, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="WhitespaceAfter">
|
||||||
|
<!-- Checks that commas, semicolons and typecasts are followed by
|
||||||
|
whitespace.
|
||||||
|
-->
|
||||||
|
<property name="elements" value="COMMA, SEMI, TYPECAST"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="NoWhitespaceAfter">
|
||||||
|
<!-- Checks that there is no whitespace after various unary operators.
|
||||||
|
Linebreaks are allowed.
|
||||||
|
-->
|
||||||
|
<property name="elements" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,
|
||||||
|
UNARY_PLUS"/>
|
||||||
|
<property name="allowLineBreaks" value="true"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="NoWhitespaceBefore">
|
||||||
|
<!-- Checks that there is no whitespace before various unary operators.
|
||||||
|
Linebreaks are allowed.
|
||||||
|
-->
|
||||||
|
<property name="elements" value="SEMI, DOT, POST_DEC, POST_INC"/>
|
||||||
|
<property name="allowLineBreaks" value="true"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ParenPad">
|
||||||
|
<!-- Checks that there is no whitespace before close parens or after
|
||||||
|
open parens.
|
||||||
|
-->
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
</module>
|
||||||
|
</module>
|
||||||
|
|
5
gradle.properties
Normal file
5
gradle.properties
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
group = org.xbib
|
||||||
|
name = net
|
||||||
|
version = 3.0.0
|
||||||
|
|
||||||
|
org.gradle.warning.mode = ALL
|
30
gradle/compile/java.gradle
Normal file
30
gradle/compile/java.gradle
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
apply plugin: 'java-library'
|
||||||
|
|
||||||
|
java {
|
||||||
|
modularity.inferModulePath.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
compileJava {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
compileTestJava {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes('Implementation-Version': project.version)
|
||||||
|
}
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
options.compilerArgs.add('-Xlint:all,-exports')
|
||||||
|
}
|
||||||
|
|
||||||
|
javadoc {
|
||||||
|
options.addStringOption('Xdoclint:none', '-quiet')
|
||||||
|
}
|
13
gradle/documentation/asciidoc.gradle
Normal file
13
gradle/documentation/asciidoc.gradle
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||||
|
|
||||||
|
asciidoctor {
|
||||||
|
attributes 'source-highlighter': 'coderay',
|
||||||
|
toc: 'left',
|
||||||
|
doctype: 'book',
|
||||||
|
icons: 'font',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
sectlink: true,
|
||||||
|
sectanchors: true,
|
||||||
|
linkattrs: true,
|
||||||
|
imagesdir: 'img'
|
||||||
|
}
|
8
gradle/ide/idea.gradle
Normal file
8
gradle/ide/idea.gradle
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
apply plugin: 'idea'
|
||||||
|
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
outputDir file('build/classes/java/main')
|
||||||
|
testOutputDir file('build/classes/java/test')
|
||||||
|
}
|
||||||
|
}
|
27
gradle/publish/ivy.gradle
Normal file
27
gradle/publish/ivy.gradle
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
apply plugin: 'ivy-publish'
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
ivy {
|
||||||
|
url = "https://xbib.org/repo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publications {
|
||||||
|
ivy(IvyPublication) {
|
||||||
|
from components.java
|
||||||
|
descriptor {
|
||||||
|
license {
|
||||||
|
name = 'The Apache License, Version 2.0'
|
||||||
|
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||||
|
}
|
||||||
|
author {
|
||||||
|
name = 'Jörg Prante'
|
||||||
|
url = 'http://example.com/users/jane'
|
||||||
|
}
|
||||||
|
descriptor.description {
|
||||||
|
text = rootProject.ext.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
gradle/publish/maven.gradle
Normal file
64
gradle/publish/maven.gradle
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
|
||||||
|
apply plugin: "de.marcphilipp.nexus-publish"
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
mavenJava(MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
pom {
|
||||||
|
name = project.name
|
||||||
|
description = rootProject.ext.description
|
||||||
|
url = rootProject.ext.url
|
||||||
|
inceptionYear = rootProject.ext.inceptionYear
|
||||||
|
packaging = 'jar'
|
||||||
|
organization {
|
||||||
|
name = 'xbib'
|
||||||
|
url = 'https://xbib.org'
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = 'jprante'
|
||||||
|
name = 'Jörg Prante'
|
||||||
|
email = 'joergprante@gmail.com'
|
||||||
|
url = 'https://github.com/jprante'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
url = rootProject.ext.scmUrl
|
||||||
|
connection = rootProject.ext.scmConnection
|
||||||
|
developerConnection = rootProject.ext.scmDeveloperConnection
|
||||||
|
}
|
||||||
|
issueManagement {
|
||||||
|
system = rootProject.ext.issueManagementSystem
|
||||||
|
url = rootProject.ext.issueManagementUrl
|
||||||
|
}
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = rootProject.ext.licenseName
|
||||||
|
url = rootProject.ext.licenseUrl
|
||||||
|
distribution = 'repo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty("signing.keyId")) {
|
||||||
|
apply plugin: 'signing'
|
||||||
|
signing {
|
||||||
|
sign publishing.publications.mavenJava
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
|
||||||
|
nexusPublishing {
|
||||||
|
repositories {
|
||||||
|
sonatype {
|
||||||
|
username = project.property('ossrhUsername')
|
||||||
|
password = project.property('ossrhPassword')
|
||||||
|
packageGroup = "org.xbib"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
gradle/publish/sonatype.gradle
Normal file
11
gradle/publish/sonatype.gradle
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
|
||||||
|
|
||||||
|
apply plugin: 'io.codearte.nexus-staging'
|
||||||
|
|
||||||
|
nexusStaging {
|
||||||
|
username = project.property('ossrhUsername')
|
||||||
|
password = project.property('ossrhPassword')
|
||||||
|
packageGroup = "org.xbib"
|
||||||
|
}
|
||||||
|
}
|
50
gradle/quality/sonarqube.gradle
Normal file
50
gradle/quality/sonarqube.gradle
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
|
||||||
|
sonarqube {
|
||||||
|
properties {
|
||||||
|
property "sonar.projectName", "${project.group} ${project.name}"
|
||||||
|
property "sonar.sourceEncoding", "UTF-8"
|
||||||
|
property "sonar.tests", "src/test/java"
|
||||||
|
property "sonar.scm.provider", "git"
|
||||||
|
property "sonar.junit.reportsPath", "build/test-results/test/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tasks.withType(Checkstyle) {
|
||||||
|
ignoreFailures = true
|
||||||
|
reports {
|
||||||
|
xml.enabled = true
|
||||||
|
html.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Pmd) {
|
||||||
|
ignoreFailures = true
|
||||||
|
reports {
|
||||||
|
xml.enabled = true
|
||||||
|
html.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
//configFile = rootProject.file('config/checkstyle/checkstyle.xml')
|
||||||
|
ignoreFailures = true
|
||||||
|
showViolations = true
|
||||||
|
}
|
||||||
|
|
||||||
|
spotbugs {
|
||||||
|
effort = "max"
|
||||||
|
reportLevel = "low"
|
||||||
|
//includeFilter = file("findbugs-exclude.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(com.github.spotbugs.SpotBugsTask) {
|
||||||
|
ignoreFailures = true
|
||||||
|
reports {
|
||||||
|
xml.enabled = false
|
||||||
|
html.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
gradle/repositories/maven.gradle
Normal file
4
gradle/repositories/maven.gradle
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
22
gradle/test/jmh.gradle
Normal file
22
gradle/test/jmh.gradle
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
sourceSets {
|
||||||
|
jmh {
|
||||||
|
java.srcDirs = ['src/jmh/java']
|
||||||
|
resources.srcDirs = ['src/jmh/resources']
|
||||||
|
compileClasspath += sourceSets.main.runtimeClasspath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
jmhImplementation 'org.openjdk.jmh:jmh-core:1.34'
|
||||||
|
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.34'
|
||||||
|
}
|
||||||
|
|
||||||
|
task jmh(type: JavaExec, group: 'jmh', dependsOn: jmhClasses) {
|
||||||
|
mainClass.set('org.openjdk.jmh.Main')
|
||||||
|
classpath = sourceSets.jmh.compileClasspath + sourceSets.jmh.runtimeClasspath
|
||||||
|
project.file('build/reports/jmh').mkdirs()
|
||||||
|
args '-rf', 'json'
|
||||||
|
args '-rff', project.file('build/reports/jmh/result.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
classes.finalizedBy(jmhClasses)
|
36
gradle/test/junit5.gradle
Normal file
36
gradle/test/junit5.gradle
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
dependencies {
|
||||||
|
testImplementation libs.junit.jupiter.api
|
||||||
|
testImplementation libs.junit.jupiter.params
|
||||||
|
testImplementation libs.hamcrest
|
||||||
|
testRuntimeOnly libs.junit.jupiter.engine
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
failFast = false
|
||||||
|
jvmArgs '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED',
|
||||||
|
'--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED',
|
||||||
|
'--add-exports=java.base/sun.nio.ch=ALL-UNNAMED',
|
||||||
|
'--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED',
|
||||||
|
'--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
|
||||||
|
'--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED',
|
||||||
|
'--add-opens=java.base/java.lang=ALL-UNNAMED',
|
||||||
|
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED',
|
||||||
|
'--add-opens=java.base/java.io=ALL-UNNAMED',
|
||||||
|
'--add-opens=java.base/java.nio=ALL-UNNAMED',
|
||||||
|
'--add-opens=java.base/java.util=ALL-UNNAMED'
|
||||||
|
systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties'
|
||||||
|
systemProperty 'io.netty.tryReflectionSetAccessible', 'true'
|
||||||
|
testLogging {
|
||||||
|
events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED'
|
||||||
|
}
|
||||||
|
afterSuite { desc, result ->
|
||||||
|
if (!desc.parent) {
|
||||||
|
println "\nTest result: ${result.resultType}"
|
||||||
|
println "Test summary: ${result.testCount} tests, " +
|
||||||
|
"${result.successfulTestCount} succeeded, " +
|
||||||
|
"${result.failedTestCount} failed, " +
|
||||||
|
"${result.skippedTestCount} skipped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
240
gradlew
vendored
Executable file
240
gradlew
vendored
Executable file
|
@ -0,0 +1,240 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://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.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
91
gradlew.bat
vendored
Normal file
91
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
4
net-bouncycastle/build.gradle
Normal file
4
net-bouncycastle/build.gradle
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
dependencies {
|
||||||
|
api project(':net-security')
|
||||||
|
api libs.bouncycastle
|
||||||
|
}
|
10
net-bouncycastle/src/main/java/module-info.java
Normal file
10
net-bouncycastle/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import org.xbib.net.security.CertificateProvider;
|
||||||
|
import org.xbib.net.bouncycastle.BouncyCastleCertificateProvider;
|
||||||
|
|
||||||
|
module org.xbib.net.bouncycastle {
|
||||||
|
requires org.xbib.net.security;
|
||||||
|
requires org.bouncycastle.pkix;
|
||||||
|
requires org.bouncycastle.provider;
|
||||||
|
exports org.xbib.net.bouncycastle;
|
||||||
|
provides CertificateProvider with BouncyCastleCertificateProvider;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package org.xbib.net.bouncycastle;
|
||||||
|
|
||||||
|
import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.PEMKeyPair;
|
||||||
|
import org.bouncycastle.openssl.PEMParser;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||||
|
import org.bouncycastle.operator.OperatorCreationException;
|
||||||
|
import org.xbib.net.security.CertificateProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class BouncyCastleCertificateProvider implements CertificateProvider {
|
||||||
|
|
||||||
|
private static final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
|
public static final Provider BOUNCYCASTLE = new BouncyCastleProvider();
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (Security.getProvider("BC") == null) {
|
||||||
|
Security.addProvider(BOUNCYCASTLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BouncyCastleCertificateProvider() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public Map.Entry<PrivateKey, Collection<? extends X509Certificate>> provide(InputStream key, String password, InputStream chain)
|
||||||
|
throws CertificateException, IOException {
|
||||||
|
PEMParser pemParser = new PEMParser(new InputStreamReader(key, StandardCharsets.US_ASCII));
|
||||||
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter()
|
||||||
|
.setProvider(BOUNCYCASTLE);
|
||||||
|
Object object = pemParser.readObject();
|
||||||
|
KeyPair kp = converter.getKeyPair((PEMKeyPair) object);
|
||||||
|
PrivateKey privateKey = kp.getPrivate();
|
||||||
|
return Map.entry(privateKey, new CertificateFactory().engineGenerateCertificates(chain));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map.Entry<PrivateKey, Collection<? extends X509Certificate>> provideSelfSigned(String fullQualifiedDomainName) throws CertificateException, IOException {
|
||||||
|
try {
|
||||||
|
SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
|
||||||
|
selfSignedCertificate.generate(fullQualifiedDomainName, secureRandom, 2048);
|
||||||
|
return Map.entry(selfSignedCertificate.getPrivateKey(), List.of(selfSignedCertificate.getCertificate()));
|
||||||
|
} catch (NoSuchProviderException | NoSuchAlgorithmException | OperatorCreationException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package org.xbib.net.bouncycastle;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
|
||||||
|
import org.bouncycastle.crypto.util.PrivateKeyFactory;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
|
||||||
|
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
|
||||||
|
import org.bouncycastle.operator.OperatorCreationException;
|
||||||
|
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
|
||||||
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static org.xbib.net.bouncycastle.BouncyCastleCertificateProvider.BOUNCYCASTLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a temporary self-signed certificate for testing purposes.
|
||||||
|
*/
|
||||||
|
public final class SelfSignedCertificate {
|
||||||
|
|
||||||
|
/** Current time minus 1 year, just in case software clock goes back due to time synchronization */
|
||||||
|
private static final Date DEFAULT_NOT_BEFORE = new Date(System.currentTimeMillis() - 86400000L * 365);
|
||||||
|
|
||||||
|
/** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */
|
||||||
|
private static final Date DEFAULT_NOT_AFTER = new Date(253402300799000L);
|
||||||
|
|
||||||
|
private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----";
|
||||||
|
|
||||||
|
private static final String END_KEY = "-----END PRIVATE KEY-----";
|
||||||
|
|
||||||
|
private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
|
||||||
|
|
||||||
|
private static final String END_CERT = "-----END CERTIFICATE-----";
|
||||||
|
|
||||||
|
private byte[] keyBytes;
|
||||||
|
|
||||||
|
private byte[] certBytes;
|
||||||
|
|
||||||
|
private X509CertificateHolder cert;
|
||||||
|
|
||||||
|
private PrivateKey key;
|
||||||
|
|
||||||
|
public SelfSignedCertificate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param fqdn a fully qualified domain name
|
||||||
|
* @param random the {@link SecureRandom} to use
|
||||||
|
* @param bits the number of bits of the generated private key
|
||||||
|
* @throws NoSuchAlgorithmException if algorithm does not exist
|
||||||
|
* @throws NoSuchProviderException if provider does not exist
|
||||||
|
* @throws OperatorCreationException if provider does not exist
|
||||||
|
* @throws IOException if generation fails
|
||||||
|
*/
|
||||||
|
public void generate(String fqdn, SecureRandom random, int bits)
|
||||||
|
throws IOException, NoSuchProviderException, NoSuchAlgorithmException, OperatorCreationException {
|
||||||
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyGen.initialize(bits, random);
|
||||||
|
KeyPair keypair = keyGen.generateKeyPair();
|
||||||
|
this.key = keypair.getPrivate();
|
||||||
|
X500Name name = new X500Name("CN=" + fqdn);
|
||||||
|
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keypair.getPublic().getEncoded());
|
||||||
|
X509v3CertificateBuilder certificateBuilder =
|
||||||
|
new X509v3CertificateBuilder(name, BigInteger.valueOf(System.currentTimeMillis()),
|
||||||
|
DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, name, subjectPublicKeyInfo);
|
||||||
|
AlgorithmIdentifier sigAlgId =
|
||||||
|
new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption");
|
||||||
|
AlgorithmIdentifier digestAlgId =
|
||||||
|
new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
|
||||||
|
AsymmetricKeyParameter caPrivateKeyParameters = PrivateKeyFactory.createKey(key.getEncoded());
|
||||||
|
ContentSigner contentSigner = new BcRSAContentSignerBuilder(sigAlgId, digestAlgId)
|
||||||
|
.build(caPrivateKeyParameters);
|
||||||
|
this.cert = certificateBuilder.build(contentSigner);
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
outputStream.write(BEGIN_KEY.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
outputStream.write('\n');
|
||||||
|
writeEncoded(key.getEncoded(), outputStream);
|
||||||
|
outputStream.write(END_KEY.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
outputStream.write('\n');
|
||||||
|
this.keyBytes = outputStream.toByteArray();
|
||||||
|
outputStream = new ByteArrayOutputStream();
|
||||||
|
outputStream.write(BEGIN_CERT.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
outputStream.write('\n');
|
||||||
|
writeEncoded(cert.getEncoded(), outputStream);
|
||||||
|
outputStream.write(END_CERT.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
outputStream.write('\n');
|
||||||
|
this.certBytes = outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the generated X.509 certificate file in PEM format.
|
||||||
|
* @return input stream of certificate
|
||||||
|
*/
|
||||||
|
public InputStream getCertificateInputStream() {
|
||||||
|
return new ByteArrayInputStream(certBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the generated RSA private key file in PEM format.
|
||||||
|
* @return input stream of private key
|
||||||
|
*/
|
||||||
|
public InputStream getPrivateKeyInputStream() {
|
||||||
|
return new ByteArrayInputStream(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the generated RSA private key.
|
||||||
|
* @return private key
|
||||||
|
*/
|
||||||
|
public PrivateKey getPrivateKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getCertificate() throws CertificateException {
|
||||||
|
return new JcaX509CertificateConverter()
|
||||||
|
.setProvider(BOUNCYCASTLE)
|
||||||
|
.getCertificate(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportPEM(OutputStream outputStream) throws IOException {
|
||||||
|
outputStream.write(keyBytes);
|
||||||
|
outputStream.write(certBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeEncoded(byte[] bytes, OutputStream outputStream) throws IOException {
|
||||||
|
byte[] buf = new byte[64];
|
||||||
|
byte[] base64 = Base64.encode(bytes);
|
||||||
|
for (int i = 0; i < base64.length; i += buf.length) {
|
||||||
|
int index = 0;
|
||||||
|
while (index != buf.length) {
|
||||||
|
if ((i + index) >= base64.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf[index] = base64[i + index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
outputStream.write(buf, 0, index);
|
||||||
|
outputStream.write('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
@ -0,0 +1 @@
|
||||||
|
org.xbib.net.bouncycastle.BouncyCastleCertificateProvider
|
5
net-mime/src/main/java/module-info.java
Normal file
5
net-mime/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module org.xbib.net.mime {
|
||||||
|
exports org.xbib.net.mime;
|
||||||
|
exports org.xbib.net.mime.stream;
|
||||||
|
requires java.logging;
|
||||||
|
}
|
24
net-mime/src/main/java/org/xbib/net/mime/Chunk.java
Normal file
24
net-mime/src/main/java/org/xbib/net/mime/Chunk.java
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
final class Chunk {
|
||||||
|
volatile Chunk next;
|
||||||
|
volatile Data data;
|
||||||
|
|
||||||
|
public Chunk(Data data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new chunk and adds to linked list.
|
||||||
|
*
|
||||||
|
* @param dataHead of the linked list
|
||||||
|
* @param buf MIME part partial data
|
||||||
|
* @return created chunk
|
||||||
|
*/
|
||||||
|
public Chunk createNext(DataHead dataHead, ByteBuffer buf) throws IOException {
|
||||||
|
return next = new Chunk(data.createNext(dataHead, buf));
|
||||||
|
}
|
||||||
|
}
|
21
net-mime/src/main/java/org/xbib/net/mime/Content.java
Normal file
21
net-mime/src/main/java/org/xbib/net/mime/Content.java
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public final class Content implements MimeEvent {
|
||||||
|
|
||||||
|
private final ByteBuffer buf;
|
||||||
|
|
||||||
|
Content(ByteBuffer buf) {
|
||||||
|
this.buf = buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getEventType() {
|
||||||
|
return Type.CONTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getData() {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
43
net-mime/src/main/java/org/xbib/net/mime/Data.java
Normal file
43
net-mime/src/main/java/org/xbib/net/mime/Data.java
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* size of the chunk given by the parser
|
||||||
|
*
|
||||||
|
* @return size of the chunk
|
||||||
|
*/
|
||||||
|
int size();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: should the return type be ByteBuffer ??
|
||||||
|
* Return part's partial data. The data is read only.
|
||||||
|
*
|
||||||
|
* @return a byte array which contains {#size()} bytes. The returned
|
||||||
|
* array may be larger than {#size()} bytes and contains data
|
||||||
|
* from offset 0.
|
||||||
|
*/
|
||||||
|
byte[] read() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write this partial data to a file
|
||||||
|
*
|
||||||
|
* @param file to which the data needs to be written
|
||||||
|
* @return file pointer before the write operation(at which the data is
|
||||||
|
* written from)
|
||||||
|
*/
|
||||||
|
long writeTo(DataFile file) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a Data. The implementation could
|
||||||
|
* be file based one or memory based one.
|
||||||
|
*
|
||||||
|
* @param dataHead start of the linked list of data objects
|
||||||
|
* @param buf contains partial content for a part
|
||||||
|
* @return Data
|
||||||
|
*/
|
||||||
|
Data createNext(DataHead dataHead, ByteBuffer buf) throws IOException;
|
||||||
|
}
|
50
net-mime/src/main/java/org/xbib/net/mime/DataFile.java
Normal file
50
net-mime/src/main/java/org/xbib/net/mime/DataFile.java
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
final class DataFile {
|
||||||
|
private WeakDataFile weak;
|
||||||
|
private long writePointer;
|
||||||
|
|
||||||
|
DataFile(File file) throws IOException {
|
||||||
|
writePointer=0;
|
||||||
|
weak = new WeakDataFile(this, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
weak.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from the given file pointer position.
|
||||||
|
*
|
||||||
|
* @param pointer read position
|
||||||
|
* @param buf that needs to be filled
|
||||||
|
* @param offset the start offset of the data.
|
||||||
|
* @param length of data that needs to be read
|
||||||
|
*/
|
||||||
|
synchronized void read(long pointer, byte[] buf, int offset, int length ) throws IOException {
|
||||||
|
weak.read(pointer, buf, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void renameTo(File f) throws IOException {
|
||||||
|
weak.renameTo(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to the file
|
||||||
|
*
|
||||||
|
* @param data that needs to written to a file
|
||||||
|
* @param offset start offset in the data
|
||||||
|
* @param length no bytes to write
|
||||||
|
* @return file pointer before the write operation(or at which the
|
||||||
|
* data is written)
|
||||||
|
*/
|
||||||
|
synchronized long writeTo(byte[] data, int offset, int length) throws IOException {
|
||||||
|
long temp = writePointer;
|
||||||
|
writePointer = weak.writeTo(writePointer, data, offset, length);
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
248
net-mime/src/main/java/org/xbib/net/mime/DataHead.java
Normal file
248
net-mime/src/main/java/org/xbib/net/mime/DataHead.java
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an attachment part in a MIME message. MIME message parsing is done
|
||||||
|
* lazily using a pull parser, so the part may not have all the data. {@link #read}
|
||||||
|
* and {@link #readOnce} may trigger the actual parsing the message. In fact,
|
||||||
|
* parsing of an attachment part may be triggered by calling {@link #read} methods
|
||||||
|
* on some other attachment parts. All this happens behind the scenes so the
|
||||||
|
* application developer need not worry about these details.
|
||||||
|
*/
|
||||||
|
final class DataHead {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linked list to keep the part's content
|
||||||
|
*/
|
||||||
|
volatile Chunk head, tail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the part is stored in a file, non-null.
|
||||||
|
*/
|
||||||
|
DataFile dataFile;
|
||||||
|
|
||||||
|
private final MimePart part;
|
||||||
|
|
||||||
|
boolean readOnce;
|
||||||
|
volatile long inMemory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used only for debugging. This records where readOnce() is called.
|
||||||
|
*/
|
||||||
|
private Throwable consumedAt;
|
||||||
|
|
||||||
|
DataHead(MimePart part) {
|
||||||
|
this.part = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addBody(ByteBuffer buf) throws IOException {
|
||||||
|
synchronized(this) {
|
||||||
|
inMemory += buf.limit();
|
||||||
|
}
|
||||||
|
if (tail != null) {
|
||||||
|
tail = tail.createNext(this, buf);
|
||||||
|
} else {
|
||||||
|
head = tail = new Chunk(new MemoryData(buf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void doneParsing() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveTo(File f) throws IOException, MimeException {
|
||||||
|
if (dataFile != null) {
|
||||||
|
dataFile.renameTo(f);
|
||||||
|
} else {
|
||||||
|
try (OutputStream os = new FileOutputStream(f)) {
|
||||||
|
InputStream in = readOnce();
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
int len;
|
||||||
|
while ((len = in.read(buf)) != -1) {
|
||||||
|
os.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
head = tail = null;
|
||||||
|
if (dataFile != null) {
|
||||||
|
dataFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can get the attachment part's content multiple times. That means
|
||||||
|
* the full content needs to be there in memory or on the file system.
|
||||||
|
* Calling this method would trigger parsing for the part's data. So
|
||||||
|
* do not call this unless it is required(otherwise, just wrap MIMEPart
|
||||||
|
* into a object that returns InputStream for e.g DataHandler)
|
||||||
|
*
|
||||||
|
* @return data for the part's content
|
||||||
|
*/
|
||||||
|
public InputStream read() throws IOException, MimeException {
|
||||||
|
if (readOnce) {
|
||||||
|
throw new IllegalStateException("readOnce() is called before, read() cannot be called later.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger parsing for the part
|
||||||
|
while(tail == null) {
|
||||||
|
if (!part.msg.makeProgress()) {
|
||||||
|
throw new IllegalStateException("No such MIME Part: "+part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (head == null) {
|
||||||
|
throw new IllegalStateException("Already read. Probably readOnce() is called before.");
|
||||||
|
}
|
||||||
|
return new ReadMultiStream();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Used for an assertion. Returns true when readOnce() is not already called.
|
||||||
|
* or otherwise throw an exception.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Calling this method also marks the stream as 'consumed'
|
||||||
|
*
|
||||||
|
* @return true if readOnce() is not called before
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("ThrowableInitCause")
|
||||||
|
private boolean unconsumed() {
|
||||||
|
if (consumedAt != null) {
|
||||||
|
AssertionError error = new AssertionError("readOnce() is already called before. See the nested exception from where it's called.");
|
||||||
|
error.initCause(consumedAt);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
consumedAt = new Exception().fillInStackTrace();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can get the attachment part's content only once. The content
|
||||||
|
* will be lost after the method. Content data is not be stored
|
||||||
|
* on the file system or is not kept in the memory for the
|
||||||
|
* following case:
|
||||||
|
* - Attachement parts contents are accessed sequentially
|
||||||
|
*
|
||||||
|
* In general, take advantage of this when the data is used only
|
||||||
|
* once.
|
||||||
|
*
|
||||||
|
* @return data for the part's content
|
||||||
|
*/
|
||||||
|
public InputStream readOnce() throws IOException, MimeException {
|
||||||
|
unconsumed();
|
||||||
|
if (readOnce) {
|
||||||
|
throw new MimeException("readOnce() is called before. It can only be called once.");
|
||||||
|
}
|
||||||
|
readOnce = true;
|
||||||
|
while(tail == null) {
|
||||||
|
if (!part.msg.makeProgress() && tail == null) {
|
||||||
|
throw new MimeException("No such Part: "+part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputStream in = new ReadOnceStream();
|
||||||
|
head = null;
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReadMultiStream extends InputStream {
|
||||||
|
Chunk current;
|
||||||
|
int offset;
|
||||||
|
int len;
|
||||||
|
byte[] buf;
|
||||||
|
boolean closed;
|
||||||
|
|
||||||
|
public ReadMultiStream() throws IOException {
|
||||||
|
this.current = head;
|
||||||
|
len = current.data.size();
|
||||||
|
buf = current.data.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte b[], int off, int sz) throws IOException {
|
||||||
|
try {
|
||||||
|
if (!fetch()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} catch (MimeException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
sz = Math.min(sz, len-offset);
|
||||||
|
System.arraycopy(buf,offset,b,off,sz);
|
||||||
|
offset += sz;
|
||||||
|
return sz;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
try {
|
||||||
|
if (!fetch()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} catch (MimeException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
return (buf[offset++] & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
void adjustInMemoryUsage() {
|
||||||
|
// Nothing to do in this case.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets to the next chunk if we are done with the current one.
|
||||||
|
* @return true if any data available
|
||||||
|
* @throws IOException when i/o error
|
||||||
|
*/
|
||||||
|
private boolean fetch() throws IOException, MimeException {
|
||||||
|
if (closed) {
|
||||||
|
throw new IOException("Stream already closed");
|
||||||
|
}
|
||||||
|
if (current == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(offset==len) {
|
||||||
|
while(!part.parsed && current.next == null) {
|
||||||
|
part.msg.makeProgress();
|
||||||
|
}
|
||||||
|
current = current.next;
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
adjustInMemoryUsage();
|
||||||
|
this.offset = 0;
|
||||||
|
this.buf = current.data.read();
|
||||||
|
this.len = current.data.size();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
current = null;
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReadOnceStream extends ReadMultiStream {
|
||||||
|
|
||||||
|
public ReadOnceStream() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void adjustInMemoryUsage() {
|
||||||
|
synchronized(DataHead.this) {
|
||||||
|
inMemory -= current.data.size(); // adjust current memory usage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
14
net-mime/src/main/java/org/xbib/net/mime/EndMessage.java
Normal file
14
net-mime/src/main/java/org/xbib/net/mime/EndMessage.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public final class EndMessage implements MimeEvent {
|
||||||
|
|
||||||
|
static final EndMessage INSTANCE = new EndMessage();
|
||||||
|
|
||||||
|
public EndMessage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getEventType() {
|
||||||
|
return Type.END_MESSAGE;
|
||||||
|
}
|
||||||
|
}
|
14
net-mime/src/main/java/org/xbib/net/mime/EndPart.java
Normal file
14
net-mime/src/main/java/org/xbib/net/mime/EndPart.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public final class EndPart implements MimeEvent {
|
||||||
|
|
||||||
|
public static final EndPart INSTANCE = new EndPart();
|
||||||
|
|
||||||
|
public EndPart() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getEventType() {
|
||||||
|
return Type.END_PART;
|
||||||
|
}
|
||||||
|
}
|
45
net-mime/src/main/java/org/xbib/net/mime/FileData.java
Normal file
45
net-mime/src/main/java/org/xbib/net/mime/FileData.java
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
final class FileData implements Data {
|
||||||
|
|
||||||
|
private final DataFile file;
|
||||||
|
|
||||||
|
private final long pointer;
|
||||||
|
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
FileData(DataFile file, ByteBuffer buf) throws IOException {
|
||||||
|
this(file, file.writeTo(buf.array(), 0, buf.limit()), buf.limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
FileData(DataFile file, long pointer, int length) {
|
||||||
|
this.file = file;
|
||||||
|
this.pointer = pointer;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] read() throws IOException {
|
||||||
|
byte[] buf = new byte[length];
|
||||||
|
file.read(pointer, buf, 0, length);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long writeTo(DataFile file) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Data createNext(DataHead dataHead, ByteBuffer buf) throws IOException {
|
||||||
|
return new FileData(file, buf);
|
||||||
|
}
|
||||||
|
}
|
19
net-mime/src/main/java/org/xbib/net/mime/Header.java
Normal file
19
net-mime/src/main/java/org/xbib/net/mime/Header.java
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public interface Header {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of this header.
|
||||||
|
*
|
||||||
|
* @return name of the header
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of this header.
|
||||||
|
*
|
||||||
|
* @return value of the header
|
||||||
|
*/
|
||||||
|
String getValue();
|
||||||
|
}
|
||||||
|
|
18
net-mime/src/main/java/org/xbib/net/mime/Headers.java
Normal file
18
net-mime/src/main/java/org/xbib/net/mime/Headers.java
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public final class Headers implements MimeEvent {
|
||||||
|
MimeParser.InternetHeaders ih;
|
||||||
|
|
||||||
|
Headers(MimeParser.InternetHeaders ih) {
|
||||||
|
this.ih = ih;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getEventType() {
|
||||||
|
return Type.HEADERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MimeParser.InternetHeaders getHeaders() {
|
||||||
|
return ih;
|
||||||
|
}
|
||||||
|
}
|
85
net-mime/src/main/java/org/xbib/net/mime/MemoryData.java
Normal file
85
net-mime/src/main/java/org/xbib/net/mime/MemoryData.java
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the Part's partial content data in memory.
|
||||||
|
*/
|
||||||
|
final class MemoryData implements Data {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(MemoryData.class.getName());
|
||||||
|
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
private final int len;
|
||||||
|
|
||||||
|
private final boolean isOnlyMemory;
|
||||||
|
|
||||||
|
private final long threshold;
|
||||||
|
|
||||||
|
private final File tempDir;
|
||||||
|
|
||||||
|
MemoryData(ByteBuffer byteBuffer) {
|
||||||
|
this(byteBuffer, false, 1048576L, new File(System.getProperty("java.io.tmpdir")));
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryData(ByteBuffer buf, boolean isOnlyMemory, long threshold, File tempDir) {
|
||||||
|
data = buf.array(); // TODO
|
||||||
|
len = buf.limit();
|
||||||
|
this.isOnlyMemory = isOnlyMemory;
|
||||||
|
this.threshold = threshold;
|
||||||
|
this.tempDir = tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] read() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long writeTo(DataFile file) throws IOException {
|
||||||
|
return file.writeTo(data, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Data createNext(DataHead dataHead, ByteBuffer buf) throws IOException {
|
||||||
|
if (!isOnlyMemory && dataHead.inMemory >= threshold) {
|
||||||
|
//String prefix = config.getTempFilePrefix();
|
||||||
|
//String suffix = config.getTempFileSuffix();
|
||||||
|
File tempFile = createTempFile("MIME", ".mime", tempDir);
|
||||||
|
if (LOGGER.isLoggable(Level.FINE)) {
|
||||||
|
LOGGER.log(Level.FINE, "Created temp file = {0}", tempFile);
|
||||||
|
}
|
||||||
|
dataHead.dataFile = new DataFile(tempFile);
|
||||||
|
if (dataHead.head != null) {
|
||||||
|
for (Chunk c = dataHead.head; c != null; c = c.next) {
|
||||||
|
long pointer = c.data.writeTo(dataHead.dataFile);
|
||||||
|
c.data = new FileData(dataHead.dataFile, pointer, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new FileData(dataHead.dataFile, buf);
|
||||||
|
} else {
|
||||||
|
return new MemoryData(buf, isOnlyMemory, threshold, tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File createTempFile(String prefix, String suffix, File dir) throws IOException {
|
||||||
|
if (dir != null) {
|
||||||
|
Path path = dir.toPath();
|
||||||
|
return Files.createTempFile(path, prefix, suffix).toFile();
|
||||||
|
} else {
|
||||||
|
return Files.createTempFile(prefix, suffix).toFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
net-mime/src/main/java/org/xbib/net/mime/MimeEvent.java
Normal file
24
net-mime/src/main/java/org/xbib/net/mime/MimeEvent.java
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public interface MimeEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a event for parser's current cursor location in the MIME message.
|
||||||
|
*
|
||||||
|
* {@link Type#START_MESSAGE} and {@link Type#START_MESSAGE} events
|
||||||
|
* are generated only once.
|
||||||
|
*
|
||||||
|
* {@link Type#START_PART}, {@link Type#END_PART}, {@link Type#HEADERS}
|
||||||
|
* events are generated only once for each attachment part.
|
||||||
|
*
|
||||||
|
* {@link Type#CONTENT} event may be generated more than once for an attachment
|
||||||
|
* part.
|
||||||
|
*
|
||||||
|
* @return event type
|
||||||
|
*/
|
||||||
|
Type getEventType();
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
START_MESSAGE, START_PART, HEADERS, CONTENT, END_PART, END_MESSAGE
|
||||||
|
}
|
||||||
|
}
|
17
net-mime/src/main/java/org/xbib/net/mime/MimeException.java
Normal file
17
net-mime/src/main/java/org/xbib/net/mime/MimeException.java
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class MimeException extends Exception {
|
||||||
|
|
||||||
|
public MimeException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MimeException(Throwable throwable) {
|
||||||
|
super(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MimeException(String message, Throwable throwable) {
|
||||||
|
super(message, throwable);
|
||||||
|
}
|
||||||
|
}
|
223
net-mime/src/main/java/org/xbib/net/mime/MimeMessage.java
Normal file
223
net-mime/src/main/java/org/xbib/net/mime/MimeMessage.java
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents MIME message. MIME message parsing is done lazily using a
|
||||||
|
* pull parser.
|
||||||
|
*/
|
||||||
|
public class MimeMessage implements Closeable {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(MimeMessage.class.getName());
|
||||||
|
|
||||||
|
private final InputStream in;
|
||||||
|
private final Iterator<MimeEvent> it;
|
||||||
|
private boolean parsed;
|
||||||
|
private MimePart currentPart;
|
||||||
|
private int currentIndex;
|
||||||
|
|
||||||
|
private final List<MimePart> partsList = new ArrayList<>();
|
||||||
|
private final Map<String, MimePart> partsMap = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a MIME message from the content's stream. The content stream
|
||||||
|
* is closed when EOF is reached.
|
||||||
|
*
|
||||||
|
* @param in MIME message stream
|
||||||
|
* @param boundary the separator for parts(pass it without --)
|
||||||
|
*/
|
||||||
|
public MimeMessage(InputStream in, String boundary) {
|
||||||
|
this.in = in;
|
||||||
|
MimeParser parser = new MimeParser(in, boundary);
|
||||||
|
it = parser.iterator();
|
||||||
|
//if (config.isParseEagerly()) {
|
||||||
|
// parseAll();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the attachments by parsing the entire MIME message. Avoid
|
||||||
|
* this if possible since it is an expensive operation.
|
||||||
|
*
|
||||||
|
* @return list of attachments.
|
||||||
|
*/
|
||||||
|
public List<MimePart> getAttachments() throws MimeException, IOException {
|
||||||
|
if (!parsed) {
|
||||||
|
parseAll();
|
||||||
|
}
|
||||||
|
return partsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates nth attachment lazily. It doesn't validate
|
||||||
|
* if the message has so many attachments. To
|
||||||
|
* do the validation, the message needs to be parsed.
|
||||||
|
* The parsing of the message is done lazily and is done
|
||||||
|
* while reading the bytes of the part.
|
||||||
|
*
|
||||||
|
* @param index sequential order of the part. starts with zero.
|
||||||
|
* @return attachemnt part
|
||||||
|
*/
|
||||||
|
public MimePart getPart(int index) throws MimeException {
|
||||||
|
LOGGER.log(Level.FINE, "index={0}", index);
|
||||||
|
MimePart part = (index < partsList.size()) ? partsList.get(index) : null;
|
||||||
|
if (parsed && part == null) {
|
||||||
|
throw new MimeException("There is no " + index + " attachment part ");
|
||||||
|
}
|
||||||
|
if (part == null) {
|
||||||
|
// Parsing will done lazily and will be driven by reading the part
|
||||||
|
part = new MimePart(this);
|
||||||
|
partsList.add(index, part);
|
||||||
|
}
|
||||||
|
LOGGER.log(Level.FINE, "Got attachment at index=" + index + " attachment=" + part);
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a lazy attachment for a given Content-ID. It doesn't validate
|
||||||
|
* if the message contains an attachment with the given Content-ID. To
|
||||||
|
* do the validation, the message needs to be parsed. The parsing of the
|
||||||
|
* message is done lazily and is done while reading the bytes of the part.
|
||||||
|
*
|
||||||
|
* @param contentId Content-ID of the part, expects Content-ID without {@code <, >}
|
||||||
|
* @return attachemnt part
|
||||||
|
*/
|
||||||
|
public MimePart getPart(String contentId) throws MimeException {
|
||||||
|
LOGGER.log(Level.FINE, "Content-ID = " + contentId);
|
||||||
|
MimePart part = getDecodedCidPart(contentId);
|
||||||
|
if (parsed && part == null) {
|
||||||
|
throw new MimeException("There is no part with Content-ID = " + contentId);
|
||||||
|
}
|
||||||
|
if (part == null) {
|
||||||
|
// Parsing is done lazily and is driven by reading the part
|
||||||
|
part = new MimePart(this, contentId);
|
||||||
|
partsMap.put(contentId, part);
|
||||||
|
}
|
||||||
|
LOGGER.log(Level.FINE, "got part for Content-ID = " + contentId + " part = " + part);
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is required for Indigo interop, it writes content-id without escaping
|
||||||
|
private MimePart getDecodedCidPart(String cid) {
|
||||||
|
MimePart part = partsMap.get(cid);
|
||||||
|
if (part == null) {
|
||||||
|
if (cid.indexOf('%') != -1) {
|
||||||
|
// TODO do not use URLDecoder
|
||||||
|
String tempCid = URLDecoder.decode(cid, StandardCharsets.UTF_8);
|
||||||
|
part = partsMap.get(tempCid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the whole MIME message eagerly
|
||||||
|
*/
|
||||||
|
public final void parseAll() throws MimeException, IOException {
|
||||||
|
while (makeProgress()) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all parsed {@link MimePart parts} and cleans up
|
||||||
|
* any resources that are held by this {@code MimeMessage} (for e.g. deletes temp files).
|
||||||
|
* This method is safe to call even if parsing of message failed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
close(partsList);
|
||||||
|
close(partsMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close(final Collection<MimePart> parts) throws IOException {
|
||||||
|
for (final MimePart part : parts) {
|
||||||
|
part.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the MIME message in a pull fashion.
|
||||||
|
*
|
||||||
|
* @return false if the parsing is completed.
|
||||||
|
*/
|
||||||
|
public synchronized boolean makeProgress() throws MimeException, IOException {
|
||||||
|
if (!it.hasNext()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MimeEvent event = it.next();
|
||||||
|
switch (event.getEventType()) {
|
||||||
|
case START_MESSAGE:
|
||||||
|
LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.START_MESSAGE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case START_PART:
|
||||||
|
LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.START_PART);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HEADERS:
|
||||||
|
LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.HEADERS);
|
||||||
|
Headers headers = (Headers) event;
|
||||||
|
MimeParser.InternetHeaders ih = headers.getHeaders();
|
||||||
|
List<String> cids = ih.getHeader("content-id");
|
||||||
|
String cid = (cids != null) ? cids.get(0) : currentIndex + "";
|
||||||
|
if (cid.length() > 2 && cid.charAt(0) == '<') {
|
||||||
|
cid = cid.substring(1, cid.length() - 1);
|
||||||
|
}
|
||||||
|
MimePart listPart = (currentIndex < partsList.size()) ? partsList.get(currentIndex) : null;
|
||||||
|
MimePart mapPart = getDecodedCidPart(cid);
|
||||||
|
if (listPart == null && mapPart == null) {
|
||||||
|
currentPart = getPart(cid);
|
||||||
|
partsList.add(currentIndex, currentPart);
|
||||||
|
} else if (listPart == null) {
|
||||||
|
currentPart = mapPart;
|
||||||
|
partsList.add(currentIndex, mapPart);
|
||||||
|
} else if (mapPart == null) {
|
||||||
|
currentPart = listPart;
|
||||||
|
currentPart.setContentId(cid);
|
||||||
|
partsMap.put(cid, currentPart);
|
||||||
|
} else if (listPart != mapPart) {
|
||||||
|
throw new MimeException("ereated two different attachments using Content-ID and index");
|
||||||
|
}
|
||||||
|
currentPart.setHeaders(ih);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONTENT:
|
||||||
|
LOGGER.log(Level.FINER, "MIMEEvent=" + MimeEvent.Type.CONTENT);
|
||||||
|
Content content = (Content) event;
|
||||||
|
ByteBuffer buf = content.getData();
|
||||||
|
currentPart.addBody(buf);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case END_PART:
|
||||||
|
LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.END_PART);
|
||||||
|
currentPart.doneParsing();
|
||||||
|
++currentIndex;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case END_MESSAGE:
|
||||||
|
LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.END_MESSAGE);
|
||||||
|
parsed = true;
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new MimeException(ioe);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
16
net-mime/src/main/java/org/xbib/net/mime/MimeMultipart.java
Normal file
16
net-mime/src/main/java/org/xbib/net/mime/MimeMultipart.java
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface MimeMultipart {
|
||||||
|
|
||||||
|
Map<String, String> getHeaders();
|
||||||
|
|
||||||
|
int getLength();
|
||||||
|
|
||||||
|
ByteBuffer getBody();
|
||||||
|
|
||||||
|
Charset getCharset();
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface MimeMultipartHandler {
|
||||||
|
|
||||||
|
void handle(MimeMultipart multipart);
|
||||||
|
}
|
|
@ -0,0 +1,267 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MIME multi part message parser (RFC 2046).
|
||||||
|
*/
|
||||||
|
public class MimeMultipartParser {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(MimeMultipartParser.class.getName());
|
||||||
|
|
||||||
|
private final byte[] boundary;
|
||||||
|
|
||||||
|
private final String subType;
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
public MimeMultipartParser(String contentType) throws MimeException {
|
||||||
|
Objects.requireNonNull(contentType);
|
||||||
|
int pos = contentType.indexOf(';');
|
||||||
|
this.type = pos >= 0 ? contentType.substring(0, pos) : contentType;
|
||||||
|
this.type = type.trim().toLowerCase();
|
||||||
|
this.subType = "multipart".equals(type) ? null : type.substring(10).trim();
|
||||||
|
Map<String, String> headers = parseHeaderLine(contentType);
|
||||||
|
this.boundary = headers.containsKey("boundary") ? headers.get("boundary").getBytes(StandardCharsets.US_ASCII) : new byte[]{};
|
||||||
|
logger.log(Level.INFO, "headers = " + headers + " boundary = " + new String(boundary));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubType() {
|
||||||
|
return subType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void parse(ByteBuffer byteBuffer, MimeMultipartHandler handler) throws MimeException {
|
||||||
|
if (boundary == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean inHeader = true;
|
||||||
|
boolean inBody = false;
|
||||||
|
Integer start = null;
|
||||||
|
Map<String, String> headers = new LinkedHashMap<>();
|
||||||
|
int eol = 0;
|
||||||
|
byte[] buffer = new byte[byteBuffer.remaining()];
|
||||||
|
byteBuffer.get(buffer);
|
||||||
|
for (int i = 0; i < buffer.length; i++) {
|
||||||
|
byte b = buffer[i];
|
||||||
|
if (inHeader) {
|
||||||
|
switch (b) {
|
||||||
|
case '\r':
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
List<String> s = new ArrayList<>();
|
||||||
|
StringTokenizer tokenizer = new StringTokenizer(sb.toString(), ":");
|
||||||
|
while (tokenizer.hasMoreElements()) {
|
||||||
|
s.add(tokenizer.nextToken());
|
||||||
|
}
|
||||||
|
String k = s.size() > 0 ? s.get(0) : "";
|
||||||
|
String v = s.size() > 1 ? s.get(1) : "";
|
||||||
|
if (!k.startsWith("--")) {
|
||||||
|
headers.put(k.toLowerCase(), v.trim());
|
||||||
|
}
|
||||||
|
eol = 0;
|
||||||
|
sb.setLength(0);
|
||||||
|
} else {
|
||||||
|
eol++;
|
||||||
|
if (eol >= 1) {
|
||||||
|
eol = 0;
|
||||||
|
sb.setLength(0);
|
||||||
|
inHeader = false;
|
||||||
|
inBody = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
eol = 0;
|
||||||
|
sb.append((char) b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inBody) {
|
||||||
|
int len = headers.containsKey("content-length") ? Integer.parseInt(headers.get("content-length")) : -1;
|
||||||
|
if (len > 0) {
|
||||||
|
inBody = false;
|
||||||
|
inHeader = true;
|
||||||
|
} else {
|
||||||
|
if (b != ('\r') && b != ('\n')) {
|
||||||
|
start = i;
|
||||||
|
}
|
||||||
|
if (start != null) {
|
||||||
|
i = indexOf(buffer, boundary, start, buffer.length);
|
||||||
|
if (i == -1) {
|
||||||
|
throw new MimeException("boundary not found");
|
||||||
|
}
|
||||||
|
int l = i - start;
|
||||||
|
if (l > 4) {
|
||||||
|
l = l - 4;
|
||||||
|
}
|
||||||
|
ByteBuffer body = ByteBuffer.wrap(buffer, start, l);
|
||||||
|
Map<String, String> m = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||||
|
m.putAll(parseHeaderLine(entry.getValue()));
|
||||||
|
}
|
||||||
|
headers.putAll(m);
|
||||||
|
Charset charset = StandardCharsets.ISO_8859_1;
|
||||||
|
if (m.containsKey("charset")) {
|
||||||
|
charset = Charset.forName(m.get("charset"));
|
||||||
|
}
|
||||||
|
if (handler != null) {
|
||||||
|
handler.handle(new MimePart(headers, body, l, charset));
|
||||||
|
}
|
||||||
|
inBody = false;
|
||||||
|
inHeader = true;
|
||||||
|
headers = new LinkedHashMap<>();
|
||||||
|
start = null;
|
||||||
|
eol = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseHeaderLine(String line) throws MimeException {
|
||||||
|
Map<String, String> params = new LinkedHashMap<>();
|
||||||
|
int pos = line.indexOf(';');
|
||||||
|
String spec = line.substring(pos + 1);
|
||||||
|
if (pos < 0) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
String key = "";
|
||||||
|
String value;
|
||||||
|
boolean inKey = true;
|
||||||
|
boolean inString = false;
|
||||||
|
int start = 0;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < spec.length(); i++) {
|
||||||
|
switch (spec.charAt(i)) {
|
||||||
|
case '=':
|
||||||
|
if (inKey) {
|
||||||
|
key = spec.substring(start, i).trim().toLowerCase();
|
||||||
|
start = i + 1;
|
||||||
|
inKey = false;
|
||||||
|
} else if (!inString) {
|
||||||
|
throw new MimeException(" value has illegal character '=' at " + i + ": " + spec);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ';':
|
||||||
|
if (inKey) {
|
||||||
|
if (spec.substring(start, i).trim().length() > 0) {
|
||||||
|
throw new MimeException(" parameter missing value at " + i + ": " + spec);
|
||||||
|
} else {
|
||||||
|
throw new MimeException("parameter key has illegal character ';' at " + i + ": " + spec);
|
||||||
|
}
|
||||||
|
} else if (!inString) {
|
||||||
|
value = spec.substring(start, i).trim();
|
||||||
|
params.put(key, value);
|
||||||
|
key = null;
|
||||||
|
start = i + 1;
|
||||||
|
inKey = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
if (inKey) {
|
||||||
|
throw new MimeException("key has illegal character '\"' at " + i + ": " + spec);
|
||||||
|
} else if (inString) {
|
||||||
|
value = spec.substring(start, i).trim();
|
||||||
|
params.put(key, value);
|
||||||
|
key = null;
|
||||||
|
for (i++; i < spec.length() && spec.charAt(i) != (';'); i++) {
|
||||||
|
if (!Character.isWhitespace(spec.charAt(i))) {
|
||||||
|
throw new MimeException(" value has garbage after quoted string at " + i + ": " + spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
inString = false;
|
||||||
|
inKey = true;
|
||||||
|
} else {
|
||||||
|
if (spec.substring(start, i).trim().length() > 0) {
|
||||||
|
throw new MimeException("value has garbage before quoted string at " + i + ": " + spec);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
inString = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inKey) {
|
||||||
|
if (pos > start && spec.substring(start, i).trim().length() > 0) {
|
||||||
|
throw new MimeException(" missing value at " + i + ": " + spec);
|
||||||
|
}
|
||||||
|
} else if (!inString) {
|
||||||
|
value = spec.substring(start, i).trim();
|
||||||
|
params.put(key, value);
|
||||||
|
} else {
|
||||||
|
throw new MimeException("has an unterminated quoted string: " + spec);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int indexOf(byte[] array, byte[] target, int start, int end) {
|
||||||
|
if (target.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
outer:
|
||||||
|
for (int i = start; i < end - target.length + 1; i++) {
|
||||||
|
for (int j = 0; j < target.length; j++) {
|
||||||
|
if (array[i + j] != target[j]) {
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MimePart implements MimeMultipart {
|
||||||
|
|
||||||
|
private final Map<String, String> headers;
|
||||||
|
|
||||||
|
private final ByteBuffer body;
|
||||||
|
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
private final Charset charset;
|
||||||
|
|
||||||
|
public MimePart(Map<String, String> headers, ByteBuffer body, int length, Charset charSet) {
|
||||||
|
this.headers = headers;
|
||||||
|
this.body = body;
|
||||||
|
this.length = length;
|
||||||
|
this.charset = charSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Charset getCharset() {
|
||||||
|
return charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
558
net-mime/src/main/java/org/xbib/net/mime/MimeParser.java
Normal file
558
net-mime/src/main/java/org/xbib/net/mime/MimeParser.java
Normal file
|
@ -0,0 +1,558 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class MimeParser implements Iterable<MimeEvent> {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(MimeParser.class.getName());
|
||||||
|
|
||||||
|
private static final int NO_WHITESPACE_LENGTH = 1000;
|
||||||
|
|
||||||
|
private final InputStream inputStream;
|
||||||
|
|
||||||
|
private final byte[] boundaryBytes;
|
||||||
|
|
||||||
|
private final int boundaryLength;
|
||||||
|
|
||||||
|
private final int[] badCharacterShift;
|
||||||
|
|
||||||
|
private final int[] goodSuffixShiftTable;
|
||||||
|
|
||||||
|
private final int capacity;
|
||||||
|
|
||||||
|
private final int chunkSize;
|
||||||
|
|
||||||
|
private State state;
|
||||||
|
|
||||||
|
private boolean parsed;
|
||||||
|
|
||||||
|
private boolean done;
|
||||||
|
|
||||||
|
private boolean eof;
|
||||||
|
|
||||||
|
private byte[] buf;
|
||||||
|
|
||||||
|
private int length;
|
||||||
|
|
||||||
|
private boolean bol;
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
|
||||||
|
public MimeParser(InputStream inputStream, String boundary) {
|
||||||
|
this(inputStream, boundary, 8192);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MimeParser(InputStream inputStream, String boundary, int chunkSize) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.boundaryBytes = getBytes("--" + boundary);
|
||||||
|
this.boundaryLength = boundaryBytes.length;
|
||||||
|
this.chunkSize = chunkSize;
|
||||||
|
this.state = State.START_MESSAGE;
|
||||||
|
this.goodSuffixShiftTable = new int[boundaryLength];
|
||||||
|
this.badCharacterShift = new int[128];
|
||||||
|
this.done = false;
|
||||||
|
compileBoundaryPattern();
|
||||||
|
this.capacity = chunkSize + 2 + boundaryLength + 4 + NO_WHITESPACE_LENGTH;
|
||||||
|
this.buf = new byte[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getBytes(String s) {
|
||||||
|
char[] chars = s.toCharArray();
|
||||||
|
int size = chars.length;
|
||||||
|
byte[] bytes = new byte[size];
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
bytes[i] = (byte) chars[i];
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MimeEventIterator iterator() {
|
||||||
|
return new MimeEventIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects the headers for the current part by parsing the stream.
|
||||||
|
*
|
||||||
|
* @return headers for the current part
|
||||||
|
*/
|
||||||
|
private InternetHeaders readHeaders() throws IOException {
|
||||||
|
if (!eof) {
|
||||||
|
fillBuf();
|
||||||
|
}
|
||||||
|
this.offset = 0;
|
||||||
|
return new InternetHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and saves the part of the current attachment part's content.
|
||||||
|
* At the end of this method, buf should have the remaining data
|
||||||
|
* at index 0.
|
||||||
|
*
|
||||||
|
* @return a chunk of the part's content
|
||||||
|
*/
|
||||||
|
private ByteBuffer readBody() throws MimeException, IOException {
|
||||||
|
if (!eof) {
|
||||||
|
fillBuf();
|
||||||
|
}
|
||||||
|
int start = matchBoundary(buf, 0, length);
|
||||||
|
if (start == -1) {
|
||||||
|
int chunkSize = eof ? length : this.chunkSize;
|
||||||
|
if (eof) {
|
||||||
|
done = true;
|
||||||
|
throw new MimeException("reached EOF, but there is no closing MIME boundary");
|
||||||
|
}
|
||||||
|
return adjustBuf(chunkSize, length - chunkSize);
|
||||||
|
}
|
||||||
|
int chunkLen = start;
|
||||||
|
if (bol && start == 0) {
|
||||||
|
} else if (start > 0 && (buf[start - 1] == '\n' || buf[start - 1] == '\r')) {
|
||||||
|
--chunkLen;
|
||||||
|
if (buf[start - 1] == '\n' && start > 1 && buf[start - 2] == '\r') {
|
||||||
|
--chunkLen;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return adjustBuf(start + 1, length - start - 1);
|
||||||
|
}
|
||||||
|
if (start + boundaryLength + 1 < length && buf[start + boundaryLength] == '-' && buf[start + boundaryLength + 1] == '-') {
|
||||||
|
state = State.END_PART;
|
||||||
|
done = true;
|
||||||
|
return adjustBuf(chunkLen, 0);
|
||||||
|
}
|
||||||
|
int whitespaceLength = 0;
|
||||||
|
for (int i = start + boundaryLength; i < length && (buf[i] == ' ' || buf[i] == '\t'); i++) {
|
||||||
|
++whitespaceLength;
|
||||||
|
}
|
||||||
|
if (start + boundaryLength + whitespaceLength < length && buf[start + boundaryLength + whitespaceLength] == '\n') {
|
||||||
|
state = State.END_PART;
|
||||||
|
return adjustBuf(chunkLen, length - start - boundaryLength - whitespaceLength - 1);
|
||||||
|
} else if (start + boundaryLength + whitespaceLength + 1 < length && buf[start + boundaryLength + whitespaceLength] == '\r' &&
|
||||||
|
buf[start + boundaryLength + whitespaceLength + 1] == '\n') {
|
||||||
|
state = State.END_PART;
|
||||||
|
return adjustBuf(chunkLen, length - start - boundaryLength - whitespaceLength - 2);
|
||||||
|
} else if (start + boundaryLength + whitespaceLength + 1 < length) {
|
||||||
|
return adjustBuf(chunkLen + 1, length - chunkLen - 1);
|
||||||
|
} else if (eof) {
|
||||||
|
done = true;
|
||||||
|
throw new MimeException("reached EOF, but there is no closing MIME boundary");
|
||||||
|
}
|
||||||
|
return adjustBuf(chunkLen, length - chunkLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a chunk from the original buffer. A new buffer is
|
||||||
|
* created with the remaining bytes.
|
||||||
|
*
|
||||||
|
* @param chunkSize create a chunk with these many bytes
|
||||||
|
* @param remaining bytes from the end of the buffer that need to be copied to
|
||||||
|
* the beginning of the new buffer
|
||||||
|
* @return chunk
|
||||||
|
*/
|
||||||
|
private ByteBuffer adjustBuf(int chunkSize, int remaining) {
|
||||||
|
byte[] temp = buf;
|
||||||
|
buf = new byte[Math.min(capacity, remaining)];
|
||||||
|
System.arraycopy(temp, length - remaining, buf, 0, remaining);
|
||||||
|
length = remaining;
|
||||||
|
return ByteBuffer.wrap(temp, 0, chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the preamble to find the first attachment part
|
||||||
|
*/
|
||||||
|
private void skipPreamble() throws MimeException, IOException {
|
||||||
|
while (true) {
|
||||||
|
if (!eof) {
|
||||||
|
fillBuf();
|
||||||
|
}
|
||||||
|
int start = matchBoundary(buf, 0, length);
|
||||||
|
if (start == -1) {
|
||||||
|
if (eof) {
|
||||||
|
throw new MimeException("missing start boundary");
|
||||||
|
} else {
|
||||||
|
adjustBuf(length - boundaryLength + 1, boundaryLength - 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (start > chunkSize) {
|
||||||
|
adjustBuf(start, length - start);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int whitespaceLength = 0;
|
||||||
|
for (int i = start + boundaryLength; i < length && (buf[i] == ' ' || buf[i] == '\t'); i++) {
|
||||||
|
++whitespaceLength;
|
||||||
|
}
|
||||||
|
if (start + boundaryLength + whitespaceLength < length && (buf[start + boundaryLength + whitespaceLength] == '\n' || buf[start + boundaryLength + whitespaceLength] == '\r')) {
|
||||||
|
if (buf[start + boundaryLength + whitespaceLength] == '\n') {
|
||||||
|
adjustBuf(start + boundaryLength + whitespaceLength + 1, length - start - boundaryLength - whitespaceLength - 1);
|
||||||
|
break;
|
||||||
|
} else if (start + boundaryLength + whitespaceLength + 1 < length && buf[start + boundaryLength + whitespaceLength + 1] == '\n') {
|
||||||
|
adjustBuf(start + boundaryLength + whitespaceLength + 2, length - start - boundaryLength - whitespaceLength - 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjustBuf(start + 1, length - start - 1);
|
||||||
|
}
|
||||||
|
logger.log(Level.FINER, "skipped the preamble. buffer length =" + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boyer-Moore search method. Copied from java.util.regex.Pattern.java
|
||||||
|
* Pre calculates arrays needed to generate the bad character
|
||||||
|
* shift and the good suffix shift. Only the last seven bits
|
||||||
|
* are used to see if chars match; This keeps the tables small
|
||||||
|
* and covers the heavily used ASCII range, but occasionally
|
||||||
|
* results in an aliased match for the bad character shift.
|
||||||
|
*/
|
||||||
|
private void compileBoundaryPattern() {
|
||||||
|
int i;
|
||||||
|
int j;
|
||||||
|
for (i = 0; i < boundaryLength; i++) {
|
||||||
|
badCharacterShift[boundaryBytes[i] & 0x7F] = i + 1;
|
||||||
|
}
|
||||||
|
NEXT:
|
||||||
|
for (i = boundaryLength; i > 0; i--) {
|
||||||
|
for (j = boundaryLength - 1; j >= i; j--) {
|
||||||
|
if (boundaryBytes[j] == boundaryBytes[j - i]) {
|
||||||
|
goodSuffixShiftTable[j - 1] = i;
|
||||||
|
} else {
|
||||||
|
continue NEXT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (j > 0) {
|
||||||
|
goodSuffixShiftTable[--j] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goodSuffixShiftTable[boundaryLength - 1] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the boundary in the given buffer using Boyer-Moore algo.
|
||||||
|
* Copied from java.util.regex.Pattern.java
|
||||||
|
*
|
||||||
|
* @param buffer boundary to be searched
|
||||||
|
* @param off start index
|
||||||
|
* @param len number of bytes
|
||||||
|
* @return -1 if there is no match or index where the match starts
|
||||||
|
*/
|
||||||
|
private int matchBoundary(byte[] buffer, int off, int len) {
|
||||||
|
logger.log(Level.FINER, "matching boundary = " + new String(boundaryBytes) + " len = " + boundaryLength);
|
||||||
|
logger.log(Level.FINER, "off = " + off + " len = " + len + " buffer = " + new String(buffer, off, len));
|
||||||
|
int last = len - boundaryLength;
|
||||||
|
NEXT:
|
||||||
|
while (off <= last) {
|
||||||
|
for (int j = boundaryLength - 1; j >= 0; j--) {
|
||||||
|
byte ch = buffer[off + j];
|
||||||
|
if (ch != boundaryBytes[j]) {
|
||||||
|
off += Math.max(j + 1 - badCharacterShift[ch & 0x7F], goodSuffixShiftTable[j]);
|
||||||
|
continue NEXT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log(Level.FINER, "found at " + off);
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
logger.log(Level.FINER, "not found!");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills the remaining buf to the full capacity
|
||||||
|
*/
|
||||||
|
private void fillBuf() throws IOException {
|
||||||
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
|
logger.log(Level.FINER, "before fillBuf(): buffer len=" + length);
|
||||||
|
}
|
||||||
|
while (length < buf.length) {
|
||||||
|
int read = inputStream.read(buf, length, buf.length - length);
|
||||||
|
if (read == -1) {
|
||||||
|
eof = true;
|
||||||
|
if (logger.isLoggable(Level.FINE)) {
|
||||||
|
logger.fine("closing the input stream");
|
||||||
|
}
|
||||||
|
inputStream.close();
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
length += read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
|
logger.log(Level.FINER, "after fillBuf(): buffer len=" + length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doubleBuf() throws IOException {
|
||||||
|
byte[] temp = new byte[2 * length];
|
||||||
|
System.arraycopy(buf, 0, temp, 0, length);
|
||||||
|
buf = temp;
|
||||||
|
if (!eof) {
|
||||||
|
fillBuf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readLine() throws IOException {
|
||||||
|
int headerLength = 0;
|
||||||
|
int whitespaceLength = 0;
|
||||||
|
while (offset + headerLength < length) {
|
||||||
|
if (buf[offset + headerLength] == '\n') {
|
||||||
|
whitespaceLength = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (offset + headerLength + 1 == length) {
|
||||||
|
doubleBuf();
|
||||||
|
}
|
||||||
|
if (offset + headerLength + 1 >= length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (buf[offset + headerLength] == '\r' && buf[offset + headerLength + 1] == '\n') {
|
||||||
|
whitespaceLength = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++headerLength;
|
||||||
|
}
|
||||||
|
if (headerLength == 0) {
|
||||||
|
adjustBuf(offset + whitespaceLength, length - offset - whitespaceLength);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String hdr = new String(buf, offset, headerLength, StandardCharsets.ISO_8859_1);
|
||||||
|
offset += headerLength + whitespaceLength;
|
||||||
|
return hdr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum State {
|
||||||
|
START_MESSAGE, SKIP_PREAMBLE, START_PART, HEADERS, BODY, END_PART, END_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Hdr implements Header {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private String line;
|
||||||
|
|
||||||
|
private Hdr(String l) {
|
||||||
|
int i = l.indexOf(':');
|
||||||
|
if (i < 0) {
|
||||||
|
name = l.trim();
|
||||||
|
} else {
|
||||||
|
name = l.substring(0, i).trim();
|
||||||
|
}
|
||||||
|
line = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
Hdr(String n, String v) {
|
||||||
|
name = n;
|
||||||
|
line = n + ": " + v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
int i = line.indexOf(':');
|
||||||
|
if (i < 0) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
int j;
|
||||||
|
if (name.equalsIgnoreCase("Content-Description")) {
|
||||||
|
// Content-Description should retain the folded whitespace after header unfolding -
|
||||||
|
// rf. RFC2822 section 2.2.3, rf. RFC2822 section 3.2.3
|
||||||
|
for (j = i + 1; j < line.length(); j++) {
|
||||||
|
char c = line.charAt(j);
|
||||||
|
if (!(c == '\t' || c == '\r' || c == '\n')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// skip whitespace after ':'
|
||||||
|
for (j = i + 1; j < line.length(); j++) {
|
||||||
|
char c = line.charAt(j);
|
||||||
|
if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line.substring(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MimeEventIterator implements Iterator<MimeEvent> {
|
||||||
|
|
||||||
|
MimeEventIterator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return !parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("fallthrough")
|
||||||
|
@Override
|
||||||
|
public MimeEvent next() {
|
||||||
|
try {
|
||||||
|
if (parsed) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
if (logger.isLoggable(Level.FINER)) {
|
||||||
|
logger.log(Level.FINER, "MIMEParser state=" + state.name());
|
||||||
|
}
|
||||||
|
switch (state) {
|
||||||
|
case START_MESSAGE:
|
||||||
|
state = State.SKIP_PREAMBLE;
|
||||||
|
return StartMessage.INSTANCE;
|
||||||
|
case SKIP_PREAMBLE:
|
||||||
|
skipPreamble();
|
||||||
|
// fall through
|
||||||
|
case START_PART:
|
||||||
|
state = State.HEADERS;
|
||||||
|
return StartPart.INSTANCE;
|
||||||
|
case HEADERS:
|
||||||
|
InternetHeaders ih = readHeaders();
|
||||||
|
state = State.BODY;
|
||||||
|
bol = true;
|
||||||
|
return new Headers(ih);
|
||||||
|
case BODY:
|
||||||
|
ByteBuffer buf = readBody();
|
||||||
|
bol = false;
|
||||||
|
return new Content(buf);
|
||||||
|
case END_PART:
|
||||||
|
if (done) {
|
||||||
|
state = State.END_MESSAGE;
|
||||||
|
} else {
|
||||||
|
state = State.START_PART;
|
||||||
|
}
|
||||||
|
return EndPart.INSTANCE;
|
||||||
|
case END_MESSAGE:
|
||||||
|
parsed = true;
|
||||||
|
return EndMessage.INSTANCE;
|
||||||
|
}
|
||||||
|
// unreachable
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new NoSuchElementException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class that manages RFC822 style
|
||||||
|
* headers. Given an RFC822 format message stream, it reads lines
|
||||||
|
* until the blank line that indicates end of header. The input stream
|
||||||
|
* is positioned at the start of the body. The lines are stored
|
||||||
|
* within the object and can be extracted as either Strings or
|
||||||
|
* {@link Header} objects.
|
||||||
|
*
|
||||||
|
* This class is mostly intended for service providers. MimeMessage
|
||||||
|
* and MimeBody use this class for holding their headers.
|
||||||
|
*
|
||||||
|
* A note on RFC822 and MIME headers
|
||||||
|
*
|
||||||
|
* RFC822 and MIME header fields must contain only
|
||||||
|
* US-ASCII characters. If a header contains non US-ASCII characters,
|
||||||
|
* it must be encoded as per the rules in RFC 2047. The MimeUtility
|
||||||
|
* class provided in this package can be used to to achieve this.
|
||||||
|
* Callers of the <code>setHeader</code>, <code>addHeader</code>, and
|
||||||
|
* <code>addHeaderLine</code> methods are responsible for enforcing
|
||||||
|
* the MIME requirements for the specified headers. In addition, these
|
||||||
|
* header fields must be folded (wrapped) before being sent if they
|
||||||
|
* exceed the line length limitation for the transport (1000 bytes for
|
||||||
|
* SMTP). Received headers may have been folded. The application is
|
||||||
|
* responsible for folding and unfolding headers as appropriate.
|
||||||
|
*/
|
||||||
|
public class InternetHeaders {
|
||||||
|
|
||||||
|
private final List<Hdr> headers = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and parse the given RFC822 message stream till the
|
||||||
|
* blank line separating the header from the body. Store the
|
||||||
|
* header lines inside this InternetHeaders object.
|
||||||
|
* Note that the header lines are added into this InternetHeaders
|
||||||
|
* object, so any existing headers in this object will not be
|
||||||
|
* affected.
|
||||||
|
*/
|
||||||
|
public InternetHeaders() throws IOException {
|
||||||
|
String line;
|
||||||
|
String prevline = null;
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
do {
|
||||||
|
line = readLine();
|
||||||
|
if (line != null && (line.startsWith(" ") || line.startsWith("\t"))) {
|
||||||
|
if (prevline != null) {
|
||||||
|
stringBuilder.append(prevline);
|
||||||
|
prevline = null;
|
||||||
|
}
|
||||||
|
stringBuilder.append("\r\n").append(line);
|
||||||
|
} else {
|
||||||
|
if (prevline != null) {
|
||||||
|
addHeaderLine(prevline);
|
||||||
|
} else if (stringBuilder.length() > 0) {
|
||||||
|
addHeaderLine(stringBuilder.toString());
|
||||||
|
stringBuilder.setLength(0);
|
||||||
|
}
|
||||||
|
prevline = line;
|
||||||
|
}
|
||||||
|
} while (line != null && line.length() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the values for the specified header. The
|
||||||
|
* values are String objects. Returns <code>null</code>
|
||||||
|
* if no headers with the specified name exist.
|
||||||
|
*
|
||||||
|
* @param name header name
|
||||||
|
* @return array of header values, or null if none
|
||||||
|
*/
|
||||||
|
public List<String> getHeader(String name) {
|
||||||
|
List<String> v = new ArrayList<>();
|
||||||
|
for (Hdr h : headers) {
|
||||||
|
if (name.equalsIgnoreCase(h.name)) {
|
||||||
|
v.add(h.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v.isEmpty() ? null : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the headers as an Enumeration of
|
||||||
|
* {@link Header} objects.
|
||||||
|
*
|
||||||
|
* @return Header objects
|
||||||
|
*/
|
||||||
|
public List<? extends Header> getAllHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an RFC822 header line to the header store.
|
||||||
|
* If the line starts with a space or tab (a continuation line),
|
||||||
|
* add it to the last header line in the list.
|
||||||
|
* Note that RFC822 headers can only contain US-ASCII characters
|
||||||
|
*
|
||||||
|
* @param line raw RFC822 header line
|
||||||
|
*/
|
||||||
|
private void addHeaderLine(String line) {
|
||||||
|
char c = line.charAt(0);
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
Hdr h = headers.get(headers.size() - 1);
|
||||||
|
h.line += "\r\n" + line;
|
||||||
|
} else {
|
||||||
|
headers.add(new Hdr(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
229
net-mime/src/main/java/org/xbib/net/mime/MimePart.java
Normal file
229
net-mime/src/main/java/org/xbib/net/mime/MimePart.java
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import org.xbib.net.mime.stream.MimeStream;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an attachment part in a MIME message. MIME message parsing is done
|
||||||
|
* lazily using a pull parser, so the part may not have all the data. {@link #read}
|
||||||
|
* and {@link #readOnce} may trigger the actual parsing the message. In fact,
|
||||||
|
* parsing of an attachment part may be triggered by calling {@link #read} methods
|
||||||
|
* on some other attachment parts. All this happens behind the scenes so the
|
||||||
|
* application developer need not worry about these details.
|
||||||
|
*/
|
||||||
|
public class MimePart implements Closeable {
|
||||||
|
|
||||||
|
private volatile boolean closed;
|
||||||
|
private volatile MimeParser.InternetHeaders headers;
|
||||||
|
private volatile String contentId;
|
||||||
|
private String contentType;
|
||||||
|
private String contentTransferEncoding;
|
||||||
|
|
||||||
|
volatile boolean parsed;
|
||||||
|
final MimeMessage msg;
|
||||||
|
private final DataHead dataHead;
|
||||||
|
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
MimePart(MimeMessage msg) {
|
||||||
|
this.msg = msg;
|
||||||
|
this.dataHead = new DataHead(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
MimePart(MimeMessage msg, String contentId) {
|
||||||
|
this(msg);
|
||||||
|
this.contentId = contentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can get the attachment part's content multiple times. That means
|
||||||
|
* the full content needs to be there in memory or on the file system.
|
||||||
|
* Calling this method would trigger parsing for the part's data. So
|
||||||
|
* do not call this unless it is required(otherwise, just wrap MimePart
|
||||||
|
* into a object that returns InputStream for e.g DataHandler)
|
||||||
|
*
|
||||||
|
* @return data for the part's content
|
||||||
|
*/
|
||||||
|
public InputStream read() throws IOException, MimeException {
|
||||||
|
return MimeStream.decode(dataHead.read(), contentTransferEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up any resources that are held by this part (for e.g. deletes
|
||||||
|
* the temp file that is used to serve this part's content). After
|
||||||
|
* calling this, one shouldn't call {@link #read()} or {@link #readOnce()}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (!closed) {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (!closed) {
|
||||||
|
dataHead.close();
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can get the attachment part's content only once. The content
|
||||||
|
* will be lost after the method. Content data is not be stored
|
||||||
|
* on the file system or is not kept in the memory for the
|
||||||
|
* following case:
|
||||||
|
* - Attachement parts contents are accessed sequentially
|
||||||
|
*
|
||||||
|
* In general, take advantage of this when the data is used only
|
||||||
|
* once.
|
||||||
|
*
|
||||||
|
* @return data for the part's content
|
||||||
|
*/
|
||||||
|
public InputStream readOnce() throws IOException, MimeException {
|
||||||
|
return MimeStream.decode(dataHead.readOnce(), contentTransferEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the content to the File
|
||||||
|
* @param f file to store the content
|
||||||
|
*/
|
||||||
|
public void moveTo(File f) throws IOException, MimeException {
|
||||||
|
dataHead.moveTo(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Content-ID MIME header for this attachment part
|
||||||
|
*
|
||||||
|
* @return Content-ID of the part
|
||||||
|
*/
|
||||||
|
public String getContentId() throws MimeException, IOException {
|
||||||
|
if (contentId == null) {
|
||||||
|
getHeaders();
|
||||||
|
}
|
||||||
|
return contentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Content-Transfer-Encoding MIME header for this attachment part
|
||||||
|
*
|
||||||
|
* @return Content-Transfer-Encoding of the part
|
||||||
|
*/
|
||||||
|
public String getContentTransferEncoding() throws MimeException, IOException {
|
||||||
|
if (contentTransferEncoding == null) {
|
||||||
|
getHeaders();
|
||||||
|
}
|
||||||
|
return contentTransferEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Content-Type MIME header for this attachment part
|
||||||
|
*
|
||||||
|
* @return Content-Type of the part
|
||||||
|
*/
|
||||||
|
public String getContentType() throws MimeException, IOException {
|
||||||
|
if (contentType == null) {
|
||||||
|
getHeaders();
|
||||||
|
}
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getHeaders() throws MimeException, IOException {
|
||||||
|
// Trigger parsing for the part headers
|
||||||
|
while (headers == null) {
|
||||||
|
if (!msg.makeProgress()) {
|
||||||
|
if (headers == null) {
|
||||||
|
throw new IllegalStateException("internal Error. Didn't get Headers even after complete parsing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the values for the specified header.
|
||||||
|
* Returns <code>null</code> if no headers with the
|
||||||
|
* specified name exist.
|
||||||
|
*
|
||||||
|
* @param name header name
|
||||||
|
* @return list of header values, or null if none
|
||||||
|
*/
|
||||||
|
public List<String> getHeader(String name) throws MimeException, IOException {
|
||||||
|
getHeaders();
|
||||||
|
return headers.getHeader(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the headers
|
||||||
|
*
|
||||||
|
* @return list of Header objects
|
||||||
|
*/
|
||||||
|
public List<? extends Header> getAllHeaders() throws MimeException, IOException {
|
||||||
|
getHeaders();
|
||||||
|
return headers.getAllHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to set headers
|
||||||
|
*
|
||||||
|
* @param headers MIME headers for the part
|
||||||
|
*/
|
||||||
|
void setHeaders(MimeParser.InternetHeaders headers) throws MimeException, IOException {
|
||||||
|
this.headers = headers;
|
||||||
|
List<String> ct = getHeader("Content-Type");
|
||||||
|
this.contentType = (ct == null) ? "application/octet-stream" : ct.get(0);
|
||||||
|
List<String> cte = getHeader("Content-Transfer-Encoding");
|
||||||
|
this.contentTransferEncoding = (cte == null) ? "binary" : cte.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to notify that there is a partial content for the part
|
||||||
|
*
|
||||||
|
* @param buf content data for the part
|
||||||
|
*/
|
||||||
|
void addBody(ByteBuffer buf) throws IOException {
|
||||||
|
dataHead.addBody(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to indicate that parsing is done for this part
|
||||||
|
* (no more update events for this part)
|
||||||
|
*/
|
||||||
|
void doneParsing() {
|
||||||
|
parsed = true;
|
||||||
|
dataHead.doneParsing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to set Content-ID for this part
|
||||||
|
* @param cid Content-ID of the part
|
||||||
|
*/
|
||||||
|
void setContentId(String cid) {
|
||||||
|
this.contentId = cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to set Content-Transfer-Encoding for this part
|
||||||
|
* @param cte Content-Transfer-Encoding of the part
|
||||||
|
*/
|
||||||
|
void setContentTransferEncoding(String cte) {
|
||||||
|
this.contentTransferEncoding = cte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if this part has already been closed, {@code false} otherwise.
|
||||||
|
*
|
||||||
|
* @return {@code true} if this part has already been closed, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isClosed() {
|
||||||
|
return closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Part="+contentId+":"+contentTransferEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
net-mime/src/main/java/org/xbib/net/mime/MimeTypeEntry.java
Normal file
21
net-mime/src/main/java/org/xbib/net/mime/MimeTypeEntry.java
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public class MimeTypeEntry {
|
||||||
|
|
||||||
|
private final String type;
|
||||||
|
|
||||||
|
private final String extension;
|
||||||
|
|
||||||
|
public MimeTypeEntry(String type, String extension) {
|
||||||
|
this.type = type;
|
||||||
|
this.extension = extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtension() {
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
}
|
105
net-mime/src/main/java/org/xbib/net/mime/MimeTypeFile.java
Normal file
105
net-mime/src/main/java/org/xbib/net/mime/MimeTypeFile.java
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
public class MimeTypeFile {
|
||||||
|
|
||||||
|
private final Map<String, MimeTypeEntry> extensions;
|
||||||
|
|
||||||
|
private String prev;
|
||||||
|
|
||||||
|
public MimeTypeFile(URL url) throws IOException {
|
||||||
|
this.extensions = new HashMap<>();
|
||||||
|
parse(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, MimeTypeEntry> getExtensions() {
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MimeTypeEntry getMimeTypeEntry(String extension) {
|
||||||
|
return extensions.get(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMimeTypeString(String extension) {
|
||||||
|
MimeTypeEntry entry = getMimeTypeEntry(extension);
|
||||||
|
return entry != null ? entry.getType() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse(URL url) throws IOException {
|
||||||
|
try (InputStream inputStream = url.openStream()) {
|
||||||
|
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||||
|
bufferedReader.lines().forEach(line -> {
|
||||||
|
if (prev == null) {
|
||||||
|
prev = line;
|
||||||
|
} else {
|
||||||
|
prev = prev + line;
|
||||||
|
}
|
||||||
|
int end = prev.length();
|
||||||
|
if (prev.length() > 0 && prev.charAt(end - 1) == 92) {
|
||||||
|
prev = prev.substring(0, end - 1);
|
||||||
|
} else{
|
||||||
|
parseEntry(prev);
|
||||||
|
prev = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (prev != null) {
|
||||||
|
parseEntry(prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseEntry(String line) {
|
||||||
|
String mimetype = null;
|
||||||
|
String ext;
|
||||||
|
String entry;
|
||||||
|
line = line.trim();
|
||||||
|
StringTokenizer strtok = new StringTokenizer(line);
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (line.startsWith("#")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (line.indexOf(61) > 0) {
|
||||||
|
while (strtok.hasMoreTokens()) {
|
||||||
|
String num_tok = strtok.nextToken();
|
||||||
|
entry = null;
|
||||||
|
if (strtok.hasMoreTokens() && "=".equals(strtok.nextToken()) && strtok.hasMoreTokens()) {
|
||||||
|
entry = strtok.nextToken();
|
||||||
|
}
|
||||||
|
if (entry == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("type".equals(num_tok)) {
|
||||||
|
mimetype = entry;
|
||||||
|
} else if ("exts".equals(num_tok)) {
|
||||||
|
StringTokenizer st = new StringTokenizer(entry, ",");
|
||||||
|
while (st.hasMoreTokens()) {
|
||||||
|
ext = st.nextToken();
|
||||||
|
MimeTypeEntry entry1 = new MimeTypeEntry(mimetype, ext);
|
||||||
|
extensions.put(ext, entry1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (strtok.countTokens() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mimetype = strtok.nextToken();
|
||||||
|
while (strtok.hasMoreTokens()) {
|
||||||
|
ext = strtok.nextToken();
|
||||||
|
MimeTypeEntry entry2 = new MimeTypeEntry(mimetype, ext);
|
||||||
|
extensions.put(ext, entry2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MimeTypeService {
|
||||||
|
|
||||||
|
public static final String DEFAULT_TYPE = "application/octet-stream";
|
||||||
|
|
||||||
|
private final Map<String, MimeTypeEntry> extensions;
|
||||||
|
|
||||||
|
public MimeTypeService() {
|
||||||
|
this.extensions = new HashMap<>();
|
||||||
|
try {
|
||||||
|
List<MimeTypeFile> mimeTypeFiles = new ArrayList<>();
|
||||||
|
String s = "META-INF/mime.types";
|
||||||
|
boolean found = false;
|
||||||
|
ClassLoader classLoader = getClass().getClassLoader();
|
||||||
|
Enumeration<URL> e = classLoader.getResources(s);
|
||||||
|
while (e != null && e.hasMoreElements()) {
|
||||||
|
URL url = e.nextElement();
|
||||||
|
if (url != null) {
|
||||||
|
mimeTypeFiles.add(new MimeTypeFile(url));
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
MimeTypeFile mtf = loadResource(classLoader, "/" + s);
|
||||||
|
if (mtf != null) {
|
||||||
|
mimeTypeFiles.add(mtf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MimeTypeFile defaultMimeTypeFile = loadResource(classLoader, "/META-INF/mimetypes.default");
|
||||||
|
if (defaultMimeTypeFile != null) {
|
||||||
|
mimeTypeFiles.add(defaultMimeTypeFile);
|
||||||
|
}
|
||||||
|
for (MimeTypeFile mimeTypeFile : mimeTypeFiles) {
|
||||||
|
extensions.putAll(mimeTypeFile.getExtensions());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType(String nameWithSuffix) {
|
||||||
|
if (nameWithSuffix == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int pos = nameWithSuffix.lastIndexOf('.');
|
||||||
|
if (pos < 0) {
|
||||||
|
return DEFAULT_TYPE;
|
||||||
|
} else {
|
||||||
|
String ext = nameWithSuffix.substring(pos + 1);
|
||||||
|
if (ext.length() == 0) {
|
||||||
|
return DEFAULT_TYPE;
|
||||||
|
} else {
|
||||||
|
return extensions.containsKey(ext) ? extensions.get(ext).getType() : DEFAULT_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MimeTypeFile loadResource(ClassLoader classLoader, String name) throws IOException {
|
||||||
|
URL url = classLoader.getResource(name);
|
||||||
|
return url != null ? new MimeTypeFile(url) : null;
|
||||||
|
}
|
||||||
|
}
|
92
net-mime/src/main/java/org/xbib/net/mime/MimeTypeUtil.java
Normal file
92
net-mime/src/main/java/org/xbib/net/mime/MimeTypeUtil.java
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
public class MimeTypeUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map from extension to MIME types, which is queried before
|
||||||
|
* {@link URLConnection#guessContentTypeFromName(String)}, so that
|
||||||
|
* important extensions are always mapped to the right MIME types.
|
||||||
|
*/
|
||||||
|
private static final Map<String, String> EXTENSION_TO_MEDIA_TYPE;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
// Text files
|
||||||
|
add(map, "text/css", "css");
|
||||||
|
add(map, "text/html", "html", "htm");
|
||||||
|
add(map, "text/plain", "txt");
|
||||||
|
|
||||||
|
// Image files
|
||||||
|
add(map, "image/gif", "gif");
|
||||||
|
add(map, "image/jpeg", "jpeg", "jpg");
|
||||||
|
add(map, "image/png", "png");
|
||||||
|
add(map, "image/svg+xml", "svg", "svgz");
|
||||||
|
add(map, "image/x-icon", "ico");
|
||||||
|
|
||||||
|
// Font files
|
||||||
|
add(map, "application/x-font-ttf", "ttc", "ttf");
|
||||||
|
add(map, "application/font-woff", "woff");
|
||||||
|
add(map, "application/font-woff2", "woff2");
|
||||||
|
add(map, "application/vnd.ms-fontobject", "eot");
|
||||||
|
add(map, "font/opentype", "otf");
|
||||||
|
|
||||||
|
// JavaScript, XML, etc
|
||||||
|
add(map, "application/javascript", "js", "map");
|
||||||
|
add(map, "application/json", "json");
|
||||||
|
add(map, "application/pdf", "pdf");
|
||||||
|
add(map, "application/xhtml+xml", "xhtml", "xhtm");
|
||||||
|
add(map, "application/xml", "xml", "xsd");
|
||||||
|
add(map, "application/xml-dtd", "dtd");
|
||||||
|
|
||||||
|
EXTENSION_TO_MEDIA_TYPE = Collections.unmodifiableMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MimeTypeUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void add(Map<String, String> extensionToMediaType,
|
||||||
|
String mediaType,
|
||||||
|
String... extensions) {
|
||||||
|
for (String s : extensions) {
|
||||||
|
extensionToMediaType.put(s, mediaType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String guessFromPath(String path, boolean preCompressed) {
|
||||||
|
requireNonNull(path, "path");
|
||||||
|
String extension = extractExtension(path, preCompressed);
|
||||||
|
if (extension == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String mediaType = EXTENSION_TO_MEDIA_TYPE.get(extension);
|
||||||
|
if (mediaType != null) {
|
||||||
|
return mediaType;
|
||||||
|
}
|
||||||
|
String guessedContentType = URLConnection.guessContentTypeFromName(path);
|
||||||
|
return guessedContentType != null ? guessedContentType : "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractExtension(String path, boolean preCompressed) {
|
||||||
|
String s = path;
|
||||||
|
// If the path is for a precompressed file, it will have an additional extension indicating the
|
||||||
|
// encoding, which we don't want to use when determining content type.
|
||||||
|
if (preCompressed) {
|
||||||
|
s = s.substring(0, s.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
int dotIdx = s.lastIndexOf('.');
|
||||||
|
int slashIdx = s.lastIndexOf('/');
|
||||||
|
if (dotIdx < 0 || slashIdx > dotIdx) {
|
||||||
|
// No extension
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return s.substring(dotIdx + 1).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
14
net-mime/src/main/java/org/xbib/net/mime/StartMessage.java
Normal file
14
net-mime/src/main/java/org/xbib/net/mime/StartMessage.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public final class StartMessage implements MimeEvent {
|
||||||
|
|
||||||
|
static final StartMessage INSTANCE = new StartMessage();
|
||||||
|
|
||||||
|
public StartMessage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getEventType() {
|
||||||
|
return Type.START_MESSAGE;
|
||||||
|
}
|
||||||
|
}
|
14
net-mime/src/main/java/org/xbib/net/mime/StartPart.java
Normal file
14
net-mime/src/main/java/org/xbib/net/mime/StartPart.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
public final class StartPart implements MimeEvent {
|
||||||
|
|
||||||
|
static final StartPart INSTANCE = new StartPart();
|
||||||
|
|
||||||
|
public StartPart() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getEventType() {
|
||||||
|
return Type.START_PART;
|
||||||
|
}
|
||||||
|
}
|
103
net-mime/src/main/java/org/xbib/net/mime/WeakDataFile.java
Normal file
103
net-mime/src/main/java/org/xbib/net/mime/WeakDataFile.java
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package org.xbib.net.mime;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.lang.ref.ReferenceQueue;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removing files based on this
|
||||||
|
* <a href="https://www.oracle.com/technical-resources/articles/javase/finalization.html">article</a>
|
||||||
|
*/
|
||||||
|
final class WeakDataFile extends WeakReference<DataFile> {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(WeakDataFile.class.getName());
|
||||||
|
private static int TIMEOUT = 10; //milliseconds
|
||||||
|
private static ReferenceQueue<DataFile> refQueue = new ReferenceQueue<DataFile>();
|
||||||
|
private static Queue<WeakDataFile> refList = new ConcurrentLinkedQueue<>();
|
||||||
|
private File file;
|
||||||
|
private final RandomAccessFile raf;
|
||||||
|
private static boolean hasCleanUpExecutor = false;
|
||||||
|
static {
|
||||||
|
int delay = 10;
|
||||||
|
try {
|
||||||
|
delay = Integer.getInteger("org.xbib.net.mime.delay", 10);
|
||||||
|
} catch (SecurityException se) {
|
||||||
|
if (LOGGER.isLoggable(Level.CONFIG)) {
|
||||||
|
LOGGER.log(Level.CONFIG, "Cannot read ''{0}'' property, using defaults.",
|
||||||
|
new Object[] {"org.xbib.net.mime.delay"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WeakDataFile(DataFile df, File file) throws IOException {
|
||||||
|
super(df, refQueue);
|
||||||
|
refList.add(this);
|
||||||
|
this.file = file;
|
||||||
|
raf = new RandomAccessFile(file, "rw");
|
||||||
|
if (!hasCleanUpExecutor) {
|
||||||
|
drainRefQueueBounded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void read(long pointer, byte[] buf, int offset, int length ) throws IOException {
|
||||||
|
raf.seek(pointer);
|
||||||
|
raf.readFully(buf, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized long writeTo(long pointer, byte[] data, int offset, int length) throws IOException {
|
||||||
|
raf.seek(pointer);
|
||||||
|
raf.write(data, offset, length);
|
||||||
|
return raf.getFilePointer(); // Update pointer for next write
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
if (LOGGER.isLoggable(Level.FINE)) {
|
||||||
|
LOGGER.log(Level.FINE, "Deleting file = {0}", file.getName());
|
||||||
|
}
|
||||||
|
refList.remove(this);
|
||||||
|
raf.close();
|
||||||
|
boolean deleted = file.delete();
|
||||||
|
if (!deleted) {
|
||||||
|
if (LOGGER.isLoggable(Level.INFO)) {
|
||||||
|
LOGGER.log(Level.INFO, "File {0} was not deleted", file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void renameTo(File f) throws IOException {
|
||||||
|
if (LOGGER.isLoggable(Level.FINE)) {
|
||||||
|
LOGGER.log(Level.FINE, "Moving file={0} to={1}", new Object[]{file, f});
|
||||||
|
}
|
||||||
|
refList.remove(this);
|
||||||
|
raf.close();
|
||||||
|
Path target = Files.move(file.toPath(), f.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
boolean renamed = f.toPath().equals(target);
|
||||||
|
if (!renamed) {
|
||||||
|
if (LOGGER.isLoggable(Level.INFO)) {
|
||||||
|
throw new IOException("File " + file.getAbsolutePath() +
|
||||||
|
" was not moved to " + f.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file = target.toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drainRefQueueBounded() throws IOException {
|
||||||
|
WeakDataFile weak;
|
||||||
|
while (( weak = (WeakDataFile) refQueue.poll()) != null ) {
|
||||||
|
if (LOGGER.isLoggable(Level.FINE)) {
|
||||||
|
LOGGER.log(Level.FINE, "Cleaning file = {0} from reference queue.", weak.file);
|
||||||
|
}
|
||||||
|
weak.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,438 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import org.xbib.net.mime.MimeException;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements a BASE64 Decoder. It is implemented as
|
||||||
|
* a FilterInputStream, so one can just wrap this class around
|
||||||
|
* any input stream and read bytes from this filter. The decoding
|
||||||
|
* is done as the bytes are read out.
|
||||||
|
*/
|
||||||
|
final class BASE64DecoderStream extends FilterInputStream {
|
||||||
|
/**
|
||||||
|
* This character array provides the character to value map
|
||||||
|
* based on RFC1521.
|
||||||
|
*/
|
||||||
|
private final static char[] pem_array = {
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0
|
||||||
|
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2
|
||||||
|
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3
|
||||||
|
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4
|
||||||
|
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5
|
||||||
|
'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6
|
||||||
|
'4', '5', '6', '7', '8', '9', '+', '/' // 7
|
||||||
|
};
|
||||||
|
private final static byte[] pem_convert_array = new byte[256];
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (int i = 0; i < 255; i++) {
|
||||||
|
pem_convert_array[i] = -1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < pem_array.length; i++) {
|
||||||
|
pem_convert_array[pem_array[i]] = (byte) i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer of decoded bytes for single byte reads
|
||||||
|
private final byte[] buffer = new byte[3];
|
||||||
|
// buffer for almost 8K of typical 76 chars + CRLF lines,
|
||||||
|
// used by getByte method. this buffer contains encoded bytes.
|
||||||
|
private final byte[] input_buffer = new byte[78 * 105];
|
||||||
|
private int bufsize = 0; // size of the cache
|
||||||
|
private int index = 0; // index into the cache
|
||||||
|
private int input_pos = 0;
|
||||||
|
private int input_len = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a BASE64 decoder that decodes the specified input stream.
|
||||||
|
* The System property <code>mail.mime.base64.ignoreerrors</code>
|
||||||
|
* controls whether errors in the encoded data cause an exception
|
||||||
|
* or are ignored. The default is false (errors cause exception).
|
||||||
|
*
|
||||||
|
* @param in the input stream
|
||||||
|
*/
|
||||||
|
public BASE64DecoderStream(InputStream in) {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 decode a byte array. No line breaks are allowed.
|
||||||
|
* This method is suitable for short strings, such as those
|
||||||
|
* in the IMAP AUTHENTICATE protocol, but not to decode the
|
||||||
|
* entire content of a MIME part.
|
||||||
|
* <p>
|
||||||
|
* NOTE: inbuf may only contain valid base64 characters.
|
||||||
|
* Whitespace is not ignored.
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] inbuf) {
|
||||||
|
int size = (inbuf.length / 4) * 3;
|
||||||
|
if (size == 0) {
|
||||||
|
return inbuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inbuf[inbuf.length - 1] == '=') {
|
||||||
|
size--;
|
||||||
|
if (inbuf[inbuf.length - 2] == '=') {
|
||||||
|
size--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] outbuf = new byte[size];
|
||||||
|
|
||||||
|
int inpos = 0, outpos = 0;
|
||||||
|
size = inbuf.length;
|
||||||
|
while (size > 0) {
|
||||||
|
int val;
|
||||||
|
int osize = 3;
|
||||||
|
val = pem_convert_array[inbuf[inpos++] & 0xff];
|
||||||
|
val <<= 6;
|
||||||
|
val |= pem_convert_array[inbuf[inpos++] & 0xff];
|
||||||
|
val <<= 6;
|
||||||
|
if (inbuf[inpos] != '=') {
|
||||||
|
val |= pem_convert_array[inbuf[inpos++] & 0xff];
|
||||||
|
} else {
|
||||||
|
osize--;
|
||||||
|
}
|
||||||
|
val <<= 6;
|
||||||
|
if (inbuf[inpos] != '=') {
|
||||||
|
val |= pem_convert_array[inbuf[inpos++] & 0xff];
|
||||||
|
} else {
|
||||||
|
osize--;
|
||||||
|
}
|
||||||
|
if (osize > 2) {
|
||||||
|
outbuf[outpos + 2] = (byte) (val & 0xff);
|
||||||
|
}
|
||||||
|
val >>= 8;
|
||||||
|
if (osize > 1) {
|
||||||
|
outbuf[outpos + 1] = (byte) (val & 0xff);
|
||||||
|
}
|
||||||
|
val >>= 8;
|
||||||
|
outbuf[outpos] = (byte) (val & 0xff);
|
||||||
|
outpos += osize;
|
||||||
|
size -= 4;
|
||||||
|
}
|
||||||
|
return outbuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the next decoded byte from this input stream. The byte
|
||||||
|
* is returned as an <code>int</code> in the range <code>0</code>
|
||||||
|
* to <code>255</code>. If no byte is available because the end of
|
||||||
|
* the stream has been reached, the value <code>-1</code> is returned.
|
||||||
|
* This method blocks until input data is available, the end of the
|
||||||
|
* stream is detected, or an exception is thrown.
|
||||||
|
*
|
||||||
|
* @return next byte of data, or <code>-1</code> if the end of the
|
||||||
|
* stream is reached.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see FilterInputStream#in
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (index >= bufsize) {
|
||||||
|
try {
|
||||||
|
bufsize = decode(buffer, 0, buffer.length);
|
||||||
|
} catch (MimeException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
if (bufsize <= 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
index = 0; // reset index into buffer
|
||||||
|
}
|
||||||
|
return buffer[index++] & 0xff; // Zero off the MSB
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads up to <code>len</code> decoded bytes of data from this input stream
|
||||||
|
* into an array of bytes. This method blocks until some input is
|
||||||
|
* available.
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @param buf the buffer into which the data is read.
|
||||||
|
* @param off the start offset of the data.
|
||||||
|
* @param len the maximum number of bytes read.
|
||||||
|
* @return the total number of bytes read into the buffer, or
|
||||||
|
* <code>-1</code> if there is no more data because the end of
|
||||||
|
* the stream has been reached.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buf, int off, int len) throws IOException {
|
||||||
|
// empty out single byte read buffer
|
||||||
|
int off0 = off;
|
||||||
|
while (index < bufsize && len > 0) {
|
||||||
|
buf[off++] = buffer[index++];
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
if (index >= bufsize) {
|
||||||
|
bufsize = index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bsize = (len / 3) * 3; // round down to multiple of 3 bytes
|
||||||
|
if (bsize > 0) {
|
||||||
|
int size = 0;
|
||||||
|
try {
|
||||||
|
size = decode(buf, off, bsize);
|
||||||
|
} catch (MimeException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
off += size;
|
||||||
|
len -= size;
|
||||||
|
|
||||||
|
if (size != bsize) { // hit EOF?
|
||||||
|
if (off == off0) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return off - off0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish up with a partial read if necessary
|
||||||
|
for (; len > 0; len--) {
|
||||||
|
int c = read();
|
||||||
|
if (c == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf[off++] = (byte) c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (off == off0) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return off - off0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips over and discards n bytes of data from this stream.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
long skipped = 0;
|
||||||
|
while (n-- > 0 && read() >= 0) {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if this input stream supports marks. Currently this class
|
||||||
|
* does not support marks
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false; // Maybe later ..
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of bytes that can be read from this input
|
||||||
|
* stream without blocking. However, this figure is only
|
||||||
|
* a close approximation in case the original encoded stream
|
||||||
|
* contains embedded CRLFs; since the CRLFs are discarded, not decoded
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
// This is only an estimate, since in.available()
|
||||||
|
// might include CRLFs too ..
|
||||||
|
return ((in.available() * 3) / 4 + (bufsize - index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decoder algorithm. Most of the complexity here is dealing
|
||||||
|
* with error cases. Returns the number of bytes decoded, which
|
||||||
|
* may be zero. Decoding is done by filling an int with 4 6-bit
|
||||||
|
* values by shifting them in from the bottom and then extracting
|
||||||
|
* 3 8-bit bytes from the int by shifting them out from the bottom.
|
||||||
|
*
|
||||||
|
* @param outbuf the buffer into which to put the decoded bytes
|
||||||
|
* @param pos position in the buffer to start filling
|
||||||
|
* @param len the number of bytes to fill
|
||||||
|
* @return the number of bytes filled, always a multiple
|
||||||
|
* of three, and may be zero
|
||||||
|
* @exception IOException if the data is incorrectly formatted
|
||||||
|
*/
|
||||||
|
private int decode(byte[] outbuf, int pos, int len) throws IOException, MimeException {
|
||||||
|
int pos0 = pos;
|
||||||
|
while (len >= 3) {
|
||||||
|
/*
|
||||||
|
* We need 4 valid base64 characters before we start decoding.
|
||||||
|
* We skip anything that's not a valid base64 character (usually
|
||||||
|
* just CRLF).
|
||||||
|
*/
|
||||||
|
int got = 0;
|
||||||
|
int val = 0;
|
||||||
|
while (got < 4) {
|
||||||
|
int i = getByte();
|
||||||
|
if (i == -1 || i == -2) {
|
||||||
|
boolean atEOF;
|
||||||
|
if (i == -1) {
|
||||||
|
if (got == 0) {
|
||||||
|
return pos - pos0;
|
||||||
|
}
|
||||||
|
throw new MimeException(
|
||||||
|
"BASE64Decoder: Error in encoded stream: " +
|
||||||
|
"needed 4 valid base64 characters " +
|
||||||
|
"but only got " + got + " before EOF" +
|
||||||
|
recentChars());
|
||||||
|
} else { // i == -2
|
||||||
|
// found a padding character, we're at EOF
|
||||||
|
// XXX - should do something to make EOF "sticky"
|
||||||
|
if (got < 2) {
|
||||||
|
throw new MimeException(
|
||||||
|
"BASE64Decoder: Error in encoded stream: " +
|
||||||
|
"needed at least 2 valid base64 characters," +
|
||||||
|
" but only got " + got +
|
||||||
|
" before padding character (=)" +
|
||||||
|
recentChars());
|
||||||
|
}
|
||||||
|
|
||||||
|
// didn't get any characters before padding character?
|
||||||
|
if (got == 0) {
|
||||||
|
return pos - pos0;
|
||||||
|
}
|
||||||
|
atEOF = false; // need to keep reading
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad partial result with zeroes
|
||||||
|
|
||||||
|
// how many bytes will we produce on output?
|
||||||
|
// (got always < 4, so size always < 3)
|
||||||
|
int size = got - 1;
|
||||||
|
if (size == 0) {
|
||||||
|
size = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the one padding character we've seen
|
||||||
|
got++;
|
||||||
|
val <<= 6;
|
||||||
|
|
||||||
|
while (got < 4) {
|
||||||
|
if (!atEOF) {
|
||||||
|
// consume the rest of the padding characters,
|
||||||
|
// filling with zeroes
|
||||||
|
i = getByte();
|
||||||
|
if (i == -1) {
|
||||||
|
throw new MimeException(
|
||||||
|
"BASE64Decoder: Error in encoded " +
|
||||||
|
"stream: hit EOF while looking for " +
|
||||||
|
"padding characters (=)" +
|
||||||
|
recentChars());
|
||||||
|
} else if (i != -2) {
|
||||||
|
throw new MimeException(
|
||||||
|
"BASE64Decoder: Error in encoded " +
|
||||||
|
"stream: found valid base64 " +
|
||||||
|
"character after a padding character " +
|
||||||
|
"(=)" + recentChars());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val <<= 6;
|
||||||
|
got++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now pull out however many valid bytes we got
|
||||||
|
val >>= 8; // always skip first one
|
||||||
|
if (size == 2) {
|
||||||
|
outbuf[pos + 1] = (byte) (val & 0xff);
|
||||||
|
}
|
||||||
|
val >>= 8;
|
||||||
|
outbuf[pos] = (byte) (val & 0xff);
|
||||||
|
// len -= size; // not needed, return below
|
||||||
|
pos += size;
|
||||||
|
return pos - pos0;
|
||||||
|
} else {
|
||||||
|
// got a valid byte
|
||||||
|
val <<= 6;
|
||||||
|
got++;
|
||||||
|
val |= i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read 4 valid characters, now extract 3 bytes
|
||||||
|
outbuf[pos + 2] = (byte) (val & 0xff);
|
||||||
|
val >>= 8;
|
||||||
|
outbuf[pos + 1] = (byte) (val & 0xff);
|
||||||
|
val >>= 8;
|
||||||
|
outbuf[pos] = (byte) (val & 0xff);
|
||||||
|
len -= 3;
|
||||||
|
pos += 3;
|
||||||
|
}
|
||||||
|
return pos - pos0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the next valid byte from the input stream.
|
||||||
|
* Buffer lots of data from underlying stream in input_buffer,
|
||||||
|
* for efficiency.
|
||||||
|
*
|
||||||
|
* @return the next byte, -1 on EOF, or -2 if next byte is '='
|
||||||
|
* (padding at end of encoded data)
|
||||||
|
*/
|
||||||
|
private int getByte() throws IOException {
|
||||||
|
int c;
|
||||||
|
do {
|
||||||
|
if (input_pos >= input_len) {
|
||||||
|
try {
|
||||||
|
input_len = in.read(input_buffer);
|
||||||
|
} catch (EOFException ex) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (input_len <= 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
input_pos = 0;
|
||||||
|
}
|
||||||
|
// get the next byte in the buffer
|
||||||
|
c = input_buffer[input_pos++] & 0xff;
|
||||||
|
// is it a padding byte?
|
||||||
|
if (c == '=') {
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
// no, convert it
|
||||||
|
c = pem_convert_array[c];
|
||||||
|
// loop until we get a legitimate byte
|
||||||
|
} while (c == -1);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the most recent characters, for use in an error message.
|
||||||
|
*/
|
||||||
|
private String recentChars() {
|
||||||
|
// reach into the input buffer and extract up to 10
|
||||||
|
// recent characters, to help in debugging.
|
||||||
|
StringBuilder errstr = new StringBuilder();
|
||||||
|
int nc = Math.min(input_pos, 10);
|
||||||
|
if (nc > 0) {
|
||||||
|
errstr.append(", the ").append(nc).append(" most recent characters were: \"");
|
||||||
|
for (int k = input_pos - nc; k < input_pos; k++) {
|
||||||
|
char c = (char) (input_buffer[k] & 0xff);
|
||||||
|
switch (c) {
|
||||||
|
case '\r':
|
||||||
|
errstr.append("\\r");
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
errstr.append("\\n");
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
errstr.append("\\t");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (c >= ' ' && c < 0177) {
|
||||||
|
errstr.append(c);
|
||||||
|
} else {
|
||||||
|
errstr.append("\\").append((int) c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errstr.append("\"");
|
||||||
|
}
|
||||||
|
return errstr.toString();
|
||||||
|
}
|
||||||
|
}
|
634
net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java
Normal file
634
net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java
Normal file
|
@ -0,0 +1,634 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for encoding and decoding the Base64 representation of
|
||||||
|
* binary data. See RFCs <a
|
||||||
|
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
|
||||||
|
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
|
||||||
|
*/
|
||||||
|
public class Base64 {
|
||||||
|
/**
|
||||||
|
* Default values for encoder/decoder flags.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT = 0;
|
||||||
|
/**
|
||||||
|
* Encoder flag bit to omit the padding '=' characters at the end
|
||||||
|
* of the output (if any).
|
||||||
|
*/
|
||||||
|
public static final int NO_PADDING = 1;
|
||||||
|
/**
|
||||||
|
* Encoder flag bit to omit all line terminators (i.e., the output
|
||||||
|
* will be on one long line).
|
||||||
|
*/
|
||||||
|
public static final int NO_WRAP = 2;
|
||||||
|
/**
|
||||||
|
* Encoder flag bit to indicate lines should be terminated with a
|
||||||
|
* CRLF pair instead of just an LF. Has no effect if {@code
|
||||||
|
* NO_WRAP} is specified as well.
|
||||||
|
*/
|
||||||
|
public static final int CRLF = 4;
|
||||||
|
/**
|
||||||
|
* Encoder/decoder flag bit to indicate using the "URL and
|
||||||
|
* filename safe" variant of Base64 (see RFC 3548 section 4) where
|
||||||
|
* {@code -} and {@code _} are used in place of {@code +} and
|
||||||
|
* {@code /}.
|
||||||
|
*/
|
||||||
|
public static final int URL_SAFE = 8;
|
||||||
|
/**
|
||||||
|
* Flag to pass to indicate that it
|
||||||
|
* should not close the output stream it is wrapping when it
|
||||||
|
* itself is closed.
|
||||||
|
*/
|
||||||
|
public static final int NO_CLOSE = 16;
|
||||||
|
|
||||||
|
private Base64() {
|
||||||
|
} // don't instantiate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the Base64-encoded data in input and return the data in
|
||||||
|
* a new byte array.
|
||||||
|
*
|
||||||
|
* <p>The padding '=' characters at the end are considered optional, but
|
||||||
|
* if any are present, there must be the correct number of them.
|
||||||
|
*
|
||||||
|
* @param input the input array to decode
|
||||||
|
* @param flags controls certain features of the decoded output.
|
||||||
|
* Pass {@code DEFAULT} to decode standard Base64.
|
||||||
|
* @throws IllegalArgumentException if the input contains
|
||||||
|
* incorrect padding
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] input, int flags) {
|
||||||
|
return decode(input, 0, input.length, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the Base64-encoded data in input and return the data in
|
||||||
|
* a new byte array.
|
||||||
|
*
|
||||||
|
* <p>The padding '=' characters at the end are considered optional, but
|
||||||
|
* if any are present, there must be the correct number of them.
|
||||||
|
*
|
||||||
|
* @param input the data to decode
|
||||||
|
* @param offset the position within the input array at which to start
|
||||||
|
* @param len the number of bytes of input to decode
|
||||||
|
* @param flags controls certain features of the decoded output.
|
||||||
|
* Pass {@code DEFAULT} to decode standard Base64.
|
||||||
|
* @throws IllegalArgumentException if the input contains
|
||||||
|
* incorrect padding
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] input, int offset, int len, int flags) {
|
||||||
|
// Allocate space for the most data the input could represent.
|
||||||
|
// (It could contain less if it contains whitespace, etc.)
|
||||||
|
Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]);
|
||||||
|
if (!decoder.process(input, offset, len, true)) {
|
||||||
|
throw new IllegalArgumentException("bad base-64");
|
||||||
|
}
|
||||||
|
// Maybe we got lucky and allocated exactly enough output space.
|
||||||
|
if (decoder.op == decoder.output.length) {
|
||||||
|
return decoder.output;
|
||||||
|
}
|
||||||
|
// Need to shorten the array, so allocate a new one of the
|
||||||
|
// right size and copy.
|
||||||
|
byte[] temp = new byte[decoder.op];
|
||||||
|
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* String with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static String encodeToString(byte[] input, int flags) {
|
||||||
|
return new String(encode(input, flags), StandardCharsets.US_ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* String with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param offset the position within the input array at which to
|
||||||
|
* start
|
||||||
|
* @param len the number of bytes of input to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static String encodeToString(byte[] input, int offset, int len, int flags) {
|
||||||
|
return new String(encode(input, offset, len, flags), StandardCharsets.US_ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* byte[] with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] input, int flags) {
|
||||||
|
return encode(input, 0, input.length, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* byte[] with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param offset the position within the input array at which to
|
||||||
|
* start
|
||||||
|
* @param len the number of bytes of input to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] input, int offset, int len, int flags) {
|
||||||
|
Encoder encoder = new Encoder(flags, null);
|
||||||
|
// Compute the exact length of the array we will produce.
|
||||||
|
int output_len = len / 3 * 4;
|
||||||
|
// Account for the tail of the data and the padding bytes, if any.
|
||||||
|
if (encoder.do_padding) {
|
||||||
|
if (len % 3 > 0) {
|
||||||
|
output_len += 4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (len % 3) {
|
||||||
|
case 0:
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
output_len += 2;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
output_len += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Account for the newlines, if any.
|
||||||
|
if (encoder.do_newline && len > 0) {
|
||||||
|
output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) *
|
||||||
|
(encoder.do_cr ? 2 : 1);
|
||||||
|
}
|
||||||
|
encoder.output = new byte[output_len];
|
||||||
|
encoder.process(input, offset, len, true);
|
||||||
|
assert encoder.op == output_len;
|
||||||
|
return encoder.output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static abstract class Coder {
|
||||||
|
public byte[] output;
|
||||||
|
public int op;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode/decode another block of input data. this.output is
|
||||||
|
* provided by the caller, and must be big enough to hold all
|
||||||
|
* the coded data. On exit, this.opwill be set to the length
|
||||||
|
* of the coded data.
|
||||||
|
*
|
||||||
|
* @param finish true if this is the final call to process for
|
||||||
|
* this object. Will finalize the coder state and
|
||||||
|
* include any final bytes in the output.
|
||||||
|
* @return true if the input so far is good; false if some
|
||||||
|
* error has been detected in the input stream..
|
||||||
|
*/
|
||||||
|
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the maximum number of bytes a call to process()
|
||||||
|
* could produce for the given number of input bytes. This may
|
||||||
|
* be an overestimate.
|
||||||
|
*/
|
||||||
|
public abstract int maxOutputSize(int len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ static class Decoder extends Coder {
|
||||||
|
/**
|
||||||
|
* Lookup table for turning bytes into their position in the
|
||||||
|
* Base64 alphabet.
|
||||||
|
*/
|
||||||
|
private static final int[] DECODE = {
|
||||||
|
-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, -1, -1, 63,
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -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, -1,
|
||||||
|
-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, -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, -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, -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, -1, -1, -1, -1,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Decode lookup table for the "web safe" variant (RFC 3548
|
||||||
|
* sec. 4) where - and _ replace + and /.
|
||||||
|
*/
|
||||||
|
private static final int[] DECODE_WEBSAFE = {
|
||||||
|
-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, -1, -1, 62, -1, -1,
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -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, -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, -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, -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, -1, -1, -1, -1,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Non-data values in the DECODE arrays.
|
||||||
|
*/
|
||||||
|
private static final int SKIP = -1;
|
||||||
|
private static final int EQUALS = -2;
|
||||||
|
final private int[] alphabet;
|
||||||
|
/**
|
||||||
|
* States 0-3 are reading through the next input tuple.
|
||||||
|
* State 4 is having read one '=' and expecting exactly
|
||||||
|
* one more.
|
||||||
|
* State 5 is expecting no more data or padding characters
|
||||||
|
* in the input.
|
||||||
|
* State 6 is the error state; an error has been detected
|
||||||
|
* in the input and no future input can "fix" it.
|
||||||
|
*/
|
||||||
|
private int state; // state number (0 to 6)
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
public Decoder(int flags, byte[] output) {
|
||||||
|
this.output = output;
|
||||||
|
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
|
||||||
|
state = 0;
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an overestimate for the number of bytes {@code
|
||||||
|
* len} bytes could decode to.
|
||||||
|
*/
|
||||||
|
public int maxOutputSize(int len) {
|
||||||
|
return len * 3 / 4 + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode another block of input data.
|
||||||
|
*
|
||||||
|
* @return true if the state machine is still healthy. false if
|
||||||
|
* bad base-64 data has been detected in the input stream.
|
||||||
|
*/
|
||||||
|
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||||
|
if (this.state == 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int p = offset;
|
||||||
|
len += offset;
|
||||||
|
// Using local variables makes the decoder about 12%
|
||||||
|
// faster than if we manipulate the member variables in
|
||||||
|
// the loop. (Even alphabet makes a measurable
|
||||||
|
// difference, which is somewhat surprising to me since
|
||||||
|
// the member variable is final.)
|
||||||
|
int state = this.state;
|
||||||
|
int value = this.value;
|
||||||
|
int op = 0;
|
||||||
|
final byte[] output = this.output;
|
||||||
|
final int[] alphabet = this.alphabet;
|
||||||
|
while (p < len) {
|
||||||
|
// Try the fast path: we're starting a new tuple and the
|
||||||
|
// next four bytes of the input stream are all data
|
||||||
|
// bytes. This corresponds to going through states
|
||||||
|
// 0-1-2-3-0. We expect to use this method for most of
|
||||||
|
// the data.
|
||||||
|
//
|
||||||
|
// If any of the next four bytes of input are non-data
|
||||||
|
// (whitespace, etc.), value will end up negative. (All
|
||||||
|
// the non-data values in decode are small negative
|
||||||
|
// numbers, so shifting any of them up and or'ing them
|
||||||
|
// together will result in a value with its top bit set.)
|
||||||
|
//
|
||||||
|
// You can remove this whole block and the output should
|
||||||
|
// be the same, just slower.
|
||||||
|
if (state == 0) {
|
||||||
|
while (p + 4 <= len &&
|
||||||
|
(value = ((alphabet[input[p] & 0xff] << 18) |
|
||||||
|
(alphabet[input[p + 1] & 0xff] << 12) |
|
||||||
|
(alphabet[input[p + 2] & 0xff] << 6) |
|
||||||
|
(alphabet[input[p + 3] & 0xff]))) >= 0) {
|
||||||
|
output[op + 2] = (byte) value;
|
||||||
|
output[op + 1] = (byte) (value >> 8);
|
||||||
|
output[op] = (byte) (value >> 16);
|
||||||
|
op += 3;
|
||||||
|
p += 4;
|
||||||
|
}
|
||||||
|
if (p >= len) break;
|
||||||
|
}
|
||||||
|
// The fast path isn't available -- either we've read a
|
||||||
|
// partial tuple, or the next four input bytes aren't all
|
||||||
|
// data, or whatever. Fall back to the slower state
|
||||||
|
// machine implementation.
|
||||||
|
int d = alphabet[input[p++] & 0xff];
|
||||||
|
switch (state) {
|
||||||
|
case 0:
|
||||||
|
if (d >= 0) {
|
||||||
|
value = d;
|
||||||
|
++state;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (d >= 0) {
|
||||||
|
value = (value << 6) | d;
|
||||||
|
++state;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if (d >= 0) {
|
||||||
|
value = (value << 6) | d;
|
||||||
|
++state;
|
||||||
|
} else if (d == EQUALS) {
|
||||||
|
// Emit the last (partial) output tuple;
|
||||||
|
// expect exactly one more padding character.
|
||||||
|
output[op++] = (byte) (value >> 4);
|
||||||
|
state = 4;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if (d >= 0) {
|
||||||
|
// Emit the output triple and return to state 0.
|
||||||
|
value = (value << 6) | d;
|
||||||
|
output[op + 2] = (byte) value;
|
||||||
|
output[op + 1] = (byte) (value >> 8);
|
||||||
|
output[op] = (byte) (value >> 16);
|
||||||
|
op += 3;
|
||||||
|
state = 0;
|
||||||
|
} else if (d == EQUALS) {
|
||||||
|
// Emit the last (partial) output tuple;
|
||||||
|
// expect no further data or padding characters.
|
||||||
|
output[op + 1] = (byte) (value >> 2);
|
||||||
|
output[op] = (byte) (value >> 10);
|
||||||
|
op += 2;
|
||||||
|
state = 5;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
if (d == EQUALS) {
|
||||||
|
++state;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!finish) {
|
||||||
|
// We're out of input, but a future call could provide
|
||||||
|
// more.
|
||||||
|
this.state = state;
|
||||||
|
this.value = value;
|
||||||
|
this.op = op;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Done reading input. Now figure out where we are left in
|
||||||
|
// the state machine and finish up.
|
||||||
|
switch (state) {
|
||||||
|
case 0:
|
||||||
|
// Output length is a multiple of three. Fine.
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
case 4:
|
||||||
|
// Read one padding '=' when we expected 2. Illegal.
|
||||||
|
// Read one extra input byte, which isn't enough to
|
||||||
|
// make another output byte. Illegal.
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
case 2:
|
||||||
|
// Read two extra input bytes, enough to emit 1 more
|
||||||
|
// output byte. Fine.
|
||||||
|
output[op++] = (byte) (value >> 4);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// Read three extra input bytes, enough to emit 2 more
|
||||||
|
// output bytes. Fine.
|
||||||
|
output[op++] = (byte) (value >> 10);
|
||||||
|
output[op++] = (byte) (value >> 2);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// Read all the padding '='s we expected and no more.
|
||||||
|
// Fine.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.state = state;
|
||||||
|
this.op = op;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Encoder extends Coder {
|
||||||
|
/**
|
||||||
|
* Emit a new line every this many output tuples. Corresponds to
|
||||||
|
* a 76-character line length (the maximum allowable according to
|
||||||
|
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
|
||||||
|
*/
|
||||||
|
public static final int LINE_GROUPS = 19;
|
||||||
|
/**
|
||||||
|
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||||
|
* into output bytes.
|
||||||
|
*/
|
||||||
|
private static final byte[] ENCODE = {
|
||||||
|
'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', '+', '/',
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||||
|
* into output bytes.
|
||||||
|
*/
|
||||||
|
private static final byte[] ENCODE_WEBSAFE = {
|
||||||
|
'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', '-', '_',
|
||||||
|
};
|
||||||
|
final public boolean do_padding;
|
||||||
|
final public boolean do_newline;
|
||||||
|
final public boolean do_cr;
|
||||||
|
final private byte[] tail;
|
||||||
|
final private byte[] alphabet;
|
||||||
|
int tailLen;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
public Encoder(int flags, byte[] output) {
|
||||||
|
this.output = output;
|
||||||
|
do_padding = (flags & NO_PADDING) == 0;
|
||||||
|
do_newline = (flags & NO_WRAP) == 0;
|
||||||
|
do_cr = (flags & CRLF) != 0;
|
||||||
|
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
|
||||||
|
tail = new byte[2];
|
||||||
|
tailLen = 0;
|
||||||
|
count = do_newline ? LINE_GROUPS : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an overestimate for the number of bytes {@code
|
||||||
|
* len} bytes could encode to.
|
||||||
|
*/
|
||||||
|
public int maxOutputSize(int len) {
|
||||||
|
return len * 8 / 5 + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||||
|
// Using local variables makes the encoder about 9% faster.
|
||||||
|
final byte[] alphabet = this.alphabet;
|
||||||
|
final byte[] output = this.output;
|
||||||
|
int op = 0;
|
||||||
|
int count = this.count;
|
||||||
|
int p = offset;
|
||||||
|
len += offset;
|
||||||
|
int v = -1;
|
||||||
|
// First we need to concatenate the tail of the previous call
|
||||||
|
// with any input bytes available now and see if we can empty
|
||||||
|
// the tail.
|
||||||
|
switch (tailLen) {
|
||||||
|
case 0:
|
||||||
|
// There was no tail.
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (p + 2 <= len) {
|
||||||
|
// A 1-byte tail with at least 2 bytes of
|
||||||
|
// input available now.
|
||||||
|
v = ((tail[0] & 0xff) << 16) |
|
||||||
|
((input[p++] & 0xff) << 8) |
|
||||||
|
(input[p++] & 0xff);
|
||||||
|
tailLen = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if (p + 1 <= len) {
|
||||||
|
// A 2-byte tail with at least 1 byte of input.
|
||||||
|
v = ((tail[0] & 0xff) << 16) |
|
||||||
|
((tail[1] & 0xff) << 8) |
|
||||||
|
(input[p++] & 0xff);
|
||||||
|
tailLen = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (v != -1) {
|
||||||
|
output[op++] = alphabet[(v >> 18) & 0x3f];
|
||||||
|
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||||
|
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op++] = alphabet[v & 0x3f];
|
||||||
|
if (--count == 0) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
count = LINE_GROUPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point either there is no tail, or there are fewer
|
||||||
|
// than 3 bytes of input available.
|
||||||
|
// The main loop, turning 3 input bytes into 4 output bytes on
|
||||||
|
// each iteration.
|
||||||
|
while (p + 3 <= len) {
|
||||||
|
v = ((input[p] & 0xff) << 16) |
|
||||||
|
((input[p + 1] & 0xff) << 8) |
|
||||||
|
(input[p + 2] & 0xff);
|
||||||
|
output[op] = alphabet[(v >> 18) & 0x3f];
|
||||||
|
output[op + 1] = alphabet[(v >> 12) & 0x3f];
|
||||||
|
output[op + 2] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op + 3] = alphabet[v & 0x3f];
|
||||||
|
p += 3;
|
||||||
|
op += 4;
|
||||||
|
if (--count == 0) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
count = LINE_GROUPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finish) {
|
||||||
|
// Finish up the tail of the input. Note that we need to
|
||||||
|
// consume any bytes in tail before any bytes
|
||||||
|
// remaining in input; there should be at most two bytes
|
||||||
|
// total.
|
||||||
|
if (p - tailLen == len - 1) {
|
||||||
|
int t = 0;
|
||||||
|
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
|
||||||
|
tailLen -= t;
|
||||||
|
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op++] = alphabet[v & 0x3f];
|
||||||
|
if (do_padding) {
|
||||||
|
output[op++] = '=';
|
||||||
|
output[op++] = '=';
|
||||||
|
}
|
||||||
|
if (do_newline) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
}
|
||||||
|
} else if (p - tailLen == len - 2) {
|
||||||
|
int t = 0;
|
||||||
|
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
|
||||||
|
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
|
||||||
|
tailLen -= t;
|
||||||
|
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||||
|
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op++] = alphabet[v & 0x3f];
|
||||||
|
if (do_padding) {
|
||||||
|
output[op++] = '=';
|
||||||
|
}
|
||||||
|
if (do_newline) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
}
|
||||||
|
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
}
|
||||||
|
assert tailLen == 0;
|
||||||
|
assert p == len;
|
||||||
|
} else {
|
||||||
|
// Save the leftovers in tail to be consumed on the next
|
||||||
|
// call to encodeInternal.
|
||||||
|
if (p == len - 1) {
|
||||||
|
tail[tailLen++] = input[p];
|
||||||
|
} else if (p == len - 2) {
|
||||||
|
tail[tailLen++] = input[p];
|
||||||
|
tail[tailLen++] = input[p + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.op = op;
|
||||||
|
this.count = count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An InputStream that does Base64 decoding on the data read through it.
|
||||||
|
*/
|
||||||
|
public class Base64InputStream extends FilterInputStream {
|
||||||
|
|
||||||
|
private static final byte[] EMPTY = new byte[0];
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 2048;
|
||||||
|
|
||||||
|
private final Base64.Coder coder;
|
||||||
|
|
||||||
|
private boolean eof;
|
||||||
|
|
||||||
|
private byte[] inputBuffer;
|
||||||
|
|
||||||
|
private int outputStart;
|
||||||
|
|
||||||
|
private int outputEnd;
|
||||||
|
|
||||||
|
public Base64InputStream(InputStream inputStream) {
|
||||||
|
this(inputStream, Base64.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An InputStream that performs Base64 decoding on the data read
|
||||||
|
* from the wrapped stream.
|
||||||
|
*
|
||||||
|
* @param inputStream the InputStream to read the source data from
|
||||||
|
* @param flags bit flags for controlling the decoder; see the
|
||||||
|
* constants in {@link Base64}
|
||||||
|
*/
|
||||||
|
public Base64InputStream(InputStream inputStream, int flags) {
|
||||||
|
this(inputStream, flags, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs Base64 encoding or decoding on the data read from the
|
||||||
|
* wrapped InputStream.
|
||||||
|
*
|
||||||
|
* @param inputStream the InputStream to read the source data from
|
||||||
|
* @param flags bit flags for controlling the decoder; see the
|
||||||
|
* constants in {@link Base64}
|
||||||
|
* @param encode true to encode, false to decode
|
||||||
|
*/
|
||||||
|
public Base64InputStream(InputStream inputStream, int flags, boolean encode) {
|
||||||
|
super(inputStream);
|
||||||
|
eof = false;
|
||||||
|
inputBuffer = new byte[BUFFER_SIZE];
|
||||||
|
if (encode) {
|
||||||
|
coder = new Base64.Encoder(flags, null);
|
||||||
|
} else {
|
||||||
|
coder = new Base64.Decoder(flags, null);
|
||||||
|
}
|
||||||
|
coder.output = new byte[coder.maxOutputSize(BUFFER_SIZE)];
|
||||||
|
outputStart = 0;
|
||||||
|
outputEnd = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mark(int readlimit) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
in.close();
|
||||||
|
inputBuffer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() {
|
||||||
|
return outputEnd - outputStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
if (outputStart >= outputEnd) {
|
||||||
|
refill();
|
||||||
|
}
|
||||||
|
if (outputStart >= outputEnd) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long bytes = Math.min(n, outputEnd-outputStart);
|
||||||
|
outputStart += bytes;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (outputStart >= outputEnd) {
|
||||||
|
refill();
|
||||||
|
}
|
||||||
|
if (outputStart >= outputEnd) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return coder.output[outputStart++] & 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (outputStart >= outputEnd) {
|
||||||
|
refill();
|
||||||
|
}
|
||||||
|
if (outputStart >= outputEnd) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int bytes = Math.min(len, outputEnd-outputStart);
|
||||||
|
System.arraycopy(coder.output, outputStart, b, off, bytes);
|
||||||
|
outputStart += bytes;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from the input stream into inputBuffer, then
|
||||||
|
* decode/encode it into the empty coder.output, and reset the
|
||||||
|
* outputStart and outputEnd pointers.
|
||||||
|
*/
|
||||||
|
private void refill() throws IOException {
|
||||||
|
if (eof) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int bytesRead = in.read(inputBuffer);
|
||||||
|
boolean success;
|
||||||
|
if (bytesRead == -1) {
|
||||||
|
eof = true;
|
||||||
|
success = coder.process(EMPTY, 0, 0, true);
|
||||||
|
} else {
|
||||||
|
success = coder.process(inputBuffer, 0, bytesRead, false);
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
throw new IOException("bad base64 data");
|
||||||
|
}
|
||||||
|
outputEnd = coder.op;
|
||||||
|
outputStart = 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OutputStream that does Base64 encoding on the data written to
|
||||||
|
* it, writing the resulting data to another OutputStream.
|
||||||
|
*/
|
||||||
|
public class Base64OutputStream extends FilterOutputStream {
|
||||||
|
|
||||||
|
private static final byte[] EMPTY = new byte[0];
|
||||||
|
|
||||||
|
private final Base64.Coder coder;
|
||||||
|
|
||||||
|
private final int flags;
|
||||||
|
|
||||||
|
private byte[] buffer = null;
|
||||||
|
|
||||||
|
private int bpos = 0;
|
||||||
|
|
||||||
|
public Base64OutputStream(OutputStream outputStream) {
|
||||||
|
this(outputStream, Base64.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs Base64 encoding on the data written to the stream,
|
||||||
|
* writing the encoded data to another OutputStream.
|
||||||
|
*
|
||||||
|
* @param outputStream the OutputStream to write the encoded data to
|
||||||
|
* @param flags bit flags for controlling the encoder; see the
|
||||||
|
* constants in {@link Base64}
|
||||||
|
*/
|
||||||
|
public Base64OutputStream(OutputStream outputStream, int flags) {
|
||||||
|
this(outputStream, flags, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Performs Base64 encoding or decoding on the data written to the
|
||||||
|
* stream, writing the encoded/decoded data to another
|
||||||
|
* OutputStream.
|
||||||
|
*
|
||||||
|
* @param outputStream the OutputStream to write the encoded data to
|
||||||
|
* @param flags bit flags for controlling the encoder; see the
|
||||||
|
* constants in {@link Base64}
|
||||||
|
* @param encode true to encode, false to decode
|
||||||
|
*/
|
||||||
|
public Base64OutputStream(OutputStream outputStream, int flags, boolean encode) {
|
||||||
|
super(outputStream);
|
||||||
|
this.flags = flags;
|
||||||
|
if (encode) {
|
||||||
|
coder = new Base64.Encoder(flags, null);
|
||||||
|
} else {
|
||||||
|
coder = new Base64.Decoder(flags, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
// To avoid invoking the encoder/decoder routines for single
|
||||||
|
// bytes, we buffer up calls to write(int) in an internal
|
||||||
|
// byte array to transform them into writes of decently-sized
|
||||||
|
// arrays.
|
||||||
|
if (buffer == null) {
|
||||||
|
buffer = new byte[1024];
|
||||||
|
}
|
||||||
|
if (bpos >= buffer.length) {
|
||||||
|
// internal buffer full; write it out.
|
||||||
|
internalWrite(buffer, 0, bpos, false);
|
||||||
|
bpos = 0;
|
||||||
|
}
|
||||||
|
buffer[bpos++] = (byte) b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush any buffered data from calls to write(int). Needed
|
||||||
|
* before doing a write(byte[], int, int) or a close().
|
||||||
|
*/
|
||||||
|
private void flushBuffer() throws IOException {
|
||||||
|
if (bpos > 0) {
|
||||||
|
internalWrite(buffer, 0, bpos, false);
|
||||||
|
bpos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (len <= 0) return;
|
||||||
|
flushBuffer();
|
||||||
|
internalWrite(b, off, len, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
IOException thrown = null;
|
||||||
|
try {
|
||||||
|
flushBuffer();
|
||||||
|
internalWrite(EMPTY, 0, 0, true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
thrown = e;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if ((flags & Base64.NO_CLOSE) == 0) {
|
||||||
|
out.close();
|
||||||
|
} else {
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (thrown == null) {
|
||||||
|
thrown = e;
|
||||||
|
} else {
|
||||||
|
thrown.addSuppressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (thrown != null) {
|
||||||
|
throw thrown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the given bytes to the encoder/decoder.
|
||||||
|
*
|
||||||
|
* @param finish true if this is the last batch of input, to cause
|
||||||
|
* encoder/decoder state to be finalized.
|
||||||
|
*/
|
||||||
|
private void internalWrite(byte[] b, int off, int len, boolean finish) throws IOException {
|
||||||
|
coder.output = adjust(coder.output, coder.maxOutputSize(len));
|
||||||
|
if (!coder.process(b, off, len, finish)) {
|
||||||
|
throw new IOException("bad base64 data");
|
||||||
|
}
|
||||||
|
out.write(coder.output, 0, coder.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If b.length is at least len, return b.
|
||||||
|
* Otherwise return a new byte array of length len.
|
||||||
|
*/
|
||||||
|
private byte[] adjust(byte[] b, int len) {
|
||||||
|
if (b == null || b.length < len) {
|
||||||
|
return new byte[len];
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
net-mime/src/main/java/org/xbib/net/mime/stream/Hex.java
Normal file
40
net-mime/src/main/java/org/xbib/net/mime/stream/Hex.java
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class Hex {
|
||||||
|
|
||||||
|
private Hex() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toHex(byte[] data) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : data) {
|
||||||
|
sb.append(Character.forDigit((b & 240) >> 4, 16)).append(Character.forDigit((b & 15), 16));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromHex(String hex) {
|
||||||
|
Objects.requireNonNull(hex);
|
||||||
|
int len = hex.length();
|
||||||
|
byte[] data = new byte[len / 2];
|
||||||
|
for (int i = 0; i < len; i += 2) {
|
||||||
|
data[i / 2] = (byte) fromHex(hex.charAt(i), hex.charAt(i + 1));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int fromHex(int b1, int b2) {
|
||||||
|
int i1 = Character.digit(b1, 16);
|
||||||
|
if (i1 == -1) {
|
||||||
|
throw new IllegalArgumentException("invalid character in hexadecimal: " + b1);
|
||||||
|
}
|
||||||
|
int i2 = Character.digit(b2, 16);
|
||||||
|
if (i2 == -1) {
|
||||||
|
throw new IllegalArgumentException("invalid character in hexadecimal: " + b2);
|
||||||
|
}
|
||||||
|
return (i1 << 4) + i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import org.xbib.net.mime.MimeException;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class MimeStream {
|
||||||
|
|
||||||
|
private MimeStream() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InputStream decode(InputStream inputStream, String encoding) throws MimeException {
|
||||||
|
if (encoding.equalsIgnoreCase("base64"))
|
||||||
|
return new BASE64DecoderStream(inputStream);
|
||||||
|
else if (encoding.equalsIgnoreCase("quoted-printable"))
|
||||||
|
return new QuotedPrintableInputStream(inputStream);
|
||||||
|
else if (encoding.equalsIgnoreCase("binary") ||
|
||||||
|
encoding.equalsIgnoreCase("7bit") ||
|
||||||
|
encoding.equalsIgnoreCase("8bit"))
|
||||||
|
return inputStream;
|
||||||
|
else {
|
||||||
|
throw new MimeException("Unknown encoding: " + encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PushbackInputStream;
|
||||||
|
|
||||||
|
public final class QuotedPrintableInputStream extends FilterInputStream {
|
||||||
|
|
||||||
|
private int spaces = 0;
|
||||||
|
|
||||||
|
public QuotedPrintableInputStream(InputStream in) {
|
||||||
|
super(new PushbackInputStream(in, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the next decoded byte from this input stream. The byte
|
||||||
|
* is returned as an <code>int</code> in the range <code>0</code>
|
||||||
|
* to <code>255</code>. If no byte is available because the end of
|
||||||
|
* the stream has been reached, the value <code>-1</code> is returned.
|
||||||
|
* This method blocks until input data is available, the end of the
|
||||||
|
* stream is detected, or an exception is thrown.
|
||||||
|
*
|
||||||
|
* @return the next byte of data, or <code>-1</code> if the end of the
|
||||||
|
* stream is reached.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (spaces > 0) {
|
||||||
|
spaces--;
|
||||||
|
return ' ';
|
||||||
|
}
|
||||||
|
int c = in.read();
|
||||||
|
if (c == ' ') {
|
||||||
|
while ((c = in.read()) == ' ') {
|
||||||
|
spaces++;
|
||||||
|
}
|
||||||
|
if (c == '\r' || c == '\n' || c == -1) {
|
||||||
|
spaces = 0;
|
||||||
|
} else {
|
||||||
|
((PushbackInputStream) in).unread(c);
|
||||||
|
c = ' ';
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
} else if (c == '=') {
|
||||||
|
int a = in.read();
|
||||||
|
if (a == '\n') {
|
||||||
|
return read();
|
||||||
|
} else if (a == '\r') {
|
||||||
|
int b = in.read();
|
||||||
|
if (b != '\n') {
|
||||||
|
((PushbackInputStream) in).unread(b);
|
||||||
|
}
|
||||||
|
return read();
|
||||||
|
} else if (a == -1) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return Hex.fromHex(a, in.read());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buf, int off, int len) throws IOException {
|
||||||
|
int i, c;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
if ((c = read()) == -1) {
|
||||||
|
if (i == 0) {
|
||||||
|
i = -1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf[off + i] = (byte) c;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
long skipped = 0;
|
||||||
|
while (n-- > 0 && read() >= 0) {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return in.available();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package org.xbib.net.mime.stream;
|
||||||
|
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class QuotedPrintableOutputStream extends FilterOutputStream {
|
||||||
|
|
||||||
|
// The encoding table
|
||||||
|
private final static char[] hex = {
|
||||||
|
'0','1', '2', '3', '4', '5', '6', '7',
|
||||||
|
'8','9', 'A', 'B', 'C', 'D', 'E', 'F'
|
||||||
|
};
|
||||||
|
|
||||||
|
private int count = 0;
|
||||||
|
|
||||||
|
private final int bytesPerLine;
|
||||||
|
|
||||||
|
private boolean gotSpace = false;
|
||||||
|
|
||||||
|
private boolean gotCR = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a QP encoder that encodes the specified input stream
|
||||||
|
* @param out the output stream
|
||||||
|
* @param bytesPerLine the number of bytes per line. The encoder
|
||||||
|
* inserts a CRLF sequence after this many number
|
||||||
|
* of bytes.
|
||||||
|
*/
|
||||||
|
public QuotedPrintableOutputStream(OutputStream out, int bytesPerLine) {
|
||||||
|
super(out);
|
||||||
|
this.bytesPerLine = bytesPerLine - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a QP encoder that encodes the specified input stream.
|
||||||
|
* Inserts the CRLF sequence after outputting 76 bytes.
|
||||||
|
* @param out the output stream
|
||||||
|
*/
|
||||||
|
public QuotedPrintableOutputStream(OutputStream out) {
|
||||||
|
this(out, 76);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes <code>len</code> bytes from the specified
|
||||||
|
* <code>byte</code> array starting at offset <code>off</code> to
|
||||||
|
* this output stream.
|
||||||
|
*
|
||||||
|
* @param b the data.
|
||||||
|
* @param off the start offset in the data.
|
||||||
|
* @param len the number of bytes to write.
|
||||||
|
* @exception IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
write(b[off + i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes <code>b.length</code> bytes to this output stream.
|
||||||
|
* @param b the data to be written.
|
||||||
|
* @exception IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
public void write(byte[] b) throws IOException {
|
||||||
|
write(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified <code>byte</code> to this output stream.
|
||||||
|
* @param c the <code>byte</code>.
|
||||||
|
* @exception IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
public void write(int c) throws IOException {
|
||||||
|
c = c & 0xff;
|
||||||
|
if (gotSpace) {
|
||||||
|
output(' ', c == '\r' || c == '\n');
|
||||||
|
gotSpace = false;
|
||||||
|
}
|
||||||
|
if (c == '\r') {
|
||||||
|
gotCR = true;
|
||||||
|
outputCRLF();
|
||||||
|
} else {
|
||||||
|
if (c == '\n') {
|
||||||
|
if (!gotCR) {
|
||||||
|
outputCRLF();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (c == ' ') {
|
||||||
|
gotSpace = true;
|
||||||
|
} else {
|
||||||
|
output(c, c < 32 || c >= 127 || c == '=');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gotCR = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes this output stream and forces any buffered output bytes
|
||||||
|
* to be encoded out to the stream.
|
||||||
|
* @exception IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
public void flush() throws IOException {
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces any buffered output bytes to be encoded out to the stream
|
||||||
|
* and closes this output stream
|
||||||
|
*/
|
||||||
|
public void close() throws IOException {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outputCRLF() throws IOException {
|
||||||
|
out.write('\r');
|
||||||
|
out.write('\n');
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void output(int c, boolean encode) throws IOException {
|
||||||
|
if (encode) {
|
||||||
|
if ((count += 3) > bytesPerLine) {
|
||||||
|
out.write('=');
|
||||||
|
out.write('\r');
|
||||||
|
out.write('\n');
|
||||||
|
count = 3; // set the next line's length
|
||||||
|
}
|
||||||
|
out.write('=');
|
||||||
|
out.write(hex[c >> 4]);
|
||||||
|
out.write(hex[c & 0xf]);
|
||||||
|
} else {
|
||||||
|
if (++count > bytesPerLine) {
|
||||||
|
out.write('=');
|
||||||
|
out.write('\r');
|
||||||
|
out.write('\n');
|
||||||
|
count = 1; // set the next line's length
|
||||||
|
}
|
||||||
|
out.write(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1853
net-mime/src/main/resources/META-INF/mime.types
Executable file
1853
net-mime/src/main/resources/META-INF/mime.types
Executable file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,26 @@
|
||||||
|
package org.xbib.net.mime.test;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.xbib.net.mime.MimeException;
|
||||||
|
import org.xbib.net.mime.MimeMultipartParser;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class MimeMultipartTest {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(MimeMultipartTest.class.getName());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multiPartTest() throws MimeException, IOException {
|
||||||
|
InputStream inputStream = getClass().getResourceAsStream("/org/xbib/net/mime/test/msg.txt");
|
||||||
|
Objects.requireNonNull(inputStream);
|
||||||
|
MimeMultipartParser parser = new MimeMultipartParser("multipart/mixed; boundary=\"----=_Part_4_910054940.1065629194743\"; charset=\"ISO-8859-1\"");
|
||||||
|
parser.parse(ByteBuffer.wrap(inputStream.readAllBytes()),
|
||||||
|
e -> logger.log(Level.INFO, e.getHeaders().toString() + " length = " + e.getLength() + " content = " + e.getCharset().decode(e.getBody())));
|
||||||
|
}
|
||||||
|
}
|
254
net-mime/src/test/java/org/xbib/net/mime/test/ParsingTest.java
Normal file
254
net-mime/src/test/java/org/xbib/net/mime/test/ParsingTest.java
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
package org.xbib.net.mime.test;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.xbib.net.mime.MimeException;
|
||||||
|
import org.xbib.net.mime.MimeMessage;
|
||||||
|
import org.xbib.net.mime.MimePart;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
public class ParsingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParser() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("msg.txt");
|
||||||
|
if (in == null) {
|
||||||
|
fail("no msg.txt");
|
||||||
|
}
|
||||||
|
String boundary = "----=_Part_4_910054940.1065629194743";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("139912840220.1065629194743.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(0).getContentId());
|
||||||
|
assertEquals("1351327060508.1065629194423.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(1).getContentId());
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
InputStream part0 = parts.get(0).read();
|
||||||
|
int len = part0.read(buf, 0, buf.length);
|
||||||
|
String str = new String(buf, 0, len);
|
||||||
|
assertTrue(str.startsWith("<soapenv:Envelope"));
|
||||||
|
assertTrue(str.endsWith("</soapenv:Envelope>"));
|
||||||
|
part0.close();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
InputStream part1 = parts.get(1).read();
|
||||||
|
assertEquals((byte) part1.read(), (byte) 0xff);
|
||||||
|
assertEquals((byte) part1.read(), (byte) 0xd8);
|
||||||
|
part1.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMsg2() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("msg2.txt");
|
||||||
|
String boundary = "----=_Part_1_807283631.1066069460327";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("1071294019496.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(0).getContentId());
|
||||||
|
assertEquals("871169419176.1066069460266.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(1).getContentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMessage1() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("message1.txt");
|
||||||
|
String boundary = "----=_Part_7_10584188.1123489648993";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("soapPart", parts.get(0).getContentId());
|
||||||
|
assertEquals("attachmentPart", parts.get(1).getContentId());
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[18];
|
||||||
|
InputStream part0 = parts.get(0).read();
|
||||||
|
int len = part0.read(buf, 0, buf.length);
|
||||||
|
String str = new String(buf, 0, len);
|
||||||
|
assertTrue(str.startsWith("<SOAP-ENV:Envelope"));
|
||||||
|
assertEquals(' ', (byte) part0.read());
|
||||||
|
buf = new byte[8192];
|
||||||
|
len = part0.read(buf, 0, buf.length);
|
||||||
|
str = new String(buf, 0, len);
|
||||||
|
assertTrue(str.endsWith("</SOAP-ENV:Envelope>"));
|
||||||
|
part0.close();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
InputStream part1 = parts.get(1).read();
|
||||||
|
int len = part1.read(buf, 0, buf.length);
|
||||||
|
String str = new String(buf, 0, len);
|
||||||
|
assertTrue(str.startsWith("<?xml version"));
|
||||||
|
assertTrue(str.endsWith("</Envelope>\n"));
|
||||||
|
part1.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadEOF() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("message1.txt");
|
||||||
|
String boundary = "----=_Part_7_10584188.1123489648993";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
for(MimePart part : parts) {
|
||||||
|
testInputStream(part.read());
|
||||||
|
testInputStream(part.readOnce());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("empty-statement")
|
||||||
|
private void testInputStream(InputStream is) throws IOException {
|
||||||
|
while (is.read() != -1);
|
||||||
|
assertEquals(-1, is.read());
|
||||||
|
is.close();
|
||||||
|
try {
|
||||||
|
int i = is.read();
|
||||||
|
fail("read() after close() should throw IOException");
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// expected exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyPart() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("/org/xbib/net/mime/test/emptypart.txt");
|
||||||
|
String boundary = "----=_Part_7_10584188.1123489648993";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("soapPart", parts.get(0).getContentId());
|
||||||
|
assertEquals("attachmentPart", parts.get(1).getContentId());
|
||||||
|
{
|
||||||
|
try (InputStream is = parts.get(0).read()) {
|
||||||
|
while (is.read() != -1) {
|
||||||
|
fail("There should be any bytes since this is empty part");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
InputStream part1 = parts.get(1).read();
|
||||||
|
int len = part1.read(buf, 0, buf.length);
|
||||||
|
String str = new String(buf, 0, len);
|
||||||
|
assertTrue(str.startsWith("<?xml version"));
|
||||||
|
assertTrue(str.endsWith("</Envelope>\n"));
|
||||||
|
part1.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoHeaders() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("noheaders.txt");
|
||||||
|
String boundary = "----=_Part_7_10584188.1123489648993";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("0", parts.get(0).getContentId());
|
||||||
|
assertEquals("1", parts.get(1).getContentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOneByte() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("onebyte.txt");
|
||||||
|
String boundary = "boundary";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("0", parts.get(0).getContentId());
|
||||||
|
assertEquals("1", parts.get(1).getContentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBoundaryWhiteSpace() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("boundary-lwsp.txt");
|
||||||
|
String boundary = "boundary";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("part1", parts.get(0).getContentId());
|
||||||
|
assertEquals("part2", parts.get(1).getContentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBoundaryInBody() throws Exception {
|
||||||
|
InputStream in = getClass().getResourceAsStream("boundary-in-body.txt");
|
||||||
|
String boundary = "boundary";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
List<MimePart> parts = mm.getAttachments();
|
||||||
|
assertEquals(2, parts.size());
|
||||||
|
assertEquals("part1", parts.get(0).getContentId());
|
||||||
|
assertEquals("part2", parts.get(1).getContentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoClosingBoundary() throws Exception {
|
||||||
|
boolean gotException = false;
|
||||||
|
try {
|
||||||
|
String fileName = "msg-no-closing-boundary.txt";
|
||||||
|
InputStream in = getClass().getResourceAsStream(fileName);
|
||||||
|
assertNotNull(in,"Failed to load test data from " + fileName);
|
||||||
|
String boundary = "----=_Part_4_910054940.1065629194743";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
} catch (MimeException | NoSuchElementException e) {
|
||||||
|
gotException = true;
|
||||||
|
String msg = e.getMessage();
|
||||||
|
assertNotNull(msg);
|
||||||
|
assertTrue(msg.contains("no closing MIME boundary"));
|
||||||
|
}
|
||||||
|
assertTrue(gotException);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidClosingBoundary() throws Exception {
|
||||||
|
boolean gotException = false;
|
||||||
|
try {
|
||||||
|
String fileName = "msg-invalid-closing-boundary.txt";
|
||||||
|
InputStream in = getClass().getResourceAsStream(fileName);
|
||||||
|
assertNotNull(in, "Failed to load test data from " + fileName);
|
||||||
|
String boundary = "----=_Part_4_910054940.1065629194743";
|
||||||
|
MimeMessage mm = new MimeMessage(in, boundary);
|
||||||
|
mm.parseAll();
|
||||||
|
} catch (MimeException | NoSuchElementException e) {
|
||||||
|
gotException = true;
|
||||||
|
String msg = e.getMessage();
|
||||||
|
assertNotNull(msg);
|
||||||
|
assertTrue(msg.contains("no closing MIME boundary"));
|
||||||
|
}
|
||||||
|
assertTrue(gotException);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidMimeMessage() throws Exception {
|
||||||
|
String invalidMessage = "--boundary\nContent-Id: part1\n\n1";
|
||||||
|
String boundary = "boundary";
|
||||||
|
MimeMessage message = new MimeMessage(new ByteArrayInputStream(invalidMessage.getBytes()), boundary);
|
||||||
|
try {
|
||||||
|
message.getAttachments();
|
||||||
|
fail("Given message is un-parseable. An exception should have been raised");
|
||||||
|
} catch (MimeException | NoSuchElementException e) {
|
||||||
|
MimePart part = message.getPart(0);
|
||||||
|
assertFalse(part.isClosed(), "Part should not be closed at this point");
|
||||||
|
message.close();
|
||||||
|
assertTrue(part.isClosed(), "Part should be closed by now");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.xbib.net.mime.test;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.xbib.net.mime.stream.QuotedPrintableInputStream;
|
||||||
|
import org.xbib.net.mime.stream.QuotedPrintableOutputStream;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public class QuotedPrintableTest {
|
||||||
|
@Test
|
||||||
|
public void testUTF8RoundTrip() throws Exception {
|
||||||
|
char[] RUSSIAN_STUFF_UNICODE = {
|
||||||
|
0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438, 0x432, 0x435, 0x442
|
||||||
|
};
|
||||||
|
String stringInput = new String(RUSSIAN_STUFF_UNICODE);
|
||||||
|
byte[] byteInput = stringInput.getBytes(StandardCharsets.UTF_8);
|
||||||
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
QuotedPrintableOutputStream outputStream = new QuotedPrintableOutputStream(byteArrayOutputStream);
|
||||||
|
outputStream.write(byteInput);
|
||||||
|
String encoded = byteArrayOutputStream.toString(StandardCharsets.UTF_8);
|
||||||
|
assertEquals("=D0=92=D1=81=D0=B5=D0=BC_=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D1=82", encoded);
|
||||||
|
QuotedPrintableInputStream inputStream = new QuotedPrintableInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
|
||||||
|
byte[] decodedInput = inputStream.readAllBytes();
|
||||||
|
assertArrayEquals(byteInput, decodedInput);
|
||||||
|
|
||||||
|
char[] SWISS_GERMAN_STUFF_UNICODE = {
|
||||||
|
0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4
|
||||||
|
};
|
||||||
|
stringInput = new String(SWISS_GERMAN_STUFF_UNICODE);
|
||||||
|
byteInput = stringInput.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
outputStream = new QuotedPrintableOutputStream(byteArrayOutputStream);
|
||||||
|
outputStream.write(byteInput);
|
||||||
|
encoded = byteArrayOutputStream.toString(StandardCharsets.UTF_8);
|
||||||
|
assertEquals("Gr=C3=BCezi_z=C3=A4m=C3=A4", encoded);
|
||||||
|
inputStream = new QuotedPrintableInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
|
||||||
|
decodedInput = inputStream.readAllBytes();
|
||||||
|
assertArrayEquals(byteInput, decodedInput);
|
||||||
|
}
|
||||||
|
}
|
5
net-mime/src/test/resources/logging.properties
Normal file
5
net-mime/src/test/resources/logging.properties
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
handlers=java.util.logging.ConsoleHandler
|
||||||
|
.level=ALL
|
||||||
|
java.util.logging.ConsoleHandler.level=ALL
|
||||||
|
java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter
|
||||||
|
jdk.event.security.level=INFO
|
|
@ -0,0 +1,10 @@
|
||||||
|
--boundary
|
||||||
|
Content-Id: part1
|
||||||
|
|
||||||
|
1 --boundary in body
|
||||||
|
--boundary
|
||||||
|
Content-Id: part2
|
||||||
|
|
||||||
|
2 --boundary in body
|
||||||
|
--boundary starts on a new line
|
||||||
|
--boundary--
|
|
@ -0,0 +1,9 @@
|
||||||
|
--boundary
|
||||||
|
Content-Id: part1
|
||||||
|
|
||||||
|
1
|
||||||
|
--boundary
|
||||||
|
Content-Id: part2
|
||||||
|
|
||||||
|
2
|
||||||
|
--boundary--
|
|
@ -0,0 +1,13 @@
|
||||||
|
------=_Part_7_10584188.1123489648993
|
||||||
|
Content-Type: text/xml; charset=utf-8
|
||||||
|
Content-Id: soapPart
|
||||||
|
|
||||||
|
------=_Part_7_10584188.1123489648993
|
||||||
|
Content-Type: text/xml
|
||||||
|
Content-ID: attachmentPart
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Body/></Envelope>
|
||||||
|
|
||||||
|
------=_Part_7_10584188.1123489648993--
|
|
@ -0,0 +1,14 @@
|
||||||
|
------=_Part_7_10584188.1123489648993
|
||||||
|
Content-Type: text/xml; charset=utf-8
|
||||||
|
Content-Id: soapPart
|
||||||
|
|
||||||
|
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ztrade:GetLastTradePrice xmlns:ztrade="http://wombat.ztrade.com"><ztrade:symbol>SUNW</ztrade:symbol></ztrade:GetLastTradePrice></SOAP-ENV:Body></SOAP-ENV:Envelope>
|
||||||
|
------=_Part_7_10584188.1123489648993
|
||||||
|
Content-Type: text/xml
|
||||||
|
Content-ID: attachmentPart
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Body/></Envelope>
|
||||||
|
|
||||||
|
------=_Part_7_10584188.1123489648993--
|
Binary file not shown.
Binary file not shown.
BIN
net-mime/src/test/resources/org/xbib/net/mime/test/msg.txt
Normal file
BIN
net-mime/src/test/resources/org/xbib/net/mime/test/msg.txt
Normal file
Binary file not shown.
53
net-mime/src/test/resources/org/xbib/net/mime/test/msg2.txt
Normal file
53
net-mime/src/test/resources/org/xbib/net/mime/test/msg2.txt
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
------=_Part_1_807283631.1066069460327
|
||||||
|
Content-Type: text/xml; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: binary
|
||||||
|
Content-Id: <1071294019496.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k>
|
||||||
|
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
|
||||||
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<soapenv:Body>
|
||||||
|
<ns-482282508:ProductCatalog
|
||||||
|
xmlns:ns-482282508="http://www.ws-i.org/SampleApplications/SupplyChainManagement/2003-07/Catalog.xsd">
|
||||||
|
<ns-482282508:Product>
|
||||||
|
<ns-482282508:Name>TV, Brand1</ns-482282508:Name>
|
||||||
|
<ns-482282508:Description>24in, Color, Advanced Velocity Scan Modulation, stereo</ns-482282508:Description>
|
||||||
|
<ns-482282508:ProductNumber>605001</ns-482282508:ProductNumber>
|
||||||
|
<ns-482282508:Category>TV</ns-482282508:Category>
|
||||||
|
<ns-482282508:Brand>Brand1</ns-482282508:Brand>
|
||||||
|
<ns-482282508:Price>299.95</ns-482282508:Price>
|
||||||
|
<ns-482282508:Thumbnail href="cid:971223732136.1066069460266.IBM.WEBSERVICES@ibm-7pr28r4m35k"/>
|
||||||
|
</ns-482282508:Product>
|
||||||
|
<ns-482282508:Product>
|
||||||
|
<ns-482282508:Name>TV, Brand2</ns-482282508:Name>
|
||||||
|
<ns-482282508:Description>32in, Super Slim Flat Panel Plasma</ns-482282508:Description>
|
||||||
|
<ns-482282508:ProductNumber>605002</ns-482282508:ProductNumber>
|
||||||
|
<ns-482282508:Category>TV</ns-482282508:Category>
|
||||||
|
<ns-482282508:Brand>Brand2</ns-482282508:Brand>
|
||||||
|
<ns-482282508:Price>1499.99</ns-482282508:Price>
|
||||||
|
<ns-482282508:Thumbnail href="cid:981213279144.1066069460317.IBM.WEBSERVICES@ibm-7pr28r4m35k"/>
|
||||||
|
</ns-482282508:Product>
|
||||||
|
<ns-482282508:Product>
|
||||||
|
<ns-482282508:Name>TV, Brand3</ns-482282508:Name>
|
||||||
|
<ns-482282508:Description>50in, Plasma Display</ns-482282508:Description>
|
||||||
|
<ns-482282508:ProductNumber>605003</ns-482282508:ProductNumber>
|
||||||
|
<ns-482282508:Category>TV</ns-482282508:Category>
|
||||||
|
<ns-482282508:Brand>Brand3</ns-482282508:Brand>
|
||||||
|
<ns-482282508:Price>5725.98</ns-482282508:Price>
|
||||||
|
<ns-482282508:Thumbnail href="cid:991207987112.1066069460317.IBM.WEBSERVICES@ibm-7pr28r4m35k"/>
|
||||||
|
</ns-482282508:Product>
|
||||||
|
<ns-482282508:Product>
|
||||||
|
<ns-482282508:Name>Video, Brand1</ns-482282508:Name>
|
||||||
|
<ns-482282508:Description>S-VHS</ns-482282508:Description>
|
||||||
|
<ns-482282508:ProductNumber>605004</ns-482282508:ProductNumber>
|
||||||
|
<ns-482282508:Category>Video</ns-482282508:Category>
|
||||||
|
<ns-482282508:Brand>Brand1</ns-482282508:Brand><ns-482282508:Price>199.95</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1001274260392.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product><ns-482282508:Product><ns-482282508:Name>Video, Brand2</ns-482282508:Name><ns-482282508:Description>HiFi, S-VHS</ns-482282508:Description><ns-482282508:ProductNumber>605005</ns-482282508:ProductNumber><ns-482282508:Category>Video</ns-482282508:Category><ns-482282508:Brand>Brand2</ns-482282508:Brand><ns-482282508:Price>400.00</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1011270475688.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product><ns-482282508:Product><ns-482282508:Name>Video, Brand3</ns-482282508:Name><ns-482282508:Description>s-vhs, mindv</ns-482282508:Description><ns-482282508:ProductNumber>605006</ns-482282508:ProductNumber><ns-482282508:Category>Video</ns-482282508:Category><ns-482282508:Brand>Brand3</ns-482282508:Brand><ns-482282508:Price>949.99</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1021265331112.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product><ns-482282508:Product><ns-482282508:Name>DVD, Brand1</ns-482282508:Name><ns-482282508:Description>DVD-Player W/Built-In Dolby Digital Decoder</ns-482282508:Description><ns-482282508:ProductNumber>605007</ns-482282508:ProductNumber><ns-482282508:Category>DVD</ns-482282508:Category><ns-482282508:Brand>Brand1</ns-482282508:Brand><ns-482282508:Price>100.00</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1031255254952.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product><ns-482282508:Product><ns-482282508:Name>DVD, Brand2</ns-482282508:Name><ns-482282508:Description>Plays DVD-Video discs, CDs, stereo and multi-channel SACDs, and audio CD-Rs & CD-RWs, 27MHz/10-bit video DAC, </ns-482282508:Description><ns-482282508:ProductNumber>605008</ns-482282508:ProductNumber><ns-482282508:Category>DVD</ns-482282508:Category><ns-482282508:Brand>Brand2</ns-482282508:Brand><ns-482282508:Price>200.00</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1041252076456.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product><ns-482282508:Product><ns-482282508:Name>DVD, Brand3</ns-482282508:Name><ns-482282508:Description>DVD Player with SmoothSlow forward/reverse; Digital Video Enhancer; DVD/CD Text; Custom Parental Control (20-disc); Digital Cinema Sound modes</ns-482282508:Description><ns-482282508:ProductNumber>605009</ns-482282508:ProductNumber><ns-482282508:Category>DVD</ns-482282508:Category><ns-482282508:Brand>Brand3</ns-482282508:Brand><ns-482282508:Price>250.00</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1051248816040.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product><ns-482282508:Product><ns-482282508:Name>TV, Brand4</ns-482282508:Name><ns-482282508:Description>Designated invalid product code that is allowed to appear in the catalog, but is unable to be ordered</ns-482282508:Description><ns-482282508:ProductNumber>605010</ns-482282508:ProductNumber><ns-482282508:Category>TV</ns-482282508:Category><ns-482282508:Brand>Brand4</ns-482282508:Brand><ns-482282508:Price>149.99</ns-482282508:Price><ns-482282508:Thumbnail href="cid:1061306454952.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k"/></ns-482282508:Product></ns-482282508:ProductCatalog></soapenv:Body></soapenv:Envelope>
|
||||||
|
------=_Part_1_807283631.1066069460327
|
||||||
|
Content-Type: image/jpeg
|
||||||
|
Content-Transfer-Encoding: binary
|
||||||
|
Content-Id: <871169419176.1066069460266.IBM.WEBSERVICES@ibm-7pr28r4m35k>
|
||||||
|
|
||||||
|
ÿØÿà JFIF ÿÛ C
|
||||||
|
|
||||||
|
|
||||||
|
------=_Part_1_807283631.1066069460327--
|
|
@ -0,0 +1,10 @@
|
||||||
|
------=_Part_7_10584188.1123489648993
|
||||||
|
|
||||||
|
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ztrade:GetLastTradePrice xmlns:ztrade="http://wombat.ztrade.com"><ztrade:symbol>SUNW</ztrade:symbol></ztrade:GetLastTradePrice></SOAP-ENV:Body></SOAP-ENV:Envelope>
|
||||||
|
------=_Part_7_10584188.1123489648993
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Body/></Envelope>
|
||||||
|
|
||||||
|
------=_Part_7_10584188.1123489648993--
|
|
@ -0,0 +1,7 @@
|
||||||
|
--boundary
|
||||||
|
|
||||||
|
1
|
||||||
|
--boundary
|
||||||
|
|
||||||
|
2
|
||||||
|
--boundary--
|
770
net-mime/src/test/resources/org/xbib/net/mime/test/quoted.txt
Normal file
770
net-mime/src/test/resources/org/xbib/net/mime/test/quoted.txt
Normal file
|
@ -0,0 +1,770 @@
|
||||||
|
------=_Part_16_799571960.1350659465464
|
||||||
|
Content-Type: text/xml; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
Content-ID: <rootpart@soapui.org>
|
||||||
|
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ip6="http://ip6.sgpf.PruebaOracleSR">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<ip6:ecoXML>
|
||||||
|
<!--Optional:-->
|
||||||
|
<XMLEntrada>cid:531309647604</XMLEntrada>
|
||||||
|
</ip6:ecoXML>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
------=_Part_16_799571960.1350659465464
|
||||||
|
Content-Type: text/xml; charset=Cp1252; name=UnXMLCopyPaste.xml
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-ID: <531309647604>
|
||||||
|
Content-Disposition: attachment; name="UnXMLCopyPaste.xml"; filename="UnXMLCopyPaste.xml"
|
||||||
|
|
||||||
|
<?xml version=3D"1.0" encoding=3D"UTF-8"?>
|
||||||
|
<GrupoInformes version=3D"2.0" xmlns=3D"http://sgpf.igae.meh.es/fml2" color=
|
||||||
|
fondo=3D"#B0FFB0">
|
||||||
|
<RecursoImagen id=3D"IDRecursoImagen1" tipo=3D"JPEG">
|
||||||
|
/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAASwAA/+4ADkFkb2JlAGTAAAAAAf/=
|
||||||
|
b
|
||||||
|
AIQAAwICAgICAwICAwUDAwMFBQQDAwQFBgUFBQUFBggGBwcHBwYICAkKCgoJCAwMDAwMDA4ODg4=
|
||||||
|
O
|
||||||
|
EBAQEBAQEBAQEAEDBAQGBgYMCAgMEg4MDhIUEBAQEBQREBAQEBARERAQEBAQEBEQEBAQEBAQEBA=
|
||||||
|
Q
|
||||||
|
EBAQEBAQEBAQEBAQEBAQEBAQ/8AAEQgAggElAwERAAIRAQMRAf/EAIQAAAEEAwEBAAAAAAAAAAA=
|
||||||
|
A
|
||||||
|
AAAFBgcIAgMEAQkBAQAAAAAAAAAAAAAAAAAAAAAQAAEDAwMCAwUGAwYFAQkAAAECAwQAEQUhEgY=
|
||||||
|
x
|
||||||
|
B0FRE2FxIhQIgZGhMkIjUmIVscFygjMW8NGiskOSg5MkNFQlGAkZEQEAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
A
|
||||||
|
/9oADAMBAAIRAxEAPwD6p0BQFAUBQeEgdaDwrAoMS6mg89YUHnrUB61AeuPOgPV9tAeqP4qADn8=
|
||||||
|
1
|
||||||
|
BmF+dB4XLUB6ooD1R4UB6tAesjxNB76qPOgxU+hNBqVNbT40GIyDP8QoNqJba+hoNnqo86DBUlC=
|
||||||
|
e
|
||||||
|
poNfzzV7bhQZolNq6G9BtStKuhoMqAoCgKAoCgKAoCgxKgKDWt4DqbUHI/kWWepFAmvchYZTZaw=
|
||||||
|
S
|
||||||
|
OqjYX+yg4neWRB/5B99BoPMIw/X+NBrc5pGSCSsH7aDnXzuOkXCqDSe4Ef8Aj/soAc+ZV0XQZf7=
|
||||||
|
4
|
||||||
|
a6+p+NBiOfR0myl/eaDejuBDA/1AD76DVJ7jw2xf1R99BwnufD3W9T8aDc13IhudHR94oNyu4UN=
|
||||||
|
K
|
||||||
|
bl0ffQcTvc+AhVi8PvoOmH3FhydG3NxsSbeQFyfcB1oGly76mO1/DuKt805HyBmFhn3Vx4c5e4J=
|
||||||
|
l
|
||||||
|
ONi6hHFrugfxJuPbQR/i/rn+nbkZ243nuPaXe3pTFriKv/7dKB+NA+sb3dxGZaTJw2RYnsrG5Ls=
|
||||||
|
V
|
||||||
|
5DySD43bJoF7H9xmFkBTgv5XoN2T718LwiQjL5uNGcPRpTyS4f8AIm6vwoGplPqd7YRTeRyFmOk=
|
||||||
|
6
|
||||||
|
JdfQ802f860BP40HRjO8/H84n1sJlI2Qb67or7bw/wCgmgcOO7hsukXcH30DrxvL4z9h6g19tA4=
|
||||||
|
4
|
||||||
|
uVYfAsoUHal1tXQ0GdAUBQFAUBQa3HQgf30CZNyjUdJKlC4oGvkuXtN7rL6UDMzHOy3uAXb3UDO=
|
||||||
|
y
|
||||||
|
HO1qUbOW+2gRpHOHNSXfxoEyR3Bcb0Dp++g5D3CfWbBw/fQYO83kFN95++g5Fc5fv/qH76D1HOn=
|
||||||
|
g
|
||||||
|
dV/jQdKeeqKdXPxoNTvOFk3Dn40GlPOHnVem07vWeiUm5P2DWgwkcjyZdLCwpt3bvLTlm17P4rL=
|
||||||
|
I
|
||||||
|
NvbQI45kp3epiQlwIUUrUhYWAodRdJOtBxyO5DsI2LpJ8r0CTK7zSRdtCju8vGgRZfdbJOD1fUK=
|
||||||
|
R
|
||||||
|
fW5oHFgu4OTynFM9DZcUXMk0ziQsGxQ3NdCHSPbs0+2gqd9afOhn+5OLw8q4xvF8NDjsREnagOS=
|
||||||
|
R
|
||||||
|
6xSgdEgIUhOngmgq7MlY+S4XU45htKv0pW4CB7bGgd3ajtr3I59kU5DhipPG8Ew8Gp3KTKXGgxb=
|
||||||
|
f
|
||||||
|
EolxS296gP0JUTQXf4XyKPj+Nq43xzuM9nXmClDmWz84y1qcAN9rbSFtto8kq3kfxE0DR5Zxbnn=
|
||||||
|
I
|
||||||
|
pxSnneHm+obqYbygii/S5C22kmgaGS7P8+ZeUycjjXVLGoGSbdQR/iQVCgSW+13dzjEn57A5ONG=
|
||||||
|
k
|
||||||
|
t/GlcTJBJP2qCU/jQPPh/wBXPdbto83A7r453IY9K/TORSiz6QOpDiPgct770FquNfUBiM1h4nI=
|
||||||
|
M
|
||||||
|
FPTJgykhbToP4EeBFBKnFO+ESShCC+CdPGglHA9yIszaPU6+2gf+LzLMxAKVA3oFUEEXFB7QFB4=
|
||||||
|
T
|
||||||
|
bU0GtbthpQIeYyyYyCTpagjXkfKjddl2T76CNs/zJphK1OPBKRe5v0oIK519UHa7izjjWVz7a3U=
|
||||||
|
E
|
||||||
|
hUeIFSXAfIhoED7SKCJJ31wcBS8swMXlJ7fg4Gmmx79XCaBNR9Z/DMg7scgT4g/mQ05b7EOXoF3=
|
||||||
|
G
|
||||||
|
/UFwnOqCIuVQ04vRLUkKYUf/AHgAP30DjY5zHUQQ8kg6g3FiPZQKjXNYy2yPVB086Dn/ANwqcZc=
|
||||||
|
m
|
||||||
|
gkR2lpbcc8lKBVYDxNhc+QoEaX3CxyG1pjPPSZCTtEeNEddN9Oq1bEg9dNfCgSJHcLLmX6SMdK9=
|
||||||
|
J
|
||||||
|
CCpa3JLDI37bgEJF9t9OutAlsc1nz3h8xCiIS0kF5ErISlkqTckKKnyNbdAn2UHNzXkDOa438xL=
|
||||||
|
x
|
||||||
|
jGIWw6lceViJJYkqWWwDdxoJUE2A8dTYDUk0EPZHuDi5vJPRfjqdafdCWpCpDqyp223couLUojc=
|
||||||
|
k
|
||||||
|
pBJNBYXh/MoUzjMZqC0hhcdCW3mWUhCQoJGtk2GvjQEzK+q9vPw6eJoIzy3NZEfLuei9tSFWuLU=
|
||||||
|
G
|
||||||
|
TnN1vgMIWF3F1K9tBJ3a7kDa8CXHF7duUhFYB6pSpBt+NBVj6qnnT3Q5Atwm+2CADrZPyjdh9go=
|
||||||
|
I
|
||||||
|
o4/x6byJnIKilLUfFxHJ+SlLNg0wlSW7gdSStaUpA6k0E6dt0Tu5nbrB4ZUtOMwfHHGcCw1chUp=
|
||||||
|
+
|
||||||
|
U+t9TqWxoFEOKK1fy0H0d7IdruAce4i3hOMphSJUaN6kiM24w4/r+ZS03vck60DiXgOHuYhaMtw=
|
||||||
|
a
|
||||||
|
VyYL3oEeBjnJSrIOpJQUpSfZQV+lcU7A8h5rN49CwruDyzSVOt8Wy7ErDT1JB+IsoWGvUA807qB=
|
||||||
|
v
|
||||||
|
8n7M8GfzUePBcyONiSmtiX4eUltvNrJ/MCtbiTY6FJTQQN3f7X5/tq66cblZOUaeQpS0S2hHnJR=
|
||||||
|
a
|
||||||
|
6XELa3wpqLddA4PG1A1+Ac25HG489EQlsQGHENh9hJbQFuqKviR+lV1ageGo0oJD4H3WnQsq0xK=
|
||||||
|
k
|
||||||
|
FSbjcAqgtfwLuO3MQ2WnvK2utBYzt9zgvbGnF+VBNuLnplMpUDfSgUaAoNa1fFtoOaU56baleQo=
|
||||||
|
I
|
||||||
|
T7oc9awra2/UAcN7a0Fau5HfWDgcM/kZ8n022htBGq3HD0QgeKjQUl513q7id3sw5xfCylR4N9z=
|
||||||
|
7
|
||||||
|
SFkNNIJsC8tNis+Sens8aDowXaSBiUplTUNzHwATLmkEFXX9tB+Efd9tA84z5itCOXEtoGiWwkJ=
|
||||||
|
0
|
||||||
|
+wWoEDkeKxuaaKJkFiQnwUtCd3vChZQ++givkfAZmNSuXhiX2E3U5DUdy0jx2H9Q9nWgReH8k5a=
|
||||||
|
c
|
||||||
|
21i+NLLiAQqQ06SY7bd9VK/h9ltaCYI2by4ykbHwiuRKluIZjxmrqU44shKUge0mgkjKKXCCMLC=
|
||||||
|
z
|
||||||
|
cZl2GhbWUSpxa1Kf3gujT4AkK+FO3U7epoEJmLBeiNfNZRMp1hJcQWy64HFA7vhAFtVDqfCgBCw=
|
||||||
|
Z
|
||||||
|
lNvqYkuNoRudcQlSl+oUj1CnUDda9vBKftoFZmLiVtlULGqUVnch55CEekkWQhv4yr/j3UEX93u=
|
||||||
|
X
|
||||||
|
xcc29ioUgONtFLJdbILanljcopCQASncbeyghbD8sxMuTFafacCm2vTX8IsS2l9RN7/zg0E2cA5=
|
||||||
|
7
|
||||||
|
KgstvhKXPWdEacgm1nkJtuT5btDb20CtmObTpswBtwNharbR4DxoG9yDJYmYdoY9Ek7ELR8Slq8=
|
||||||
|
S
|
||||||
|
qgbysqjDSSA6Ol1X6edqCSuzXM2JmEyDSnLOJyETYnzBW2bfcDQRn9VqVP8AcjKyEgBMiHhpLew=
|
||||||
|
3
|
||||||
|
GxyAwev20EX8c5Jj8Hx/lWInJVuzkFqOw6kX2LZktvi/sISQaCX+yh4bx7jmS4f3QmOsJD0DMSI=
|
||||||
|
e
|
||||||
|
PU6mWxEcCXFIWpISkKdQpAO1V0pOttaC9f09fUn9PuJyDXDuF4EYRieyteKQthiLHkpaBKrv3Kl=
|
||||||
|
K
|
||||||
|
UEqsVE7tqtb0HH3D+vnjHazmMzC4TELyzGO9P1HIr6Cwlcgbw2CQq5soflv5+NAl8x+s7t73Yah=
|
||||||
|
Y
|
||||||
|
Luf2lmNpaWlMOa5KaEqI9uTtdjq2NrQsFQspKh5XoI75v9RfanHswZWJyypKA46WG3gr5qwUBsU=
|
||||||
|
3
|
||||||
|
YqUu6SDYEXoG5le4PMO7TMlntzwWdAbaCTKlTnUNRVJdG1tYhKQ46XCr8vppFz4Ggg3kvbzmHbk=
|
||||||
|
t
|
||||||
|
yOWMekZa1seqw4h1r1iPUKFKZW4lKwAfhUd1r+RoOPFz24ikuMD9zzPX30EuduOeZWJk2/XeOzQ=
|
||||||
|
B
|
||||||
|
PuoLp9reWiY0w8leul6C2PBMt81Fbuq+goH6g7kg0GVBqWn4t1Ajclnox2KkylmwQkmgox3V5VK=
|
||||||
|
y
|
||||||
|
ORlzFuXbSVWv0AHjQfP7vb3Em53JrERSloUssYyOD13Hbvt/Es/hQSBwPtvD4TiY7WRdAeCPmMm=
|
||||||
|
8
|
||||||
|
LErfIuoD2D8o91Ahcv5q1ElfMLdLhSbNJvbakdNPCgaz/cNt0FxK1Ff5iCdLnpQOXDv5PLQPmXZ=
|
||||||
|
T
|
||||||
|
UezLcr0nTZZjuLLYWL2FrjxNByymJ09tyPDntNlYKA76jaVa3F03WdaBDxWDn4CI7GxDTSGgoqe=
|
||||||
|
k
|
||||||
|
OutoU6rX4ipSvi06UC/wSNMe5ezIlSo4dhtuSIzge3NtuoSdpWpu5FjqBpe1BKSclNs85/U8WyL=
|
||||||
|
q
|
||||||
|
UwHPTdVtbG1KtW03IJv5UHj2aZ9P/wC65mGwsoSz6kVpClJQrroRYEgdfL76BIcyGAnIcS1yFDe=
|
||||||
|
4
|
||||||
|
qQgem1cpGqhdSehASD91Alzsri2cS/kpWQ+bQy4ktQ7Da8VXCTZI6a6XoK88g5UzkM+XMqw0+wy=
|
||||||
|
p
|
||||||
|
za2t1xtCnFKstQLfgNoSB7KDnad4mHEvRoEVlRSofBJlfquki6vGxoFzA5xhOZLJKEQckAl5KFq=
|
||||||
|
W
|
||||||
|
lL6blKviAIO0eFA6OR5MY2Cme+bKWmyli5IUk2KwfHd199A2sHyGKXlvPTEqQLlKVK1ufK9B5mR=
|
||||||
|
E
|
||||||
|
lsLltyApSr/DQdHb3NPYXJysVvKHEvxpQTe11Mg3B9xIoFTu4lXIFY/NpuW3YTWOkL6+m7FTsQF=
|
||||||
|
e
|
||||||
|
V0WIoISnNLQstOjaoBSFA+drUFjuz3EsL3A5ny7lc1CiJMHHPY5KBuSBMShMgi42qU0UbCk+dBJ=
|
||||||
|
5
|
||||||
|
7I8Z5/3G4l2wgT1NyORzExlvMNlsQYMdsvvu7bg3ARoLgbiKCOshxRzEdx5nD5z7hahPksyydqk=
|
||||||
|
u
|
||||||
|
MqS0pZOv5kBB19tBOs/AcPhcRyDmXnR3lKYL65q3UXAas6CkXuLFINqBu8W7VYDiPbDH51vHIHK=
|
||||||
|
M
|
||||||
|
yhOVzGTfQFvMrnvKfQ0nd+X02VpFv4ib60CzxL+oYybJcxIJVJhSETZa1pStqOkoL0hK1ahaGyo=
|
||||||
|
g
|
||||||
|
jXXSgbf1RvYvj/AsDx9qYZc3NyY+VSp0JQpEPHRnIrZCUWASsu/CbC9jQVhx7slzIFwLt7KCTeI=
|
||||||
|
u
|
||||||
|
usPBStSq1iaC1XZrkDjXpNLV4igvN2onGREaN76CgmWObtig20GDuib+VBGnefKKg8UkhJsVpIo=
|
||||||
|
P
|
||||||
|
nT365bIxfG5UaMra5NPy5cB1Ac0Vb/LegpZByERzuhh5GRUBEhSA+sKF0/sJK0i3+ICgcnO+5Ux=
|
||||||
|
2
|
||||||
|
Y4jHPlSFHQAK8aBmSMVzXNMfPNwXHW/zLUrTTzF6DpxPBM5MlQ43qNhUxbbaUBYJus2JJ8LCgd/=
|
||||||
|
G
|
||||||
|
szjcj3By2Jc9UrlsOwsa2lKDG+VipKQVXO4HcjSwtQdfzbMBpXzLEbdqUBTJGvlcCg50ZWRIf/Z=
|
||||||
|
Y
|
||||||
|
adU8UhiI2klS3HPhShKbaknSgmCJxrG8aiN8ZyeDdlTWgl7IP49RbbdkOIJWPUAspLY+HTT4fMm=
|
||||||
|
g
|
||||||
|
5OQZLt5xhr18ngMgrSzAkvrCHFW1CAq2gta9tB76BmIlc47gN/O8dxiMDx9Tiktris+o4sDRW1x=
|
||||||
|
3
|
||||||
|
VVrdRYe+g9HajIvpJXIybTpKUiSvY82QdFKKElKttwba0EcZuRncf6uNkyFrbUVpbfBKkObCU7k=
|
||||||
|
F
|
||||||
|
V9QfuoGU9xzJZSQ03i2PmBHbO8BSEkDdfooi/WgkLi/FGGO3fIMzyLDtzncA3HksxJLjiW98qaz=
|
||||||
|
F
|
||||||
|
JUYrravyrNtbUCQOfcUgo/pkrtri21NKTIbdbn5rYVDorSYSPdQd8/vZx6fjhipfBMYplP5CZeY=
|
||||||
|
I
|
||||||
|
Sen/ANSCdPOgSmefcH9MBvtph31DUuqVm1X+6YBQKmG5fxPkEyFgmeGYbHKmSGYxebTl1uJDzgb=
|
||||||
|
N
|
||||||
|
g7NWm43aXFB6126y/KM3PyuIz+Exq4rwadOXyrGPdUtKEhRS25qUm/UUCye3nKWFb08z4i8kpKZ=
|
||||||
|
M
|
||||||
|
V7O+ol1A/SdrVrjwUDegb83tBJyqlKc5NxuOP0bco66oeQ+GOb0Ej/Rx2vjI+qPinbfnLLGcwPJ=
|
||||||
|
o
|
||||||
|
uRLzMeTI9AtssOrS+0tlTKkuJcY2i/gT50F54fbPgHaHu/D5jxHiEqDCjRp8R+Wp96bLbXIjuNt=
|
||||||
|
r
|
||||||
|
Wl911aWuhFj4fF4UEXTOxmQ7k82j8khcTlrgupeelZLIsuRWpQSremxYdCwhy523spPW1BKuO7S=
|
||||||
|
/
|
||||||
|
Tw/BBg9tQ3mYZbDglrkzWo7o1utMl1Seo0uCDQR/3xQiBGzrgSG0stMLAToCpbibaDzoGPxGVwG=
|
||||||
|
H
|
||||||
|
lI8fuRlYuHwr8OQqXJn3DC9imHS0emq0ghI8TQVF7q9zV9zu4/IecTnSqHKkKZw7NikNQGD6cZt=
|
||||||
|
K
|
||||||
|
fABsA28yaDUrBxuLY5vPcpWphT6QuHi0mzpSdQpw9U38E9fO1An4/nuRdmBcF4x0E/CgpCx9tBJ=
|
||||||
|
X
|
||||||
|
Hu6PL8S427DkKiykfE0838TTgH8SFXB9oIoPoR9D31MxO6bj/D8wyiHyTFtpdkR0H9uQxcJ9doH=
|
||||||
|
W
|
||||||
|
wUQFp/SSPA0F742rKT5ig20GDn5bGghL6hXXFcYkNN6/AoWHuNB8ve/eSRBwEBtbu5xco/BfoPS=
|
||||||
|
V
|
||||||
|
bTr1oKy4dt6ZzeDHaZL7k14Rm2goIKlPnYLKVoDrQS1kuz0zGKw7yeO5Zybkp7UARJT8Zpjcv1C=
|
||||||
|
A
|
||||||
|
h8k33BFwrbtt7aBS5pxblXGIsFnJYSXGZkyY8QORchDfas4TdG4qbsogaaWoG+1BTgJrS8REW1I=
|
||||||
|
k
|
||||||
|
IkJbk5SbAUhopaVqEtvqN9f+WtB1ROStYbjkLBxuOtJmsshtjOFRU8VuK3Fe70ArapXhu6UEdKi=
|
||||||
|
8
|
||||||
|
1kSXnzkFrKyFbSg7bm5Fvh0oHd209TH83iyOd5X5aClmU6lYbBUl0NfBsuBbX/jWgl9zu/xZLyo=
|
||||||
|
k
|
||||||
|
XmTzyRZXrEtqFz8FtoTrdWv2UEJd0uaz+UZcOTprrzG75eK4+bqbj7uthoCfzGgk5/n+AxcBnBw=
|
||||||
|
M
|
||||||
|
8/GxkVltmM8GQUJa2k7glF7Cw8dfOgTJ3P4sOIUYjk68m7kNrXyygQG2AbKUVHoolVkjyudNKCK=
|
||||||
|
O
|
||||||
|
RPTszyJ1ERtRiQU+g2kLSG/U6u7QSAQDYC3lQNyYosLI8D4XI/stQSNxmAmD267hOsJQW52Kxyw=
|
||||||
|
t
|
||||||
|
t4OlITl4agFi90nrQRN6M3J8mexcVCnXXXWmGGk9VLWbJSPeTQOl7GR42fj8I4biFcm5E4Qh5bL=
|
||||||
|
a
|
||||||
|
n97x6pYRY2Qk6BVtyvzaAgUHRyXhPOuHqSOa4FMdsqKHmok6K/IYVa+1bbLqig+xW2gVeG4KNAz=
|
||||||
|
/
|
||||||
|
AB7IEodhT5cRUWaBtKVh9N23B4KBBHvFqDbhuG/7nHJpceX8vMx09SXPWQgsFpQGu4pUoKFj4Ee=
|
||||||
|
6
|
||||||
|
gSuR8T5pxWV6E5va1tQtE6OliRGWlYJSQ60hSdbdDr7KBW7e8M5zzgvSGnGWMW1uQ9l8kpMeI24=
|
||||||
|
k
|
||||||
|
ghKVIQVrUP4W0qPnagkHhfLeO9iPqW7V8rXyRrJRcE4WM5kUxXY8ZqPNW6lah6hUpSAHjc2Gg6U=
|
||||||
|
H
|
||||||
|
0E+oOBy/l89eW7UZl9lmQ1FemQce2y446p1KilaVrPxNgAEpSU7wq+7S1BxdmOHd928FJyGS55k=
|
||||||
|
o
|
||||||
|
0GMp0O47LfJvlxSUglKGkN/Ak+FlH3igesbkeNjYHKsvupbdcWwZAv0DG9RJ+/Sgqb3r5OeRZ0c=
|
||||||
|
d
|
||||||
|
jfB82+3NyJP/AIYscbmkK/mVbeR4D30EI90sBL5r2+5Fk49x8iuOMY2fyqHrBDh+26QPdQQA/wA=
|
||||||
|
S
|
||||||
|
5dxhtnJzcWtcaIpLqJrQEiMVDUErb3Jt76DgyGdy/PpTuRybnqLbUEXHTpegXOOcXAWlSkFVA+G=
|
||||||
|
Y
|
||||||
|
8mQlGLwcReSn3/ajxk7ykkW+JQ+FP2mgtj9C30n9x8D3YwvenlPIGMKnGl23HoqDKfmMyGlNLaf=
|
||||||
|
d
|
||||||
|
ultCTuB+HebgUH1XiqCmEEeVBuoNb35fsNBD3eSKZeKeRa+h0oPln9RXF40XFZIxsM6mVDkpfey=
|
||||||
|
a
|
||||||
|
kpJDIUUqClFRXYhYPS1BV5qW1is7AzL0ZExuFIZkuxHUhbbyGlham1JOhCgLUFr8twtHOEYfN8S=
|
||||||
|
4
|
||||||
|
9ExjCJEfJRi1Ok7XY5QpWwojbW033A6WOlqDZO7VoyUtiTmRGadZcQ6hEeLuO5FyAVSlOqKbnUB=
|
||||||
|
O
|
||||||
|
vjQb09s8NAU3IaiIlFu5bccS2kJKhY2Q2kJsfbQJGZ4uIGPfd/qSscwkKbU4pSkR20Wtb4iB08L=
|
||||||
|
X
|
||||||
|
oIvU3x5xwsMcrO+9gERnDe3tKKDgzeE4whv1s1n1yEpSpLaRHdT16dU+YoG/A4lgpxXksY46WGy=
|
||||||
|
6
|
||||||
|
4yogpStLeosFWNtDegQOSqS5LQlf5NAfdfWgW1cDbhpcTFkqQ2CTsSNoP7W8nr42oNbfCXEvJLc=
|
||||||
|
p
|
||||||
|
xB3bfh/xhP8AfQdMPjUJDThdU44tq4VuWoblfvnwI/gFA2OQs+hKU1axSSPuoHfwSOsdv+fuJ09=
|
||||||
|
X
|
||||||
|
GwwdTrsyMZen3UDM4k+GedZGWkfustuLjHyeLJQlXvG6491BJParJ/7J+n/uR3Jwj5Y5bNltYeL=
|
||||||
|
M
|
||||||
|
bNn4sR5VntiuqSoHbceFBD/G0rDfzjq1LkPqV6qyokm/gaCcMBCZRjCwUbEMPYWc2CLbXpQJUQf=
|
||||||
|
b
|
||||||
|
8uk/bQMJ3mLvEeQcmgAKX/VpjyNoNglSVkA++6qBXxveeXAJxMoLXFZjqjzwFfDIj327CjoSNCC=
|
||||||
|
f
|
||||||
|
bQa4vcpUfiqOO41ClxcXNmFkN/6hYllLyN9tTY7hQNyNh+b95uUxuNcKwr2VyZbDTWOhILzm0KJ=
|
||||||
|
3
|
||||||
|
ukfA2kbtVLUkCgvRwTl3cP6cu3GJ4LzHOx+Q5jFsmPIRGQpRwkU3UzF+cb3GQloE7rp+C9knaKB=
|
||||||
|
4
|
||||||
|
Y/vlzZ7DofagqV6iErTkX5DTsdYVpuQttRBHvoGfP7iZgJfXJe9dbi1OllsktKcPQrXYFaR5WAv=
|
||||||
|
4
|
||||||
|
UDNhYfK5dMmXNUovT3f33BqsoJuU38Co0HfncEn/AGwePM/sPZEoDRVcNtQ4Kky5b7h1+FCGwke=
|
||||||
|
a
|
||||||
|
1AeNBBvG1ZHFNtiDKXHStbscKBuEhtXwpWk6EFBFAuS8Lx+YpcblmKEN5dlHMYpDbbnxD4VqQUl=
|
||||||
|
K
|
||||||
|
hboaDXgeyDsdxyTBy73J4D59RO39tbaR4OMo+I280kj2Cgk7g8SFx+Q002whlIIIQlISPwoLk9j=
|
||||||
|
8
|
||||||
|
68tbPp/l0oLcYSQXoaCfEUClQYOfloI87g48yIjgtcEGgo93z4iymRJ+Za9SHkmnIktG2/5klJ0=
|
||||||
|
8
|
||||||
|
yk3HtFB86OY8bl8Xz0zj88ErjKIacIsHWVatuD2KTrQWP+kju/8AM49rsxmEMfMoU45x6Y+SFut=
|
||||||
|
q
|
||||||
|
+JUUE3G5JupAtqm48KCwOe4w8d5R6ON3pPrKBV8ZHs3qWdPO1A20ccmNEJjqMi/5ULGxLgtqQnw=
|
||||||
|
t
|
||||||
|
52tQJHIeBx8qW5XIOOpyHp/AwX0fMC/kEA2v/NagbeT4LxyDjp62cDDhvIjvltKYbTbqFBtVibJ=
|
||||||
|
u
|
||||||
|
PeaCn/OJc1CUsuuX2p/uoJzg9oNkKK47mXI7BZQUs7Wzb1ED2XtrQQhyrHSYGSk4+Yna9FcUy4P=
|
||||||
|
8
|
||||||
|
JtcewjUUEgcRwXIOT8fazMLJtKSoqZkMuISVNuIR6ZBJIOqSCKBWd4ry6AUOuBlQJKgS2oG5Nzo=
|
||||||
|
F
|
||||||
|
UCSniPJHnilt9LLbhKrJjlZKiXD4q6fuEUEcchTJn596ExeVIdfLLexO0uLKtgskXtc0EvI4HJ4=
|
||||||
|
d
|
||||||
|
295eJMhS1u4yOFM2Rs3IlMkqSQL+FtTQQtx2Mt3PZV5n4XGlslKz0sUG4PvoHNx5nI4aVkmIuNX=
|
||||||
|
m
|
||||||
|
+P5shOYw7SiHW3UncCEoueuugoMsdw92Rk3nYmAmRoQX+zFe3spQgjo4+8hISL9Ta9qB+ocl57K=
|
||||||
|
w
|
||||||
|
OJ4xaJcx+U3Pz0uO36TCEthDaG20D8qG2m0tNJ6/mUdSaCKu4EOND5VLy0s7Go8h93aQfjcC/hS=
|
||||||
|
P
|
||||||
|
eaBj45Gc5Pmo+D4rAfyWVyS0sx4UZpT77zijfYhtAJP2UF3Pp6//AFxrnsjlHfyS8rYkLHEcRIS=
|
||||||
|
3
|
||||||
|
sJ/RPni6EG/VtkqUPFQOlBeHivaHifBsC3xbiqI3FcOvaleO462llBUenzkxxKn3VK6b7j30DL5=
|
||||||
|
L
|
||||||
|
wXDsZPJcTehIYVJjlWLctr6qVbjcnUqP5SfG9/GghaD28jOSnYeDkM47Itne1FllacZNKgDseDZ=
|
||||||
|
u
|
||||||
|
2s/pcTpfRQPUBpfa4tiZ4wHcXjzfE8gfyyVLkoaWem9t9v1W1p9322oJSb4vxjC8fYykeS3LgLR=
|
||||||
|
/
|
||||||
|
8OYJDintLhtu2pUsny99BHHPc9gnYM/DQnGlT8i2hjIhl0K9GG2oLTDasb7Sr4nV/rV7AKCHMFw=
|
||||||
|
O
|
||||||
|
VksDJegtqU4vKO+gm17oaAa0+6gsXw3sXhOUcaTg8yPQyiUKcxkk6EKtuU0u3VKuvs6ig4OHdoY=
|
||||||
|
v
|
||||||
|
H8wX0vOMPwXbPRSbLbcSdCSPwI60Eh8j4Hh+eyEx5ZbgZMtkxJyEBIddT+ly1tT5+NAtdioeYwO=
|
||||||
|
X
|
||||||
|
ewGbZUzJiq2qB6EX0I9hoLk8ZcJiov5CgX6Dwi4tQIeexwlR1C1+tBXHu3wRGRiSYzqSkL1Ssfp=
|
||||||
|
U
|
||||||
|
NUqFvI0FEe9/aOHyKG7HfT8nm8cSIcvYpSVX1LbhA1bV1B/SftoKl5KDl+MZQRMm05j50dQcaWC=
|
||||||
|
U
|
||||||
|
qCkm6VtrT5EXCgaCfeAfVTkgyzj+4RXOWyAhrKNqspQHi8lOpV/Mnr4iglmB9Q+Fmpvi2GlJSmw=
|
||||||
|
e
|
||||||
|
SU6k21UkXNx7dKDCT3pTJ3BU4gqG1SUaXSfC51+zp7qBNzPdnFpxExhxwMJdYeQXnFD1CVNlIBJ=
|
||||||
|
6
|
||||||
|
9ff7aCl3N8tGlvqSyb7U2/CgvdHwUV/C42fjZTCHlxY6vSbso6tp0A1Av7aCCe9HEY+XmHJx3EN=
|
||||||
|
Z
|
||||||
|
VtIQ60r4UyUJ6WV0Cx4X8NKCMOGcryPB8m60oLEWQQJcbooFPRab6XH4igklHOsdPHzDWRQhB6h=
|
||||||
|
1
|
||||||
|
QSse9JIIoETknds4iI9DwcoPSHklG9A+FskW3bvFXlagb3btqLgp7fJskUuTLExELG4NbtCv2rt=
|
||||||
|
0
|
||||||
|
8qCUMzyOLmuA8vBfBIxqnDdJBul9o6k0FfsLlExpE6Qy16rbxRtWVpQCUi2m7rQOKFMmvuJdjpS=
|
||||||
|
w
|
||||||
|
7ptUmW2hfsFwaBwt8T5xmUerNkPORtLpM5Fj/wCkXoJB4Q85wWKXmcE2EH/UkCUneSRa9tmv2mg=
|
||||||
|
j
|
||||||
|
V/tNy76gO72Q43w9IbgY9xT2cyzm4RILbjqrFduqz0SgaqPsBID6KfTr9M/afsVhPncdCVIyUpI=
|
||||||
|
b
|
||||||
|
lZJ+xly021Clj8jR6+k3YH9ZXQStnsyh2ElEFaRH2n9lACUJCdAnaLACg4eLZ45GG4lz40pSUuI=
|
||||||
|
V
|
||||||
|
qFIJ2lJoOflOPdzWNERpZXlcWDLwspR+N9pH5mVK8VAfD7dD50Ebct41/WMbF5fxptO9aSuRGJK=
|
||||||
|
Q
|
||||||
|
TclxBIBsd1/cffQQ3i+2vMO+GbnZ3k2an4vjEZ35TE4ph9TfqIj/AAKcJBI3KXcnTQWFAp817Yu=
|
||||||
|
8
|
||||||
|
CgwOLcby2Rx+I5K+3Fnu/MKXICmQpQaQ6SC2h5KzvCbFWzyNA5uEfR127WynKIkTYEmx/fjPBL2=
|
||||||
|
/
|
||||||
|
zXuSoEexV6CRsB2849xDIJ4xikKfj4eE24ZEiynnX5Mh15biykAblEk6AAdBQOaChuHlGJ23/TV=
|
||||||
|
c
|
||||||
|
kdetv7CaDZy7jpXPey0H4ZBtvsNHEWukH/nQMTMT3FxC4gltxlQsQbEG9BKfCHshlWoaHZDbOUg=
|
||||||
|
K
|
||||||
|
jplT32i6p3HvG6XAApN19U3NwDqQelBZ/CxTHZSn3UCvQFBrdbC0kHoetAzOWcXayDC7ouSKCq/=
|
||||||
|
e
|
||||||
|
TtNLmx3n8egImISoNLI0WP4Vf3GgqDy3tmMxvg8jjtSC2pQ9J1OxSV+Nj1FvZQRXlvpdya1rk8f=
|
||||||
|
y
|
||||||
|
Bjtk/DHfG+3uVobeVA3l/T93QiLPoOx1FPQhakn+yg6YvY/u8+EgTWEFX6A48tQPuAFAtM/Sn3O=
|
||||||
|
y
|
||||||
|
SEuZjOtR2l9SltROvtWTQOviX0ncNxb/AK/IpDuTko1Tc/Bu9t9Le21BJ2L7L45LhMf5h5pH5mk=
|
||||||
|
v
|
||||||
|
LQkeAuQeg8zQc2f7S4qY8mMZxlLOhYYustk+BUbjQ+F9aBl5n6aZeSUY5eVYfkLidqk38iP7zQc=
|
||||||
|
s
|
||||||
|
f6OmV/FOzHoote4WkfeCaDtw/wBKPDkSgXJD0gJP/wAyVD09PIq0oH5j/p24RFUFFPrgeIX6hv7=
|
||||||
|
d
|
||||||
|
osPvoHVH7TcLjRH4CMS28iW2WpMZ4oWh1HWyhY6XFxcUCK99Nfa+cSf9vMpGhWmOi6vaLqCUAfb=
|
||||||
|
Q
|
||||||
|
dh+mDg0aHvxWEjJUhJ9NDzbDhJPibIOv20Fb+7Dmc4dl8tHwP9MhY3EoSVtHGQ1vhwISpSQ4tpS=
|
||||||
|
j
|
||||||
|
qQOtAwsFO+o/nWK/qfGoSJOKutK5EeBimkDYbHcotoVpQXq7P8HHajsBwrHZCOGM3yyUM5yV4pA=
|
||||||
|
d
|
||||||
|
dclO/tpcV47WQkAdBc0EvOv4yUtWTgNqhx2HPSdQypW238RRfb9woEzPiTgpIlKdDsSYn9t4H4V=
|
||||||
|
+
|
||||||
|
OtvH2+PvoEvimYIZzDaF7VDVsDrZwm/9tA+XFOTIyUx3PTeTZyM8OqHE9PsPQ+yg4cIERM5Ix6k=
|
||||||
|
h
|
||||||
|
uNmAuU2z4MzEW9ZA9h0UKDmzPrYyO5NhoSfQUHFNW/QFfHYD+W9Am93eP/13gMyVGRvehejkohT=
|
||||||
|
1
|
||||||
|
3xVhzT3ouPtoHxxFQcjNm9/WabWCNL3SDeg5/wCnOozecnOC5eDSE21+FtGg/Gg49vW/tFA5dqp=
|
||||||
|
m
|
||||||
|
IQ+NVekR/mRQRNzGMIUmcwRYuWWPvoJJ4s83Hb4NnVf6OQbdw2Qt0KVgqbJ9oUnSgs9w2U9KwbS=
|
||||||
|
Z
|
||||||
|
QIfilUZ1R/UW9Ar7U2NAu0BQFBqeZS4kpIuDQNLknEY2RaVdAN/ZQV17o9gkZZ1eRxoEaaBYO7b=
|
||||||
|
h
|
||||||
|
xPkq2vuIoK/cj4ByLjLpORgrG2+2U3+4g/5yFbftAoG6S98OxoKt+bepSiftAFB1R8nkYSVIQAF=
|
||||||
|
O
|
||||||
|
/mKEJSLW81a0GL0qcAPmHisK/Mm5NyfDTWgzZyjcZsNHaSdQ0lJcdT7rWA+2g2OZp8tqjJdWlHi=
|
||||||
|
E
|
||||||
|
kn3ApRYf20Czx3KGESqUpLKbXbWoAuA+YCLfiaDonciYlSQ9LkOyVJFkbFejoOl7bifdoKDWy1n=
|
||||||
|
M
|
||||||
|
okoxeHceCujwaddUftOlAp47tL3EyI3JgrjoVY/vqS308gBegcUXsPzd4AyJyGyeoHqLt99qBYi=
|
||||||
|
f
|
||||||
|
TpkFJ3Tcq6VdbIQAP+omg6x2TmRTt/qkpYHTVIt+FAj8l7e5zj+NfycfNeg1GSXHVTFIbaSkeJc=
|
||||||
|
N
|
||||||
|
gn3nSgoJ9RKcVH5JJKsq0tnLNKEkNOJe2OfkJJaKgdUg0EP9sW52Y55xfiz7ryYsvJQoThjvOIH=
|
||||||
|
p
|
||||||
|
SZKUK0Gh0UaD6w/UQ61FyuMhREhEfHhgNIToEpbNgB7gKBQ4Yll6CsEBSXlELB6Gg2yoMFgu8az=
|
||||||
|
2
|
||||||
|
5eEyJIYkJ/1Ibx6LQfYfCgjLC8e5Bw7uBk+G8oWF+s2zJxORQLNTI28p3p9ouNw8DQTExDEdDQQ=
|
||||||
|
b
|
||||||
|
hIFj5mgwn43e/HyDQspDiFA/wupuAfcoEpPvoFPJYdl6Ml9pPwvAhYP8woObi8ETONIxz4K/TL0=
|
||||||
|
V
|
||||||
|
RV4pSopAP+UigUMFAGPdjxUj4WUJaA66ISE/3UHejHrdRMeCf9YuKF/IjYn8BQJzmCUIn5bL/Ub=
|
||||||
|
a
|
||||||
|
6nr91Ar8fxy14xbCrXZcI9903oI87h8Qy0/LNt42MuQ7IbAabbSVFRTYEaeNBNnbDsvMTwvBwua=
|
||||||
|
o
|
||||||
|
LDuPebmpiJPxBxBKgFkdOuooJraabZSENJCQLCwHkLUGdAUBQFBipAULGgS5+HYlJIUka+FAz8v=
|
||||||
|
w
|
||||||
|
GHL3D0xc+ygj7NfT3xXMOFcrHI3nUutAtLv70WvQN6T9KHE3xZsy2f8AA9u/70qoOdf0hYJwWRk=
|
||||||
|
5
|
||||||
|
qL+fpKH/AGCg1f8A4f4VsfFm5iR5ekyP7qDZD+kjizToMrJzHk/wpLLf4hBNA7cT9NHbGFtK8Wq=
|
||||||
|
W
|
||||||
|
oeMl51y/vG4J/Cgd2P7T8QxQH9Nw8WN/M2w2lX32vQKrXFYTOiW0gCg3p4/EGgQPuoM/6FHGu38=
|
||||||
|
K
|
||||||
|
AOGatbZ+FBwyeONr1CCKCIvqc47KidkOUTod0qjoiOOqHgymawXf+i96D5lZjnXLDl5wGQLn7zo=
|
||||||
|
C
|
||||||
|
ihpRsFkAXKTpQe8e5pnRyvjip0hK2U5bGFy7LQKUmY1uIISCNKC9n1KQgxyZUdJ3WCbn7DQdfb0=
|
||||||
|
F
|
||||||
|
eJaXbVWt/soHXOw6MnDchuj84+BX8KvA0EddxW5jvb9OfUT/AFHgsxqSpXVRgyCI0lHuAUlf+Wg=
|
||||||
|
e
|
||||||
|
3DZo5HgY8pGrgOxwfzCgdTWKMht2KQTuTe9uhHjQdkXGmREUCj4uvT9VviA+0UCbxOOq2RZCCPT=
|
||||||
|
l
|
||||||
|
/Am3Xe2m/wCIoFBEVYyqWGUlSidwQBc9NaBabxGTebLbcVwlWtwg9KBRx/B87LWlL8JSW19Suw0=
|
||||||
|
o
|
||||||
|
F/B9sHozrhyLyEMqsQ20SVEi/U6CgeuMwWKxTYTCYSCP/IRdV/eaBQoCgKAoCgKAoPCL9aDBbKV=
|
||||||
|
U
|
||||||
|
Gow0HwoPPkkmgPkUedBgqD5Gg0qx1z0H3CgzRAtQbPkhQYjHo8RQbUwWgNRQHyTdBkIbQoAw2j4=
|
||||||
|
U
|
||||||
|
CXybiOF5dxzKcVzjAkY7LxnoU1k/qafQUKHvsdKClX/8ju0wdK2+e8jaQb2QgwRa/QXLB6UFb/r=
|
||||||
|
L
|
||||||
|
+irjv0143imZ4XzSdk5uQlvKfjZkMKUhEQNuNuNCOhs6LNlE36igs/3Wko5phsHzaEQ+1l4UKWH=
|
||||||
|
E
|
||||||
|
ap/eZCydPaaBZ7W4abLxcRmOwt1VlGyEKVpf2CgliF275I/tX8p6Q6guEJ/DrQc2R7BS8w1mocy=
|
||||||
|
W
|
||||||
|
0xGzsN+DKQElwpDySncBoLg69aBU7fdhMRwiAYa57+QKtpUVpS2m6UhOgFz4edBIUPiWJjJ2txk=
|
||||||
|
6
|
||||||
|
9bi96BQj4WA1omMhIBv+VPWg6E4rGJVv+Ua3fxemm/8AZQbEQoTSt7UdtCjoVJQkH8BQbglI6AD=
|
||||||
|
3
|
||||||
|
Cg9oPLX0NB7QFAUBQFAUBQFAUBQFAUBQFAUBQFAUBQFAUBQFAUEad8OwvEu/OJx+I5U/IjIx7jq=
|
||||||
|
0
|
||||||
|
OxSkLW28jY42d4ULEhKr9QUigUOI9luA8P4rjOIRoPz8HEsJiRTPIfX6SFFSQrQJNug06UDziwI=
|
||||||
|
U
|
||||||
|
FlMeEw3HaQLIbaQlCQB4AJAFBu2p8qAKEmg8DaRQZAAdKD2gKAoCgKAoCgKAoCgKAoCgKAoCgKA=
|
||||||
|
o
|
||||||
|
CgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKD//Z=09
|
||||||
|
</RecursoImagen>
|
||||||
|
<RecursoImagen id=3D"IDRecursoImagenPunto" tipo=3D"GIF">
|
||||||
|
R0lGODlhCQAIAOcPAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//
|
||||||
|
AAAA//8A/wD//////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAA
|
||||||
|
mQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBmmQBmzABm/wCZAACZ
|
||||||
|
MwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD/
|
||||||
|
/zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
|
||||||
|
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/
|
||||||
|
MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz
|
||||||
|
/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbM
|
||||||
|
mWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkz
|
||||||
|
M5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ
|
||||||
|
/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
|
||||||
|
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZ
|
||||||
|
M8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz/
|
||||||
|
//8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9m
|
||||||
|
mf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//
|
||||||
|
M///Zv//mf//zP///yH5BAEAAPwALAAAAAAJAAgAAAgyAP/5g4MChYpt//71I1jQ
|
||||||
|
IEJiDRve+GcnYsEY20BZRBHjn0aLOAQyLKjiVsKBJE3+CwgAOw=3D=3D
|
||||||
|
</RecursoImagen>
|
||||||
|
<Caratula>
|
||||||
|
<DefinicionPagina orientacion=3D"P">
|
||||||
|
<Margen superior=3D"10" inferior=3D"10" derecho=3D"10" izquierdo=3D"1=
|
||||||
|
0"/>
|
||||||
|
</DefinicionPagina>
|
||||||
|
<Contenido>
|
||||||
|
<Parrafo multilinea=3D"false" rellenarconpuntos=3D"true">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"20"/>
|
||||||
|
<Texto>Esta es una frase t=C3=ADpica</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
<Parrafo multilinea=3D"true" interlineado=3D"1.5">
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#000000" grosor=3D"0.5"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#000000" grosor=3D"0.5"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#000000" grosor=3D"0.5"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"12"/>
|
||||||
|
<FragmentoTexto>
|
||||||
|
<Texto>Este es un ejemplo de un p=C3=A1rrafo compuesto por varios=
|
||||||
|
fragmentos, </Texto>
|
||||||
|
</FragmentoTexto>
|
||||||
|
<FragmentoTexto colorfondo=3D"#FFFF00">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"16"/> =20
|
||||||
|
<Texto>=C3=81lgunos de ellos con un fondo distinto, y varias T=C3=
|
||||||
|
=8Dldes. </Texto>
|
||||||
|
</FragmentoTexto>
|
||||||
|
<FragmentoTexto colorfondo=3D"#FFBB00">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"16"/> =20
|
||||||
|
<Texto>=C3=93tras c=C3=93sas =C3=8Dnteresantes, caray.</Texto>
|
||||||
|
</FragmentoTexto>
|
||||||
|
</Parrafo>
|
||||||
|
<Tabla colorfondo=3D"#FFBBFF" extenderhastapie=3D"false" etiqueta=3D"=
|
||||||
|
Tabla exterior">
|
||||||
|
<Margen izquierdo=3D"20" derecho=3D"20" superior=3D"20" inferior=3D=
|
||||||
|
"20"/>
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#00AA00" grosor=3D"1"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#00AA00" grosor=3D"1"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#00AA00" grosor=3D"1"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#00AA00" grosor=3D"1"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Fuente nombre=3D"Andalus" tamanio=3D"20" color=3D"#000000"/>
|
||||||
|
<Columnas>
|
||||||
|
<Columna ancho=3D"25"/>
|
||||||
|
<!--<Columna ancho=3D"25"/>
|
||||||
|
<Columna ancho=3D"25"/>-->
|
||||||
|
</Columnas>
|
||||||
|
<Cabecera repetirtrassaltopagina=3D"true" etiqueta=3D"Cabecera de t=
|
||||||
|
abla exterior">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#FF0000" grosor=3D"1.5"/=
|
||||||
|
>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#FF0000" grosor=3D"1.5"/=
|
||||||
|
>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#FF0000" grosor=3D"2.5"=
|
||||||
|
/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#FF0000" grosor=3D"3.5"/>
|
||||||
|
</Bordes>
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"8"/>
|
||||||
|
<Fila>
|
||||||
|
<Celda rowspan=3D"1" alineacionvertical=3D"C" alineacionhorizon=
|
||||||
|
tal=3D"D">
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#00000F" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#00000F" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#00000F" grosor=3D"=
|
||||||
|
0.5"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#00000F" grosor=3D"0.=
|
||||||
|
5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Padding superior=3D"1" izquierdo=3D"1" derecho=3D"1" inferio=
|
||||||
|
r=3D"1"/>
|
||||||
|
<Parrafo>
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"12"/>
|
||||||
|
<Texto>Celda que ocupa dos filas</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
<!--<ImagenByReferencia ancho=3D"20" alto=3D"6" refid=3D"IDRe=
|
||||||
|
cursoImagen1" etiqueta=3D"Este es el texto alternativo para la imagen"/>-->
|
||||||
|
</Celda><!--
|
||||||
|
<Celda colspan=3D"2" alineacionvertical=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
</Bordes>
|
||||||
|
<Padding superior=3D"1" izquierdo=3D"1" derecho=3D"1" inferio=
|
||||||
|
r=3D"1"/>
|
||||||
|
<Parrafo>
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"12"/>
|
||||||
|
<Texto>Celda que ocupa dos columnas</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>-->
|
||||||
|
</Fila>
|
||||||
|
<!--<Fila>
|
||||||
|
<Celda alineacionhorizontal=3D"C" alineacionvertical=3D"I">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.=
|
||||||
|
5"/>
|
||||||
|
</Bordes>
|
||||||
|
<Padding superior=3D"1" izquierdo=3D"1" derecho=3D"1" inferio=
|
||||||
|
r=3D"1"/>
|
||||||
|
</Celda>
|
||||||
|
<Celda alineacionhorizontal=3D"C" alineacionvertical=3D"I" colo=
|
||||||
|
rfondo=3D"#FFFF00">
|
||||||
|
<Padding superior=3D"1" izquierdo=3D"1" derecho=3D"1" inferio=
|
||||||
|
r=3D"1"/>
|
||||||
|
<Parrafo>
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"18"/>
|
||||||
|
<Texto>Subtexto pqg</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
</Fila>
|
||||||
|
<Fila colorfondo=3D"#F0A040">
|
||||||
|
<Celda colspan=3D"2" alineacionhorizontal=3D"C" alineacionverti=
|
||||||
|
cal=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.=
|
||||||
|
5"/>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
</Bordes>
|
||||||
|
<Padding superior=3D"1" izquierdo=3D"1" derecho=3D"1" inferio=
|
||||||
|
r=3D"1"/>
|
||||||
|
<Parrafo>
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"16"/>
|
||||||
|
<Texto>ZZZ</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
<Celda alineacionhorizontal=3D"C" alineacionvertical=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
</Bordes>
|
||||||
|
<Padding superior=3D"1" izquierdo=3D"1" derecho=3D"1" inferio=
|
||||||
|
r=3D"1"/>
|
||||||
|
<Parrafo>
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"10"/>
|
||||||
|
<Texto>TTT</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
</Fila>-->
|
||||||
|
</Cabecera>
|
||||||
|
<Pijama colores=3D"#B0B0B0"/>
|
||||||
|
<Filas>
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#AA00FF" grosor=3D"1.5"/=
|
||||||
|
>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#AA00FF" grosor=3D"1.5"/=
|
||||||
|
>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#AA00FF" grosor=3D"2.5"=
|
||||||
|
/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#AA00FF" grosor=3D"3.5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Fila alto=3D"50">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"10"/>
|
||||||
|
<Celda alineacionvertical=3D"S" alineacionhorizontal=3D"I">
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#AAAAFF" grosor=3D"2=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#AAAAFF" grosor=3D"2=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#AAAAFF" grosor=3D"=
|
||||||
|
2.5"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#AAAAFF" grosor=3D"2.=
|
||||||
|
5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>A</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda><!--
|
||||||
|
<Celda alineacionvertical=3D"I">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.=
|
||||||
|
3"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Otro m=C3=A1s, que ocupa dos l=C3=ADneas con qqq</Te=
|
||||||
|
xto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
<Celda alineacionvertical=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.1"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Y otro</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>-->
|
||||||
|
</Fila>
|
||||||
|
<Filas>
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#AA00FF" grosor=3D"1.5"/=
|
||||||
|
>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#AA00FF" grosor=3D"1.5"/=
|
||||||
|
>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#AA00FF" grosor=3D"2.5"=
|
||||||
|
/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#AA00FF" grosor=3D"3.5"/>
|
||||||
|
</Bordes>
|
||||||
|
<Fila alto=3D"5">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"10"/>
|
||||||
|
<Celda alineacionvertical=3D"S" alineacionhorizontal=3D"I">
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#0000FF" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#0000FF" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#0000FF" grosor=3D"=
|
||||||
|
0.5"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#0000FF" grosor=3D"3.=
|
||||||
|
5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>B</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda><!--
|
||||||
|
<Celda alineacionvertical=3D"I">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.=
|
||||||
|
3"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Otro m=C3=A1s, que ocupa dos l=C3=ADneas con qqq</Te=
|
||||||
|
xto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
<Celda alineacionvertical=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.1"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Y otro</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>-->
|
||||||
|
</Fila>
|
||||||
|
<Fila alto=3D"5">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"10"/>
|
||||||
|
<Celda alineacionvertical=3D"S" alineacionhorizontal=3D"I">
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#0000FF" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#0000FF" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#0000FF" grosor=3D"=
|
||||||
|
0.5"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#0000FF" grosor=3D"3.=
|
||||||
|
5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>C</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda><!--
|
||||||
|
<Celda alineacionvertical=3D"I">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.=
|
||||||
|
3"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Otro m=C3=A1s, que ocupa dos l=C3=ADneas con qqq</Te=
|
||||||
|
xto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
<Celda alineacionvertical=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.1"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Y otro</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>-->
|
||||||
|
</Fila>
|
||||||
|
</Filas>
|
||||||
|
<Fila alto=3D"5">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"10"/>
|
||||||
|
<Celda alineacionvertical=3D"S" alineacionhorizontal=3D"I">
|
||||||
|
<!--<Bordes>
|
||||||
|
<Borde ubicacion=3D"superior" color=3D"#0000FF" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#0000FF" grosor=3D"0=
|
||||||
|
.5"/>
|
||||||
|
<Borde ubicacion=3D"izquierdo" color=3D"#0000FF" grosor=3D"=
|
||||||
|
0.5"/>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#0000FF" grosor=3D"0.=
|
||||||
|
5"/>
|
||||||
|
</Bordes>-->
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>D</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda><!--
|
||||||
|
<Celda alineacionvertical=3D"I">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"derecho" color=3D"#000000" grosor=3D"0.=
|
||||||
|
3"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Otro m=C3=A1s, que ocupa dos l=C3=ADneas con qqq</Te=
|
||||||
|
xto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>
|
||||||
|
<Celda alineacionvertical=3D"C">
|
||||||
|
<Bordes>
|
||||||
|
<Borde ubicacion=3D"inferior" color=3D"#000000" grosor=3D"0=
|
||||||
|
.1"/>
|
||||||
|
</Bordes>
|
||||||
|
<Parrafo>
|
||||||
|
<Texto>Y otro</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Celda>-->
|
||||||
|
</Fila>
|
||||||
|
</Filas>
|
||||||
|
=20
|
||||||
|
</Tabla>
|
||||||
|
<Parrafo multilinea=3D"false" rellenarconpuntos=3D"true">
|
||||||
|
<Fuente nombre=3D"Helvetica" tamanio=3D"20"/>
|
||||||
|
<Texto>Esta es otra frase t=C3=ADpica</Texto>
|
||||||
|
</Parrafo>
|
||||||
|
</Contenido>
|
||||||
|
</Caratula>
|
||||||
|
</GrupoInformes>
|
||||||
|
|
||||||
|
------=_Part_16_799571960.1350659465464--
|
3
net-oauth/build.gradle
Normal file
3
net-oauth/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
api project(':net-http')
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.UrlStringRequestAdapter;
|
||||||
|
import org.xbib.net.oauth.sign.AuthorizationHeaderSigningStrategy;
|
||||||
|
import org.xbib.net.oauth.sign.HmacSha1MessageSigner;
|
||||||
|
import org.xbib.net.oauth.sign.OAuthMessageSigner;
|
||||||
|
import org.xbib.net.oauth.sign.QueryStringSigningStrategy;
|
||||||
|
import org.xbib.net.oauth.sign.SigningStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABC for consumer implementations. If you're developing a custom consumer you
|
||||||
|
* will probably inherit from this class to save you a lot of work.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public abstract class AbstractOAuthConsumer implements OAuthConsumer {
|
||||||
|
|
||||||
|
private String consumerKey, consumerSecret;
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
private OAuthMessageSigner messageSigner;
|
||||||
|
|
||||||
|
private SigningStrategy signingStrategy;
|
||||||
|
|
||||||
|
// these are params that may be passed to the consumer directly (i.e.
|
||||||
|
// without going through the request object)
|
||||||
|
private HttpParameters additionalParameters;
|
||||||
|
|
||||||
|
// these are the params which will be passed to the message signer
|
||||||
|
private HttpParameters requestParameters;
|
||||||
|
|
||||||
|
private boolean sendEmptyTokens;
|
||||||
|
|
||||||
|
private final Random random = new SecureRandom();
|
||||||
|
|
||||||
|
public AbstractOAuthConsumer(String consumerKey, String consumerSecret) {
|
||||||
|
this.consumerKey = consumerKey;
|
||||||
|
this.consumerSecret = consumerSecret;
|
||||||
|
setMessageSigner(new HmacSha1MessageSigner());
|
||||||
|
setSigningStrategy(new AuthorizationHeaderSigningStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageSigner(OAuthMessageSigner messageSigner) {
|
||||||
|
this.messageSigner = messageSigner;
|
||||||
|
messageSigner.setConsumerSecret(consumerSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSigningStrategy(SigningStrategy signingStrategy) {
|
||||||
|
this.signingStrategy = signingStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdditionalParameters(HttpParameters additionalParameters) {
|
||||||
|
this.additionalParameters = additionalParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException {
|
||||||
|
if (consumerKey == null) {
|
||||||
|
throw new OAuthExpectationFailedException("consumer key not set");
|
||||||
|
}
|
||||||
|
if (consumerSecret == null) {
|
||||||
|
throw new OAuthExpectationFailedException("consumer secret not set");
|
||||||
|
}
|
||||||
|
requestParameters = new HttpParameters();
|
||||||
|
try {
|
||||||
|
if (additionalParameters != null) {
|
||||||
|
requestParameters.putAll(additionalParameters, false);
|
||||||
|
}
|
||||||
|
collectHeaderParameters(request, requestParameters);
|
||||||
|
collectQueryParameters(request, requestParameters);
|
||||||
|
collectBodyParameters(request, requestParameters);
|
||||||
|
// add any OAuth params that haven't already been set
|
||||||
|
completeOAuthParameters(requestParameters);
|
||||||
|
requestParameters.remove(OAuth.OAUTH_SIGNATURE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OAuthCommunicationException(e);
|
||||||
|
}
|
||||||
|
String signature = messageSigner.sign(request, requestParameters);
|
||||||
|
try {
|
||||||
|
signingStrategy.writeSignature(signature, request, requestParameters);
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized HttpRequest sign(Object request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException {
|
||||||
|
return sign(wrap(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String sign(String url) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException {
|
||||||
|
HttpRequest request = new UrlStringRequestAdapter(url);
|
||||||
|
// switch to URL signing
|
||||||
|
SigningStrategy oldStrategy = this.signingStrategy;
|
||||||
|
this.signingStrategy = new QueryStringSigningStrategy();
|
||||||
|
sign(request);
|
||||||
|
// revert to old strategy
|
||||||
|
this.signingStrategy = oldStrategy;
|
||||||
|
return request.getRequestUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the given request object to a Signpost {@link HttpRequest}. How
|
||||||
|
* this is done depends on the consumer implementation.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the native HTTP request instance
|
||||||
|
* @return the adapted request
|
||||||
|
*/
|
||||||
|
protected abstract HttpRequest wrap(Object request);
|
||||||
|
|
||||||
|
public void setTokenWithSecret(String token, String tokenSecret) {
|
||||||
|
this.token = token;
|
||||||
|
messageSigner.setTokenSecret(tokenSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenSecret() {
|
||||||
|
return messageSigner.getTokenSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConsumerKey() {
|
||||||
|
return this.consumerKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConsumerSecret() {
|
||||||
|
return this.consumerSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Helper method that adds any OAuth parameters to the given request
|
||||||
|
* parameters which are missing from the current request but required for
|
||||||
|
* signing. A good example is the oauth_nonce parameter, which is typically
|
||||||
|
* not provided by the client in advance.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* It's probably not a very good idea to override this method. If you want
|
||||||
|
* to generate different nonces or timestamps, override
|
||||||
|
* {@link #generateNonce()} or {@link #generateTimestamp()} instead.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param out
|
||||||
|
* the request parameter which should be completed
|
||||||
|
*/
|
||||||
|
protected void completeOAuthParameters(HttpParameters out)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_CONSUMER_KEY)) {
|
||||||
|
out.put(OAuth.OAUTH_CONSUMER_KEY, consumerKey, true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) {
|
||||||
|
out.put(OAuth.OAUTH_SIGNATURE_METHOD, messageSigner.getSignatureMethod(), true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_TIMESTAMP)) {
|
||||||
|
out.put(OAuth.OAUTH_TIMESTAMP, generateTimestamp(), true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_NONCE)) {
|
||||||
|
out.put(OAuth.OAUTH_NONCE, generateNonce(), true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_VERSION)) {
|
||||||
|
out.put(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0, true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_TOKEN)) {
|
||||||
|
if (token != null && !token.equals("") || sendEmptyTokens) {
|
||||||
|
out.put(OAuth.OAUTH_TOKEN, token, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpParameters getRequestParameters() {
|
||||||
|
return requestParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSendEmptyTokens(boolean enable) {
|
||||||
|
this.sendEmptyTokens = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects OAuth Authorization header parameters as per OAuth Core 1.0 spec
|
||||||
|
* section 9.1.1
|
||||||
|
*/
|
||||||
|
protected void collectHeaderParameters(HttpRequest request, HttpParameters out)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
HttpParameters headerParams = OAuth.oauthHeaderToParamsMap(request.getHeader(OAuth.HTTP_AUTHORIZATION_HEADER));
|
||||||
|
out.putAll(headerParams, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects x-www-form-urlencoded body parameters as per OAuth Core 1.0 spec
|
||||||
|
* section 9.1.1
|
||||||
|
*/
|
||||||
|
protected void collectBodyParameters(HttpRequest request, HttpParameters out)
|
||||||
|
throws IOException {
|
||||||
|
// collect x-www-form-urlencoded body params
|
||||||
|
String contentType = request.getContentType();
|
||||||
|
if (contentType != null && contentType.startsWith(OAuth.FORM_ENCODED)) {
|
||||||
|
InputStream payload = request.getMessagePayload();
|
||||||
|
out.putAll(OAuth.decodeForm(payload), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects HTTP GET query string parameters as per OAuth Core 1.0 spec
|
||||||
|
* section 9.1.1
|
||||||
|
*/
|
||||||
|
protected void collectQueryParameters(HttpRequest request, HttpParameters out)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String url = request.getRequestUrl();
|
||||||
|
int q = url.indexOf('?');
|
||||||
|
if (q >= 0) {
|
||||||
|
// Combine the URL query string with the other parameters:
|
||||||
|
out.putAll(OAuth.decodeForm(url.substring(q + 1)), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generateTimestamp() {
|
||||||
|
return Long.toString(System.currentTimeMillis() / 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generateNonce() {
|
||||||
|
return Long.toString(random.nextLong());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,299 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For all provider implementations. If you're writing a custom provider,
|
||||||
|
* you will probably inherit from this class, since it takes a lot of work from
|
||||||
|
* you.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractOAuthProvider implements OAuthProvider {
|
||||||
|
|
||||||
|
private final String requestTokenEndpointUrl;
|
||||||
|
|
||||||
|
private final String accessTokenEndpointUrl;
|
||||||
|
|
||||||
|
private final String authorizationWebsiteUrl;
|
||||||
|
|
||||||
|
private HttpParameters responseParameters;
|
||||||
|
|
||||||
|
private Map<String, String> defaultHeaders;
|
||||||
|
|
||||||
|
private boolean isOAuth10a;
|
||||||
|
|
||||||
|
private OAuthProviderListener listener;
|
||||||
|
|
||||||
|
public AbstractOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl,
|
||||||
|
String authorizationWebsiteUrl) {
|
||||||
|
this.requestTokenEndpointUrl = requestTokenEndpointUrl;
|
||||||
|
this.accessTokenEndpointUrl = accessTokenEndpointUrl;
|
||||||
|
this.authorizationWebsiteUrl = authorizationWebsiteUrl;
|
||||||
|
this.responseParameters = new HttpParameters();
|
||||||
|
this.defaultHeaders = new HashMap<String, String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException {
|
||||||
|
// invalidate current credentials, if any
|
||||||
|
consumer.setTokenWithSecret(null, null);
|
||||||
|
// 1.0a expects the callback to be sent while getting the request token.
|
||||||
|
// 1.0 service providers would simply ignore this parameter.
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
try {
|
||||||
|
params.putAll(customOAuthParams, true);
|
||||||
|
params.put(OAuth.OAUTH_CALLBACK, callbackUrl, true);
|
||||||
|
retrieveToken(consumer, requestTokenEndpointUrl, params);
|
||||||
|
String callbackConfirmed = responseParameters.getFirst(OAuth.OAUTH_CALLBACK_CONFIRMED);
|
||||||
|
responseParameters.remove(OAuth.OAUTH_CALLBACK_CONFIRMED);
|
||||||
|
isOAuth10a = Boolean.TRUE.toString().equals(callbackConfirmed);
|
||||||
|
// 1.0 service providers expect the callback as part of the auth URL,
|
||||||
|
// Do not send when 1.0a.
|
||||||
|
if (isOAuth10a) {
|
||||||
|
return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN,
|
||||||
|
consumer.getToken());
|
||||||
|
} else {
|
||||||
|
return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN,
|
||||||
|
consumer.getToken(), OAuth.OAUTH_CALLBACK, callbackUrl);
|
||||||
|
}
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException {
|
||||||
|
if (consumer.getToken() == null || consumer.getTokenSecret() == null) {
|
||||||
|
throw new OAuthExpectationFailedException(
|
||||||
|
"Authorized request token or token secret not set. "
|
||||||
|
+ "Did you retrieve an authorized request token before?");
|
||||||
|
}
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
try {
|
||||||
|
params.putAll(customOAuthParams, true);
|
||||||
|
if (isOAuth10a && oauthVerifier != null) {
|
||||||
|
params.put(OAuth.OAUTH_VERIFIER, oauthVerifier, true);
|
||||||
|
}
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
retrieveToken(consumer, accessTokenEndpointUrl, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by subclasses. The responsibility of this method is to
|
||||||
|
* contact the service provider at the given endpoint URL and fetch a
|
||||||
|
* request or access token. What kind of token is retrieved solely depends
|
||||||
|
* on the URL being used.
|
||||||
|
* Correct implementations of this method must guarantee the following
|
||||||
|
* post-conditions:
|
||||||
|
* <ul>
|
||||||
|
* <li>the {@link OAuthConsumer} passed to this method must have a valid
|
||||||
|
* {@link OAuth#OAUTH_TOKEN} and {@link OAuth#OAUTH_TOKEN_SECRET} set by
|
||||||
|
* calling {@link OAuthConsumer#setTokenWithSecret(String, String)}</li>
|
||||||
|
* <li>{@link #getResponseParameters()} must return the set of query
|
||||||
|
* parameters served by the service provider in the token response, with all
|
||||||
|
* OAuth specific parameters being removed</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param consumer the {@link OAuthConsumer} that should be used to sign the request
|
||||||
|
* @param endpointUrl the URL at which the service provider serves the OAuth token that
|
||||||
|
* is to be fetched
|
||||||
|
* @param customOAuthParams you can pass custom OAuth parameters here (such as oauth_callback
|
||||||
|
* or oauth_verifier) which will go directly into the signer, i.e.
|
||||||
|
* you don't have to put them into the request first.
|
||||||
|
* @throws OAuthMessageSignerException if signing the token request fails
|
||||||
|
* @throws OAuthCommunicationException if a network communication error occurs
|
||||||
|
* @throws OAuthNotAuthorizedException if the server replies 401 - Unauthorized
|
||||||
|
* @throws OAuthExpectationFailedException if an expectation has failed, e.g. because the server didn't
|
||||||
|
* reply in the expected format
|
||||||
|
*/
|
||||||
|
protected void retrieveToken(OAuthConsumer consumer, String endpointUrl,
|
||||||
|
HttpParameters customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthCommunicationException, OAuthNotAuthorizedException,
|
||||||
|
OAuthExpectationFailedException {
|
||||||
|
Map<String, String> defaultHeaders = getRequestHeaders();
|
||||||
|
if (consumer.getConsumerKey() == null || consumer.getConsumerSecret() == null) {
|
||||||
|
throw new OAuthExpectationFailedException("Consumer key or secret not set");
|
||||||
|
}
|
||||||
|
HttpRequest request = null;
|
||||||
|
HttpResponse response = null;
|
||||||
|
try {
|
||||||
|
request = createRequest(endpointUrl);
|
||||||
|
for (String header : defaultHeaders.keySet()) {
|
||||||
|
request.setHeader(header, defaultHeaders.get(header));
|
||||||
|
}
|
||||||
|
if (customOAuthParams != null && !customOAuthParams.isEmpty()) {
|
||||||
|
consumer.setAdditionalParameters(customOAuthParams);
|
||||||
|
}
|
||||||
|
if (this.listener != null) {
|
||||||
|
this.listener.prepareRequest(request);
|
||||||
|
}
|
||||||
|
consumer.sign(request);
|
||||||
|
if (this.listener != null) {
|
||||||
|
this.listener.prepareSubmission(request);
|
||||||
|
}
|
||||||
|
response = sendRequest(request);
|
||||||
|
int statusCode = response.getStatusCode();
|
||||||
|
boolean requestHandled = false;
|
||||||
|
if (this.listener != null) {
|
||||||
|
requestHandled = this.listener.onResponseReceived(request, response);
|
||||||
|
}
|
||||||
|
if (requestHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusCode >= 300) {
|
||||||
|
handleUnexpectedResponse(statusCode, response);
|
||||||
|
}
|
||||||
|
HttpParameters responseParams = OAuth.decodeForm(response.getContent());
|
||||||
|
String token = responseParams.getFirst(OAuth.OAUTH_TOKEN);
|
||||||
|
String secret = responseParams.getFirst(OAuth.OAUTH_TOKEN_SECRET);
|
||||||
|
responseParams.remove(OAuth.OAUTH_TOKEN);
|
||||||
|
responseParams.remove(OAuth.OAUTH_TOKEN_SECRET);
|
||||||
|
setResponseParameters(responseParams);
|
||||||
|
if (token == null || secret == null) {
|
||||||
|
throw new OAuthExpectationFailedException(
|
||||||
|
"Request token or token secret not set in server reply. "
|
||||||
|
+ "The service provider you use is probably buggy.");
|
||||||
|
}
|
||||||
|
consumer.setTokenWithSecret(token, secret);
|
||||||
|
} catch (OAuthNotAuthorizedException | OAuthExpectationFailedException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OAuthCommunicationException(e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
closeConnection(request, response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OAuthCommunicationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleUnexpectedResponse(int statusCode, HttpResponse response) throws Exception {
|
||||||
|
if (response == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getContent()));
|
||||||
|
StringBuilder responseBody = new StringBuilder();
|
||||||
|
String line = reader.readLine();
|
||||||
|
while (line != null) {
|
||||||
|
responseBody.append(line);
|
||||||
|
line = reader.readLine();
|
||||||
|
}
|
||||||
|
if (statusCode == 401) {
|
||||||
|
throw new OAuthNotAuthorizedException(responseBody.toString());
|
||||||
|
}
|
||||||
|
throw new OAuthCommunicationException("Service provider responded in error: "
|
||||||
|
+ statusCode + " (" + response.getReasonPhrase() + ")", responseBody.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrride this method if you want to customize the logic for building a
|
||||||
|
* request object for the given endpoint URL.
|
||||||
|
*
|
||||||
|
* @param endpointUrl
|
||||||
|
* the URL to which the request will go
|
||||||
|
* @return the request object
|
||||||
|
* @throws Exception
|
||||||
|
* if something breaks
|
||||||
|
*/
|
||||||
|
protected abstract HttpRequest createRequest(String endpointUrl) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method if you want to customize the logic for how the given
|
||||||
|
* request is sent to the server.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to send
|
||||||
|
* @return the response to the request
|
||||||
|
* @throws Exception
|
||||||
|
* if something breaks
|
||||||
|
*/
|
||||||
|
protected abstract HttpResponse sendRequest(HttpRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the connection is being finalized after receiving the
|
||||||
|
* response. Use this to do any cleanup / resource freeing.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request that has been sent
|
||||||
|
* @param response
|
||||||
|
* the response that has been received
|
||||||
|
* @throws Exception
|
||||||
|
* if something breaks
|
||||||
|
*/
|
||||||
|
protected void closeConnection(HttpRequest request, HttpResponse response) throws Exception {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpParameters getResponseParameters() {
|
||||||
|
return responseParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single query parameter as served by the service provider in a
|
||||||
|
* token reply. You must call {@link #setResponseParameters} with the set of
|
||||||
|
* parameters before using this method.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* the parameter name
|
||||||
|
* @return the parameter value
|
||||||
|
*/
|
||||||
|
protected String getResponseParameter(String key)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return responseParameters.getFirst(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResponseParameters(HttpParameters parameters) {
|
||||||
|
this.responseParameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOAuth10a(boolean isOAuth10aProvider) {
|
||||||
|
this.isOAuth10a = isOAuth10aProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOAuth10a() {
|
||||||
|
return isOAuth10a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestTokenEndpointUrl() {
|
||||||
|
return this.requestTokenEndpointUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessTokenEndpointUrl() {
|
||||||
|
return this.accessTokenEndpointUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationWebsiteUrl() {
|
||||||
|
return this.authorizationWebsiteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestHeader(String header, String value) {
|
||||||
|
defaultHeaders.put(header, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getRequestHeaders() {
|
||||||
|
return defaultHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(OAuthProviderListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(OAuthProviderListener listener) {
|
||||||
|
this.listener = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation for an OAuth consumer. Only supports signing
|
||||||
|
* {@link HttpURLConnection} type requests.
|
||||||
|
*/
|
||||||
|
public class DefaultOAuthConsumer extends AbstractOAuthConsumer {
|
||||||
|
|
||||||
|
public DefaultOAuthConsumer(String consumerKey, String consumerSecret) {
|
||||||
|
super(consumerKey, consumerSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpRequest wrap(Object request) {
|
||||||
|
if (!(request instanceof HttpURLConnection)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The default consumer expects requests of type java.net.HttpURLConnection");
|
||||||
|
}
|
||||||
|
return new HttpURLConnectionRequestAdapter((HttpURLConnection) request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This default implementation uses {@link HttpURLConnection} type GET
|
||||||
|
* requests to receive elements from a service provider.
|
||||||
|
*/
|
||||||
|
public class DefaultOAuthProvider extends AbstractOAuthProvider {
|
||||||
|
|
||||||
|
public DefaultOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl,
|
||||||
|
String authorizationWebsiteUrl) {
|
||||||
|
super(requestTokenEndpointUrl, accessTokenEndpointUrl, authorizationWebsiteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpRequest createRequest(String endpointUrl) throws IOException {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) new URL(endpointUrl).openConnection();
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setAllowUserInteraction(false);
|
||||||
|
connection.setRequestProperty("Content-Length", "0");
|
||||||
|
return new HttpURLConnectionRequestAdapter(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpResponse sendRequest(HttpRequest request) throws IOException {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) request.unwrap();
|
||||||
|
connection.connect();
|
||||||
|
return new HttpURLConnectionResponseAdapter(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void closeConnection(HttpRequest request, HttpResponse response) {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) request.unwrap();
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class HttpURLConnectionRequestAdapter implements HttpRequest {
|
||||||
|
|
||||||
|
protected HttpURLConnection connection;
|
||||||
|
|
||||||
|
public HttpURLConnectionRequestAdapter(HttpURLConnection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMethod() {
|
||||||
|
return connection.getRequestMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestUrl() {
|
||||||
|
return connection.getURL().toExternalForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestUrl(String url) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeader(String name, String value) {
|
||||||
|
connection.setRequestProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHeader(String name) {
|
||||||
|
return connection.getRequestProperty(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAllHeaders() {
|
||||||
|
Map<String, List<String>> origHeaders = connection.getRequestProperties();
|
||||||
|
Map<String, String> headers = new HashMap<String, String>(origHeaders.size());
|
||||||
|
for (String name : origHeaders.keySet()) {
|
||||||
|
List<String> values = origHeaders.get(name);
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
headers.put(name, values.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getMessagePayload() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return connection.getRequestProperty("Content-Type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpURLConnection unwrap() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
public class HttpURLConnectionResponseAdapter implements HttpResponse {
|
||||||
|
|
||||||
|
private HttpURLConnection connection;
|
||||||
|
|
||||||
|
public HttpURLConnectionResponseAdapter(HttpURLConnection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getContent() {
|
||||||
|
try {
|
||||||
|
return connection.getInputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return connection.getErrorStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStatusCode() throws IOException {
|
||||||
|
return connection.getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReasonPhrase() throws Exception {
|
||||||
|
return connection.getResponseMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object unwrap() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
285
net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java
Normal file
285
net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.PercentDecoder;
|
||||||
|
import org.xbib.net.PercentEncoder;
|
||||||
|
import org.xbib.net.PercentEncoders;
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OAuth {
|
||||||
|
|
||||||
|
public static final String VERSION_1_0 = "1.0";
|
||||||
|
|
||||||
|
public static final String ENCODING = "UTF-8";
|
||||||
|
|
||||||
|
public static final String FORM_ENCODED = "application/x-www-form-urlencoded";
|
||||||
|
|
||||||
|
public static final String HTTP_AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
|
public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
|
||||||
|
|
||||||
|
public static final String OAUTH_TOKEN = "oauth_token";
|
||||||
|
|
||||||
|
public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret";
|
||||||
|
|
||||||
|
public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
|
||||||
|
|
||||||
|
public static final String OAUTH_SIGNATURE = "oauth_signature";
|
||||||
|
|
||||||
|
public static final String OAUTH_TIMESTAMP = "oauth_timestamp";
|
||||||
|
|
||||||
|
public static final String OAUTH_NONCE = "oauth_nonce";
|
||||||
|
|
||||||
|
public static final String OAUTH_VERSION = "oauth_version";
|
||||||
|
|
||||||
|
public static final String OAUTH_CALLBACK = "oauth_callback";
|
||||||
|
|
||||||
|
public static final String OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed";
|
||||||
|
|
||||||
|
public static final String OAUTH_VERIFIER = "oauth_verifier";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass this value as the callback "url" upon retrieving a request token if
|
||||||
|
* your application cannot receive callbacks (e.g. because it's a desktop
|
||||||
|
* app). This will tell the service provider that verification happens
|
||||||
|
* out-of-band, which basically means that it will generate a PIN code (the
|
||||||
|
* OAuth verifier) and display that to your user. You must obtain this code
|
||||||
|
* from your user and pass it to
|
||||||
|
* {@link OAuthProvider#retrieveAccessToken(OAuthConsumer, String, String...)} in order
|
||||||
|
* to complete the token handshake.
|
||||||
|
*/
|
||||||
|
public static final String OUT_OF_BAND = "oob";
|
||||||
|
|
||||||
|
public static final PercentEncoder percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
public static final PercentDecoder percentDecoder = new PercentDecoder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a x-www-form-urlencoded document containing the given sequence
|
||||||
|
* of name/value pairs. Use OAuth percent encoding (not exactly the encoding
|
||||||
|
* mandated by x-www-form-urlencoded).
|
||||||
|
*/
|
||||||
|
public static <T extends Map.Entry<String, String>> void formEncode(Collection<T> parameters,
|
||||||
|
OutputStream into) throws IOException {
|
||||||
|
if (parameters != null) {
|
||||||
|
boolean first = true;
|
||||||
|
for (Map.Entry<String, String> entry : parameters) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
into.write('&');
|
||||||
|
}
|
||||||
|
into.write(percentEncoder.encode(safeToString(entry.getKey())).getBytes());
|
||||||
|
into.write('=');
|
||||||
|
into.write(percentEncoder.encode(safeToString(entry.getValue())).getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a x-www-form-urlencoded document containing the given sequence
|
||||||
|
* of name/value pairs. Use OAuth percent encoding (not exactly the encoding
|
||||||
|
* mandated by x-www-form-urlencoded).
|
||||||
|
*/
|
||||||
|
public static <T extends Map.Entry<String, String>> String formEncode(Collection<T> parameters)
|
||||||
|
throws IOException {
|
||||||
|
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||||
|
formEncode(parameters, b);
|
||||||
|
return new String(b.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a form-urlencoded document.
|
||||||
|
*/
|
||||||
|
public static HttpParameters decodeForm(String form)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
if (isEmpty(form)) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
for (String nvp : form.split("\\&")) {
|
||||||
|
int equals = nvp.indexOf('=');
|
||||||
|
String name;
|
||||||
|
String value;
|
||||||
|
if (equals < 0) {
|
||||||
|
name = percentDecoder.decode(nvp);
|
||||||
|
value = null;
|
||||||
|
} else {
|
||||||
|
name = percentDecoder.decode(nvp.substring(0, equals));
|
||||||
|
value = percentDecoder.decode(nvp.substring(equals + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
params.put(name, value);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpParameters decodeForm(InputStream content)
|
||||||
|
throws IOException {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||||
|
content));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line = reader.readLine();
|
||||||
|
while (line != null) {
|
||||||
|
sb.append(line);
|
||||||
|
line = reader.readLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeForm(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a Map containing a copy of the given parameters. If several
|
||||||
|
* parameters have the same name, the Map will contain the first value,
|
||||||
|
* only.
|
||||||
|
*/
|
||||||
|
public static <T extends Map.Entry<String, String>> Map<String, String> toMap(Collection<T> from) {
|
||||||
|
HashMap<String, String> map = new HashMap<String, String>();
|
||||||
|
if (from != null) {
|
||||||
|
for (Map.Entry<String, String> entry : from) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (!map.containsKey(key)) {
|
||||||
|
map.put(key, entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String safeToString(Object from) {
|
||||||
|
return (from == null) ? null : from.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEmpty(String str) {
|
||||||
|
return (str == null) || (str.length() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a list of key/value pairs to the given URL, e.g.:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* String url = OAuth.addQueryParameters("http://example.com?a=1", b, 2, c, 3);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* which yields:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* http://example.com?a=1&b=2&c=3
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* All parameters will be encoded according to OAuth's percent encoding
|
||||||
|
* rules.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the URL
|
||||||
|
* @param kvPairs
|
||||||
|
* the list of key/value pairs
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static String addQueryParameters(String url, String... kvPairs)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String queryDelim = url.contains("?") ? "&" : "?";
|
||||||
|
StringBuilder sb = new StringBuilder(url + queryDelim);
|
||||||
|
for (int i = 0; i < kvPairs.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append(percentEncoder.encode(kvPairs[i])).append("=")
|
||||||
|
.append(percentEncoder.encode(kvPairs[i + 1]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String addQueryParameters(String url, Map<String, String> params)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String[] kvPairs = new String[params.size() * 2];
|
||||||
|
int idx = 0;
|
||||||
|
for (String key : params.keySet()) {
|
||||||
|
kvPairs[idx] = key;
|
||||||
|
kvPairs[idx + 1] = params.get(key);
|
||||||
|
idx += 2;
|
||||||
|
}
|
||||||
|
return addQueryParameters(url, kvPairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String addQueryString(String url, String queryString) {
|
||||||
|
String queryDelim = url.contains("?") ? "&" : "?";
|
||||||
|
return url + queryDelim + queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an OAuth header from the given list of header fields. All
|
||||||
|
* parameters starting in 'oauth_*' will be percent encoded.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* String authHeader = OAuth.prepareOAuthHeader("realm", "http://example.com", "oauth_token", "x%y");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* which yields:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* OAuth realm="http://example.com", oauth_token="x%25y"
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param kvPairs
|
||||||
|
* the list of key/value pairs
|
||||||
|
* @return a string eligible to be used as an OAuth HTTP Authorization
|
||||||
|
* header.
|
||||||
|
*/
|
||||||
|
public static String prepareOAuthHeader(String... kvPairs)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
StringBuilder sb = new StringBuilder("OAuth ");
|
||||||
|
for (int i = 0; i < kvPairs.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
boolean isOAuthElem = kvPairs[i].startsWith("oauth_")
|
||||||
|
|| kvPairs[i].startsWith("x_oauth_");
|
||||||
|
String value = isOAuthElem ? percentEncoder.encode(kvPairs[i + 1]) : kvPairs[i + 1];
|
||||||
|
sb.append(percentEncoder.encode(kvPairs[i])).append("=\"").append(value).append("\"");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpParameters oauthHeaderToParamsMap(String oauthHeader)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
if (oauthHeader == null || !oauthHeader.startsWith("OAuth ")) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
String[] elements = oauthHeader.substring("OAuth ".length()).split(",");
|
||||||
|
for (String keyValuePair : elements) {
|
||||||
|
String[] keyValue = keyValuePair.split("=");
|
||||||
|
params.put(keyValue[0].trim(), keyValue[1].replace("\"", "").trim());
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to concatenate a parameter and its value to a pair that can
|
||||||
|
* be used in an HTTP header. This method percent encodes both parts before
|
||||||
|
* joining them.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* the OAuth parameter name, e.g. oauth_token
|
||||||
|
* @param value
|
||||||
|
* the OAuth parameter value, e.g. 'hello oauth'
|
||||||
|
* @return a name/value pair, e.g. oauth_token="hello%20oauth"
|
||||||
|
*/
|
||||||
|
public static String toHeaderElement(String name, String value)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return percentEncoder.encode(name) + "=\"" + percentEncoder.encode(value) + "\"";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthCommunicationException extends OAuthException {
|
||||||
|
|
||||||
|
private String responseBody;
|
||||||
|
|
||||||
|
public OAuthCommunicationException(Exception cause) {
|
||||||
|
super("Communication with the service provider failed: "
|
||||||
|
+ cause.getLocalizedMessage(), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthCommunicationException(String message, String responseBody) {
|
||||||
|
super(message);
|
||||||
|
this.responseBody = responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResponseBody() {
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
}
|
157
net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java
Normal file
157
net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.sign.OAuthMessageSigner;
|
||||||
|
import org.xbib.net.oauth.sign.SigningStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Exposes a simple interface to sign HTTP requests using a given OAuth token
|
||||||
|
* and secret. Refer to {@link OAuthProvider} how to retrieve a valid token and
|
||||||
|
* token secret.
|
||||||
|
* </p>
|
||||||
|
* HTTP messages are signed as follows:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* // exchange the arguments with the actual token/secret pair
|
||||||
|
* OAuthConsumer consumer = new DefaultOAuthConsumer("1234", "5678");
|
||||||
|
* URL url = new URL("http://example.com/protected.xml");
|
||||||
|
* HttpURLConnection request = (HttpURLConnection) url.openConnection();
|
||||||
|
* consumer.sign(request);
|
||||||
|
* request.connect();
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface OAuthConsumer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the message signer that should be used to generate the OAuth
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* @param messageSigner
|
||||||
|
* the signer
|
||||||
|
*/
|
||||||
|
void setMessageSigner(OAuthMessageSigner messageSigner);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to add parameters (typically OAuth parameters such as
|
||||||
|
* oauth_callback or oauth_verifier) which will go directly into the signer,
|
||||||
|
* i.e. you don't have to put them into the request first. The consumer's
|
||||||
|
* signing strategy will then take care of writing them to the
|
||||||
|
* correct part of the request before it is sent. This is useful if you want
|
||||||
|
* to pre-set custom OAuth parameters. Note that these parameters are
|
||||||
|
* expected to already be percent encoded -- they will be simply merged
|
||||||
|
* as-is. <b>BE CAREFUL WITH THIS METHOD! Your service provider may decide
|
||||||
|
* to ignore any non-standard OAuth params when computing the signature.</b>
|
||||||
|
*
|
||||||
|
* @param additionalParameters
|
||||||
|
* the parameters
|
||||||
|
*/
|
||||||
|
void setAdditionalParameters(HttpParameters additionalParameters);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines which strategy should be used to write a signature to an HTTP
|
||||||
|
* request.
|
||||||
|
*
|
||||||
|
* @param signingStrategy
|
||||||
|
* the strategy
|
||||||
|
*/
|
||||||
|
void setSigningStrategy(SigningStrategy signingStrategy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Causes the consumer to always include the oauth_token parameter to be
|
||||||
|
* sent, even if blank. If you're seeing 401s during calls to
|
||||||
|
* {@link OAuthProvider#retrieveRequestToken}, try setting this to true.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param enable
|
||||||
|
* true or false
|
||||||
|
*/
|
||||||
|
void setSendEmptyTokens(boolean enable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the given HTTP request by writing an OAuth signature (and other
|
||||||
|
* required OAuth parameters) to it. Where these parameters are written
|
||||||
|
* depends on the current {@link SigningStrategy}.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to sign
|
||||||
|
* @return the request object passed as an argument
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
*/
|
||||||
|
HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Signs the given HTTP request by writing an OAuth signature (and other
|
||||||
|
* required OAuth parameters) to it. Where these parameters are written
|
||||||
|
* depends on the current {@link SigningStrategy}.
|
||||||
|
* </p>
|
||||||
|
* This method accepts HTTP library specific request objects; the consumer
|
||||||
|
* implementation must ensure that only those request types are passed which
|
||||||
|
* it supports.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to sign
|
||||||
|
* @return the request object passed as an argument
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
*/
|
||||||
|
HttpRequest sign(Object request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Signs" the given URL by appending all OAuth parameters to it which are
|
||||||
|
* required for message signing. The assumed HTTP method is GET.
|
||||||
|
* Essentially, this is equivalent to signing an HTTP GET request, but it
|
||||||
|
* can be useful if your application requires clickable links to protected
|
||||||
|
* resources, i.e. when your application does not have access to the actual
|
||||||
|
* request that is being sent.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the input URL. May have query parameters.
|
||||||
|
* @return the input URL, with all necessary OAuth parameters attached as a
|
||||||
|
* query string. Existing query parameters are preserved.
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
*/
|
||||||
|
String sign(String url) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the OAuth token and token secret used for message signing.
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* the token
|
||||||
|
* @param tokenSecret
|
||||||
|
* the token secret
|
||||||
|
*/
|
||||||
|
void setTokenWithSecret(String token, String tokenSecret);
|
||||||
|
|
||||||
|
String getToken();
|
||||||
|
|
||||||
|
String getTokenSecret();
|
||||||
|
|
||||||
|
String getConsumerKey();
|
||||||
|
|
||||||
|
String getConsumerSecret();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all parameters collected from the HTTP request during message
|
||||||
|
* signing (this means the return value may be NULL before a call to
|
||||||
|
* {@link #sign}), plus all required OAuth parameters that were added
|
||||||
|
* because the request didn't contain them beforehand. In other words, this
|
||||||
|
* is the exact set of parameters that were used for creating the message
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* @return the request parameters used for message signing
|
||||||
|
*/
|
||||||
|
HttpParameters getRequestParameters();
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public abstract class OAuthException extends Exception {
|
||||||
|
|
||||||
|
public OAuthException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthExpectationFailedException extends OAuthException {
|
||||||
|
|
||||||
|
public OAuthExpectationFailedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthMessageSignerException extends OAuthException {
|
||||||
|
|
||||||
|
public OAuthMessageSignerException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthMessageSignerException(Exception cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthNotAuthorizedException extends OAuthException {
|
||||||
|
|
||||||
|
private static final String ERROR = "Authorization failed (server replied with a 401). "
|
||||||
|
+ "This can happen if the consumer key was not correct or "
|
||||||
|
+ "the signatures did not match.";
|
||||||
|
|
||||||
|
private String responseBody;
|
||||||
|
|
||||||
|
public OAuthNotAuthorizedException() {
|
||||||
|
super(ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthNotAuthorizedException(String responseBody) {
|
||||||
|
super(ERROR);
|
||||||
|
this.responseBody = responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResponseBody() {
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
}
|
206
net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java
Normal file
206
net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Supplies an interface that can be used to retrieve request and access elements
|
||||||
|
* from an OAuth 1.0(a) service provider. A provider object requires an
|
||||||
|
* {@link OAuthConsumer} to sign the token request message; after a token has
|
||||||
|
* been retrieved, the consumer is automatically updated with the token and the
|
||||||
|
* corresponding secret.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* To initiate the token exchange, create a new provider instance and configure
|
||||||
|
* it with the URLs the service provider exposes for requesting elements and
|
||||||
|
* resource authorization, e.g.:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* OAuthProvider provider = new DefaultOAuthProvider("http://twitter.com/oauth/request_token",
|
||||||
|
* "http://twitter.com/oauth/access_token", "http://twitter.com/oauth/authorize");
|
||||||
|
* </pre>
|
||||||
|
* <p>
|
||||||
|
* Depending on the HTTP library you use, you may need a different provider
|
||||||
|
* type, refer to the website documentation for how to do that.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* To receive a request token which the user must authorize, you invoke it using
|
||||||
|
* a consumer instance and a callback URL:
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* String url = provider.retrieveRequestToken(consumer, "http://www.example.com/callback");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* That url must be opened in a Web browser, where the user can grant access to
|
||||||
|
* the resources in question. If that succeeds, the service provider will
|
||||||
|
* redirect to the callback URL and append the blessed request token.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* That token must now be exchanged for an access token, as such:
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* provider.retrieveAccessToken(consumer, nullOrVerifierCode);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* where nullOrVerifierCode is either null if your provided a callback URL in
|
||||||
|
* the previous step, or the pin code issued by the service provider to the user
|
||||||
|
* if the request was out-of-band (cf. {@link OAuth#OUT_OF_BAND}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The consumer used during token handshakes is now ready for signing.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see OAuthProviderListener
|
||||||
|
*/
|
||||||
|
public interface OAuthProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the service provider for a request token.
|
||||||
|
* <p>
|
||||||
|
* <b>Pre-conditions:</b> the given {@link OAuthConsumer} must have a valid
|
||||||
|
* consumer key and consumer secret already set.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Post-conditions:</b> the given {@link OAuthConsumer} will have an
|
||||||
|
* unauthorized request token and token secret set.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param consumer
|
||||||
|
* the {@link OAuthConsumer} that should be used to sign the request
|
||||||
|
* @param callbackUrl
|
||||||
|
* Pass an actual URL if your app can receive callbacks and you want
|
||||||
|
* to get informed about the result of the authorization process.
|
||||||
|
* Pass OUT_OF_BAND if the service provider implements
|
||||||
|
* OAuth 1.0a and your app cannot receive callbacks. Pass null if the
|
||||||
|
* service provider implements OAuth 1.0 and your app cannot receive
|
||||||
|
* callbacks. Please note that some services (among them Twitter)
|
||||||
|
* will fail authorization if you pass a callback URL but register
|
||||||
|
* your application as a desktop app (which would only be able to
|
||||||
|
* handle OOB requests).
|
||||||
|
* @param customOAuthParams
|
||||||
|
* you can pass custom OAuth parameters here which will go directly
|
||||||
|
* into the signer, i.e. you don't have to put them into the request
|
||||||
|
* first. This is useful for pre-setting OAuth params for signing.
|
||||||
|
* Pass them sequentially in key/value order.
|
||||||
|
* @return The URL to which the user must be sent in order to authorize the
|
||||||
|
* consumer. It includes the unauthorized request token (and in the
|
||||||
|
* case of OAuth 1.0, the callback URL -- 1.0a clients send along
|
||||||
|
* with the token request).
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* if signing the request failed
|
||||||
|
* @throws OAuthNotAuthorizedException
|
||||||
|
* if the service provider rejected the consumer
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* if required parameters were not correctly set by the consumer or
|
||||||
|
* service provider
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
* if server communication failed
|
||||||
|
*/
|
||||||
|
String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the service provider for an access token.
|
||||||
|
* <p>
|
||||||
|
* <b>Pre-conditions:</b> the given {@link OAuthConsumer} must have a valid
|
||||||
|
* consumer key, consumer secret, authorized request token and token secret
|
||||||
|
* already set.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Post-conditions:</b> the given {@link OAuthConsumer} will have an
|
||||||
|
* access token and token secret set.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param consumer
|
||||||
|
* the {@link OAuthConsumer} that should be used to sign the request
|
||||||
|
* @param oauthVerifier
|
||||||
|
* <b>NOTE: Only applies to service providers implementing OAuth
|
||||||
|
* 1.0a. Set to null if the service provider is still using OAuth
|
||||||
|
* 1.0.</b> The verification code issued by the service provider
|
||||||
|
* after the the user has granted the consumer authorization. If the
|
||||||
|
* callback method provided in the previous step was
|
||||||
|
* OUT_OF_BAND, then you must ask the user for this
|
||||||
|
* value. If your app has received a callback, the verfication code
|
||||||
|
* was passed as part of that request instead.
|
||||||
|
* @param customOAuthParams
|
||||||
|
* you can pass custom OAuth parameters here which will go directly
|
||||||
|
* into the signer, i.e. you don't have to put them into the request
|
||||||
|
* first. This is useful for pre-setting OAuth params for signing.
|
||||||
|
* Pass them sequentially in key/value order.
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* if signing the request failed
|
||||||
|
* @throws OAuthNotAuthorizedException
|
||||||
|
* if the service provider rejected the consumer
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* if required parameters were not correctly set by the consumer or
|
||||||
|
* service provider
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
* if server communication failed
|
||||||
|
*/
|
||||||
|
void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any additional non-OAuth parameters returned in the response body of a
|
||||||
|
* token request can be obtained through this method. These parameters will
|
||||||
|
* be preserved until the next token request is issued. The return value is
|
||||||
|
* never null.
|
||||||
|
*/
|
||||||
|
HttpParameters getResponseParameters();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses must use this setter to preserve any non-OAuth query
|
||||||
|
* parameters contained in the server response. It's the caller's
|
||||||
|
* responsibility that any OAuth parameters be removed beforehand.
|
||||||
|
*
|
||||||
|
* @param parameters
|
||||||
|
* the map of query parameters served by the service provider in the
|
||||||
|
* token response
|
||||||
|
*/
|
||||||
|
void setResponseParameters(HttpParameters parameters);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isOAuth10aProvider
|
||||||
|
* set to true if the service provider supports OAuth 1.0a. Note that
|
||||||
|
* you need only call this method if you reconstruct a provider
|
||||||
|
* object in between calls to retrieveRequestToken() and
|
||||||
|
* retrieveAccessToken() (i.e. if the object state isn't preserved).
|
||||||
|
* If instead those two methods are called on the same provider
|
||||||
|
* instance, this flag will be deducted automatically based on the
|
||||||
|
* server response during retrieveRequestToken(), so you can simply
|
||||||
|
* ignore this method.
|
||||||
|
*/
|
||||||
|
void setOAuth10a(boolean isOAuth10aProvider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the service provider supports OAuth 1.0a. Note that the
|
||||||
|
* value returned here is only meaningful after you have already
|
||||||
|
* performed the token handshake, otherwise there is no way to
|
||||||
|
* determine what version of the OAuth protocol the service provider
|
||||||
|
* implements.
|
||||||
|
*/
|
||||||
|
boolean isOAuth10a();
|
||||||
|
|
||||||
|
String getRequestTokenEndpointUrl();
|
||||||
|
|
||||||
|
String getAccessTokenEndpointUrl();
|
||||||
|
|
||||||
|
String getAuthorizationWebsiteUrl();
|
||||||
|
|
||||||
|
void setListener(OAuthProviderListener listener);
|
||||||
|
|
||||||
|
void removeListener(OAuthProviderListener listener);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides hooks into the token request handling procedure executed by
|
||||||
|
* {@link OAuthProvider}.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface OAuthProviderListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the request has been created and default headers added, but
|
||||||
|
* before the request has been signed.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to be sent
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
void prepareRequest(HttpRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the request has been signed, but before it's being sent.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to be sent
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
void prepareSubmission(HttpRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the server response has been received. You can implement this
|
||||||
|
* to manually handle the response data.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request that was sent
|
||||||
|
* @param response
|
||||||
|
* the response that was received
|
||||||
|
* @return returning true means you have handled the response, and the
|
||||||
|
* provider will return immediately. Return false to let the event
|
||||||
|
* propagate and let the provider execute its default response
|
||||||
|
* handling.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
boolean onResponseReceived(HttpRequest request, HttpResponse response) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to the HTTP Authorization header field.
|
||||||
|
*/
|
||||||
|
public class AuthorizationHeaderSigningStrategy implements SigningStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String writeSignature(String signature, HttpRequest request,
|
||||||
|
HttpParameters requestParameters) throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("OAuth ");
|
||||||
|
// add the realm parameter, if any
|
||||||
|
if (requestParameters.containsKey("realm")) {
|
||||||
|
sb.append(requestParameters.getAsHeaderElement("realm"));
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
// add all (x_)oauth parameters
|
||||||
|
HttpParameters oauthParams = requestParameters.getOAuthParameters();
|
||||||
|
oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true);
|
||||||
|
Iterator<String> iterator = oauthParams.keySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
String key = iterator.next();
|
||||||
|
sb.append(oauthParams.getAsHeaderElement(key));
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String header = sb.toString();
|
||||||
|
request.setHeader(OAuth.HTTP_AUTHORIZATION_HEADER, header);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue