Skip to content

Commit 4401b6c

Browse files
thboileaujlouvelThierry Boileau
authored
2.6 multipart jetty (#1456)
* Added MultiPartFormDataRepresentation Added MultiPartFormDataRepresentation to Jetty extension to support generation and parsing. * Added support for the "charset" parameter in HTTP BASIC challenges See issue #1455 * Removed unused imports * Update changes.md * Update MultiPartFormDataRepresentation.java Added "location" parameter to help set a useful MultiPartConfig in addition to the default Jetty values * HttpBasic test refacto * HttpBasicHelper: fix potential NPE * HttpBasicHelper: fix potential NPE * Added tests cases for multipart * Added logic to duplicate MediaType along with the addition of parameter * Enhanced MultiPartFormDataRepresentation Cleanup behavior to generate random boundary just before its usage and only when needed. Added method to create a Part based on a Representation * Update MultiPartFormDataRepresentation.java Cleanup behavior to generate random boundary just before its usage and only when needed. Added method to create a Part based on a Representation * Fixed boundary setting issue and adjusted test case * Renamed MultiPartFormDataRep to MultiPartRep Eventually the logic could be reused for related media types * Update MultiPartRepresentation.java * Fixed multipart parsing logic Also added related unit test * Update changes.md * Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau <[email protected]> * Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau <[email protected]> * Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau <[email protected]> * Update org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/MultiPartRepresentation.java Co-authored-by: Thierry Boileau <[email protected]> --------- Co-authored-by: Jerome Louvel <[email protected]> Co-authored-by: Thierry Boileau <[email protected]>
1 parent 81b9bd3 commit 4401b6c

File tree

33 files changed

+1567
-1127
lines changed

33 files changed

+1567
-1127
lines changed

Diff for: changes.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ Changes log
22
===========
33

44
- 2.6 Release Candidate 1 (??-03-2025)
5+
- Enhancements
6+
- Added MultiPartRepresentation to Jetty extension to support generation and parsing.
7+
- Added support for the "charset" parameter in HTTP BASIC challenges. Reported by Marc Lafon.
8+
- Added MediaType constructors to help with cloning and customization needs.
59
- Misc
6-
- Upgrade the thymeleaf library to 3.1.3.RELEASE.
7-
- Upgraded the Slf4j library to 5.12.0.
8-
- Upgraded the GWT libraries to version 2.12.2.
9-
- Upgraded the Jetty library to version 2.0.17.
10+
- Upgraded Thymeleaf library to 3.1.3.RELEASE.
11+
- Upgraded Slf4j library to 5.12.0.
12+
- Upgraded GWT libraries to version 2.12.2.
13+
- Upgraded Jetty library to version 2.0.17.
1014

1115
- 2.6 Milestone 2 (02-03-2025)
1216
- Enhancements

Diff for: org.restlet.java/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/CookieAuthenticatorTestCase.java

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
package org.restlet.ext.crypto;
1111

12-
import org.junit.jupiter.api.Assertions;
1312
import org.junit.jupiter.api.Test;
1413
import org.restlet.*;
1514
import org.restlet.data.CookieSetting;

Diff for: org.restlet.java/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.io.FileWriter;
1919
import java.nio.file.Files;
2020
import java.util.Map;
21-
import java.util.TreeMap;
2221

2322
import static org.junit.jupiter.api.Assertions.assertEquals;
2423

Diff for: org.restlet.java/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.util.Date;
2121

22-
import static org.junit.jupiter.api.Assertions.assertEquals;
2322
import static org.junit.jupiter.api.Assertions.assertThrows;
2423

2524
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
/**
2+
* Copyright 2005-2024 Qlik
3+
* <p>
4+
* The contents of this file is subject to the terms of the Apache 2.0 open
5+
* source license available at http://www.opensource.org/licenses/apache-2.0
6+
* <p>
7+
* Restlet is a registered trademark of QlikTech International AB.
8+
*/
9+
10+
package org.restlet.ext.jetty;
11+
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.nio.file.Path;
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
19+
import org.eclipse.jetty.http.HttpFields;
20+
import org.eclipse.jetty.http.MultiPart;
21+
import org.eclipse.jetty.http.MultiPart.Part;
22+
import org.eclipse.jetty.http.MultiPartConfig;
23+
import org.eclipse.jetty.http.MultiPartFormData;
24+
import org.eclipse.jetty.io.Content;
25+
import org.eclipse.jetty.io.content.InputStreamContentSource;
26+
import org.eclipse.jetty.util.Attributes;
27+
import org.eclipse.jetty.util.Promise;
28+
import org.restlet.data.MediaType;
29+
import org.restlet.representation.InputRepresentation;
30+
import org.restlet.representation.Representation;
31+
32+
/**
33+
* Input representation that can either parse or generate a multipart form data
34+
* representation depending on which constructor is invoked.
35+
*
36+
* @author Jerome Louvel
37+
*/
38+
public class MultiPartRepresentation extends InputRepresentation {
39+
40+
/**
41+
* Creates a #{@link Part} object based on a {@link Representation} plus
42+
* metadata.
43+
*
44+
* @param name The name of the part.
45+
* @param fileName The client suggests file name for storing the part.
46+
* @param partContent The part content.
47+
* @return The Jetty #{@link Part} object created.
48+
* @throws IOException
49+
*/
50+
public static Part createPart(String name, String fileName,
51+
Representation partContent) throws IOException {
52+
return new MultiPart.ContentSourcePart(name, fileName, HttpFields.EMPTY,
53+
new InputStreamContentSource(partContent.getStream()));
54+
}
55+
56+
/**
57+
* Returns the value of the first media-type parameter with "boundary" name.
58+
*
59+
* @param mediaType The media type that might contain a "boundary"
60+
* parameter.
61+
* @return The value of the first media-type parameter with "boundary" name.
62+
*/
63+
public static String getBoundary(MediaType mediaType) {
64+
final String result;
65+
66+
if (mediaType != null) {
67+
result = mediaType.getParameters().getFirstValue("boundary");
68+
} else {
69+
result = null;
70+
}
71+
72+
return result;
73+
}
74+
75+
/**
76+
* Sets a boundary to an existing media type. If the original mediatype
77+
* already has a "boundary" parameter, it will be erased. *
78+
*
79+
* @param mediaType The media type to update.
80+
* @param boundary The boundary to add as a parameter.
81+
* @return The updated media type.
82+
*/
83+
public static MediaType setBoundary(MediaType mediaType, String boundary) {
84+
MediaType result = null;
85+
86+
if (mediaType != null) {
87+
if (mediaType.getParameters().getFirst("boundary") != null) {
88+
result = new MediaType(mediaType.getParent(), "boundary",
89+
boundary);
90+
} else {
91+
result = new MediaType(mediaType, "boundary", boundary);
92+
}
93+
}
94+
95+
return result;
96+
}
97+
98+
/**
99+
* The boundary used to separate each part for the parsed or generated form.
100+
*/
101+
private volatile String boundary;
102+
103+
/** The wrapped multipart form data either parsed or to be generated. */
104+
private volatile List<Part> parts;
105+
106+
/**
107+
* Constructor that wraps multiple parts, set a random boundary, then
108+
* GENERATES the content via {@link #getStream()} as a
109+
* {@link MediaType#MULTIPART_FORM_DATA}.
110+
*
111+
* @param parts The source parts to use when generating the representation.
112+
*/
113+
public MultiPartRepresentation(List<Part> parts) {
114+
this(MultiPart.generateBoundary(null, 24), parts);
115+
}
116+
117+
/**
118+
* Constructor that wraps multiple parts, set a media type with a boundary,
119+
* then GENERATES the content via {@link #getStream()} as a
120+
* {@link MediaType#MULTIPART_FORM_DATA}.
121+
*
122+
* @param mediaType The media type to set.
123+
* @param boundary The boundary to add as a parameter.
124+
* @param parts The source parts to use when generating the
125+
* representation.
126+
*/
127+
public MultiPartRepresentation(MediaType mediaType, String boundary,
128+
List<Part> parts) {
129+
super(null, setBoundary(mediaType, boundary));
130+
this.boundary = boundary;
131+
this.parts = parts;
132+
}
133+
134+
/**
135+
* Constructor that wraps multiple parts, set a random boundary, then
136+
* GENERATES the content via {@link #getStream()} as a
137+
* {@link MediaType#MULTIPART_FORM_DATA}.
138+
*
139+
* @param parts The source parts to use when generating the representation.
140+
*/
141+
public MultiPartRepresentation(Part... parts) {
142+
this(Arrays.asList(parts));
143+
}
144+
145+
/**
146+
* Constructor that PARSES the content based on a given configuration into
147+
* {@link #getParts()}.
148+
*
149+
* @param multiPartEntity The multipart entity to parse which should have a
150+
* media type based on
151+
* {@link MediaType#MULTIPART_FORM_DATA}, with a
152+
* "boundary" parameter.
153+
* @param config The multipart configuration.
154+
* @throws IOException
155+
*/
156+
public MultiPartRepresentation(Representation multiPartEntity,
157+
MultiPartConfig config) throws IOException {
158+
this(multiPartEntity.getMediaType(), multiPartEntity.getStream(),
159+
config);
160+
}
161+
162+
/**
163+
* Constructor that PARSES the content based on a given configuration into
164+
* {@link #getParts()}. Uses a default {@link MultiPartConfig}.
165+
*
166+
* @param multiPartEntity The multipart entity to parse which should have a
167+
* media type based on
168+
* {@link MediaType#MULTIPART_FORM_DATA}, with a
169+
* "boundary" parameter.
170+
* @param storageLocation The location where parsed files are stored for
171+
* easier access.
172+
* @throws IOException
173+
*/
174+
public MultiPartRepresentation(Representation multiPartEntity,
175+
Path storageLocation) throws IOException {
176+
this(multiPartEntity, new MultiPartConfig.Builder()
177+
.location(storageLocation).build());
178+
}
179+
180+
/**
181+
* Constructor that PARSES the content based on a given configuration into
182+
* {@link #getParts()}.
183+
*
184+
* @param mediaType The media type that should be based on
185+
* {@link MediaType#MULTIPART_FORM_DATA}, with a
186+
* "boundary" parameter.
187+
* @param multiPartEntity The multipart entity to parse.
188+
* @param config The multipart configuration.
189+
* @throws IOException
190+
*/
191+
public MultiPartRepresentation(MediaType mediaType,
192+
InputStream multiPartEntity, MultiPartConfig config)
193+
throws IOException {
194+
super(null, mediaType);
195+
196+
if (MediaType.MULTIPART_FORM_DATA.equals(getMediaType(), true)) {
197+
this.boundary = getMediaType().getParameters()
198+
.getFirstValue("boundary");
199+
200+
if (this.boundary != null) {
201+
if (multiPartEntity != null) {
202+
Content.Source contentSource = Content.Source
203+
.from(multiPartEntity);
204+
Attributes.Mapped attributes = new Attributes.Mapped();
205+
206+
// Convert the request content into parts.
207+
MultiPartFormData.onParts(contentSource, attributes,
208+
mediaType.toString(), config,
209+
new Promise.Invocable<>() {
210+
@Override
211+
public void failed(Throwable failure) {
212+
throw new IllegalStateException(
213+
"Unable to parse the multipart form data representation",
214+
failure);
215+
}
216+
217+
@Override
218+
public InvocationType getInvocationType() {
219+
return InvocationType.BLOCKING;
220+
}
221+
222+
@Override
223+
public void succeeded(
224+
MultiPartFormData.Parts parts) {
225+
// Store the resulting parts
226+
MultiPartRepresentation.this.parts = new ArrayList<>();
227+
parts.iterator().forEachRemaining(
228+
part -> MultiPartRepresentation.this.parts
229+
.add(part));
230+
}
231+
});
232+
} else {
233+
throw new IllegalArgumentException(
234+
"The multipart entity can't be null");
235+
}
236+
} else {
237+
throw new IllegalArgumentException(
238+
"The content type must have a \"boundary\" parameter");
239+
}
240+
} else {
241+
throw new IllegalArgumentException(
242+
"The content type must be \"multipart/form-data\" with a \"boundary\" parameter");
243+
}
244+
}
245+
246+
/**
247+
* Constructor that wraps multiple parts, set a boundary, then GENERATES the
248+
* content via {@link #getStream()} as a
249+
* {@link MediaType#MULTIPART_FORM_DATA}.
250+
*
251+
* @param boundary The boundary to add as a parameter.
252+
* @param parts The source parts to use when generating the
253+
* representation.
254+
*/
255+
public MultiPartRepresentation(String boundary, List<Part> parts) {
256+
this(MediaType.MULTIPART_FORM_DATA, boundary, parts);
257+
}
258+
259+
/**
260+
* Constructor that wraps multiple parts, set a boundary, then GENERATES the
261+
* content via {@link #getStream()} as a
262+
* {@link MediaType#MULTIPART_FORM_DATA}.
263+
*
264+
* @param parts The source parts to use when generating the representation.
265+
*/
266+
public MultiPartRepresentation(String boundary, Part... parts) {
267+
this(boundary, Arrays.asList(parts));
268+
}
269+
270+
/**
271+
* Returns the boundary used to separate each part for the parsed or
272+
* generated form.
273+
*
274+
* @return The boundary used to separate each part for the parsed or
275+
* generated form.
276+
*/
277+
public String getBoundary() {
278+
return boundary;
279+
}
280+
281+
/**
282+
* Returns the wrapped multipart form data either parsed or to be generated.
283+
*
284+
* @return The wrapped multipart form data either parsed or to be generated.
285+
*/
286+
public List<Part> getParts() {
287+
return parts;
288+
}
289+
290+
/**
291+
* Returns an input stream that generates the multipart form data
292+
* serialization for the wrapped {@link #getParts()} object. The "boundary"
293+
* must be non-null when invoking this method.
294+
*
295+
* @return An input stream that generates the multipart form data.
296+
*/
297+
@Override
298+
public InputStream getStream() throws IOException {
299+
if (getBoundary() == null) {
300+
throw new IllegalArgumentException("The boundary can't be null");
301+
}
302+
303+
MultiPartFormData.ContentSource content = new MultiPartFormData.ContentSource(
304+
getBoundary());
305+
306+
for (Part part : this.parts) {
307+
content.addPart(part);
308+
}
309+
310+
content.close();
311+
setStream(null);
312+
return Content.Source.asInputStream(content);
313+
}
314+
315+
/**
316+
* Sets the boundary used to separate each part for the parsed or generated
317+
* form. It will also update the {@link MediaType}'s "boundary" attribute.
318+
*
319+
* @param boundary The boundary used to separate each part for the parsed or
320+
* generated form.
321+
*/
322+
public void setBoundary(String boundary) {
323+
this.boundary = boundary;
324+
325+
if (getMediaType() == null) {
326+
setMediaType(new MediaType(MediaType.MULTIPART_FORM_DATA,
327+
"boundary", boundary));
328+
} else {
329+
setMediaType(setBoundary(getMediaType(), boundary));
330+
}
331+
}
332+
333+
}

0 commit comments

Comments
 (0)