├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE-2.0.txt
├── README.md
├── build.gradle
├── config
├── checkstyle
│ └── checkstyle.xml
├── pmd
│ └── rulesSets.xml
└── spotbugs
│ └── spotbugs-exclude.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── upload-parser-core
├── build.gradle
└── src
│ └── main
│ └── java
│ ├── com
│ └── github
│ │ └── elopteryx
│ │ └── upload
│ │ ├── OnError.java
│ │ ├── OnPartBegin.java
│ │ ├── OnPartEnd.java
│ │ ├── OnRequestComplete.java
│ │ ├── PartOutput.java
│ │ ├── PartStream.java
│ │ ├── UploadContext.java
│ │ ├── UploadParser.java
│ │ ├── errors
│ │ ├── MultipartException.java
│ │ ├── PartSizeException.java
│ │ ├── RequestSizeException.java
│ │ ├── UploadSizeException.java
│ │ └── package-info.java
│ │ ├── internal
│ │ ├── AbstractUploadParser.java
│ │ ├── AsyncUploadParser.java
│ │ ├── Base64Decoder.java
│ │ ├── BlockingUploadParser.java
│ │ ├── Headers.java
│ │ ├── MultipartParser.java
│ │ ├── PartStreamImpl.java
│ │ ├── UploadContextImpl.java
│ │ └── package-info.java
│ │ ├── package-info.java
│ │ └── util
│ │ ├── ByteBufferBackedInputStream.java
│ │ ├── ByteBufferBackedOutputStream.java
│ │ ├── InputStreamBackedChannel.java
│ │ ├── NullChannel.java
│ │ ├── OutputStreamBackedChannel.java
│ │ └── package-info.java
│ └── module-info.java
└── upload-parser-tests
├── build.gradle
└── src
└── test
├── java
└── com
│ └── github
│ └── elopteryx
│ └── upload
│ ├── PartOutputTest.java
│ ├── UploadParserTest.java
│ ├── internal
│ ├── AbstractUploadParserTest.java
│ ├── AsyncUploadParserTest.java
│ ├── Base64DecoderTest.java
│ ├── Base64EncodingTest.java
│ ├── BlockingUploadParserTest.java
│ ├── HeadersTest.java
│ ├── IdentityEncodingTest.java
│ ├── MultipartParserTest.java
│ ├── PartStreamImplTest.java
│ ├── QuotedPrintableEncodingTest.java
│ └── integration
│ │ ├── AsyncUploadServlet.java
│ │ ├── BlockingUploadServlet.java
│ │ ├── ClientRequest.java
│ │ ├── JettyIntegrationTest.java
│ │ ├── RequestBuilder.java
│ │ ├── RequestSupplier.java
│ │ ├── TomcatIntegrationTest.java
│ │ └── UndertowIntegrationTest.java
│ └── util
│ ├── ByteBufferBackedInputStreamTest.java
│ ├── ByteBufferBackedOutputStreamTest.java
│ ├── InputStreamBackedChannelTest.java
│ ├── MockServletInputStream.java
│ ├── NullChannelTest.java
│ ├── OutputStreamBackedChannelTest.java
│ └── Servlets.java
└── resources
└── com
└── github
└── elopteryx
└── upload
└── internal
├── integration
├── test.docx
├── test.jpg
└── test.xlsx
├── mime-utf8.txt
├── mime1.txt
├── mime2.txt
├── mime3.txt
├── mime4.txt
├── mime5_malformed.txt
├── mime6_malformed.txt
└── mime7_malformed.txt
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Upload Parser CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | java: [ 17, 21 ]
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Setup java
14 | uses: actions/setup-java@v4
15 | with:
16 | cache: gradle
17 | distribution: temurin
18 | java-version: ${{ matrix.java }}
19 | - name: Build with Gradle
20 | run: ./gradlew build
21 | - name: Create coverage report
22 | run: ./gradlew check
23 | - uses: codecov/codecov-action@v4
24 | with:
25 | files: upload-parser-tests/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml
26 | flags: unittests
27 | name: codecov-umbrella
28 | fail_ci_if_error: true
29 | token: ${{ secrets.codecov_token }}
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | target/
3 | classes/
4 | .gradle/
5 | .idea/
6 |
7 | # Intellij #
8 | *.iml
9 | *.ipr
10 | *.iws
11 |
12 | # Eclipse#
13 | *.project
14 | *.classpath
15 | *.settings
16 |
17 | # Compiled source #
18 | *.class
19 | *.jar
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Upload Parser
2 | =========
3 |
4 | [](http://www.apache.org/licenses/LICENSE-2.0)
5 | [](https://github.com/Elopteryx/upload-parser/actions)
6 | [](https://codecov.io/gh/Elopteryx/upload-parser)
7 | [](https://maven-badges.herokuapp.com/maven-central/com.github.elopteryx/upload-parser)
8 | [](http://www.javadoc.io/doc/com.github.elopteryx/upload-parser)
9 |
10 | Upload Parser is a file upload library for servlets and web applications. Although you can already use the standard
11 | servlet API to retrieve part items from a multipart request this library provides extra functionality not found
12 | elsewhere. First, it has a fluent API which allows you to completely control the uploading process. No more waiting for
13 | the user to upload everything, you can run your business logic, like file type validation and writing to a permanent
14 | location as soon as the bytes arrive. Another great feature is that if you choose it and your servlet supports it the
15 | upload request can run in asynchronous mode, using the async IO API introduced in the 3.1 version of the servlet API.
16 | This will allow a much better use of system resources, no more waiting threads.
17 |
18 | I consider the modules complete, when it comes to features. I don't think I can add more without bloating the
19 | library, but if you have suggestions for new stuff, or if you have found a bug I would be more than happy to fix that.
20 | If a new version of Java SE or EE comes out I will try to experiment with it, maybe make a new major version.
21 |
22 | Features
23 | --------
24 | * Async and blocking multipart request parsing
25 | * Unopinionated, fully customizable, just pass your custom logic
26 | * ```.onPartBegin(…)``` when the client starts sending a part, with optional buffering
27 | * ```.onPartEnd(…)``` when the client finishes sending a part
28 | * ```.onRequestComplete(…)``` after everything has been uploaded
29 | * ```.onError(…)``` if an error occurs
30 | * Lightweight, less than 40Kb size, no dependencies other than the servlet API
31 | * Available from the Maven Central repository
32 |
33 | Requirements
34 | --------
35 | | Versions | Min JVM | Min Servlet |
36 | |----------|---------|-------------|
37 | | 4.0.0 | 17 | 5.0 |
38 | | 3.0.0 | 11 | 3.1 |
39 | | 2.2.1 | 8 | 3.1 |
40 |
41 | Motivation
42 | --------
43 |
44 | Although the Servlet API already has support for handling multipart requests since version 3.0, I found it lacking in several situations.
45 | First, it uses blocking IO which can cause performance problems, especially because an upload can take a very long time. To fix this problem in general,
46 | the ReadListener and WriteListener interfaces have been introduced in version 3.1, to prevent blocking, but to use them, you have to use your own parsing
47 | code, you can't rely on the servlet container to do the job for you in the async mode.
48 |
49 | Also, the classic blocking method parses the whole request
50 | and only lets you run your code after it is finished. Why no support to check for the correct file extension, or request size right when they become available?
51 | And so I decided to write this small library which handles those situations. If you don't have these requirements then the Servlet API will do the job
52 | just fine. Otherwise, I think you will find my library useful.
53 |
54 | Issues
55 | ------
56 |
57 | Does the library have bugs? Needs extra functionality? Do you like the API? Feel free to open an issue!
58 |
59 | Examples
60 | --------
61 |
62 | Very simple to set up, have a servlet which has no MultiPartConfig annotation or the identical section in the web.xml,
63 | import the UploadParser class and pass your logic. The parser class will take care of starting async mode and
64 | registering the necessary classes to start the parsing. Note that if you can't enable async support
65 | for your servlet then you can also use blocking mode, by calling the doBlockingParse method instead. Your functions will
66 | still be called just like in async mode.
67 |
68 | ```java
69 |
70 | @WebServlet(value = "/UploadServlet", asyncSupported = true)
71 | public class UploadServlet extends HttpServlet {
72 |
73 | /**
74 | * Directory where uploaded files will be saved, relative to
75 | * the web application directory.
76 | */
77 | private static final String UPLOAD_DIR = "uploads";
78 |
79 | protected void doPost(HttpServletRequest request, HttpServletResponse response)
80 | throws IOException, ServletException {
81 |
82 | String applicationPath = request.getServletContext().getRealPath("");
83 | final Path uploadFilePath = Paths.get(applicationPath, UPLOAD_DIR);
84 |
85 | if (!Files.isDirectory(uploadFilePath)) {
86 | Files.createDirectories(uploadFilePath);
87 | }
88 |
89 | // Check that we have a file upload request
90 | if (!UploadParser.isMultipart(request)) {
91 | return;
92 | }
93 |
94 | UploadParser.newParser()
95 | .onPartBegin((context, buffer) -> {
96 | PartStream part = context.getCurrentPart();
97 | Path path = uploadFilePath.resolve(part.getSubmittedFileName());
98 | return PartOutput.from(path);
99 | })
100 | .onRequestComplete(context -> response.setStatus(200))
101 | .onError((context, throwable) -> response.sendError(500))
102 | .sizeThreshold(4096)
103 | .maxPartSize(1024 * 1024 * 25)
104 | .maxRequestSize(1024 * 1024 * 500)
105 | .setupAsyncParse(request);
106 | }
107 | }
108 | ```
109 |
110 | You can also use the parser with web frameworks, like Spring WebMVC. The following example shows how to use it with a JAX-RS endpoint:
111 |
112 | ```java
113 |
114 | @Path("upload")
115 | public class UploadController implements OnPartBegin, OnPartEnd, OnRequestComplete, OnError {
116 |
117 | @POST
118 | @Path("upload")
119 | public void multipart(@Context HttpServletRequest request, @Suspended final AsyncResponse asyncResponse) throws IOException, ServletException {
120 | UploadParser.newParser()
121 | .onPartBegin(this)
122 | .onPartEnd(this)
123 | .onRequestComplete(this)
124 | .onError(this)
125 | .userObject(asyncResponse)
126 | .setupAsyncParse(request);
127 | }
128 |
129 | @Override
130 | @Nonnull
131 | public PartOutput onPartBegin(UploadContext context, ByteBuffer buffer) throws IOException {
132 | // Your business logic here, check the part, you can use the bytes in the buffer to check
133 | // the real mime type, then return with a channel, stream or path to write the part
134 | return PartOutput.from(new NullChannel());
135 | }
136 |
137 | @Override
138 | public void onPartEnd(UploadContext context) throws IOException {
139 | // Your business logic here
140 | }
141 |
142 | @Override
143 | public void onRequestComplete(UploadContext context) throws IOException, ServletException {
144 | // Your business logic here, send a response to the client
145 | context.getUserObject(AsyncResponse.class).resume(Response.ok().build());
146 | }
147 |
148 | @Override
149 | public void onError(UploadContext context, Throwable throwable) {
150 | // Your business logic here, handle the error
151 | context.getUserObject(AsyncResponse.class).resume(Response.serverError().build());
152 | }
153 | }
154 | ```
155 |
156 | For more information, please check the [Javadoc][1].
157 |
158 | Gradle
159 | -----
160 |
161 | ```gradle
162 | implementation 'com.github.elopteryx:upload-parser:4.0.0'
163 | ```
164 | Maven
165 | -----
166 |
167 | ```xml
168 |
169 | com.github.elopteryx
170 | upload-parser
171 | 4.0.0
172 |
173 | ```
174 |
175 | Find available versions on [Maven Central Repository](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.elopteryx%22%20AND%20a%3A%22upload-parser%22).
176 |
177 | [1]: http://www.javadoc.io/doc/com.github.elopteryx/upload-parser/4.0.0
178 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.github.ben-manes.versions' version '0.51.0'
3 | id 'com.github.spotbugs' version '6.0.15'
4 | }
5 |
6 | allprojects {
7 | apply plugin: 'jacoco'
8 | jacoco {
9 | toolVersion = '0.8.12'
10 | }
11 | }
12 |
13 | subprojects {
14 |
15 | apply plugin: 'java-library'
16 | apply plugin: 'maven-publish'
17 | apply plugin: 'signing'
18 | apply plugin: 'com.github.ben-manes.versions'
19 | apply plugin: 'checkstyle'
20 | apply plugin: 'pmd'
21 | apply plugin: 'com.github.spotbugs'
22 |
23 | group = 'com.github.elopteryx'
24 | version = '4.1.0-SNAPSHOT'
25 |
26 | repositories {
27 | mavenCentral()
28 | mavenLocal()
29 | }
30 |
31 | ext {
32 | servletApiVersion = '6.0.0'
33 | undertowVersion = '2.3.13.Final'
34 | jettyVersion = '11.0.21'
35 | tomcatVersion = '10.1.24'
36 | junitVersion = '5.10.2'
37 | tikaVersion = '2.9.2'
38 | jimfsVersion = '1.3.0'
39 | mockitoVersion = '5.12.0'
40 |
41 | checkStyleVersion = '10.17.0'
42 | pmdVersion = '6.55.0'
43 | spotbugsVersion = '4.8.5'
44 | }
45 |
46 | tasks.withType(JavaCompile) {
47 | sourceCompatibility = 17
48 | targetCompatibility = 17
49 | }
50 |
51 | java {
52 | withJavadocJar()
53 | withSourcesJar()
54 | modularity.inferModulePath = true
55 | }
56 |
57 | javadoc {
58 | afterEvaluate {
59 | configure(options) {
60 | windowTitle = 'Upload Parser API Documentation'
61 | docTitle = 'Upload Parser API Documentation'
62 | bottom = 'Copyright © 2021 Creative Elopteryx'
63 | breakIterator = true
64 | author = false
65 | source = '17'
66 | encoding = 'UTF-8'
67 | docEncoding = 'UTF-8'
68 | failOnError = true
69 | links = [
70 | 'https://docs.oracle.com/en/java/javase/17/docs/api/',
71 | 'https://jakarta.ee/specifications/platform/9/apidocs/'
72 | ]
73 | modulePath = configurations.compileClasspath.asList()
74 | }
75 | }
76 | }
77 |
78 | checkstyle {
79 | toolVersion = checkStyleVersion
80 | configFile = file("${projectDir}/../config/checkstyle/checkstyle.xml")
81 | }
82 |
83 | pmd {
84 | toolVersion = pmdVersion
85 | incrementalAnalysis = true
86 | ruleSets = []
87 | ruleSetConfig = resources.text.fromFile(file("${rootDir}/config/pmd/rulesSets.xml"))
88 | }
89 |
90 | spotbugs {
91 | toolVersion = spotbugsVersion
92 | effort = com.github.spotbugs.snom.Effort.valueOf('MAX')
93 | excludeFilter = file("${projectDir}/../config/spotbugs/spotbugs-exclude.xml")
94 | }
95 |
96 | spotbugsTest {
97 | enabled = false
98 | }
99 |
100 | signing {
101 | required { !version.endsWith('SNAPSHOT') && gradle.taskGraph.hasTask('publish') }
102 | sign configurations.archives
103 | }
104 |
105 | test {
106 | useJUnitPlatform()
107 | testLogging.showStandardStreams = true
108 | }
109 |
110 | }
111 |
112 | repositories {
113 | mavenCentral()
114 | }
115 |
--------------------------------------------------------------------------------
/config/checkstyle/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
51 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
76 |
77 |
78 |
80 |
81 |
82 |
83 |
85 |
86 |
87 |
88 |
90 |
91 |
92 |
93 |
94 |
95 |
97 |
98 |
99 |
100 |
102 |
103 |
104 |
105 |
107 |
108 |
109 |
110 |
112 |
114 |
116 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/config/pmd/rulesSets.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Every Java Rule in PMD
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/config/spotbugs/spotbugs-exclude.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ossrhUsername=username
2 | ossrhPassword=password
3 |
4 | org.gradle.warning.mode=all
5 |
6 | org.gradle.caching=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elopteryx/upload-parser/ae2ea72fedea25bd051823677241c05489d2df06/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=`expr $i + 1`
158 | done
159 | case $i in
160 | 0) set -- ;;
161 | 1) set -- "$args0" ;;
162 | 2) set -- "$args0" "$args1" ;;
163 | 3) set -- "$args0" "$args1" "$args2" ;;
164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=`save "$@"`
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | exec "$JAVACMD" "$@"
184 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'upload-parser-parent'
2 |
3 | include 'upload-parser-core'
4 | include 'upload-parser-tests'
5 |
--------------------------------------------------------------------------------
/upload-parser-core/build.gradle:
--------------------------------------------------------------------------------
1 | ext.moduleName = 'com.github.elopteryx.upload'
2 |
3 | dependencies {
4 |
5 | /* Servlet API. */
6 | compileOnly("jakarta.servlet:jakarta.servlet-api:$servletApiVersion")
7 |
8 | }
9 |
10 | compileJava {
11 | inputs.property('moduleName', moduleName)
12 | doFirst {
13 | options.compilerArgs = [
14 | '--module-path', classpath.asPath,
15 | ]
16 | classpath = files()
17 | }
18 | }
19 |
20 | jar {
21 | manifest {
22 | attributes(
23 | 'Created-By': 'Creative Elopteryx',
24 | 'Class-Path': configurations.compileClasspath.collect { it.getName() }.join(' '),
25 | 'Automatic-Module-Name': 'com.github.elopteryx.upload',
26 | 'Implementation-Version': archiveVersion
27 | )
28 | }
29 | }
30 |
31 | publishing {
32 | publications {
33 | mavenJava(MavenPublication) {
34 | artifactId = 'upload-parser'
35 | from components.java
36 | versionMapping {
37 | usage('java-api') {
38 | fromResolutionOf('runtimeClasspath')
39 | }
40 | usage('java-runtime') {
41 | fromResolutionResult()
42 | }
43 | }
44 | pom {
45 | name = 'Upload Parser'
46 | groupId = 'com.github.elopteryx'
47 | artifactId = 'upload-parser'
48 |
49 | description = 'Upload Parser is an asynchronous file upload library for servlets.'
50 | url = 'https://github.com/Elopteryx/upload-parser'
51 |
52 | scm {
53 | connection = 'scm:git:git@github.com/Elopteryx/upload-parser.git'
54 | developerConnection = 'scm:git:git@github.com/Elopteryx/upload-parser.git'
55 | url = 'scm:git:git@github.com/Elopteryx/upload-parser.git'
56 | }
57 |
58 | licenses {
59 | license {
60 | name = 'The Apache License, Version 2.0'
61 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
62 | }
63 | }
64 |
65 | developers {
66 | developer {
67 | id = 'elopteryx'
68 | name = 'Adam Forgacs'
69 | email = 'creative.elopteryx@gmail.com'
70 | }
71 | }
72 | }
73 | }
74 | }
75 | repositories {
76 | maven {
77 | name = 'ossrh'
78 | credentials(PasswordCredentials)
79 | def releasesRepoUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
80 | def snapshotsRepoUrl = 'https://oss.sonatype.org/content/repositories/snapshots/'
81 | url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
82 | }
83 | }
84 | }
85 |
86 | signing {
87 | sign publishing.publications.mavenJava
88 | }
89 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/OnError.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.io.IOException;
20 | import jakarta.servlet.ServletException;
21 |
22 | /**
23 | * A functional interface. An implementation of it must be passed in the
24 | * {@link UploadParser#onError(OnError)} method to call it after an error occurs.
25 | */
26 | @FunctionalInterface
27 | public interface OnError {
28 |
29 | /**
30 | * The consumer function to implement.
31 | * @param context The upload context
32 | * @param throwable The error that occurred
33 | * @throws IOException If an error occurs with the IO
34 | * @throws ServletException If and error occurred with the servlet
35 | */
36 | void onError(UploadContext context, Throwable throwable) throws IOException, ServletException;
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/OnPartBegin.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.io.IOException;
20 | import java.nio.ByteBuffer;
21 |
22 | /**
23 | * A functional interface. An implementation of it must be passed in the
24 | * {@link UploadParser#onPartBegin(OnPartBegin)} method to call it at the start of parsing for each part.
25 | */
26 | @FunctionalInterface
27 | public interface OnPartBegin {
28 |
29 | /**
30 | * The function to implement. When it's called depends on the size threshold. If enough bytes
31 | * have been read or if the part is fully uploaded then this method is called
32 | * with a buffer containing the read bytes. Note that the buffer is only passed
33 | * for validation, it should not be written out. The buffered and the upcoming
34 | * bytes will be written out to the output object returned by this method. If the callback
35 | * is not set then the uploaded bytes are discarded.
36 | * @param context The upload context
37 | * @param buffer The byte buffer containing the first bytes of the part
38 | * @return A non-null output object (a channel or stream) to write out the part
39 | * @throws IOException If an error occurred with the channel
40 | */
41 | PartOutput onPartBegin(UploadContext context, ByteBuffer buffer) throws IOException;
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/OnPartEnd.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.io.IOException;
20 |
21 | /**
22 | * A functional interface. An implementation of it must be passed in the
23 | * {@link UploadParser#onPartEnd(OnPartEnd)} method to call it at the end of parsing for each part.
24 | *
25 | *
This function is called after every byte has been read for the given part. There will be
26 | * an attempt to close the current output object just before calling this. That means setting
27 | * this can be skipped if all you want to do is to close the provided channel or stream.
28 | */
29 | @FunctionalInterface
30 | public interface OnPartEnd {
31 |
32 | /**
33 | * The consumer function to implement.
34 | * @param context The upload context
35 | * @throws IOException If an error occurred with the current channel
36 | */
37 | void onPartEnd(UploadContext context) throws IOException;
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/OnRequestComplete.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.io.IOException;
20 | import jakarta.servlet.ServletException;
21 |
22 | /**
23 | * A functional interface. An implementation of it must be passed in the
24 | * {@link UploadParser#onRequestComplete(OnRequestComplete)} method to call it after every part has been processed.
25 | */
26 | @FunctionalInterface
27 | public interface OnRequestComplete {
28 |
29 | /**
30 | * The consumer function to implement.
31 | * @param context The upload context
32 | * @throws IOException If an error occurs with the IO
33 | * @throws ServletException If and error occurred with the servlet
34 | */
35 | void onRequestComplete(UploadContext context) throws IOException, ServletException;
36 | }
37 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/PartOutput.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.io.OutputStream;
20 | import java.nio.channels.WritableByteChannel;
21 | import java.nio.file.Path;
22 |
23 | /**
24 | * A value holder class, allowing the caller to provide
25 | * various output objects, like a byte channel or an output stream.
26 | */
27 | public class PartOutput {
28 |
29 | /**
30 | * The value object.
31 | */
32 | private final Object value;
33 |
34 | /**
35 | * Protected constructor, no need for public access.
36 | * The parser will use the given object here, which is why using
37 | * the static factory methods is encouraged. Passing an invalid
38 | * object will terminate the upload process.
39 | * @param value The value object.
40 | */
41 | protected PartOutput(final Object value) {
42 | this.value = value;
43 | }
44 |
45 | /**
46 | * Returns whether it is safe to retrieve the value object
47 | * with the class parameter.
48 | * @param clazz The class type to check
49 | * @param Type parameter
50 | * @return Whether it is safe to cast or not
51 | */
52 | public boolean safeToCast(final Class clazz) {
53 | return value != null && clazz.isAssignableFrom(value.getClass());
54 | }
55 |
56 | /**
57 | * Retrieves the value object, casting it to the
58 | * given type.
59 | * @param clazz The class to cast
60 | * @param Type parameter
61 | * @return The stored value object
62 | */
63 | public T unwrap(final Class clazz) {
64 | return clazz.cast(value);
65 | }
66 |
67 | /**
68 | * Creates a new instance from the given channel object. The parser will
69 | * use the channel to write out the bytes and will attempt to close it.
70 | * @param byteChannel A channel which can be used for writing
71 | * @return A new PartOutput instance
72 | */
73 | public static PartOutput from(final WritableByteChannel byteChannel) {
74 | return new PartOutput(byteChannel);
75 | }
76 |
77 | /**
78 | * Creates a new instance from the given stream object. The parser will
79 | * create a channel from the stream to write out the bytes and
80 | * will attempt to close it.
81 | * @param outputStream A stream which can be used for writing
82 | * @return A new PartOutput instance
83 | */
84 | public static PartOutput from(final OutputStream outputStream) {
85 | return new PartOutput(outputStream);
86 | }
87 |
88 | /**
89 | * Creates a new instance from the given path object. The parser will
90 | * create a channel from the path to write out the bytes and
91 | * will attempt to close it. If the file represented by the path
92 | * does not exist, it will be created. If the file exists and is not
93 | * empty then the uploaded bytes will be appended to the end of it.
94 | * @param path A file path which can be used for writing
95 | * @return A new PartOutput instance
96 | */
97 | public static PartOutput from(final Path path) {
98 | return new PartOutput(path);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/PartStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.util.Collection;
20 |
21 | /**
22 | * This interface represents a part item, which is being
23 | * streamed from the client. As the object is created
24 | * before the item finished uploading available information
25 | * is limited.
26 | */
27 | public interface PartStream {
28 |
29 | /**
30 | * Returns the content type of this part, as it was submitted by
31 | * the client.
32 | *
33 | * @return The content type of this part
34 | */
35 | String getContentType();
36 |
37 | /**
38 | * Returns the name of this part, which equals the name of the form
39 | * field the part was selected for.
40 | *
41 | * @return The name of this part as a String
42 | */
43 | String getName();
44 |
45 | /**
46 | * Returns the known size of this part. This means that the
47 | * returned value depends on how many bytes have been processed. When called
48 | * during the start of the part processing the returned value will be equal
49 | * to the specified buffer threshold or the actual part size if it's smaller
50 | * than that. When called during the end the returned value is always the
51 | * actual size, because by that time it has been fully processed.
52 | *
53 | * @return A long specifying the known size of this part, in bytes.
54 | */
55 | long getKnownSize();
56 |
57 | /**
58 | * Returns the file name specified by the client or null if the
59 | * part is a normal form field.
60 | *
61 | * @return The submitted file name
62 | */
63 | String getSubmittedFileName();
64 |
65 | /**
66 | * Determines whether or not this PartStream instance represents
67 | * a file item. If it's a normal form field then it will return false.
68 | * Consequently, if this returns true then the {@link PartStream#getSubmittedFileName}
69 | * will return with a non-null value and vice-versa.
70 | *
71 | * @return True if the instance represents an uploaded file; false if it represents a simple form field.
72 | */
73 | boolean isFile();
74 |
75 | /**
76 | * Returns whether the part has been completely uploaded. The
77 | * part begin callback is called only after the given size threshold
78 | * is reached or if the part is completely uploaded. Therefore, the
79 | * parts that are smaller than the threshold are passed to the part begin
80 | * callback after their upload is finished and their actual size is known.
81 | * This method can be used to determine that. In the part end callback
82 | * this will always return true.
83 | *
84 | * @return True if the part is completely uploaded, false otherwise.
85 | */
86 | boolean isFinished();
87 |
88 | /**
89 | * Returns the value of the specified mime header as a String. If
90 | * the Part did not include a header of the specified name, this
91 | * method returns null. If there are multiple headers with the same name,
92 | * this method returns the first header in the part. The header name
93 | * is case insensitive. You can use this method with any request header.
94 | *
95 | * @param name a String specifying the header name
96 | * @return a String containing the value of the requested header, or null if the part does not have a header of that name
97 | */
98 | String getHeader(String name);
99 |
100 | /**
101 | * Returns the values of the part header with the given name.
102 | *
103 | * @param name the header name whose values to return
104 | * @return a (possibly empty) Collection of the values of the header with the given name
105 | */
106 | Collection getHeaders(String name);
107 |
108 | /**
109 | * Returns the header names of this part.
110 | *
111 | * @return a (possibly empty) Collection of the header names of this Part
112 | */
113 | Collection getHeaderNames();
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/UploadContext.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload;
18 |
19 | import java.util.List;
20 | import jakarta.servlet.http.HttpServletRequest;
21 |
22 | /**
23 | * The context object which is passed to the user-supplied functions,
24 | * allowing the user to get the necessary objects during the various
25 | * stages of the parsing.
26 | */
27 | public interface UploadContext {
28 |
29 | /**
30 | * Returns the given request object for this parser,
31 | * allowing customization during the stages of the
32 | * asynchronous processing.
33 | *
34 | * @return The request object
35 | */
36 | HttpServletRequest getRequest();
37 |
38 | /**
39 | * Returns the given user object for this parser,
40 | * allowing customization during the stages of the
41 | * asynchronous processing.
42 | *
43 | * @param clazz The class used for casting
44 | * @param Type parameter
45 | * @return The user object, or null if it was not supplied
46 | */
47 | T getUserObject(Class clazz);
48 |
49 | /**
50 | * Returns the currently processed part stream,
51 | * allowing the caller to get available information.
52 | *
53 | * @return The currently processed part
54 | */
55 | PartStream getCurrentPart();
56 |
57 | /**
58 | * Returns the currently active output, which was returned
59 | * in the latest UploadParser#onPartBegin method. This will
60 | * return a null during the part begin stage and might be
61 | * null during the error stage.
62 | *
63 | * @return The latest output provided by the caller
64 | */
65 | PartOutput getCurrentOutput();
66 |
67 | /**
68 | * Returns the parts which have already been processed. Before
69 | * the onPartBegin method is called the current PartStream is
70 | * added into the List returned by this method, meaning that
71 | * the UploadContext#getCurrentPart will return with the last
72 | * element of the list.
73 | * @return The list of the processed parts, in the order they are uploaded
74 | */
75 | List getPartStreams();
76 | }
77 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/errors/MultipartException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.errors;
18 |
19 | import java.io.IOException;
20 |
21 | /**
22 | * Exception thrown by the multipart parser. Usually it's
23 | * impossible to recover from this error.
24 | */
25 | public class MultipartException extends IOException {
26 |
27 | /**
28 | * Public constructor.
29 | * @param message The exception message.
30 | */
31 | public MultipartException(final String message) {
32 | super(message);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/errors/PartSizeException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.errors;
18 |
19 | /**
20 | * Exception thrown when there is a maximum size limit set for the individual
21 | * parts and it is exceeded for the first time.
22 | */
23 | public class PartSizeException extends UploadSizeException {
24 |
25 | /**
26 | * Public constructor.
27 | * @param message The message of the exception
28 | * @param actual The known size at the time of the exception
29 | * @param permitted The maximum permitted size
30 | */
31 | public PartSizeException(final String message, final long actual, final long permitted) {
32 | super(message, actual, permitted);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/errors/RequestSizeException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.errors;
18 |
19 | /**
20 | * Exception thrown when there is a maximum size limit set for the whole
21 | * request and it is exceeded for the first time.
22 | */
23 | public class RequestSizeException extends UploadSizeException {
24 |
25 | /**
26 | * Public constructor.
27 | * @param message The message of the exception
28 | * @param actual The known size at the time of the exception in bytes
29 | * @param permitted The maximum permitted size in bytes
30 | */
31 | public RequestSizeException(final String message, final long actual, final long permitted) {
32 | super(message, actual, permitted);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/errors/UploadSizeException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.errors;
18 |
19 | /**
20 | * Base class for the size related exceptions.
21 | */
22 | public abstract class UploadSizeException extends RuntimeException {
23 |
24 | /**
25 | * The known size.
26 | */
27 | private final long actual;
28 |
29 | /**
30 | * The maximum permitted size.
31 | */
32 | private final long permitted;
33 |
34 | /**
35 | * Package private constructor.
36 | * @param message The message of the exception
37 | * @param actual The known size at the time of the exception in bytes
38 | * @param permitted The maximum permitted size in bytes
39 | */
40 | UploadSizeException(final String message, final long actual, final long permitted) {
41 | super(message);
42 | this.actual = actual;
43 | this.permitted = permitted;
44 | }
45 |
46 | /**
47 | * Returns the actual size.
48 | *
49 | * @return The actual size.
50 | */
51 | public long getActualSize() {
52 | return actual;
53 | }
54 |
55 | /**
56 | * Returns the permitted size.
57 | *
58 | * @return The permitted size.
59 | */
60 | public long getPermittedSize() {
61 | return permitted;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/errors/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This package contains the exception classes used in this library.
3 | */
4 | package com.github.elopteryx.upload.errors;
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/AsyncUploadParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.internal;
18 |
19 | import static java.util.Objects.requireNonNull;
20 |
21 | import com.github.elopteryx.upload.errors.MultipartException;
22 |
23 | import java.io.IOException;
24 | import jakarta.servlet.ReadListener;
25 | import jakarta.servlet.ServletException;
26 | import jakarta.servlet.ServletInputStream;
27 | import jakarta.servlet.http.HttpServletRequest;
28 |
29 | /**
30 | * The asynchronous implementation of the parser. This parser can be used to perform a parse
31 | * only if the calling servlet supports async mode.
32 | * Implements the listener interface. Called by the servlet container whenever data is available.
33 | */
34 | public final class AsyncUploadParser extends AbstractUploadParser implements ReadListener {
35 |
36 | /**
37 | * The request object.
38 | */
39 | private final HttpServletRequest request;
40 |
41 | /**
42 | * The input stream associated with the request.
43 | */
44 | private ServletInputStream servletInputStream;
45 |
46 | public AsyncUploadParser(final HttpServletRequest request) {
47 | this.request = requireNonNull(request);
48 | }
49 |
50 | /**
51 | * Sets up the necessary objects to start the parsing. Depending upon
52 | * the environment the concrete implementations can be very different.
53 | * @throws IOException If an error occurs with the IO
54 | */
55 | private void init() throws IOException {
56 | init(request);
57 | servletInputStream = request.getInputStream();
58 | }
59 |
60 | /**
61 | * Setups the async parsing by registering the instance to
62 | * the servlet stream as a read listener.
63 | * @throws IOException If an error occurred with I/O
64 | */
65 | public void setupAsyncParse() throws IOException {
66 | init();
67 | if (!request.isAsyncSupported()) {
68 | throw new IllegalStateException("The servlet does not support async mode! Enable it or use a blocking parser.");
69 | }
70 | if (!request.isAsyncStarted()) {
71 | request.startAsync();
72 | }
73 | servletInputStream.setReadListener(this);
74 | }
75 |
76 | /**
77 | * When an instance of the ReadListener is registered with a ServletInputStream, this method will be invoked
78 | * by the container the first time when it is possible to read data. Subsequently the container will invoke
79 | * this method if and only if ServletInputStream.isReady() method has been called and has returned false.
80 | * @throws IOException if an I/O related error has occurred during processing
81 | */
82 | @Override
83 | public void onDataAvailable() throws IOException {
84 | while (servletInputStream.isReady() && !servletInputStream.isFinished()) {
85 | parseCurrentItem();
86 | }
87 | }
88 |
89 | /**
90 | * Parses the servlet stream once. Will switch to a new item
91 | * if the current one is fully read.
92 | *
93 | * @return Whether it should be called again
94 | * @throws IOException if an I/O related error has occurred during processing
95 | */
96 | private boolean parseCurrentItem() throws IOException {
97 | var count = -1;
98 | if (!servletInputStream.isFinished()) {
99 | count = servletInputStream.read(dataBuffer.array());
100 | }
101 | if (count == -1) {
102 | if (!parseState.isComplete()) {
103 | throw new MultipartException("Stream ended unexpectedly!");
104 | }
105 | } else {
106 | checkRequestSize(count);
107 | dataBuffer.position(0);
108 | dataBuffer.limit(count);
109 | parseState.parse(dataBuffer);
110 | }
111 | return !parseState.isComplete();
112 | }
113 |
114 | /**
115 | * Invoked when all data for the current request has been read.
116 | * @throws IOException if an I/O related error has occurred during processing
117 | */
118 | @Override
119 | public void onAllDataRead() throws IOException {
120 | // After the servlet input stream is finished there are still unread bytes or
121 | // in case of fast uploads or small sizes the initial parse can read the whole
122 | // input stream, causing the {@link #onDataAvailable} not to be called even once.
123 | while (true) {
124 | if (!parseCurrentItem()) {
125 | break;
126 | }
127 | }
128 | try {
129 | if (requestCallback != null) {
130 | requestCallback.onRequestComplete(context);
131 | }
132 | } catch (final ServletException e) {
133 | throw new RuntimeException(e);
134 | }
135 | }
136 |
137 | /**
138 | * Invoked when an error occurs processing the request.
139 | * @param throwable The unhandled error that happened
140 | */
141 | @Override
142 | public void onError(final Throwable throwable) {
143 | try {
144 | if (errorCallback != null) {
145 | errorCallback.onError(context, throwable);
146 | }
147 | } catch (final IOException | ServletException e) {
148 | throw new RuntimeException(e);
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/Base64Decoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * JBoss, Home of Professional Open Source.
3 | * Copyright 2014 Red Hat, Inc., and individual contributors
4 | * as indicated by the @author tags.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package com.github.elopteryx.upload.internal;
20 |
21 | import static java.nio.charset.StandardCharsets.US_ASCII;
22 |
23 | import java.io.IOException;
24 | import java.nio.ByteBuffer;
25 |
26 | /**
27 | * Copied from Undertow. Stripped out the unnecessary parts, like the
28 | * encoder and the methods which accepted a different parameter,
29 | * for example a byte array, instead of a ByteBuffer.
30 | *
31 | * An efficient and flexible MIME Base64 implementation.
32 | *
33 | * @author Jason T. Greene
34 | */
35 | class Base64Decoder {
36 |
37 | private static final byte[] ENCODING_TABLE;
38 | private static final byte[] DECODING_TABLE = new byte[80];
39 |
40 | static {
41 | ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes(US_ASCII);
42 | for (var i = 0; i < ENCODING_TABLE.length; i++) {
43 | final var offSet = (ENCODING_TABLE[i] & 0xFF) - 43;
44 | DECODING_TABLE[offSet] = (byte)(i + 1); // zero = illegal
45 | }
46 | }
47 |
48 | private int state;
49 | private int last;
50 | private static final int SKIP = 0x0FD00;
51 | private static final int MARK = 0x0FE00;
52 | private static final int DONE = 0x0FF00;
53 | private static final int ERROR = 0xF0000;
54 |
55 | private static int nextByte(final ByteBuffer buffer, final int state, final int last, final boolean ignoreErrors) throws IOException {
56 | return nextByte(buffer.get() & 0xFF, state, last, ignoreErrors);
57 | }
58 |
59 | private static int nextByte(final int charInt, final int state, final int last, final boolean ignoreErrors) throws IOException {
60 | if (last == MARK) {
61 | if (charInt != '=') {
62 | throw new IOException("Expected padding character");
63 | }
64 | return DONE;
65 | }
66 | if (charInt == '=') {
67 | if (state == 2) {
68 | return MARK;
69 | } else if (state == 3) {
70 | return DONE;
71 | } else {
72 | throw new IOException("Unexpected padding character");
73 | }
74 | }
75 | if (charInt == ' ' || charInt == '\t' || charInt == '\r' || charInt == '\n') {
76 | return SKIP;
77 | }
78 | if (charInt < 43 || charInt > 122) {
79 | if (ignoreErrors) {
80 | return ERROR;
81 | }
82 | throw new IOException("Invalid base64 character encountered: " + charInt);
83 | }
84 | final var byteInt = (DECODING_TABLE[charInt - 43] & 0xFF) - 1;
85 | if (byteInt < 0) {
86 | if (ignoreErrors) {
87 | return ERROR;
88 | }
89 | throw new IOException("Invalid base64 character encountered: " + charInt);
90 | }
91 | return byteInt;
92 | }
93 |
94 | /**
95 | * Decodes one Base64 byte buffer into another. This method will return and save state
96 | * if the target does not have the required capacity. Subsequent calls with a new target will
97 | * resume reading where it last left off (the source buffer's position). Similarly not all of the
98 | * source data need be available, this method can be repetitively called as data is made available.
99 | *
100 | * The decoder will skip white space, but will error if it detects corruption.
101 | *
102 | * @param source the byte buffer to read encoded data from
103 | * @param target the byte buffer to write decoded data to
104 | * @throws java.io.IOException if the encoded data is corrupted
105 | */
106 | void decode(final ByteBuffer source, final ByteBuffer target) throws IOException {
107 | if (target == null) {
108 | throw new IllegalStateException();
109 | }
110 |
111 | var last = this.last;
112 | var state = this.state;
113 |
114 | var remaining = source.remaining();
115 | var targetRemaining = target.remaining();
116 | var byteInt = 0;
117 | while (remaining-- > 0 && targetRemaining > 0) {
118 | byteInt = nextByte(source, state, last, false);
119 | if (byteInt == MARK) {
120 | last = MARK;
121 | if (--remaining <= 0) {
122 | break;
123 | }
124 | byteInt = nextByte(source, state, last, false);
125 | }
126 | if (byteInt == DONE) {
127 | last = state = 0;
128 | break;
129 | }
130 | if (byteInt == SKIP) {
131 | continue;
132 | }
133 | // ( 6 | 2) (4 | 4) (2 | 6)
134 | if (state == 0) {
135 | last = byteInt << 2;
136 | state++;
137 | if (remaining-- <= 0) {
138 | break;
139 | }
140 | byteInt = nextByte(source, state, last, false);
141 | if ((byteInt & 0xF000) != 0) {
142 | source.position(source.position() - 1);
143 | continue;
144 | }
145 | }
146 | if (state == 1) {
147 | target.put((byte)(last | (byteInt >>> 4)));
148 | last = (byteInt & 0x0F) << 4;
149 | state++;
150 | if (remaining-- <= 0 || --targetRemaining <= 0) {
151 | break;
152 | }
153 | byteInt = nextByte(source, state, last, false);
154 | if ((byteInt & 0xF000) != 0) {
155 | source.position(source.position() - 1);
156 | continue;
157 | }
158 | }
159 | if (state == 2) {
160 | target.put((byte) (last | (byteInt >>> 2)));
161 | last = (byteInt & 0x3) << 6;
162 | state++;
163 | if (remaining-- <= 0 || --targetRemaining <= 0) {
164 | break;
165 | }
166 | byteInt = nextByte(source, state, last, false);
167 | if ((byteInt & 0xF000) != 0) {
168 | source.position(source.position() - 1);
169 | continue;
170 | }
171 | }
172 | if (state == 3) {
173 | target.put((byte)(last | byteInt));
174 | last = state = 0;
175 | targetRemaining--;
176 | }
177 | }
178 |
179 | if (remaining > 0) {
180 | drain(source, byteInt, state, last);
181 | }
182 |
183 | this.last = last;
184 | this.state = state;
185 | }
186 |
187 | private static void drain(final ByteBuffer source, int byteInt, final int state, int last) {
188 | while (byteInt != DONE && source.remaining() > 0) {
189 | try {
190 | byteInt = nextByte(source, state, last, true);
191 | } catch (final IOException e) {
192 | byteInt = 0;
193 | }
194 |
195 | if (byteInt == MARK) {
196 | last = MARK;
197 | continue;
198 | }
199 |
200 | // Not WS/pad
201 | if ((byteInt & 0xF000) == 0) {
202 | source.position(source.position() - 1);
203 | break;
204 | }
205 | }
206 |
207 | if (byteInt == DONE) {
208 | // SKIP one line of trailing whitespace
209 | while (source.remaining() > 0) {
210 | byteInt = source.get();
211 | if (byteInt == '\n') {
212 | break;
213 | } else if (byteInt != ' ' && byteInt != '\t' && byteInt != '\r') {
214 | source.position(source.position() - 1);
215 | break;
216 | }
217 |
218 | }
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/BlockingUploadParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.internal;
18 |
19 | import com.github.elopteryx.upload.UploadContext;
20 | import com.github.elopteryx.upload.errors.MultipartException;
21 |
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import jakarta.servlet.ServletException;
25 | import jakarta.servlet.http.HttpServletRequest;
26 |
27 | /**
28 | * The blocking implementation of the parser. This parser can be used to perform a
29 | * blocking parse, whether the servlet supports async mode or not.
30 | */
31 | public final class BlockingUploadParser extends AbstractUploadParser {
32 |
33 | /**
34 | * The request object.
35 | */
36 | private final HttpServletRequest request;
37 |
38 | /**
39 | * The stream to read.
40 | */
41 | private InputStream inputStream;
42 |
43 | public BlockingUploadParser(final HttpServletRequest request) {
44 | this.request = request;
45 | }
46 |
47 | /**
48 | * Sets up the necessary objects to start the parsing. Depending upon
49 | * the environment the concrete implementations can be very different.
50 | * @throws IOException If an error occurs with the IO
51 | */
52 | private void init() throws IOException {
53 | init(request);
54 | inputStream = request.getInputStream();
55 | }
56 |
57 | /**
58 | * Performs a full parsing and returns the used context object.
59 | * @return The upload context
60 | * @throws IOException If an error occurred with the I/O
61 | * @throws ServletException If an error occurred with the servlet
62 | */
63 | public UploadContext doBlockingParse() throws IOException, ServletException {
64 | init();
65 | try {
66 | blockingRead();
67 | if (requestCallback != null) {
68 | requestCallback.onRequestComplete(context);
69 | }
70 | } catch (final Exception e) {
71 | if (errorCallback != null) {
72 | errorCallback.onError(context, e);
73 | }
74 | }
75 | return context;
76 | }
77 |
78 | /**
79 | * Reads everything from the input stream in a blocking mode. It will
80 | * throw an exception if the data is malformed, for example
81 | * it is not closed with the proper characters.
82 | * @throws IOException If an error occurred with the I/O
83 | */
84 | private void blockingRead() throws IOException {
85 | while (true) {
86 | final var count = inputStream.read(dataBuffer.array());
87 | if (count == -1) {
88 | if (parseState.isComplete()) {
89 | break;
90 | } else {
91 | throw new MultipartException("Stream ended unexpectedly!");
92 | }
93 | } else if (count > 0) {
94 | checkRequestSize(count);
95 | dataBuffer.position(0);
96 | dataBuffer.limit(count);
97 | parseState.parse(dataBuffer);
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/Headers.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.internal;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Collection;
21 | import java.util.Collections;
22 | import java.util.LinkedHashMap;
23 | import java.util.List;
24 | import java.util.Locale;
25 | import java.util.Map;
26 |
27 | /**
28 | * This class is used to extract, store and retrieve header keys
29 | * and values. Supports the HTTP request headers and also the headers
30 | * for the part items received in the multipart request.
31 | */
32 | public class Headers {
33 |
34 | private static final String BOUNDARY = "boundary";
35 |
36 | public static final String CONTENT_DISPOSITION = "Content-Disposition";
37 |
38 | public static final String CONTENT_ENCODING = "Content-Encoding";
39 |
40 | public static final String CONTENT_LENGTH = "Content-Length";
41 |
42 | public static final String CONTENT_TYPE = "Content-Type";
43 |
44 | private final Map> headerNameToValueListMap = new LinkedHashMap<>();
45 |
46 | String getHeader(final String name) {
47 | final var nameLower = name.toLowerCase(Locale.ENGLISH);
48 | final var headerValueList = headerNameToValueListMap.get(nameLower);
49 | return headerValueList == null ? null : headerValueList.get(0);
50 | }
51 |
52 | Collection getHeaders(final String name) {
53 | final var nameLower = name.toLowerCase(Locale.ENGLISH);
54 | return headerNameToValueListMap.getOrDefault(nameLower, Collections.emptyList());
55 | }
56 |
57 | Collection getHeaderNames() {
58 | return headerNameToValueListMap.keySet();
59 | }
60 |
61 | /**
62 | * Method to add header values to this instance.
63 | *
64 | * @param name name of this header
65 | * @param value value of this header
66 | */
67 | void addHeader(final String name, final String value) {
68 | final var nameLower = name.toLowerCase(Locale.ENGLISH);
69 | headerNameToValueListMap.computeIfAbsent(nameLower, key -> new ArrayList<>()).add(value);
70 | }
71 |
72 | /**
73 | * Extracts a token from a header that has a given key. For instance if the header is
74 | * content-type=multipart/form-data boundary=myboundary
75 | * and the key is boundary the myboundary will be returned.
76 | *
77 | * @param header The header
78 | * @return The token, or null if it was not found
79 | */
80 | public static String extractBoundaryFromHeader(final String header) {
81 |
82 | final var pos = header.indexOf(BOUNDARY + '=');
83 | if (pos == -1) {
84 | return null;
85 | }
86 | int end;
87 | final var start = pos + BOUNDARY.length() + 1;
88 | for (end = start; end < header.length(); ++end) {
89 | final var character = header.charAt(end);
90 | if (character == ' ' || character == '\t' || character == ';') {
91 | break;
92 | }
93 | }
94 | return header.substring(start, end);
95 | }
96 |
97 | /**
98 | * Extracts a quoted value from a header that has a given key. For instance if the header is
99 | * content-disposition=form-data; name="my field"
100 | * and the key is name then "my field" will be returned without the quotes.
101 | *
102 | * @param header The header
103 | * @param key The key that identifies the token to extract
104 | * @return The token, or null if it was not found
105 | */
106 | public static String extractQuotedValueFromHeader(final String header, final String key) {
107 |
108 | var keyPosition = 0;
109 | var pos = -1;
110 | var inQuotes = false;
111 | for (var i = 0; i < header.length() - 1; ++i) { //-1 because we need room for the = at the end
112 | final var character = header.charAt(i);
113 | if (inQuotes) {
114 | if (character == '"') {
115 | inQuotes = false;
116 | }
117 | } else {
118 | if (key.charAt(keyPosition) == character) {
119 | keyPosition++;
120 | } else if (character == '"') {
121 | keyPosition = 0;
122 | inQuotes = true;
123 | } else {
124 | keyPosition = 0;
125 | }
126 | if (keyPosition == key.length()) {
127 | if (header.charAt(i + 1) == '=') {
128 | pos = i + 2;
129 | break;
130 | } else {
131 | keyPosition = 0;
132 | }
133 | }
134 | }
135 |
136 | }
137 | if (pos == -1) {
138 | return null;
139 | }
140 |
141 | int end;
142 | var start = pos;
143 | if (header.charAt(start) == '"') {
144 | start++;
145 | for (end = start; end < header.length(); ++end) {
146 | final var character = header.charAt(end);
147 | if (character == '"') {
148 | break;
149 | }
150 | }
151 |
152 | } else {
153 | //no quotes
154 | for (end = start; end < header.length(); ++end) {
155 | final var character = header.charAt(end);
156 | if (character == ' ' || character == '\t') {
157 | break;
158 | }
159 | }
160 | }
161 | return header.substring(start, end);
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/PartStreamImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.internal;
18 |
19 | import com.github.elopteryx.upload.PartOutput;
20 | import com.github.elopteryx.upload.PartStream;
21 |
22 | import java.util.Collection;
23 |
24 | /**
25 | * Default implementation of {@link PartStream}.
26 | */
27 | public class PartStreamImpl implements PartStream {
28 |
29 | /**
30 | * The content type of the part.
31 | */
32 | private final String contentType;
33 | /**
34 | * The file name of the part.
35 | */
36 | private final String fileName;
37 | /**
38 | * The field name of the part.
39 | */
40 | private final String fieldName;
41 | /**
42 | * Whether the part is a file field.
43 | */
44 | private final boolean fileField;
45 | /**
46 | * The headers, if any.
47 | */
48 | private final Headers headers;
49 | /**
50 | * The size of the part, updated on each read.
51 | */
52 | private long size;
53 | /**
54 | * Boolean flag storing whether the part is
55 | * completely uploaded.
56 | */
57 | private boolean finished;
58 | /**
59 | * The output object supplied by the caller.
60 | */
61 | private PartOutput output;
62 |
63 | /**
64 | * Creates a new instance.
65 | * @param fileName The file name.
66 | * @param fieldName The form field name.
67 | * @param headers The object containing the headers
68 | */
69 | public PartStreamImpl(final String fileName, final String fieldName, final Headers headers) {
70 | this.fileName = fileName;
71 | this.fieldName = fieldName;
72 | this.contentType = headers.getHeader(Headers.CONTENT_TYPE);
73 | this.fileField = fileName != null;
74 | this.headers = headers;
75 | }
76 |
77 | @Override
78 | public String getContentType() {
79 | return contentType;
80 | }
81 |
82 | @Override
83 | public String getName() {
84 | return fieldName;
85 | }
86 |
87 | @Override
88 | public long getKnownSize() {
89 | return size;
90 | }
91 |
92 | @Override
93 | public String getSubmittedFileName() {
94 | return checkFileName(fileName);
95 | }
96 |
97 | @Override
98 | public boolean isFile() {
99 | return fileField;
100 | }
101 |
102 | @Override
103 | public boolean isFinished() {
104 | return finished;
105 | }
106 |
107 | @Override
108 | public String getHeader(final String name) {
109 | return headers.getHeader(name);
110 | }
111 |
112 | @Override
113 | public Collection getHeaderNames() {
114 | return headers.getHeaderNames();
115 | }
116 |
117 | @Override
118 | public Collection getHeaders(final String name) {
119 | return headers.getHeaders(name);
120 | }
121 |
122 | void setSize(final long size) {
123 | this.size = size;
124 | }
125 |
126 | void markAsFinished() {
127 | this.finished = true;
128 | }
129 |
130 | public PartOutput getOutput() {
131 | return output;
132 | }
133 |
134 | void setOutput(final PartOutput output) {
135 | this.output = output;
136 | }
137 |
138 | private String checkFileName(final String fileName) {
139 | if (fileName != null && fileName.indexOf('\u0000') != -1) {
140 | final var sb = new StringBuilder();
141 | for (var i = 0; i < fileName.length(); i++) {
142 | final var character = fileName.charAt(i);
143 | final var append = character == 0 ? "\\0" : character;
144 | sb.append(append);
145 | }
146 | throw new IllegalArgumentException(fileName + " Invalid file name: " + sb);
147 | }
148 | return fileName;
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/UploadContextImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.internal;
18 |
19 | import com.github.elopteryx.upload.PartOutput;
20 | import com.github.elopteryx.upload.PartStream;
21 | import com.github.elopteryx.upload.UploadContext;
22 |
23 | import java.util.ArrayList;
24 | import java.util.Collections;
25 | import java.util.List;
26 | import jakarta.servlet.http.HttpServletRequest;
27 |
28 | /**
29 | * Default implementation of {@link UploadContext}.
30 | */
31 | public class UploadContextImpl implements UploadContext {
32 |
33 | /**
34 | * The request containing the bytes of the file. It's always a multipart, POST request.
35 | */
36 | private final HttpServletRequest request;
37 | /**
38 | * The user object. Only used by the callers, not necessary for these classes.
39 | */
40 | private final Object userObject;
41 | /**
42 | * The currently processed item.
43 | */
44 | private PartStreamImpl currentPart;
45 | /**
46 | * The active output.
47 | */
48 | private PartOutput output;
49 | /**
50 | * The list of the already processed items.
51 | */
52 | private final List partStreams = new ArrayList<>();
53 | /**
54 | * Determines whether the current item is buffering, that is, should new bytes be
55 | * stored in memory or written out the channel. It is set to false after the
56 | * part begin function is called.
57 | */
58 | private boolean buffering = true;
59 | /**
60 | * The total number for the bytes read for the current part.
61 | */
62 | private int partBytesRead;
63 |
64 | public UploadContextImpl(final HttpServletRequest request, final Object userObject) {
65 | this.request = request;
66 | this.userObject = userObject;
67 | }
68 |
69 | @Override
70 | public HttpServletRequest getRequest() {
71 | return request;
72 | }
73 |
74 | @Override
75 | public T getUserObject(final Class clazz) {
76 | return userObject == null ? null : clazz.cast(userObject);
77 | }
78 |
79 | @Override
80 | public PartStreamImpl getCurrentPart() {
81 | return currentPart;
82 | }
83 |
84 | @Override
85 | public PartOutput getCurrentOutput() {
86 | return output;
87 | }
88 |
89 | @Override
90 | public List getPartStreams() {
91 | return Collections.unmodifiableList(partStreams);
92 | }
93 |
94 | void reset(final PartStreamImpl newPart) {
95 | buffering = true;
96 | partBytesRead = 0;
97 | currentPart = newPart;
98 | partStreams.add(newPart);
99 | output = null;
100 | }
101 |
102 | void setOutput(final PartOutput output) {
103 | this.output = output;
104 | this.currentPart.setOutput(output);
105 | }
106 |
107 | boolean isBuffering() {
108 | return buffering;
109 | }
110 |
111 | void finishBuffering() {
112 | buffering = false;
113 | }
114 |
115 | void updatePartBytesRead() {
116 | currentPart.setSize(partBytesRead);
117 | }
118 |
119 | int getPartBytesRead() {
120 | return partBytesRead;
121 | }
122 |
123 | int incrementAndGetPartBytesRead(final int additional) {
124 | partBytesRead += additional;
125 | return partBytesRead;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/internal/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * The package for the actual implementation. Classes of this
3 | * package are considered internal, can change a lot during
4 | * releases, but are otherwise usable. If the public API
5 | * does not meet the needs then these classes can be used
6 | * instead.
7 | */
8 | package com.github.elopteryx.upload.internal;
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * The top level of the package hierarchy which contains the
3 | * public API of this library. Interfaces and classes here will
4 | * not be changed frequently.
5 | *
6 | * The library has been designed to hide the implementation details
7 | * from the users. When using it the users should not
8 | * need to import from the internal package, using the public
9 | * classes and the exception classes should be more than enough.
10 | *
11 | * The parser object provides full control over the uploading
12 | * process. The functional interfaces can be used to perform
13 | * operations on the part objects. Information about the process
14 | * is provided in the upload context object, which is available
15 | * in every stage of the process.
16 | *
17 | * The classes here have been designed to work with servlets. There
18 | * are two kinds of parsing, you can do it asynchronously or in
19 | * a blocking way. For the former the servlet must support support
20 | * async mode. Both of them are incompatible with the multipart
21 | * API of the servlet specification. Using that makes the
22 | * servlet input stream unavailable for this library or any code
23 | * that is written by the users.
24 | */
25 | package com.github.elopteryx.upload;
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/util/ByteBufferBackedInputStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.util;
18 |
19 | import java.io.IOException;
20 | import java.io.InputStream;
21 | import java.nio.ByteBuffer;
22 | import java.util.Objects;
23 |
24 | /**
25 | * An input stream implementation which reads from the given byte buffer.
26 | * The stream will not copy bytes to a temporary buffer, therefore read-only
27 | * and direct buffers are not supported.
28 | */
29 | public class ByteBufferBackedInputStream extends InputStream {
30 |
31 | /**
32 | * The byte buffer. Cannot be read-only or direct.
33 | */
34 | private final ByteBuffer buffer;
35 |
36 | /**
37 | * Flag to determine whether the channel is closed or not.
38 | */
39 | private boolean open = true;
40 |
41 | /**
42 | * Public constructor.
43 | * @param buffer The byte buffer
44 | */
45 | public ByteBufferBackedInputStream(final ByteBuffer buffer) {
46 | this.buffer = Objects.requireNonNull(buffer);
47 | if (buffer.isDirect() || buffer.isReadOnly()) {
48 | throw new IllegalArgumentException("The buffer cannot be direct or read-only!");
49 | }
50 | }
51 |
52 | @Override
53 | public int available() throws IOException {
54 | return buffer.remaining();
55 | }
56 |
57 | @Override
58 | public int read() throws IOException {
59 | if (!open) {
60 | throw new IOException("The stream was closed!");
61 | }
62 | if (!buffer.hasRemaining()) {
63 | return -1;
64 | }
65 | return buffer.get() & 0xFF;
66 | }
67 |
68 | @Override
69 | public int read(final byte[] bytes, final int off, int len) throws IOException {
70 | if (!open) {
71 | throw new IOException("The stream was closed!");
72 | }
73 | if (!buffer.hasRemaining()) {
74 | return -1;
75 | }
76 | len = Math.min(len, buffer.remaining());
77 | buffer.get(bytes, off, len);
78 | return len;
79 | }
80 |
81 | @Override
82 | public void close() throws IOException {
83 | open = false;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/util/ByteBufferBackedOutputStream.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 | import java.nio.ByteBuffer;
6 | import java.util.Objects;
7 |
8 | /**
9 | * An output stream implementation which writes to the given byte buffer.
10 | * The stream will not copy bytes to a temporary buffer, therefore read-only
11 | * and direct buffers are not supported.
12 | */
13 | public class ByteBufferBackedOutputStream extends OutputStream {
14 |
15 | /**
16 | * The byte buffer. Cannot be read-only or direct.
17 | */
18 | private final ByteBuffer buffer;
19 |
20 | /**
21 | * Flag to determine whether the channel is closed or not.
22 | */
23 | private boolean open = true;
24 |
25 | /**
26 | * Public constructor.
27 | * @param buffer The byte buffer
28 | */
29 | public ByteBufferBackedOutputStream(final ByteBuffer buffer) {
30 | this.buffer = Objects.requireNonNull(buffer);
31 | if (buffer.isDirect() || buffer.isReadOnly()) {
32 | throw new IllegalArgumentException("The buffer cannot be direct or read-only!");
33 | }
34 | }
35 |
36 | @Override
37 | public void write(final int byteToWrite) throws IOException {
38 | if (!open) {
39 | throw new IOException("The stream was closed!");
40 | }
41 | buffer.put((byte) byteToWrite);
42 | }
43 |
44 | @Override
45 | public void write(final byte[] bytes, final int off, final int len) throws IOException {
46 | if (!open) {
47 | throw new IOException("The stream was closed!");
48 | }
49 | buffer.put(bytes, off, len);
50 | }
51 |
52 | @Override
53 | public void close() throws IOException {
54 | open = false;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/util/InputStreamBackedChannel.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.nio.ByteBuffer;
6 | import java.nio.channels.ClosedChannelException;
7 | import java.nio.channels.ReadableByteChannel;
8 | import java.util.Objects;
9 |
10 | /**
11 | * A channel implementation which reads the ByteBuffer
12 | * data from the given InputStream instance.
13 | *
14 | * This implementation differs from the one returned
15 | * in {@link java.nio.channels.Channels#newChannel(InputStream)}
16 | * by one notable thing, it does not use a temporary buffer, because
17 | * it will not handle read-only or direct ByteBuffers.
18 | *
19 | * The channel honors the close contract, it cannot be used after closing.
20 | */
21 | public class InputStreamBackedChannel implements ReadableByteChannel {
22 |
23 | /**
24 | * Flag to determine whether the channel is closed or not.
25 | */
26 | private boolean open = true;
27 |
28 | /**
29 | * The stream the channel will read from.
30 | */
31 | private final InputStream inputStream;
32 |
33 | /**
34 | * Public constructor.
35 | * @param inputStream The input stream
36 | */
37 | public InputStreamBackedChannel(final InputStream inputStream) {
38 | this.inputStream = Objects.requireNonNull(inputStream);
39 | }
40 |
41 | @Override
42 | public int read(final ByteBuffer dst) throws IOException {
43 | if (!open) {
44 | throw new ClosedChannelException();
45 | }
46 | if (dst.isDirect() || dst.isReadOnly()) {
47 | throw new IllegalArgumentException("The buffer cannot be direct or read-only!");
48 | }
49 | final var buf = dst.array();
50 | final var offset = dst.position();
51 | final var len = dst.remaining();
52 | final var read = inputStream.read(buf, offset, len);
53 | dst.position(offset + read);
54 | return read;
55 | }
56 |
57 | @Override
58 | public boolean isOpen() {
59 | return open;
60 | }
61 |
62 | @Override
63 | public void close() throws IOException {
64 | inputStream.close();
65 | open = false;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/util/NullChannel.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.util;
18 |
19 | import com.github.elopteryx.upload.OnPartBegin;
20 |
21 | import java.io.IOException;
22 | import java.nio.ByteBuffer;
23 | import java.nio.channels.ClosedChannelException;
24 | import java.nio.channels.ReadableByteChannel;
25 | import java.nio.channels.WritableByteChannel;
26 |
27 | /**
28 | * A channel implementation which provides no data and discards the data supplied.
29 | * Used by the parser if it doesn't have a channel to write to.
30 | * The purpose of this is to make the {@link OnPartBegin} callback
31 | * optional, which is useful for testing.
32 | *
33 | * The channel honors the close contract, it cannot be used after closing.
34 | */
35 | public class NullChannel implements ReadableByteChannel, WritableByteChannel {
36 |
37 | /**
38 | * Flag to determine whether the channel is closed or not.
39 | */
40 | private boolean open = true;
41 |
42 | @Override
43 | public int read(final ByteBuffer dst) throws IOException {
44 | if (!open) {
45 | throw new ClosedChannelException();
46 | }
47 | return -1;
48 | }
49 |
50 | @Override
51 | public int write(final ByteBuffer src) throws IOException {
52 | if (!open) {
53 | throw new ClosedChannelException();
54 | }
55 | final var remaining = src.remaining();
56 | src.position(src.limit());
57 | return remaining;
58 | }
59 |
60 | @Override
61 | public boolean isOpen() {
62 | return open;
63 | }
64 |
65 | @Override
66 | public void close() {
67 | open = false;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/util/OutputStreamBackedChannel.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Adam Forgacs
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.github.elopteryx.upload.util;
18 |
19 | import java.io.IOException;
20 | import java.io.OutputStream;
21 | import java.nio.ByteBuffer;
22 | import java.nio.channels.ClosedChannelException;
23 | import java.nio.channels.WritableByteChannel;
24 | import java.util.Objects;
25 |
26 | /**
27 | * A channel implementation which writes the ByteBuffer data
28 | * to the given OutputStream instance.
29 | *
30 | * This implementation differs from the one returned
31 | * in {@link java.nio.channels.Channels#newChannel(OutputStream)}
32 | * by one notable thing, it does not use a temporary buffer, because
33 | * it will not handle read-only or direct ByteBuffers.
34 | *
35 | * The channel honors the close contract, it cannot be used after closing.
36 | */
37 | public class OutputStreamBackedChannel implements WritableByteChannel {
38 |
39 | /**
40 | * Flag to determine whether the channel is closed or not.
41 | */
42 | private boolean open = true;
43 |
44 | /**
45 | * The stream the channel will write to.
46 | */
47 | private final OutputStream outputStream;
48 |
49 | /**
50 | * Public constructor.
51 | * @param outputStream The output stream
52 | */
53 | public OutputStreamBackedChannel(final OutputStream outputStream) {
54 | this.outputStream = Objects.requireNonNull(outputStream);
55 | }
56 |
57 | @Override
58 | public int write(final ByteBuffer src) throws IOException {
59 | if (!open) {
60 | throw new ClosedChannelException();
61 | }
62 | if (src.isDirect() || src.isReadOnly()) {
63 | throw new IllegalArgumentException("The buffer cannot be direct or read-only!");
64 | }
65 | final var buf = src.array();
66 | final var offset = src.position();
67 | final var len = src.remaining();
68 | outputStream.write(buf, offset, len);
69 | src.position(offset + len);
70 | return len;
71 | }
72 |
73 | @Override
74 | public boolean isOpen() {
75 | return open;
76 | }
77 |
78 | @Override
79 | public void close() throws IOException {
80 | outputStream.close();
81 | open = false;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/com/github/elopteryx/upload/util/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This package contains utility classes for channels and
3 | * streams used by this library. As they can be useful
4 | * not just for the internal implementation these classes
5 | * have their own package and can be used safely.
6 | */
7 | package com.github.elopteryx.upload.util;
--------------------------------------------------------------------------------
/upload-parser-core/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Async file upload module for servlets.
3 | */
4 | module com.github.elopteryx.upload {
5 | requires jakarta.servlet;
6 | exports com.github.elopteryx.upload;
7 | exports com.github.elopteryx.upload.errors;
8 | exports com.github.elopteryx.upload.util;
9 | }
--------------------------------------------------------------------------------
/upload-parser-tests/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'jacoco-report-aggregation'
3 | }
4 |
5 | dependencies {
6 |
7 | /* Upload parser. */
8 | implementation(project(':upload-parser-core'))
9 |
10 | /* Test runner. */
11 | testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
12 | testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
13 | testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
14 |
15 | /* Undertow Http server. */
16 | testImplementation("io.undertow:undertow-core:$undertowVersion")
17 | testImplementation("io.undertow:undertow-servlet:$undertowVersion")
18 |
19 | /* Jetty Http server. */
20 | testImplementation("org.eclipse.jetty:jetty-server:$jettyVersion")
21 | testImplementation("org.eclipse.jetty:jetty-servlet:$jettyVersion")
22 |
23 | /* Tomcat Http server. */
24 | testImplementation("org.apache.tomcat.embed:tomcat-embed-core:$tomcatVersion")
25 |
26 | /* Document and file parser. */
27 | testImplementation("org.apache.tika:tika-core:$tikaVersion")
28 | testImplementation("org.apache.tika:tika-parsers:$tikaVersion")
29 |
30 | /* In-memory filesystem. */
31 | testImplementation("com.google.jimfs:jimfs:$jimfsVersion")
32 |
33 | /* Object mocking. */
34 | testImplementation("org.mockito:mockito-core:$mockitoVersion")
35 |
36 | /* Servlet API. */
37 | testImplementation("jakarta.servlet:jakarta.servlet-api:$servletApiVersion")
38 |
39 | }
40 |
41 | tasks.named('check') {
42 | dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
43 | }
44 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/PartOutputTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertFalse;
4 | import static org.junit.jupiter.api.Assertions.assertNotNull;
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
6 |
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.io.ByteArrayOutputStream;
10 | import java.io.OutputStream;
11 | import java.nio.channels.Channel;
12 | import java.nio.channels.Channels;
13 | import java.nio.channels.WritableByteChannel;
14 | import java.nio.file.Path;
15 | import java.nio.file.Paths;
16 |
17 | class PartOutputTest {
18 |
19 | @Test
20 | void create_channel_output() {
21 | final var byteArrayOutputStream = new ByteArrayOutputStream();
22 | final var channel = Channels.newChannel(byteArrayOutputStream);
23 | final var output = PartOutput.from(channel);
24 |
25 | assertTrue(output.safeToCast(Channel.class));
26 | assertTrue(output.safeToCast(WritableByteChannel.class));
27 | assertFalse(output.safeToCast(OutputStream.class));
28 | assertFalse(output.safeToCast(ByteArrayOutputStream.class));
29 |
30 | assertNotNull(output.unwrap(WritableByteChannel.class));
31 | }
32 |
33 | @Test
34 | void create_stream_output() {
35 | final var byteArrayOutputStream = new ByteArrayOutputStream();
36 | final var output = PartOutput.from(byteArrayOutputStream);
37 |
38 | assertTrue(output.safeToCast(OutputStream.class));
39 | assertTrue(output.safeToCast(ByteArrayOutputStream.class));
40 | assertFalse(output.safeToCast(Channel.class));
41 | assertFalse(output.safeToCast(WritableByteChannel.class));
42 |
43 | assertNotNull(output.unwrap(OutputStream.class));
44 | }
45 |
46 | @Test
47 | void create_path_output() {
48 | final var path = Paths.get("");
49 | final var output = PartOutput.from(path);
50 |
51 | assertTrue(output.safeToCast(Path.class));
52 |
53 | assertNotNull(output.unwrap(Path.class));
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/UploadParserTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload;
2 |
3 | import static com.github.elopteryx.upload.util.Servlets.newRequest;
4 | import static com.github.elopteryx.upload.util.Servlets.newResponse;
5 | import static org.junit.jupiter.api.Assertions.assertAll;
6 | import static org.junit.jupiter.api.Assertions.assertFalse;
7 | import static org.junit.jupiter.api.Assertions.assertThrows;
8 | import static org.junit.jupiter.api.Assertions.assertTrue;
9 | import static org.mockito.Mockito.mock;
10 | import static org.mockito.Mockito.when;
11 |
12 | import com.github.elopteryx.upload.util.NullChannel;
13 | import com.google.common.jimfs.Jimfs;
14 | import org.junit.jupiter.api.BeforeAll;
15 | import org.junit.jupiter.api.Test;
16 |
17 | import java.nio.ByteBuffer;
18 | import java.nio.file.FileSystem;
19 | import java.nio.file.Files;
20 | import jakarta.servlet.AsyncContext;
21 | import jakarta.servlet.ServletInputStream;
22 | import jakarta.servlet.http.HttpServletResponse;
23 |
24 | class UploadParserTest implements OnPartBegin, OnPartEnd, OnRequestComplete, OnError {
25 |
26 | private static FileSystem fileSystem;
27 |
28 | @BeforeAll
29 | static void setUp() {
30 | fileSystem = Jimfs.newFileSystem();
31 | }
32 |
33 | @Test
34 | void valid_content_type() throws Exception {
35 | final var request = newRequest();
36 |
37 | when(request.getContentType()).thenReturn("multipart/");
38 | assertTrue(UploadParser.isMultipart(request));
39 | }
40 |
41 | @Test
42 | void invalid_numeric_arguments() {
43 | assertAll(
44 | () -> assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().sizeThreshold(-1)),
45 | () -> assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().maxPartSize(-1)),
46 | () -> assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().maxRequestSize(-1)),
47 | () -> assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().maxBytesUsed(-1))
48 | );
49 | }
50 |
51 | @Test
52 | void invalid_content_type_async() throws Exception {
53 | final var request = newRequest();
54 |
55 | when(request.getContentType()).thenReturn("text/plain;charset=UTF-8");
56 | assertFalse(UploadParser.isMultipart(request));
57 | assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().userObject(newResponse()).setupAsyncParse(request));
58 | }
59 |
60 | @Test
61 | void invalid_content_type_blocking() throws Exception {
62 | final var request = newRequest();
63 |
64 | when(request.getContentType()).thenReturn("text/plain;charset=UTF-8");
65 | assertFalse(UploadParser.isMultipart(request));
66 | assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().userObject(newResponse()).doBlockingParse(request));
67 | }
68 |
69 | @Test
70 | void use_the_full_api() throws Exception {
71 | final var request = newRequest();
72 | final var response = newResponse();
73 |
74 | when(request.startAsync()).thenReturn(mock(AsyncContext.class));
75 | when(request.getInputStream()).thenReturn(mock(ServletInputStream.class));
76 |
77 | UploadParser.newParser()
78 | .onPartBegin(this)
79 | .onPartEnd(this)
80 | .onRequestComplete(this)
81 | .onError(this)
82 | .userObject(response)
83 | .maxBytesUsed(4096)
84 | .sizeThreshold(1024 * 1024 * 10)
85 | .maxPartSize(1024 * 1024 * 50)
86 | .maxRequestSize(1024 * 1024 * 50)
87 | .setupAsyncParse(request);
88 | }
89 |
90 | @Test
91 | void output_channel() {
92 | UploadParser.newParser()
93 | .onPartBegin((context, buffer) -> {
94 | final var test = fileSystem.getPath("test1");
95 | Files.createFile(test);
96 | return PartOutput.from(Files.newByteChannel(test));
97 | });
98 | }
99 |
100 | @Test
101 | void output_stream() {
102 | UploadParser.newParser()
103 | .onPartBegin((context, buffer) -> {
104 | final var test = fileSystem.getPath("test2");
105 | Files.createFile(test);
106 | return PartOutput.from(Files.newOutputStream(test));
107 | });
108 | }
109 |
110 | @Test
111 | void output_path() {
112 | UploadParser.newParser()
113 | .onPartBegin((context, buffer) -> {
114 | final var test = fileSystem.getPath("test2");
115 | Files.createFile(test);
116 | return PartOutput.from(test);
117 | });
118 | }
119 |
120 | @Test
121 | void use_with_custom_object() {
122 | UploadParser.newParser()
123 | .userObject(newResponse())
124 | .onPartBegin((context, buffer) -> {
125 | final var test = fileSystem.getPath("test2");
126 | Files.createFile(test);
127 | return PartOutput.from(test);
128 | })
129 | .onRequestComplete(context -> context.getUserObject(HttpServletResponse.class).setStatus(HttpServletResponse.SC_OK));
130 | }
131 |
132 | @Override
133 | public PartOutput onPartBegin(final UploadContext context, final ByteBuffer buffer) {
134 | return PartOutput.from(new NullChannel());
135 | }
136 |
137 | @Override
138 | public void onPartEnd(final UploadContext context) {
139 | // No-op
140 | }
141 |
142 | @Override
143 | public void onRequestComplete(final UploadContext context) {
144 | // No-op
145 | }
146 |
147 | @Override
148 | public void onError(final UploadContext context, final Throwable throwable) {
149 | // No-op
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/AbstractUploadParserTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
6 | import static org.mockito.Mockito.when;
7 |
8 | import com.github.elopteryx.upload.errors.PartSizeException;
9 | import com.github.elopteryx.upload.errors.RequestSizeException;
10 | import com.github.elopteryx.upload.util.Servlets;
11 | import org.junit.jupiter.api.Test;
12 |
13 | class AbstractUploadParserTest {
14 |
15 | private static final long SIZE = 1024 * 1024 * 100L;
16 | private static final long SMALL_SIZE = 1024;
17 |
18 | private AbstractUploadParser runSetupForSize(final long requestSize, final long allowedRequestSize, final long allowedPartSize) throws Exception {
19 | final var request = Servlets.newRequest();
20 |
21 | when(request.getContentLengthLong()).thenReturn(requestSize);
22 |
23 | final var parser = new AsyncUploadParser(request);
24 | parser.setMaxPartSize(allowedPartSize);
25 | parser.setMaxRequestSize(allowedRequestSize);
26 | parser.setupAsyncParse();
27 | return parser;
28 | }
29 |
30 | @Test
31 | void setup_should_work_if_lesser() throws Exception {
32 | runSetupForSize(SIZE - 1, SIZE, -1);
33 | }
34 |
35 | @Test
36 | void setup_should_work_if_equals() throws Exception {
37 | runSetupForSize(SIZE, SIZE, -1);
38 | }
39 |
40 | @Test
41 | void setup_should_throw_size_exception_if_greater() {
42 | assertThrows(RequestSizeException.class, () -> runSetupForSize(SIZE + 1, SIZE, -1));
43 | }
44 |
45 | @Test
46 | void parser_should_throw_exception_for_request_size() {
47 | final var exception = assertThrows(RequestSizeException.class, () -> {
48 | final var parser = runSetupForSize(0, SMALL_SIZE, -1);
49 | for (var i = 0; i < 11; i++) {
50 | parser.checkRequestSize(100);
51 | }
52 | });
53 | assertEquals(exception.getPermittedSize(), SMALL_SIZE);
54 | assertTrue(exception.getActualSize() > SMALL_SIZE);
55 | }
56 |
57 | @Test
58 | void parser_should_throw_exception_for_part_size() {
59 | final var exception = assertThrows(PartSizeException.class, () -> {
60 | final var parser = runSetupForSize(0, -1, SMALL_SIZE);
61 | for (var i = 0; i < 11; i++) {
62 | parser.checkPartSize(100);
63 | }
64 | });
65 | assertEquals(exception.getPermittedSize(), SMALL_SIZE);
66 | assertTrue(exception.getActualSize() > SMALL_SIZE);
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/AsyncUploadParserTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertThrows;
4 | import static org.mockito.Mockito.when;
5 |
6 | import com.github.elopteryx.upload.UploadParser;
7 | import com.github.elopteryx.upload.errors.MultipartException;
8 | import com.github.elopteryx.upload.util.MockServletInputStream;
9 | import com.github.elopteryx.upload.util.Servlets;
10 | import org.junit.jupiter.api.Test;
11 |
12 | class AsyncUploadParserTest {
13 |
14 | @Test
15 | void this_should_end_with_multipart_exception() throws Exception {
16 | final var request = Servlets.newRequest();
17 |
18 | when(request.isAsyncSupported()).thenReturn(true);
19 | when(request.getHeader(Headers.CONTENT_TYPE)).thenReturn("multipart/form-data; boundary=----1234");
20 |
21 | UploadParser.newParser().setupAsyncParse(request);
22 | final var servletInputStream = (MockServletInputStream)request.getInputStream();
23 | assertThrows(MultipartException.class, servletInputStream::onDataAvailable);
24 | }
25 |
26 | @Test
27 | void this_should_end_with_illegal_state_exception() throws Exception {
28 | final var request = Servlets.newRequest();
29 |
30 | when(request.isAsyncSupported()).thenReturn(false);
31 |
32 | assertThrows(IllegalStateException.class, () -> UploadParser.newParser().setupAsyncParse(request));
33 | }
34 |
35 | @Test
36 | void this_should_end_with_illegal_argument_exception() throws Exception {
37 | final var request = Servlets.newRequest();
38 |
39 | when(request.isAsyncSupported()).thenReturn(true);
40 | when(request.getHeader(Headers.CONTENT_TYPE)).thenReturn("multipart/form-data; boundary;");
41 |
42 | assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().setupAsyncParse(request));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/Base64EncodingTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static java.nio.charset.StandardCharsets.US_ASCII;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 | import static org.junit.jupiter.api.Assertions.assertThrows;
6 |
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.io.IOException;
10 | import java.nio.ByteBuffer;
11 |
12 | class Base64EncodingTest {
13 |
14 | @Test
15 | void these_values_should_work() throws IOException {
16 | checkEncoding("", "");
17 | checkEncoding("f", "Zg==");
18 | checkEncoding("fo", "Zm8=");
19 | checkEncoding("foo", "Zm9v");
20 | checkEncoding("foob", "Zm9vYg==");
21 | checkEncoding("fooba", "Zm9vYmE=");
22 | checkEncoding("foobar", "Zm9vYmFy");
23 | }
24 |
25 | @Test
26 | void must_throw_exception_on_invalid_data() {
27 | assertThrows(IOException.class, () -> checkEncoding("f", "Zg=�="));
28 | }
29 |
30 | private static void checkEncoding(final String original, final String encoded) throws IOException {
31 |
32 | final var encoding = new MultipartParser.Base64Encoding(1024);
33 | encoding.handle(new MultipartParser.PartHandler() {
34 |
35 | @Override
36 | public void beginPart(final Headers headers) {
37 | // No-op
38 | }
39 |
40 | @Override
41 | public void data(final ByteBuffer buffer) {
42 | final var parserResult = new String(buffer.array(), US_ASCII).trim();
43 | assertEquals(parserResult, original);
44 | }
45 |
46 | @Override
47 | public void endPart() {
48 | // No-op
49 | }
50 |
51 | }, ByteBuffer.wrap(encoded.getBytes(US_ASCII)));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/BlockingUploadParserTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertFalse;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
6 | import static org.mockito.Mockito.when;
7 |
8 | import com.github.elopteryx.upload.OnError;
9 | import com.github.elopteryx.upload.OnPartBegin;
10 | import com.github.elopteryx.upload.OnPartEnd;
11 | import com.github.elopteryx.upload.PartOutput;
12 | import com.github.elopteryx.upload.UploadContext;
13 | import com.github.elopteryx.upload.UploadParser;
14 | import com.github.elopteryx.upload.errors.MultipartException;
15 | import com.github.elopteryx.upload.util.Servlets;
16 | import org.junit.jupiter.api.Test;
17 |
18 | import java.io.ByteArrayOutputStream;
19 | import java.nio.ByteBuffer;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | class BlockingUploadParserTest implements OnPartBegin, OnPartEnd, OnError {
24 |
25 | private final List strings = new ArrayList<>();
26 |
27 | @Test
28 | void this_should_end_with_multipart_exception() throws Exception {
29 | final var request = Servlets.newRequest();
30 | final var response = Servlets.newResponse();
31 |
32 | when(request.isAsyncSupported()).thenReturn(false);
33 | when(request.getHeader(Headers.CONTENT_TYPE)).thenReturn("multipart/form-data; boundary=----1234");
34 |
35 | UploadParser.newParser()
36 | .onPartBegin(this)
37 | .onPartEnd(this)
38 | .onError(this)
39 | .userObject(response)
40 | .doBlockingParse(request);
41 | }
42 |
43 | @Test
44 | void this_should_end_with_illegal_argument_exception() throws Exception {
45 | final var request = Servlets.newRequest();
46 |
47 | when(request.isAsyncSupported()).thenReturn(false);
48 | when(request.getHeader(Headers.CONTENT_TYPE)).thenReturn("multipart/form-data;");
49 |
50 | assertThrows(IllegalArgumentException.class, () -> UploadParser.newParser().doBlockingParse(request));
51 | }
52 |
53 | @Override
54 | public PartOutput onPartBegin(final UploadContext context, final ByteBuffer buffer) {
55 | final var baos = new ByteArrayOutputStream();
56 | strings.add(baos);
57 | return PartOutput.from(baos);
58 | }
59 |
60 | @Override
61 | public void onPartEnd(final UploadContext context) {
62 | assertFalse(strings.isEmpty());
63 | }
64 |
65 | @Override
66 | public void onError(final UploadContext context, final Throwable throwable) {
67 | assertTrue(throwable instanceof MultipartException);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/HeadersTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertFalse;
5 | import static org.junit.jupiter.api.Assertions.assertNull;
6 | import static org.junit.jupiter.api.Assertions.assertTrue;
7 |
8 | import org.junit.jupiter.api.Test;
9 |
10 | import java.util.Iterator;
11 | import java.util.Locale;
12 |
13 | class HeadersTest {
14 |
15 | @Test
16 | void add_and_retrieve_headers() {
17 | final var headers = new Headers();
18 | headers.addHeader(Headers.CONTENT_DISPOSITION, "form-data; name=\"FileItem\"; filename=\"file1.txt\"");
19 | headers.addHeader(Headers.CONTENT_TYPE, "text/plain");
20 | headers.addHeader("TestHeader", "headerValue1");
21 | headers.addHeader("TestHeader", "headerValue2");
22 | headers.addHeader("TestHeader", "headerValue3");
23 | headers.addHeader("testheader", "headerValue4");
24 |
25 | final var headerNames = headers.getHeaderNames().iterator();
26 | assertEquals(Headers.CONTENT_DISPOSITION.toLowerCase(Locale.ENGLISH), headerNames.next());
27 | assertEquals(Headers.CONTENT_TYPE.toLowerCase(Locale.ENGLISH), headerNames.next());
28 | assertEquals("testheader", headerNames.next());
29 | assertFalse(headerNames.hasNext());
30 |
31 | assertEquals(headers.getHeader(Headers.CONTENT_DISPOSITION), "form-data; name=\"FileItem\"; filename=\"file1.txt\"");
32 | assertEquals(headers.getHeader(Headers.CONTENT_TYPE), "text/plain");
33 | assertEquals(headers.getHeader(Headers.CONTENT_TYPE), "text/plain");
34 | assertEquals(headers.getHeader("TestHeader"), "headerValue1");
35 | assertNull(headers.getHeader("DummyHeader"));
36 |
37 | Iterator headerValues;
38 |
39 | headerValues = headers.getHeaders("Content-Type").iterator();
40 | assertTrue(headerValues.hasNext());
41 | assertEquals(headerValues.next(), "text/plain");
42 | assertFalse(headerValues.hasNext());
43 |
44 | headerValues = headers.getHeaders("content-type").iterator();
45 | assertTrue(headerValues.hasNext());
46 | assertEquals(headerValues.next(), "text/plain");
47 | assertFalse(headerValues.hasNext());
48 |
49 | headerValues = headers.getHeaders("TestHeader").iterator();
50 | assertTrue(headerValues.hasNext());
51 | assertEquals(headerValues.next(), "headerValue1");
52 | assertTrue(headerValues.hasNext());
53 | assertEquals(headerValues.next(), "headerValue2");
54 | assertTrue(headerValues.hasNext());
55 | assertEquals(headerValues.next(), "headerValue3");
56 | assertTrue(headerValues.hasNext());
57 | assertEquals(headerValues.next(), "headerValue4");
58 | assertFalse(headerValues.hasNext());
59 |
60 | headerValues = headers.getHeaders("DummyHeader").iterator();
61 | assertFalse(headerValues.hasNext());
62 | }
63 |
64 | @Test
65 | void charset_parsing() {
66 | assertNull(Headers.extractQuotedValueFromHeader("text/html; other-data=\"charset=UTF-8\"", "charset"));
67 | assertNull(Headers.extractQuotedValueFromHeader("text/html;", "charset"));
68 | assertEquals("UTF-8", Headers.extractQuotedValueFromHeader("text/html; charset=\"UTF-8\"", "charset"));
69 | assertEquals("UTF-8", Headers.extractQuotedValueFromHeader("text/html; charset=UTF-8", "charset"));
70 | assertEquals("UTF-8", Headers.extractQuotedValueFromHeader("text/html; charset=\"UTF-8\"; foo=bar", "charset"));
71 | assertEquals("UTF-8", Headers.extractQuotedValueFromHeader("text/html; charset=UTF-8 foo=bar", "charset"));
72 | }
73 |
74 | @Test
75 | void extract_existing_boundary() {
76 | assertEquals("--xyz", Headers.extractBoundaryFromHeader("multipart/form-data; boundary=--xyz; param=abc"));
77 | }
78 |
79 | @Test
80 | void extract_missing_boundary() {
81 | assertNull(Headers.extractBoundaryFromHeader("multipart/form-data; boundary;"));
82 | }
83 |
84 | @Test
85 | void extract_param_with_tailing_whitespace() {
86 | assertEquals("abc", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=abc", "param"));
87 | assertEquals("abc", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=abc ", "param"));
88 | assertEquals("", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param= abc", "param"));
89 | assertEquals("", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param= abc ", "param"));
90 |
91 | assertEquals("abc", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=\"abc\"", "param"));
92 | assertEquals("abc ", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=\"abc \"", "param"));
93 | assertEquals(" abc", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=\" abc\"", "param"));
94 | assertEquals(" abc ", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=\" abc \"", "param"));
95 |
96 | assertNull(Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param\t=abc", "param"));
97 | assertEquals("ab", Headers.extractQuotedValueFromHeader("multipart/form-data; boundary=--xyz; param=ab\tc", "param"));
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/IdentityEncodingTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static java.nio.charset.StandardCharsets.UTF_8;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 |
11 | class IdentityEncodingTest {
12 |
13 | @Test
14 | void these_values_should_work() throws IOException {
15 | checkEncoding("");
16 | checkEncoding("abc");
17 | checkEncoding("öüóúőűáéí");
18 | }
19 |
20 | private static void checkEncoding(final String original) throws IOException {
21 |
22 | final var encoding = new MultipartParser.IdentityEncoding();
23 | encoding.handle(new MultipartParser.PartHandler() {
24 |
25 | @Override
26 | public void beginPart(final Headers headers) {
27 | // No-op
28 | }
29 |
30 | @Override
31 | public void data(final ByteBuffer buffer) {
32 | final var parserResult = new String(buffer.array(), UTF_8);
33 | assertEquals(parserResult, original);
34 | }
35 |
36 | @Override
37 | public void endPart() {
38 | // No-op
39 | }
40 |
41 | }, ByteBuffer.wrap(original.getBytes(UTF_8)));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/MultipartParserTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static java.nio.charset.StandardCharsets.ISO_8859_1;
4 | import static java.nio.charset.StandardCharsets.UTF_8;
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 | import static org.junit.jupiter.api.Assertions.assertFalse;
7 | import static org.junit.jupiter.api.Assertions.assertThrows;
8 | import static org.junit.jupiter.api.Assertions.assertTrue;
9 |
10 | import io.undertow.util.FileUtils;
11 | import org.junit.jupiter.params.ParameterizedTest;
12 | import org.junit.jupiter.params.provider.MethodSource;
13 |
14 | import java.io.IOException;
15 | import java.nio.ByteBuffer;
16 | import java.util.ArrayList;
17 | import java.util.List;
18 |
19 | class MultipartParserTest {
20 |
21 | private static int[] bufferSizeProvider() {
22 | return new int[]{2, 10, 1024, 4096};
23 | }
24 |
25 | @ParameterizedTest
26 | @MethodSource("bufferSizeProvider")
27 | void mime_decoding_with_preamble(final int bufferSize) throws IOException {
28 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime1.txt"));
29 | final var handler = new MockPartHandler();
30 | final var parser = MultipartParser.beginParse(handler, "unique-boundary-1".getBytes(), bufferSize, ISO_8859_1);
31 |
32 | final var buf = ByteBuffer.wrap(data.getBytes());
33 | parser.parse(buf);
34 | assertTrue(parser.isComplete());
35 | assertEquals(2, handler.parts.size());
36 | assertEquals("Here is some text.", handler.parts.get(0).data.toString());
37 | assertEquals("Here is some more text.", handler.parts.get(1).data.toString());
38 |
39 | assertEquals("text/plain", handler.parts.get(0).map.getHeader(Headers.CONTENT_TYPE));
40 | }
41 |
42 |
43 | @ParameterizedTest
44 | @MethodSource("bufferSizeProvider")
45 | void mime_decoding_with_utf8_headers(final int bufferSize) throws IOException {
46 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime-utf8.txt"));
47 | final var handler = new MockPartHandler();
48 | final var parser = MultipartParser.beginParse(handler, "unique-boundary-1".getBytes(), bufferSize, UTF_8);
49 |
50 | final var buf = ByteBuffer.wrap(data.getBytes());
51 | parser.parse(buf);
52 | assertTrue(parser.isComplete());
53 | assertEquals(1, handler.parts.size());
54 | assertEquals("Just some chinese characters I copied from the internet, no idea what it says.", handler.parts.get(0).data.toString());
55 |
56 | assertEquals("text/plain", handler.parts.get(0).map.getHeader(Headers.CONTENT_TYPE));
57 | }
58 |
59 | @ParameterizedTest
60 | @MethodSource("bufferSizeProvider")
61 | void mime_decoding_without_preamble(final int bufferSize) throws IOException {
62 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime2.txt"));
63 | final var handler = new MockPartHandler();
64 | final var parser = MultipartParser.beginParse(handler, "unique-boundary-1".getBytes(), bufferSize, ISO_8859_1);
65 |
66 | final var buf = ByteBuffer.wrap(data.getBytes());
67 | parser.parse(buf);
68 | assertTrue(parser.isComplete());
69 | assertEquals(2, handler.parts.size());
70 | assertEquals("Here is some text.", handler.parts.get(0).data.toString());
71 | assertEquals("Here is some more text.", handler.parts.get(1).data.toString());
72 |
73 | assertEquals("text/plain", handler.parts.get(0).map.getHeader(Headers.CONTENT_TYPE));
74 | }
75 |
76 | @ParameterizedTest
77 | @MethodSource("bufferSizeProvider")
78 | void base64_mime_decoding(final int bufferSize) throws IOException {
79 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime3.txt"));
80 | final var handler = new MockPartHandler();
81 | final var parser = MultipartParser.beginParse(handler, "unique-boundary-1".getBytes(), bufferSize, ISO_8859_1);
82 |
83 | final var buf = ByteBuffer.wrap(data.getBytes());
84 | parser.parse(buf);
85 | assertTrue(parser.isComplete());
86 | assertEquals(2, handler.parts.size());
87 | assertEquals("This is some base64 text.", handler.parts.get(0).data.toString());
88 | assertEquals("This is some more base64 text.", handler.parts.get(1).data.toString());
89 |
90 | assertEquals("text/plain", handler.parts.get(0).map.getHeader(Headers.CONTENT_TYPE));
91 | }
92 |
93 | @ParameterizedTest
94 | @MethodSource("bufferSizeProvider")
95 | void quoted_printable(final int bufferSize) throws IOException {
96 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime4.txt"));
97 | final var handler = new MockPartHandler();
98 | final var parser = MultipartParser.beginParse(handler, "someboundarytext".getBytes(), bufferSize, ISO_8859_1);
99 |
100 | final var buf = ByteBuffer.wrap(data.getBytes());
101 | parser.parse(buf);
102 | assertTrue(parser.isComplete());
103 | assertEquals(1, handler.parts.size());
104 | assertEquals("time=money.", handler.parts.get(0).data.toString());
105 |
106 | assertEquals("text/plain", handler.parts.get(0).map.getHeader(Headers.CONTENT_TYPE));
107 | }
108 |
109 | @ParameterizedTest
110 | @MethodSource("bufferSizeProvider")
111 | void mime_decoding_malformed(final int bufferSize) throws IOException {
112 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime5_malformed.txt"));
113 | final var handler = new MockPartHandler();
114 | final var parser = MultipartParser.beginParse(handler, "someboundarytext".getBytes(), bufferSize, ISO_8859_1);
115 |
116 | final var buf = ByteBuffer.wrap(data.getBytes());
117 | parser.parse(buf);
118 | assertFalse(parser.isComplete());
119 | }
120 |
121 | @ParameterizedTest
122 | @MethodSource("bufferSizeProvider")
123 | void base64_mime_decoding_malformed(final int bufferSize) {
124 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime6_malformed.txt"));
125 | final var handler = new MockPartHandler();
126 | final var parser = MultipartParser.beginParse(handler, "unique-boundary-1".getBytes(), bufferSize, ISO_8859_1);
127 |
128 | final var buf = ByteBuffer.wrap(data.getBytes());
129 | assertThrows(IOException.class, () -> parser.parse(buf));
130 | }
131 |
132 | @ParameterizedTest
133 | @MethodSource("bufferSizeProvider")
134 | void quoted_printable_malformed(final int bufferSize) throws IOException {
135 | final var data = fixLineEndings(FileUtils.readFile(MultipartParserTest.class, "mime7_malformed.txt"));
136 | final var handler = new MockPartHandler();
137 | final var parser = MultipartParser.beginParse(handler, "someboundarytext".getBytes(), bufferSize, ISO_8859_1);
138 |
139 | final var buf = ByteBuffer.wrap(data.getBytes());
140 | parser.parse(buf);
141 | assertTrue(parser.isComplete());
142 | assertEquals(1, handler.parts.size());
143 | assertEquals("time\nmoney.", handler.parts.get(0).data.toString());
144 |
145 | assertEquals("text/plain", handler.parts.get(0).map.getHeader(Headers.CONTENT_TYPE));
146 | }
147 |
148 | private static class MockPartHandler implements MultipartParser.PartHandler {
149 |
150 | private final List parts = new ArrayList<>();
151 | private Part current;
152 |
153 | @Override
154 | public void beginPart(final Headers headers) {
155 | current = new Part(headers);
156 | parts.add(current);
157 | }
158 |
159 | @Override
160 | public void data(final ByteBuffer buffer) {
161 | while (buffer.hasRemaining()) {
162 | current.data.append((char) buffer.get());
163 | }
164 | }
165 |
166 | @Override
167 | public void endPart() {
168 |
169 | }
170 | }
171 |
172 | private static class Part {
173 | private final Headers map;
174 | private final StringBuilder data = new StringBuilder();
175 |
176 | private Part(final Headers map) {
177 | this.map = map;
178 | }
179 | }
180 |
181 | private static String fixLineEndings(final String string) {
182 | final var builder = new StringBuilder();
183 | for (var i = 0; i < string.length(); ++i) {
184 | final var character = string.charAt(i);
185 | if (character == '\n') {
186 | if (i == 0 || string.charAt(i - 1) != '\r') {
187 | builder.append("\r\n");
188 | } else {
189 | builder.append('\n');
190 | }
191 | } else if (character == '\r') {
192 | if (i + 1 == string.length() || string.charAt(i + 1) != '\n') {
193 | builder.append("\r\n");
194 | } else {
195 | builder.append('\r');
196 | }
197 | } else {
198 | builder.append(character);
199 | }
200 | }
201 | return builder.toString();
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/PartStreamImplTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertFalse;
5 | import static org.junit.jupiter.api.Assertions.assertThrows;
6 |
7 | import com.github.elopteryx.upload.PartStream;
8 | import org.junit.jupiter.api.Test;
9 |
10 | class PartStreamImplTest {
11 |
12 | @Test
13 | void it_should_return_the_correct_data() {
14 | final var fileName = "r-" + System.currentTimeMillis();
15 | final var fieldName = "r-" + System.currentTimeMillis();
16 | final var contentType = "r-" + System.currentTimeMillis();
17 | final var headers = new Headers();
18 | headers.addHeader(Headers.CONTENT_TYPE, contentType);
19 | final PartStream partStream = new PartStreamImpl(fileName, fieldName, headers);
20 | assertEquals(fileName, partStream.getSubmittedFileName());
21 | assertEquals(fieldName, partStream.getName());
22 | assertEquals(contentType, partStream.getContentType());
23 | assertEquals(partStream.isFile(), partStream.getSubmittedFileName() != null);
24 | assertFalse(partStream.isFinished());
25 | }
26 |
27 | @Test
28 | void invalid_file_names_are_not_allowed() {
29 | final var fileName = "r-" + System.currentTimeMillis() + '\u0000';
30 | final PartStream partStream = new PartStreamImpl(fileName, null, new Headers());
31 | assertThrows(IllegalArgumentException.class, partStream::getSubmittedFileName);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/QuotedPrintableEncodingTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal;
2 |
3 | import static java.nio.charset.StandardCharsets.US_ASCII;
4 | import static org.junit.jupiter.api.Assertions.assertEquals;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 |
11 | class QuotedPrintableEncodingTest {
12 |
13 | @Test
14 | void empty_decode() throws IOException {
15 | checkEncoding("", "");
16 | }
17 |
18 | @Test
19 | void plain_decode() throws IOException {
20 | checkEncoding("A longer sentence without special characters.", "A longer sentence without special characters.");
21 | }
22 |
23 | @Test
24 | void basic_encode_decode() throws IOException {
25 | checkEncoding("= Hello world =\r\n", "=3D Hello world =3D=0D=0A");
26 | }
27 |
28 | @Test
29 | void unsafe_decode() throws IOException {
30 | checkEncoding("=\r\n", "=3D=0D=0A");
31 | }
32 |
33 | @Test
34 | void unsafe_decode_lowercase() throws IOException {
35 | checkEncoding("=\r\n", "=3d=0d=0a");
36 | }
37 |
38 | private static void checkEncoding(final String original, final String encoded) throws IOException {
39 | final var encoding = new MultipartParser.QuotedPrintableEncoding(1024);
40 | encoding.handle(new MultipartParser.PartHandler() {
41 |
42 | @Override
43 | public void beginPart(final Headers headers) {
44 | // No-op
45 | }
46 |
47 | @Override
48 | public void data(final ByteBuffer buffer) {
49 | final var parserResult = new String(buffer.array(), US_ASCII).trim();
50 | assertEquals(parserResult, original.trim());
51 | }
52 |
53 | @Override
54 | public void endPart() {
55 | // No-op
56 | }
57 |
58 | }, ByteBuffer.wrap(encoded.getBytes(US_ASCII)));
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/AsyncUploadServlet.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static java.nio.charset.StandardCharsets.ISO_8859_1;
4 | import static java.nio.file.StandardOpenOption.CREATE;
5 | import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
6 | import static java.nio.file.StandardOpenOption.WRITE;
7 | import static org.junit.jupiter.api.Assertions.assertArrayEquals;
8 | import static org.junit.jupiter.api.Assertions.assertEquals;
9 | import static org.junit.jupiter.api.Assertions.assertFalse;
10 | import static org.junit.jupiter.api.Assertions.assertTrue;
11 | import static org.junit.jupiter.api.Assertions.fail;
12 |
13 | import com.github.elopteryx.upload.OnRequestComplete;
14 | import com.github.elopteryx.upload.PartOutput;
15 | import com.github.elopteryx.upload.UploadParser;
16 | import com.github.elopteryx.upload.util.ByteBufferBackedInputStream;
17 | import com.github.elopteryx.upload.util.NullChannel;
18 |
19 | import java.io.ByteArrayOutputStream;
20 | import java.io.IOException;
21 | import java.nio.channels.Channel;
22 | import java.nio.file.Files;
23 | import java.util.*;
24 | import java.util.concurrent.atomic.AtomicInteger;
25 | import jakarta.servlet.ServletException;
26 | import jakarta.servlet.annotation.WebServlet;
27 | import jakarta.servlet.http.HttpServlet;
28 | import jakarta.servlet.http.HttpServletRequest;
29 | import jakarta.servlet.http.HttpServletResponse;
30 |
31 | @WebServlet(value = "/async", asyncSupported = true)
32 | public class AsyncUploadServlet extends HttpServlet {
33 |
34 | @Override
35 | @SuppressWarnings("PMD.SwitchStmtsShouldHaveDefault")
36 | protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
37 | final var query = request.getQueryString();
38 | switch (query) {
39 | case ClientRequest.SIMPLE -> simple(request, response);
40 | case ClientRequest.THRESHOLD_LESSER -> thresholdLesser(request, response);
41 | case ClientRequest.THRESHOLD_GREATER -> thresholdGreater(request, response);
42 | case ClientRequest.ERROR -> error(request, response);
43 | case ClientRequest.IO_ERROR_UPON_ERROR -> ioErrorUponError(request);
44 | case ClientRequest.SERVLET_ERROR_UPON_ERROR -> servletErrorUponError(request);
45 | case ClientRequest.COMPLEX -> complex(request, response);
46 | }
47 | }
48 |
49 | private void simple(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
50 | UploadParser.newParser()
51 | .onPartBegin((context, buffer) -> {
52 | if (context.getPartStreams().size() == 1) {
53 | final var dir = ClientRequest.FILE_SYSTEM.getPath("");
54 | final var temp = dir.resolve(context.getCurrentPart().getSubmittedFileName());
55 | return PartOutput.from(Files.newByteChannel(temp, EnumSet.of(CREATE, TRUNCATE_EXISTING, WRITE)));
56 | } else if (context.getPartStreams().size() == 2) {
57 | final var dir = ClientRequest.FILE_SYSTEM.getPath("");
58 | final var temp = dir.resolve(context.getCurrentPart().getSubmittedFileName());
59 | return PartOutput.from(Files.newOutputStream(temp));
60 | } else if (context.getPartStreams().size() == 3) {
61 | final var dir = ClientRequest.FILE_SYSTEM.getPath("");
62 | final var temp = dir.resolve(context.getCurrentPart().getSubmittedFileName());
63 | return PartOutput.from(temp);
64 | } else {
65 | return PartOutput.from(new NullChannel());
66 | }
67 | })
68 | .onPartEnd(context -> {
69 | if (context.getCurrentOutput() != null && context.getCurrentOutput().safeToCast(Channel.class)) {
70 | final var channel = context.getCurrentOutput().unwrap(Channel.class);
71 | if (channel.isOpen()) {
72 | fail("The parser should close it!");
73 | }
74 | }
75 | })
76 | .onRequestComplete(context -> {
77 | request.getAsyncContext().complete();
78 | response.setStatus(200);
79 | })
80 | .setupAsyncParse(request);
81 | }
82 |
83 | private static class EvilOutput extends PartOutput {
84 | EvilOutput(final Object value) {
85 | super(value);
86 | }
87 | }
88 |
89 | private void thresholdLesser(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
90 |
91 | UploadParser.newParser()
92 | .onPartBegin((context, buffer) -> {
93 | final var currentPart = context.getCurrentPart();
94 | assertTrue(currentPart.isFinished());
95 | return PartOutput.from(new NullChannel());
96 | })
97 | .onRequestComplete(onSuccessfulFinish(request, response, 512))
98 | .sizeThreshold(1024)
99 | .setupAsyncParse(request);
100 | }
101 |
102 | private void thresholdGreater(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
103 |
104 | UploadParser.newParser()
105 | .onPartBegin((context, buffer) -> {
106 | final var currentPart = context.getCurrentPart();
107 | assertFalse(currentPart.isFinished());
108 | return PartOutput.from(new NullChannel());
109 | })
110 | .onRequestComplete(onSuccessfulFinish(request, response, 2048))
111 | .sizeThreshold(1024)
112 | .setupAsyncParse(request);
113 | }
114 |
115 | private void error(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
116 |
117 | UploadParser.newParser()
118 | .onPartBegin((context, buffer) -> new EvilOutput("This will cause an error!"))
119 | .onError((context, throwable) -> response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR))
120 | .setupAsyncParse(request);
121 | }
122 |
123 | private void ioErrorUponError(final HttpServletRequest request) throws ServletException, IOException {
124 | request.startAsync().setTimeout(500);
125 | UploadParser.newParser()
126 | .onRequestComplete(context -> {
127 | throw new IOException();
128 | })
129 | .onError((context, throwable) -> {
130 | throw new ServletException();
131 | })
132 | .setupAsyncParse(request);
133 | }
134 |
135 | private void servletErrorUponError(final HttpServletRequest request) throws ServletException, IOException {
136 | request.startAsync().setTimeout(500);
137 | // onError will not be called for ServletException!
138 | UploadParser.newParser()
139 | .onRequestComplete(context -> {
140 | throw new ServletException();
141 | })
142 | .setupAsyncParse(request);
143 | }
144 |
145 | private void complex(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
146 |
147 | if (!UploadParser.isMultipart(request)) {
148 | throw new ServletException("Not multipart!");
149 | }
150 |
151 | final var partCounter = new AtomicInteger(0);
152 | final List formFields = new ArrayList<>();
153 |
154 | final var expectedContentTypes = Arrays.asList(
155 | "text/plain",
156 | "text/plain",
157 | "text/plain",
158 | "text/plain",
159 | "text/plain",
160 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
161 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
162 | "image/jpeg");
163 |
164 | UploadParser.newParser()
165 | .onPartBegin((context, buffer) -> {
166 | final var part = context.getCurrentPart();
167 |
168 | final var detectedType = ClientRequest.TIKA.detect(new ByteBufferBackedInputStream(buffer), part.getSubmittedFileName());
169 | final var expectedType = expectedContentTypes.get(partCounter.getAndIncrement());
170 | if ("text/plain".equals(expectedType)) {
171 | assertTrue("text/plain".equals(detectedType) || "application/octet-stream".equals(detectedType));
172 | } else {
173 | assertEquals(detectedType, expectedType);
174 | }
175 | assertEquals(part.getHeaderNames(), Set.of("content-disposition", "content-type"));
176 | if (part.isFile()) {
177 | if ("".equals(part.getSubmittedFileName())) {
178 | throw new IOException("No file was chosen for the form field!");
179 | }
180 | assertFalse(part.getHeaders("content-type").isEmpty());
181 | assertEquals(part.getContentType(), part.getHeader("content-type"));
182 | final var baos = new ByteArrayOutputStream();
183 | formFields.add(baos);
184 | return PartOutput.from(baos);
185 | } else {
186 | final var baos = new ByteArrayOutputStream();
187 | formFields.add(baos);
188 | return PartOutput.from(baos);
189 | }
190 | })
191 | .onPartEnd(context -> {})
192 | .onRequestComplete(context -> {
193 | assertArrayEquals(formFields.get(0).toByteArray(), RequestSupplier.LARGE_FILE);
194 | assertArrayEquals(formFields.get(1).toByteArray(), RequestSupplier.EMPTY_FILE);
195 | assertArrayEquals(formFields.get(2).toByteArray(), RequestSupplier.SMALL_FILE);
196 | assertArrayEquals(formFields.get(3).toByteArray(), RequestSupplier.TEXT_VALUE_1.getBytes(ISO_8859_1));
197 | assertArrayEquals(formFields.get(4).toByteArray(), RequestSupplier.TEXT_VALUE_2.getBytes(ISO_8859_1));
198 |
199 | context.getUserObject(HttpServletResponse.class).setStatus(HttpServletResponse.SC_OK);
200 | context.getRequest().getAsyncContext().complete();
201 | })
202 | .onError((context, throwable) -> response.sendError(500))
203 | .userObject(response)
204 | .sizeThreshold(4096)
205 | .maxPartSize(Long.MAX_VALUE)
206 | .maxRequestSize(Long.MAX_VALUE)
207 | .setupAsyncParse(request);
208 | }
209 |
210 | private static OnRequestComplete onSuccessfulFinish(final HttpServletRequest request, final HttpServletResponse response, final int size) {
211 | return context -> {
212 | final var currentPart = context.getCurrentPart();
213 | assertTrue(currentPart.isFinished());
214 | assertEquals(size, currentPart.getKnownSize());
215 | assertTrue(request.isAsyncStarted());
216 | request.getAsyncContext().complete();
217 | response.setStatus(200);
218 | };
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/BlockingUploadServlet.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertFalse;
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
6 | import static org.junit.jupiter.api.Assertions.fail;
7 |
8 | import com.github.elopteryx.upload.OnRequestComplete;
9 | import com.github.elopteryx.upload.PartOutput;
10 | import com.github.elopteryx.upload.UploadParser;
11 | import com.github.elopteryx.upload.util.NullChannel;
12 |
13 | import java.io.IOException;
14 | import java.nio.channels.Channel;
15 | import jakarta.servlet.ServletException;
16 | import jakarta.servlet.annotation.WebServlet;
17 | import jakarta.servlet.http.HttpServlet;
18 | import jakarta.servlet.http.HttpServletRequest;
19 | import jakarta.servlet.http.HttpServletResponse;
20 |
21 | @WebServlet("/blocking")
22 | public class BlockingUploadServlet extends HttpServlet {
23 |
24 | @Override
25 | @SuppressWarnings("PMD.SwitchStmtsShouldHaveDefault")
26 | protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
27 | final var query = request.getQueryString();
28 | switch (query) {
29 | case ClientRequest.SIMPLE -> simple(request, response);
30 | case ClientRequest.THRESHOLD_LESSER -> thresholdLesser(request, response);
31 | case ClientRequest.THRESHOLD_GREATER -> thresholdGreater(request, response);
32 | case ClientRequest.ERROR -> error(request, response);
33 | }
34 | }
35 |
36 | private void simple(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
37 | final var context = UploadParser.newParser()
38 | .onPartEnd(context1 -> {
39 | if (context1.getCurrentOutput() != null && context1.getCurrentOutput().safeToCast(Channel.class)) {
40 | final var channel = context1.getCurrentOutput().unwrap(Channel.class);
41 | if (channel.isOpen()) {
42 | fail("The parser should close it!");
43 | }
44 | }
45 | })
46 | .onRequestComplete(context1 -> response.setStatus(200))
47 | .doBlockingParse(request);
48 | assertEquals(8, context.getPartStreams().size());
49 | }
50 |
51 | private void thresholdLesser(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
52 |
53 | UploadParser.newParser()
54 | .onPartBegin((context, buffer) -> {
55 | final var currentPart = context.getCurrentPart();
56 | assertTrue(currentPart.isFinished());
57 | return PartOutput.from(new NullChannel());
58 | })
59 | .onRequestComplete(onSuccessfulFinish(request, response, 512))
60 | .sizeThreshold(1024)
61 | .doBlockingParse(request);
62 | }
63 |
64 | private void thresholdGreater(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
65 |
66 | UploadParser.newParser()
67 | .onPartBegin((context, buffer) -> {
68 | final var currentPart = context.getCurrentPart();
69 | assertFalse(currentPart.isFinished());
70 | return PartOutput.from(new NullChannel());
71 | })
72 | .onRequestComplete(onSuccessfulFinish(request, response, 2048))
73 | .sizeThreshold(1024)
74 | .doBlockingParse(request);
75 | }
76 |
77 | private void error(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
78 |
79 | UploadParser.newParser()
80 | .onError((context, throwable) -> response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR))
81 | .maxRequestSize(4096)
82 | .doBlockingParse(request);
83 | }
84 |
85 | private static OnRequestComplete onSuccessfulFinish(final HttpServletRequest request, final HttpServletResponse response, final int size) {
86 | return context -> {
87 | final var currentPart = context.getCurrentPart();
88 | assertTrue(currentPart.isFinished());
89 | assertEquals(size, currentPart.getKnownSize());
90 | assertFalse(request.isAsyncStarted());
91 | response.setStatus(200);
92 | };
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/ClientRequest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withSeveralFields;
4 | import static java.net.http.HttpClient.Version.HTTP_1_1;
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | import com.google.common.jimfs.Jimfs;
8 | import org.apache.tika.Tika;
9 |
10 | import java.io.IOException;
11 | import java.net.URI;
12 | import java.net.http.HttpClient;
13 | import java.net.http.HttpRequest;
14 | import java.net.http.HttpResponse;
15 | import java.nio.ByteBuffer;
16 | import java.nio.charset.StandardCharsets;
17 | import java.nio.file.FileSystem;
18 | import java.time.Duration;
19 |
20 | /**
21 | * Utility class for making multipart requests.
22 | */
23 | public final class ClientRequest {
24 |
25 | static final String BOUNDARY = "--TNoK9riv6EjfMhxBzj22SKGnOaIhZlxhar";
26 |
27 | static final String SIMPLE = "simple";
28 | static final String THRESHOLD_LESSER = "threshold_lesser";
29 | static final String THRESHOLD_GREATER = "threshold_greater";
30 | static final String ERROR = "error";
31 | static final String IO_ERROR_UPON_ERROR = "io_error_upon_error";
32 | static final String SERVLET_ERROR_UPON_ERROR = "servlet_error_upon_error";
33 | static final String COMPLEX = "complex";
34 |
35 | static final FileSystem FILE_SYSTEM = Jimfs.newFileSystem();
36 |
37 | static final Tika TIKA = new Tika();
38 |
39 | /**
40 | * Creates and sends a randomized multipart request for the
41 | * given address.
42 | * @param url The target address
43 | * @param expectedStatus The expected HTTP response, can be null
44 | * @throws IOException If an IO error occurred
45 | */
46 | public static void performRequest(final String url, final Integer expectedStatus) throws IOException {
47 | performRequest(url, expectedStatus, withSeveralFields());
48 | }
49 |
50 | /**
51 | * Creates and sends a randomized multipart request for the
52 | * given address.
53 | * @param url The target address
54 | * @param expectedStatus The expected HTTP response, can be null
55 | * @param requestData The multipart body, can't be null
56 | * @throws IOException If an IO error occurred
57 | */
58 | public static void performRequest(final String url, final Integer expectedStatus, final ByteBuffer requestData) throws IOException {
59 | final var client = HttpClient.newBuilder().version(HTTP_1_1).build();
60 | final var request = HttpRequest.newBuilder()
61 | .uri(URI.create(url))
62 | .timeout(Duration.ofSeconds(5))
63 | .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY)
64 | .POST(HttpRequest.BodyPublishers.ofByteArray(requestData.array(), 0, requestData.limit()))
65 | .build();
66 | try {
67 | client.send(request, responseInfo -> {
68 | final var statusCode = responseInfo.statusCode();
69 | if (expectedStatus != null) {
70 | assertEquals((int) expectedStatus, statusCode);
71 | }
72 | return HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
73 | });
74 | } catch (final InterruptedException e) {
75 | throw new RuntimeException(e);
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/JettyIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withOneLargerPicture;
4 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withOneSmallerPicture;
5 | import static org.junit.jupiter.api.Assertions.fail;
6 |
7 | import org.eclipse.jetty.server.Server;
8 | import org.eclipse.jetty.servlet.ServletHandler;
9 | import org.junit.jupiter.api.AfterAll;
10 | import org.junit.jupiter.api.BeforeAll;
11 | import org.junit.jupiter.api.Test;
12 |
13 | import java.io.IOException;
14 | import java.nio.ByteBuffer;
15 | import jakarta.servlet.http.HttpServletResponse;
16 |
17 | class JettyIntegrationTest {
18 |
19 | private static Server server;
20 |
21 | /**
22 | * Sets up the test environment, generates data to upload, starts a
23 | * Jetty instance which will receive the client requests.
24 | * @throws Exception If an error occurred with the servlets
25 | */
26 | @BeforeAll
27 | static void setUpClass() throws Exception {
28 | server = new Server(8090);
29 |
30 | final var handler = new ServletHandler();
31 | server.setHandler(handler);
32 |
33 | handler.addServletWithMapping(AsyncUploadServlet.class, "/async");
34 | handler.addServletWithMapping(BlockingUploadServlet.class, "/blocking");
35 |
36 | server.start();
37 | }
38 |
39 | @Test
40 | void test_with_a_real_request_simple_async() {
41 | performRequest("http://localhost:8090/async?" + ClientRequest.SIMPLE, HttpServletResponse.SC_OK);
42 | }
43 |
44 | @Test
45 | void test_with_a_real_request_simple_blocking() {
46 | performRequest("http://localhost:8090/blocking?" + ClientRequest.SIMPLE, HttpServletResponse.SC_OK);
47 | }
48 |
49 | @Test
50 | void test_with_a_real_request_threshold_lesser_async() {
51 | performRequest("http://localhost:8090/async?" + ClientRequest.THRESHOLD_LESSER, HttpServletResponse.SC_OK, withOneSmallerPicture());
52 | }
53 |
54 | @Test
55 | void test_with_a_real_request_threshold_lesser_blocking() {
56 | performRequest("http://localhost:8090/blocking?" + ClientRequest.THRESHOLD_LESSER, HttpServletResponse.SC_OK, withOneSmallerPicture());
57 | }
58 |
59 | @Test
60 | void test_with_a_real_request_threshold_greater_async() {
61 | performRequest("http://localhost:8090/async?" + ClientRequest.THRESHOLD_GREATER, HttpServletResponse.SC_OK, withOneLargerPicture());
62 | }
63 |
64 | @Test
65 | void test_with_a_real_request_threshold_greater_blocking() {
66 | performRequest("http://localhost:8090/blocking?" + ClientRequest.THRESHOLD_GREATER, HttpServletResponse.SC_OK, withOneLargerPicture());
67 | }
68 |
69 | @Test
70 | void test_with_a_real_request_error_async() {
71 | performRequest("http://localhost:8090/async?" + ClientRequest.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
72 | }
73 |
74 | @Test
75 | void test_with_a_real_request_io_error_upon_error_async() {
76 | performRequest("http://localhost:8090/async?" + ClientRequest.IO_ERROR_UPON_ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
77 | }
78 |
79 | @Test
80 | void test_with_a_real_request_servlet_error_upon_error_async() {
81 | performRequest("http://localhost:8090/async?" + ClientRequest.SERVLET_ERROR_UPON_ERROR, null);
82 | }
83 |
84 | @Test
85 | void test_with_a_real_request_error_blocking() {
86 | performRequest("http://localhost:8090/blocking?" + ClientRequest.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
87 | }
88 |
89 | @Test
90 | void test_with_a_real_request_complex() {
91 | performRequest("http://localhost:8090/async?" + ClientRequest.COMPLEX, HttpServletResponse.SC_OK);
92 | }
93 |
94 | private void performRequest(final String url, final Integer expectedStatus) {
95 | try {
96 | ClientRequest.performRequest(url, expectedStatus);
97 | } catch (final IOException e) {
98 | if (expectedStatus != null) {
99 | fail("Status returned: " + expectedStatus);
100 | }
101 | }
102 | }
103 |
104 | private void performRequest(final String url, final Integer expectedStatus, final ByteBuffer requestData) {
105 | try {
106 | ClientRequest.performRequest(url, expectedStatus, requestData);
107 | } catch (final IOException e) {
108 | if (expectedStatus != null) {
109 | fail("Status returned: " + expectedStatus);
110 | }
111 | }
112 | }
113 |
114 | @AfterAll
115 | static void tearDown() throws Exception {
116 | server.stop();
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/RequestBuilder.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static java.nio.charset.StandardCharsets.ISO_8859_1;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | final class RequestBuilder {
8 |
9 | private static final byte[] LINE_FEED = "\r\n".getBytes(ISO_8859_1);
10 |
11 | private final ByteBuffer buffer;
12 |
13 | private final String boundary;
14 |
15 | static RequestBuilder newBuilder(final String boundary) {
16 | return new RequestBuilder(boundary);
17 | }
18 |
19 | private RequestBuilder(final String boundary) {
20 | this.buffer = ByteBuffer.allocate(300_000);
21 | this.boundary = boundary;
22 | }
23 |
24 | RequestBuilder addFormField(final String name, final String value) {
25 | buffer.put(LINE_FEED);
26 | buffer.put(("--" + boundary).getBytes(ISO_8859_1));
27 | buffer.put(LINE_FEED);
28 | buffer.put(("Content-Disposition: form-data; name=\"" + name + "\"").getBytes(ISO_8859_1));
29 | buffer.put(LINE_FEED);
30 | buffer.put(("Content-Type: text/plain; charset=" + ISO_8859_1.name()).getBytes(ISO_8859_1));
31 | buffer.put(LINE_FEED);
32 | buffer.put(LINE_FEED);
33 | buffer.put(value.getBytes(ISO_8859_1));
34 |
35 | return this;
36 | }
37 |
38 | RequestBuilder addFilePart(final String fieldName, final byte[] content, final String contentType, final String fileName) {
39 | buffer.put(LINE_FEED);
40 | buffer.put(("--" + boundary).getBytes(ISO_8859_1));
41 | buffer.put(LINE_FEED);
42 | buffer.put(("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"").getBytes(ISO_8859_1));
43 | buffer.put(LINE_FEED);
44 | buffer.put(("Content-Type: " + contentType).getBytes(ISO_8859_1));
45 | buffer.put(LINE_FEED);
46 | buffer.put(LINE_FEED);
47 | buffer.put(content);
48 |
49 | return this;
50 | }
51 |
52 | ByteBuffer finish() {
53 | buffer.put(LINE_FEED);
54 | buffer.put(("--" + boundary + "--").getBytes(ISO_8859_1));
55 | buffer.put(LINE_FEED);
56 |
57 | buffer.flip();
58 | return buffer;
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/RequestSupplier.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static com.github.elopteryx.upload.internal.integration.ClientRequest.BOUNDARY;
4 | import static java.nio.charset.StandardCharsets.UTF_8;
5 |
6 | import java.io.IOException;
7 | import java.net.URISyntaxException;
8 | import java.nio.ByteBuffer;
9 | import java.nio.file.Files;
10 | import java.nio.file.Paths;
11 | import java.util.Random;
12 |
13 | /**
14 | * Utility class which is responsible for the request body creation.
15 | */
16 | final class RequestSupplier {
17 |
18 | static final byte[] EMPTY_FILE;
19 | static final byte[] SMALL_FILE;
20 | static final byte[] LARGE_FILE;
21 |
22 | static final String TEXT_VALUE_1 = "íéáűúőóüö";
23 | static final String TEXT_VALUE_2 = "abcdef";
24 |
25 | static {
26 | EMPTY_FILE = new byte[0];
27 | SMALL_FILE = "0123456789".getBytes(UTF_8);
28 | final var random = new Random();
29 | final var builder = new StringBuilder();
30 | for (var i = 0; i < 100_000; i++) {
31 | builder.append(random.nextInt(100));
32 | }
33 | LARGE_FILE = builder.toString().getBytes(UTF_8);
34 | }
35 |
36 | static ByteBuffer withSeveralFields() {
37 | return RequestBuilder.newBuilder(BOUNDARY)
38 | .addFilePart("filefield1", LARGE_FILE, "application/octet-stream", "file1.txt")
39 | .addFilePart("filefield2", EMPTY_FILE, "text/plain", "file2.txt")
40 | .addFilePart("filefield3", SMALL_FILE, "application/octet-stream", "file3.txt")
41 | .addFormField("textfield1", TEXT_VALUE_1)
42 | .addFormField("textfield2", TEXT_VALUE_2)
43 | .addFilePart("filefield4", getContents("test.xlsx"), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "test.xlsx")
44 | .addFilePart("filefield5", getContents("test.docx"), "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "test.docx")
45 | .addFilePart("filefield6", getContents("test.jpg"), "image/jpeg", "test.jpg")
46 | .finish();
47 | }
48 |
49 | static ByteBuffer withOneSmallerPicture() {
50 | return RequestBuilder.newBuilder(BOUNDARY)
51 | .addFilePart("filefield", new byte[512], "image/jpeg", "test.jpg")
52 | .finish();
53 | }
54 |
55 | static ByteBuffer withOneLargerPicture() {
56 | return RequestBuilder.newBuilder(BOUNDARY)
57 | .addFilePart("filefield", new byte[2048], "image/jpeg", "test.jpg")
58 | .finish();
59 | }
60 |
61 | private static byte[] getContents(final String resource) {
62 | try {
63 | final var path = Paths.get(ClientRequest.class.getResource(resource).toURI());
64 | return Files.readAllBytes(path);
65 | } catch (final URISyntaxException | IOException e) {
66 | throw new RuntimeException(e);
67 | }
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/TomcatIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withOneLargerPicture;
4 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withOneSmallerPicture;
5 | import static org.junit.jupiter.api.Assertions.fail;
6 |
7 | import org.apache.catalina.WebResourceRoot;
8 | import org.apache.catalina.core.StandardContext;
9 | import org.apache.catalina.startup.Tomcat;
10 | import org.apache.catalina.webresources.DirResourceSet;
11 | import org.apache.catalina.webresources.StandardRoot;
12 | import org.junit.jupiter.api.AfterAll;
13 | import org.junit.jupiter.api.BeforeAll;
14 | import org.junit.jupiter.api.Test;
15 |
16 | import java.io.IOException;
17 | import java.nio.ByteBuffer;
18 | import java.nio.file.Files;
19 | import java.nio.file.Paths;
20 | import jakarta.servlet.http.HttpServletResponse;
21 |
22 | class TomcatIntegrationTest {
23 |
24 | private static Tomcat server;
25 |
26 | /**
27 | * Sets up the test environment, generates data to upload, starts a
28 | * Tomcat instance which will receive the client requests.
29 | * @throws Exception If an error occurred with the servlets
30 | */
31 | @BeforeAll
32 | static void setUpClass() throws Exception {
33 | server = new Tomcat();
34 |
35 | final var base = Paths.get("build/tomcat");
36 | Files.createDirectories(base);
37 |
38 | server.setPort(8100);
39 | server.setBaseDir("build/tomcat");
40 | server.getHost().setAppBase("build/tomcat");
41 | server.getHost().setAutoDeploy(true);
42 | server.getHost().setDeployOnStartup(true);
43 |
44 | final var context = (StandardContext) server.addWebapp("", base.toAbsolutePath().toString());
45 |
46 | final var additionWebInfClasses = Paths.get("build/classes");
47 | final WebResourceRoot resources = new StandardRoot(context);
48 | resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes",
49 | additionWebInfClasses.toAbsolutePath().toString(), "/"));
50 | context.setResources(resources);
51 | context.getJarScanner().setJarScanFilter((jarScanType, jarName) -> false);
52 |
53 | server.getConnector();
54 | server.start();
55 | }
56 |
57 | @Test
58 | void test_with_a_real_request_simple_async() {
59 | performRequest("http://localhost:8100/async?" + ClientRequest.SIMPLE, HttpServletResponse.SC_OK);
60 | }
61 |
62 | @Test
63 | void test_with_a_real_request_simple_blocking() {
64 | performRequest("http://localhost:8100/blocking?" + ClientRequest.SIMPLE, HttpServletResponse.SC_OK);
65 | }
66 |
67 | @Test
68 | void test_with_a_real_request_threshold_lesser_async() {
69 | performRequest("http://localhost:8100/async?" + ClientRequest.THRESHOLD_LESSER, HttpServletResponse.SC_OK, withOneSmallerPicture());
70 | }
71 |
72 | @Test
73 | void test_with_a_real_request_threshold_lesser_blocking() {
74 | performRequest("http://localhost:8100/blocking?" + ClientRequest.THRESHOLD_LESSER, HttpServletResponse.SC_OK, withOneSmallerPicture());
75 | }
76 |
77 | @Test
78 | void test_with_a_real_request_threshold_greater_async() {
79 | performRequest("http://localhost:8100/async?" + ClientRequest.THRESHOLD_GREATER, HttpServletResponse.SC_OK, withOneLargerPicture());
80 | }
81 |
82 | @Test
83 | void test_with_a_real_request_threshold_greater_blocking() {
84 | performRequest("http://localhost:8100/blocking?" + ClientRequest.THRESHOLD_GREATER, HttpServletResponse.SC_OK, withOneLargerPicture());
85 | }
86 |
87 | @Test
88 | void test_with_a_real_request_error_async() {
89 | performRequest("http://localhost:8100/async?" + ClientRequest.ERROR, null);
90 | }
91 |
92 | @Test
93 | void test_with_a_real_request_io_error_upon_error_async() {
94 | performRequest("http://localhost:8100/async?" + ClientRequest.IO_ERROR_UPON_ERROR, null);
95 | }
96 |
97 | @Test
98 | void test_with_a_real_request_servlet_error_upon_error_async() {
99 | performRequest("http://localhost:8100/async?" + ClientRequest.SERVLET_ERROR_UPON_ERROR, null);
100 | }
101 |
102 | @Test
103 | void test_with_a_real_request_error_blocking() {
104 | performRequest("http://localhost:8100/blocking?" + ClientRequest.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
105 | }
106 |
107 | @Test
108 | void test_with_a_real_request_complex() {
109 | performRequest("http://localhost:8100/async?" + ClientRequest.COMPLEX, HttpServletResponse.SC_OK);
110 | }
111 |
112 | private void performRequest(final String url, final Integer expectedStatus) {
113 | try {
114 | ClientRequest.performRequest(url, expectedStatus);
115 | } catch (final IOException e) {
116 | if (expectedStatus != null) {
117 | fail("Status returned: " + expectedStatus);
118 | }
119 | }
120 | }
121 |
122 | private void performRequest(final String url, final Integer expectedStatus, final ByteBuffer requestData) {
123 | try {
124 | ClientRequest.performRequest(url, expectedStatus, requestData);
125 | } catch (final IOException e) {
126 | if (expectedStatus != null) {
127 | fail("Status returned: " + expectedStatus);
128 | }
129 | }
130 | }
131 |
132 | @AfterAll
133 | static void tearDown() throws Exception {
134 | server.stop();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/internal/integration/UndertowIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.internal.integration;
2 |
3 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withOneLargerPicture;
4 | import static com.github.elopteryx.upload.internal.integration.RequestSupplier.withOneSmallerPicture;
5 |
6 | import io.undertow.Handlers;
7 | import io.undertow.Undertow;
8 | import io.undertow.servlet.Servlets;
9 | import org.junit.jupiter.api.AfterAll;
10 | import org.junit.jupiter.api.BeforeAll;
11 | import org.junit.jupiter.api.Test;
12 |
13 | import java.io.IOException;
14 | import java.nio.ByteBuffer;
15 | import jakarta.servlet.http.HttpServletResponse;
16 |
17 | class UndertowIntegrationTest {
18 |
19 | private static Undertow server;
20 |
21 | /**
22 | * Sets up the test environment, generates data to upload, starts an
23 | * Undertow instance which will receive the client requests.
24 | * @throws Exception If an error occurred with the servlets
25 | */
26 | @BeforeAll
27 | static void setUpClass() throws Exception {
28 | final var servletBuilder = Servlets.deployment()
29 | .setClassLoader(Thread.currentThread().getContextClassLoader())
30 | .setContextPath("/")
31 | .setDeploymentName("ROOT.war")
32 | .addServlets(
33 | Servlets.servlet("AsyncUploadServlet", AsyncUploadServlet.class)
34 | .addMapping("/async")
35 | .setAsyncSupported(true),
36 | Servlets.servlet("BlockingUploadServlet", BlockingUploadServlet.class)
37 | .addMapping("/blocking")
38 | .setAsyncSupported(false)
39 | );
40 |
41 | final var manager = Servlets.defaultContainer().addDeployment(servletBuilder);
42 | manager.deploy();
43 | final var path = Handlers.path(Handlers.redirect("/")).addPrefixPath("/", manager.start());
44 |
45 | server = Undertow.builder()
46 | .addHttpListener(8080, "localhost")
47 | .setHandler(path)
48 | .build();
49 | server.start();
50 | }
51 |
52 | @Test
53 | void test_with_a_real_request_simple_async() throws IOException {
54 | performRequest("http://localhost:8080/async?" + ClientRequest.SIMPLE, HttpServletResponse.SC_OK);
55 | }
56 |
57 | @Test
58 | void test_with_a_real_request_simple_blocking() throws IOException {
59 | performRequest("http://localhost:8080/blocking?" + ClientRequest.SIMPLE, HttpServletResponse.SC_OK);
60 | }
61 |
62 | @Test
63 | void test_with_a_real_request_threshold_lesser_async() throws IOException {
64 | performRequest("http://localhost:8080/async?" + ClientRequest.THRESHOLD_LESSER, HttpServletResponse.SC_OK, withOneSmallerPicture());
65 | }
66 |
67 | @Test
68 | void test_with_a_real_request_threshold_lesser_blocking() throws IOException {
69 | performRequest("http://localhost:8080/blocking?" + ClientRequest.THRESHOLD_LESSER, HttpServletResponse.SC_OK, withOneSmallerPicture());
70 | }
71 |
72 | @Test
73 | void test_with_a_real_request_threshold_greater_async() throws IOException {
74 | performRequest("http://localhost:8080/async?" + ClientRequest.THRESHOLD_GREATER, HttpServletResponse.SC_OK, withOneLargerPicture());
75 | }
76 |
77 | @Test
78 | void test_with_a_real_request_threshold_greater_blocking() throws IOException {
79 | performRequest("http://localhost:8080/blocking?" + ClientRequest.THRESHOLD_GREATER, HttpServletResponse.SC_OK, withOneLargerPicture());
80 | }
81 |
82 | @Test
83 | void test_with_a_real_request_error_async() throws IOException {
84 | performRequest("http://localhost:8080/async?" + ClientRequest.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
85 | }
86 |
87 | @Test
88 | void test_with_a_real_request_io_error_upon_error_async() throws IOException {
89 | performRequest("http://localhost:8080/async?" + ClientRequest.IO_ERROR_UPON_ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
90 | }
91 |
92 | @Test
93 | void test_with_a_real_request_servlet_error_upon_error_async() throws IOException {
94 | performRequest("http://localhost:8080/async?" + ClientRequest.SERVLET_ERROR_UPON_ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
95 | }
96 |
97 | @Test
98 | void test_with_a_real_request_error_blocking() throws IOException {
99 | performRequest("http://localhost:8080/blocking?" + ClientRequest.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
100 | }
101 |
102 | @Test
103 | void test_with_a_real_request_complex() throws IOException {
104 | performRequest("http://localhost:8080/async?" + ClientRequest.COMPLEX, HttpServletResponse.SC_OK);
105 | }
106 |
107 | private void performRequest(final String url, final int expectedStatus) throws IOException {
108 | ClientRequest.performRequest(url, expectedStatus);
109 | }
110 |
111 | private void performRequest(final String url, final int expectedStatus, final ByteBuffer requestData) throws IOException {
112 | ClientRequest.performRequest(url, expectedStatus, requestData);
113 | }
114 |
115 | @AfterAll
116 | static void tearDown() {
117 | server.stop();
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/ByteBufferBackedInputStreamTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 |
11 | class ByteBufferBackedInputStreamTest {
12 |
13 | private static final String TEST_TEXT = "Test text.";
14 |
15 | @Test
16 | void create_with_direct() {
17 | assertThrows(IllegalArgumentException.class, () -> new ByteBufferBackedInputStream(ByteBuffer.allocate(0).asReadOnlyBuffer()));
18 | }
19 |
20 | @Test
21 | void create_with_read_only() {
22 | assertThrows(IllegalArgumentException.class, () -> new ByteBufferBackedInputStream(ByteBuffer.allocateDirect(0)));
23 | }
24 |
25 | @Test
26 | void check_available_bytes() throws Exception {
27 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.wrap(TEST_TEXT.getBytes()));
28 | final var length = TEST_TEXT.getBytes().length;
29 | assertEquals(length, stream.available());
30 | final var read = stream.read(new byte[1024]);
31 | assertEquals(length, read);
32 | assertEquals(0, stream.available());
33 | }
34 |
35 | @Test
36 | void read_a_byte() throws Exception {
37 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.wrap(TEST_TEXT.getBytes()));
38 | final var read = stream.read();
39 | assertEquals(read, "T".getBytes()[0]);
40 | }
41 |
42 | @Test
43 | void try_to_read_an_exhausted_stream() throws Exception {
44 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.allocate(0));
45 | final var read = stream.read();
46 | assertEquals(read, -1);
47 | }
48 |
49 | @Test
50 | void try_to_read_a_closed_stream() throws Exception {
51 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.wrap(TEST_TEXT.getBytes()));
52 | stream.close();
53 | assertThrows(IOException.class, stream::read);
54 | }
55 |
56 | @Test
57 | void read_into_byte_array() throws Exception {
58 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.wrap(TEST_TEXT.getBytes()));
59 | final var buf = new byte[1024];
60 | final var read = stream.read(buf);
61 | assertEquals(TEST_TEXT, new String(buf, 0, read));
62 | }
63 |
64 | @Test
65 | void try_to_read_an_exhausted_stream_to_array() throws Exception {
66 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.allocate(0));
67 | final var buf = new byte[1024];
68 | final var read = stream.read(buf);
69 | assertEquals(read, -1);
70 | }
71 |
72 | @Test
73 | void try_to_read_a_closed_stream_to_array() throws Exception {
74 | final var stream = new ByteBufferBackedInputStream(ByteBuffer.wrap(TEST_TEXT.getBytes()));
75 | stream.close();
76 | final var buf = new byte[1024];
77 | assertThrows(IOException.class, () -> stream.read(buf));
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/ByteBufferBackedOutputStreamTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.IOException;
9 | import java.nio.BufferOverflowException;
10 | import java.nio.ByteBuffer;
11 |
12 | class ByteBufferBackedOutputStreamTest {
13 |
14 | private static final String TEST_TEXT = "Test text.";
15 |
16 | @Test
17 | void create_with_direct() {
18 | assertThrows(IllegalArgumentException.class, () -> new ByteBufferBackedOutputStream(ByteBuffer.allocate(0).asReadOnlyBuffer()));
19 | }
20 |
21 | @Test
22 | void create_with_read_only() {
23 | assertThrows(IllegalArgumentException.class, () -> new ByteBufferBackedOutputStream(ByteBuffer.allocateDirect(0)));
24 | }
25 |
26 | @Test
27 | void write_a_byte() throws Exception {
28 | final var buffer = ByteBuffer.allocate(1024);
29 | final var stream = new ByteBufferBackedOutputStream(buffer);
30 | final var oneByte = "T".getBytes()[0];
31 | stream.write(oneByte);
32 | assertEquals(buffer.get(0), oneByte);
33 | }
34 |
35 | @Test
36 | void try_to_write_an_exhausted_stream() {
37 | final var stream = new ByteBufferBackedOutputStream(ByteBuffer.allocate(0));
38 | assertThrows(BufferOverflowException.class, () -> stream.write(1));
39 | }
40 |
41 | @Test
42 | void try_to_write_a_closed_stream() throws Exception {
43 | final var stream = new ByteBufferBackedOutputStream(ByteBuffer.wrap(TEST_TEXT.getBytes()));
44 | stream.close();
45 | assertThrows(IOException.class, () -> stream.write(1));
46 | }
47 |
48 | @Test
49 | void write_into_byte_array() throws IOException {
50 | final var bytes = TEST_TEXT.getBytes();
51 | final var buffer = ByteBuffer.allocate(1024);
52 | final var stream = new ByteBufferBackedOutputStream(buffer);
53 | stream.write(bytes);
54 | assertEquals(TEST_TEXT, new String(buffer.array(), 0, bytes.length));
55 | }
56 |
57 | @Test
58 | void try_to_read_an_exhausted_stream_to_array() {
59 | final var stream = new ByteBufferBackedOutputStream(ByteBuffer.allocate(0));
60 | assertThrows(BufferOverflowException.class, () -> stream.write(TEST_TEXT.getBytes()));
61 | }
62 |
63 | @Test
64 | void try_to_read_a_closed_stream_to_array() throws Exception {
65 | final var stream = new ByteBufferBackedOutputStream(ByteBuffer.allocate(1024));
66 | stream.close();
67 | assertThrows(IOException.class, () -> stream.write(TEST_TEXT.getBytes()));
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/InputStreamBackedChannelTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
6 |
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.io.ByteArrayInputStream;
10 | import java.io.IOException;
11 | import java.nio.ByteBuffer;
12 | import java.nio.channels.ClosedChannelException;
13 |
14 | class InputStreamBackedChannelTest {
15 |
16 | private static final String TEST_TEXT = "Test text.";
17 |
18 | @Test
19 | void read_the_string() throws Exception {
20 | final var stream = new ByteArrayInputStream(TEST_TEXT.getBytes());
21 | final var channel = new InputStreamBackedChannel(stream);
22 | final var buffer = ByteBuffer.allocate(1024);
23 | channel.read(buffer);
24 | assertEquals(TEST_TEXT, new String(buffer.array(), 0, buffer.position()));
25 | }
26 |
27 | @Test
28 | void try_to_read_to_direct_buffer() {
29 | final var stream = new ByteArrayInputStream(TEST_TEXT.getBytes());
30 | final var channel = new InputStreamBackedChannel(stream);
31 | final var buffer = ByteBuffer.allocateDirect(1024);
32 | assertThrows(IllegalArgumentException.class, () -> channel.read(buffer));
33 | }
34 |
35 | @Test
36 | void try_to_read_to_read_only_buffer() {
37 | final var stream = new ByteArrayInputStream(TEST_TEXT.getBytes());
38 | final var channel = new InputStreamBackedChannel(stream);
39 | final var buffer = ByteBuffer.allocateDirect(1024).asReadOnlyBuffer();
40 | assertThrows(IllegalArgumentException.class, () -> channel.read(buffer));
41 | }
42 |
43 | @Test
44 | void open_and_close_and_try_to_read() throws IOException {
45 | final var stream = new ByteArrayInputStream(TEST_TEXT.getBytes());
46 | final var channel = new InputStreamBackedChannel(stream);
47 | assertTrue(channel.isOpen());
48 | channel.close();
49 | assertThrows(ClosedChannelException.class, () -> channel.read(ByteBuffer.allocate(0)));
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/MockServletInputStream.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.IOException;
5 | import java.nio.charset.StandardCharsets;
6 | import jakarta.servlet.ReadListener;
7 | import jakarta.servlet.ServletInputStream;
8 |
9 | public class MockServletInputStream extends ServletInputStream {
10 |
11 | private static final String FILE_NAME = "foo.txt";
12 | private static final String REQUEST_DATA =
13 | "-----1234\r\n"
14 | + "Content-Disposition: form-data; name=\"file\"; filename=\"" + FILE_NAME + "\"\r\n"
15 | + "Content-Type: text/whatever\r\n"
16 | + "\r\n"
17 | + "This is the content of the file\n"
18 | + "\r\n"
19 | + "-----1234\r\n"
20 | + "Content-Disposition: form-data; name=\"field\"\r\n"
21 | + "\r\n"
22 | + "fieldValue\r\n"
23 | + "-----1234\r\n"
24 | + "Content-Disposition: form-data; name=\"multi\"\r\n"
25 | + "\r\n"
26 | + "value1\r\n"
27 | + "-----1234\r\n"
28 | + "Content-Disposition: form-data; name=\"multi\"\r\n"
29 | + "\r\n"
30 | + "value2\r\n"
31 | + "-----1234--\r\n";
32 |
33 | private final ByteArrayInputStream sourceStream;
34 |
35 | private ReadListener readListener;
36 |
37 | MockServletInputStream() {
38 | this.sourceStream = new ByteArrayInputStream(REQUEST_DATA.getBytes(StandardCharsets.US_ASCII));
39 | }
40 |
41 | public void onDataAvailable() throws IOException {
42 | readListener.onDataAvailable();
43 | }
44 |
45 | @Override
46 | public int read() {
47 | return this.sourceStream.read();
48 | }
49 |
50 | @Override
51 | public void close() throws IOException {
52 | super.close();
53 | this.sourceStream.close();
54 | }
55 |
56 | @Override
57 | public boolean isFinished() {
58 | return false;
59 | }
60 |
61 | @Override
62 | public boolean isReady() {
63 | return true;
64 | }
65 |
66 | @Override
67 | public void setReadListener(final ReadListener readListener) {
68 | this.readListener = readListener;
69 | }
70 | }
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/NullChannelTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertFalse;
5 | import static org.junit.jupiter.api.Assertions.assertThrows;
6 | import static org.junit.jupiter.api.Assertions.assertTrue;
7 |
8 | import org.junit.jupiter.api.Test;
9 |
10 | import java.io.IOException;
11 | import java.nio.ByteBuffer;
12 | import java.nio.channels.ClosedChannelException;
13 |
14 | class NullChannelTest {
15 |
16 | @Test
17 | void read_from_open_channel() throws Exception {
18 | final var channel = new NullChannel();
19 | assertEquals(-1, channel.read(ByteBuffer.allocate(0)));
20 | }
21 |
22 | @Test
23 | void read_from_closed_channel() {
24 | final var channel = new NullChannel();
25 | assertTrue(channel.isOpen());
26 | channel.close();
27 | assertFalse(channel.isOpen());
28 | assertThrows(ClosedChannelException.class, () -> channel.read(ByteBuffer.allocate(0)));
29 | }
30 |
31 | @Test
32 | void write_to_open_channel() throws IOException {
33 | final var channel = new NullChannel();
34 | channel.write(ByteBuffer.allocate(0));
35 | }
36 |
37 | @Test
38 | void write_to_closed_channel() {
39 | final var channel = new NullChannel();
40 | assertTrue(channel.isOpen());
41 | channel.close();
42 | assertFalse(channel.isOpen());
43 | assertThrows(ClosedChannelException.class, () -> channel.write(ByteBuffer.allocate(0)));
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/OutputStreamBackedChannelTest.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
6 |
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.io.ByteArrayOutputStream;
10 | import java.io.IOException;
11 | import java.nio.ByteBuffer;
12 | import java.nio.channels.ClosedChannelException;
13 |
14 | class OutputStreamBackedChannelTest {
15 |
16 | private static final String TEST_TEXT = "Test text.";
17 |
18 | @Test
19 | void write_the_string() throws IOException {
20 | final var stream = new ByteArrayOutputStream(1024);
21 | final var channel = new OutputStreamBackedChannel(stream);
22 | final var buffer = ByteBuffer.wrap(TEST_TEXT.getBytes());
23 | channel.write(buffer);
24 | assertEquals(TEST_TEXT, new String(buffer.array(), 0, buffer.position()));
25 | }
26 |
27 | @Test
28 | void try_to_write_from_direct_buffer() {
29 | final var stream = new ByteArrayOutputStream(1024);
30 | final var channel = new OutputStreamBackedChannel(stream);
31 | final var buffer = ByteBuffer.allocateDirect(0);
32 | assertThrows(IllegalArgumentException.class, () -> channel.write(buffer));
33 | }
34 |
35 | @Test
36 | void try_to_write_from_read_only_buffer() {
37 | final var stream = new ByteArrayOutputStream(1024);
38 | final var channel = new OutputStreamBackedChannel(stream);
39 | final var buffer = ByteBuffer.allocate(0).asReadOnlyBuffer();
40 | assertThrows(IllegalArgumentException.class, () -> channel.write(buffer));
41 | }
42 |
43 | @Test
44 | void open_and_close_and_try_to_write() throws Exception {
45 | final var stream = new ByteArrayOutputStream(1024);
46 | final var channel = new OutputStreamBackedChannel(stream);
47 | assertTrue(channel.isOpen());
48 | channel.close();
49 | assertThrows(ClosedChannelException.class, () -> channel.write(ByteBuffer.wrap(TEST_TEXT.getBytes())));
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/java/com/github/elopteryx/upload/util/Servlets.java:
--------------------------------------------------------------------------------
1 | package com.github.elopteryx.upload.util;
2 |
3 | import static org.mockito.Mockito.mock;
4 | import static org.mockito.Mockito.when;
5 |
6 | import jakarta.servlet.http.HttpServletRequest;
7 | import jakarta.servlet.http.HttpServletResponse;
8 |
9 | public final class Servlets {
10 |
11 | private Servlets() {
12 | // No need to instantiate
13 | }
14 |
15 | /**
16 | * Creates a new mock servlet request.
17 | * @return The mocked request.
18 | * @throws Exception If an error occurred
19 | */
20 | public static HttpServletRequest newRequest() throws Exception {
21 | final var request = mock(HttpServletRequest.class);
22 |
23 | when(request.getMethod()).thenReturn("POST");
24 | when(request.getContentType()).thenReturn("multipart/");
25 | when(request.getContentLengthLong()).thenReturn(1024 * 1024L);
26 | when(request.getInputStream()).thenReturn(new MockServletInputStream());
27 | when(request.isAsyncSupported()).thenReturn(true);
28 |
29 | return request;
30 | }
31 |
32 | /**
33 | * Creates a new mock servlet response.
34 | * @return The mocked response.
35 | */
36 | public static HttpServletResponse newResponse() {
37 | final var response = mock(HttpServletResponse.class);
38 |
39 | when(response.getStatus()).thenReturn(200);
40 |
41 | return response;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/integration/test.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elopteryx/upload-parser/ae2ea72fedea25bd051823677241c05489d2df06/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/integration/test.docx
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/integration/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elopteryx/upload-parser/ae2ea72fedea25bd051823677241c05489d2df06/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/integration/test.jpg
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/integration/test.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elopteryx/upload-parser/ae2ea72fedea25bd051823677241c05489d2df06/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/integration/test.xlsx
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime-utf8.txt:
--------------------------------------------------------------------------------
1 | This is a preamble
2 | --unique-boundary-1
3 | Content-type: text/plain
4 | Content-Disposition: attachment; filename=个专为语文教学而设计的电脑软件.txt
5 |
6 | Just some chinese characters I copied from the internet, no idea what it says.
7 | --unique-boundary-1--
8 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime1.txt:
--------------------------------------------------------------------------------
1 | this is a preamble
2 | --unique-boundary-1
3 | Content-type: text/plain
4 |
5 | Here is some text.
6 | --unique-boundary-1
7 |
8 | Here is some more text.
9 | --unique-boundary-1--
10 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime2.txt:
--------------------------------------------------------------------------------
1 | --unique-boundary-1
2 | Content-type: text/plain
3 |
4 | Here is some text.
5 | --unique-boundary-1
6 |
7 | Here is some more text.
8 | --unique-boundary-1--
9 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime3.txt:
--------------------------------------------------------------------------------
1 | --unique-boundary-1
2 | Content-type: text/plain
3 | Content-Transfer-encoding: base64
4 |
5 | VGhpcyBpcyBzb21lIGJhc2U2NCB0ZXh0Lg==
6 | --unique-boundary-1
7 | Content-Transfer-encoding: base64
8 |
9 | VGhpcyBpcyBzb21lIG1vcmUgYmFzZTY0IHRleHQu
10 | --unique-boundary-1--
11 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime4.txt:
--------------------------------------------------------------------------------
1 | --someboundarytext
2 | Content-type: text/plain
3 | Content-Transfer-encoding: quoted-printable
4 |
5 | time=3Dmoney.
6 | --someboundarytext--
7 |
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime5_malformed.txt:
--------------------------------------------------------------------------------
1 | this is a preamble
2 | --unique-boundary-1
3 | Content-type: text/plain
4 |
5 | Here is some text.
6 | --unique-boundary-1
7 |
8 | Here is some more text.
9 | --unique-boundary-1---
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime6_malformed.txt:
--------------------------------------------------------------------------------
1 | --unique-boundary-1
2 | Content-type: text/plain
3 | Content-Transfer-encoding: base64
4 |
5 | vGhpcyBpcyBzb21lIGJhc2U2NCB0ZXh0Lg===
6 | --unique-boundary-1
7 | Content-Transfer-encoding: base64
8 |
9 | vGhpcyBpcyBzb21lIG1vcmUgYmFzZTY0IHRleHQu=
10 | --unique-boundary-1--
--------------------------------------------------------------------------------
/upload-parser-tests/src/test/resources/com/github/elopteryx/upload/internal/mime7_malformed.txt:
--------------------------------------------------------------------------------
1 | --someboundarytext
2 | Content-type: text/plain
3 | Content-Transfer-encoding: quoted-printable
4 |
5 | time=
6 | money.
7 | --someboundarytext--
--------------------------------------------------------------------------------