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