initial commit

This commit is contained in:
Jörg Prante 2022-10-20 09:43:33 +02:00
commit a8d46c86f7
490 changed files with 56530 additions and 0 deletions

80
.github/workflows/codeql.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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')
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View 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
View 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')

View 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
View file

@ -0,0 +1,5 @@
group = org.xbib
name = net
version = 3.0.0
org.gradle.warning.mode = ALL

View 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')
}

View 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
View 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
View 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
}
}
}
}
}

View 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"
}
}
}
}

View 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"
}
}

View 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
}
}
}

View file

@ -0,0 +1,4 @@
repositories {
mavenLocal()
mavenCentral()
}

22
gradle/test/jmh.gradle Normal file
View 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
View 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

Binary file not shown.

View 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
View 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
View 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

View file

@ -0,0 +1,4 @@
dependencies {
api project(':net-security')
api libs.bouncycastle
}

View 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;
}

View file

@ -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);
}
}
}

View file

@ -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');
}
}
}

View file

@ -0,0 +1 @@
org.bouncycastle.jce.provider.BouncyCastleProvider

View file

@ -0,0 +1 @@
org.xbib.net.bouncycastle.BouncyCastleCertificateProvider

View file

@ -0,0 +1,5 @@
module org.xbib.net.mime {
exports org.xbib.net.mime;
exports org.xbib.net.mime.stream;
requires java.logging;
}

View 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));
}
}

View 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;
}
}

View 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;
}

View 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;
}
}

View 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
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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();
}

View 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;
}
}

View 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();
}
}
}

View 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
}
}

View 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);
}
}

View 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;
}
}

View 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();
}

View file

@ -0,0 +1,7 @@
package org.xbib.net.mime;
@FunctionalInterface
public interface MimeMultipartHandler {
void handle(MimeMultipart multipart);
}

View file

@ -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;
}
}
}

View 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));
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}
}
}

View file

@ -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;
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}
}

View file

@ -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();
}
}

View 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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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())));
}
}

View 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");
}
}
}

View file

@ -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);
}
}

View 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

View file

@ -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--

View file

@ -0,0 +1,9 @@
--boundary
Content-Id: part1
1
--boundary
Content-Id: part2
2
--boundary--

View file

@ -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--

View file

@ -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--

View 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 &amp; 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--

View file

@ -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--

View file

@ -0,0 +1,7 @@
--boundary
1
--boundary
2
--boundary--

View 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
View file

@ -0,0 +1,3 @@
dependencies {
api project(':net-http')
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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(&quot;http://example.com?a=1&quot;, 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(&quot;realm&quot;, &quot;http://example.com&quot;, &quot;oauth_token&quot;, &quot;x%y&quot;);
* </pre>
*
* which yields:
*
* <pre>
* OAuth realm=&quot;http://example.com&quot;, oauth_token=&quot;x%25y&quot;
* </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) + "\"";
}
}

View file

@ -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;
}
}

View 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(&quot;1234&quot;, &quot;5678&quot;);
* URL url = new URL(&quot;http://example.com/protected.xml&quot;);
* 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();
}

View file

@ -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);
}
}

View file

@ -0,0 +1,9 @@
package org.xbib.net.oauth;
@SuppressWarnings("serial")
public class OAuthExpectationFailedException extends OAuthException {
public OAuthExpectationFailedException(String message) {
super(message);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View 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(&quot;http://twitter.com/oauth/request_token&quot;,
* &quot;http://twitter.com/oauth/access_token&quot;, &quot;http://twitter.com/oauth/authorize&quot;);
* </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, &quot;http://www.example.com/callback&quot;);
* </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);
}

View file

@ -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;
}

View file

@ -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