2424import java .util .ArrayList ;
2525import java .util .Collections ;
2626import java .util .Enumeration ;
27- import java .util .Iterator ;
2827import java .util .List ;
2928import java .util .Objects ;
3029import java .util .UUID ;
3635 */
3736public class StandardMultipartFormDataStreamBuilder implements MultipartFormDataStreamBuilder {
3837 private static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition: form-data; name=\" %s\" " ;
38+ private static final String CONTENT_DISPOSITION_FILE_HEADER = "Content-Disposition: form-data; name=\" %s\" ; filename=\" %s\" " ;
3939
4040 private static final String CONTENT_TYPE_HEADER = "Content-Type: %s" ;
4141
@@ -47,38 +47,37 @@ public class StandardMultipartFormDataStreamBuilder implements MultipartFormData
4747
4848 private static final String BOUNDARY_FORMAT = "FormDataBoundary-%s" ;
4949
50- private static final String MULTIPART_FORM_DATA_FORMAT = "multipart/form-data; boundary=\" %s \" " ;
50+ private static final String MULTIPART_FORM_DATA_FORMAT = "multipart/form-data; boundary=%s " ;
5151
5252 private static final Charset HEADERS_CHARACTER_SET = StandardCharsets .US_ASCII ;
5353
5454 private final String boundary = BOUNDARY_FORMAT .formatted (UUID .randomUUID ());
5555
5656 private final List <Part > parts = new ArrayList <>();
5757
58- /**
59- * Build Sequence Input Stream from collection of Form Data Parts formatted with boundaries
60- *
61- * @return Input Stream
62- */
6358 @ Override
6459 public InputStream build () {
6560 if (parts .isEmpty ()) {
6661 throw new IllegalStateException ("Parts required" );
6762 }
6863
69- final List <InputStream > partInputStreams = new ArrayList <>();
64+ final List <InputStream > streams = new ArrayList <>();
65+ for (int index = 0 ; index < parts .size (); index ++) {
66+ final Part part = parts .get (index );
67+ final String boundaryPrefix = (index == 0 ? BOUNDARY_SEPARATOR + boundary + CARRIAGE_RETURN_LINE_FEED
68+ : CARRIAGE_RETURN_LINE_FEED + BOUNDARY_SEPARATOR + boundary + CARRIAGE_RETURN_LINE_FEED );
7069
71- final Iterator <Part > selectedParts = parts .iterator ();
72- while (selectedParts .hasNext ()) {
73- final Part part = selectedParts .next ();
74- final String footer = getFooter (selectedParts );
75-
76- final InputStream partInputStream = getPartInputStream (part , footer );
77- partInputStreams .add (partInputStream );
70+ streams .add (new ByteArrayInputStream (boundaryPrefix .getBytes (HEADERS_CHARACTER_SET )));
71+ final String partHeaders = getPartHeaders (part );
72+ streams .add (new ByteArrayInputStream (partHeaders .getBytes (HEADERS_CHARACTER_SET )));
73+ streams .add (part .inputStream );
7874 }
7975
80- final Enumeration <InputStream > enumeratedPartInputStreams = Collections .enumeration (partInputStreams );
81- return new SequenceInputStream (enumeratedPartInputStreams );
76+ final String closingBoundary = CARRIAGE_RETURN_LINE_FEED + BOUNDARY_SEPARATOR + boundary + BOUNDARY_SEPARATOR + CARRIAGE_RETURN_LINE_FEED ;
77+ streams .add (new ByteArrayInputStream (closingBoundary .getBytes (HEADERS_CHARACTER_SET )));
78+
79+ final Enumeration <InputStream > enumeratedStreams = Collections .enumeration (streams );
80+ return new SequenceInputStream (enumeratedStreams );
8281 }
8382
8483 /**
@@ -102,13 +101,23 @@ public HttpContentType getHttpContentType() {
102101 */
103102 @ Override
104103 public MultipartFormDataStreamBuilder addPart (final String name , final HttpContentType httpContentType , final InputStream inputStream ) {
104+ return addPartInternal (name , null , httpContentType , inputStream );
105+ }
106+
107+ @ Override
108+ public MultipartFormDataStreamBuilder addPart (final String name , final String fileName , final HttpContentType httpContentType , final InputStream inputStream ) {
109+ final String sanitizedFileName = sanitizeFileName (fileName );
110+ return addPartInternal (name , sanitizedFileName , httpContentType , inputStream );
111+ }
112+
113+ private MultipartFormDataStreamBuilder addPartInternal (final String name , final String fileName , final HttpContentType httpContentType , final InputStream inputStream ) {
105114 Objects .requireNonNull (name , "Name required" );
106115 Objects .requireNonNull (httpContentType , "Content Type required" );
107116 Objects .requireNonNull (inputStream , "Input Stream required" );
108117
109118 final Matcher nameMatcher = ALLOWED_NAME_PATTERN .matcher (name );
110119 if (nameMatcher .matches ()) {
111- final Part part = new Part (name , httpContentType , inputStream );
120+ final Part part = new Part (name , fileName , httpContentType , inputStream );
112121 parts .add (part );
113122 } else {
114123 throw new IllegalArgumentException ("Name contains characters outside of ASCII character set" );
@@ -132,18 +141,19 @@ public MultipartFormDataStreamBuilder addPart(final String name, final HttpConte
132141 return addPart (name , httpContentType , inputStream );
133142 }
134143
135- private InputStream getPartInputStream (final Part part , final String footer ) {
136- final String partHeaders = getPartHeaders (part );
137- final InputStream headersInputStream = new ByteArrayInputStream (partHeaders .getBytes (HEADERS_CHARACTER_SET ));
138- final InputStream footerInputStream = new ByteArrayInputStream (footer .getBytes (HEADERS_CHARACTER_SET ));
139- final Enumeration <InputStream > inputStreams = Collections .enumeration (List .of (headersInputStream , part .inputStream , footerInputStream ));
140- return new SequenceInputStream (inputStreams );
144+ @ Override
145+ public MultipartFormDataStreamBuilder addPart (final String name , final String fileName , final HttpContentType httpContentType , final byte [] bytes ) {
146+ Objects .requireNonNull (bytes , "Byte Array required" );
147+ final InputStream inputStream = new ByteArrayInputStream (bytes );
148+ return addPart (name , fileName , httpContentType , inputStream );
141149 }
142150
143151 private String getPartHeaders (final Part part ) {
144152 final StringBuilder headersBuilder = new StringBuilder ();
145153
146- final String contentDispositionHeader = CONTENT_DISPOSITION_HEADER .formatted (part .name );
154+ final String contentDispositionHeader = part .fileName == null
155+ ? CONTENT_DISPOSITION_HEADER .formatted (part .name )
156+ : CONTENT_DISPOSITION_FILE_HEADER .formatted (part .name , part .fileName );
147157 headersBuilder .append (contentDispositionHeader );
148158 headersBuilder .append (CARRIAGE_RETURN_LINE_FEED );
149159
@@ -156,21 +166,6 @@ private String getPartHeaders(final Part part) {
156166 return headersBuilder .toString ();
157167 }
158168
159- private String getFooter (final Iterator <Part > selectedParts ) {
160- final StringBuilder footerBuilder = new StringBuilder ();
161- footerBuilder .append (CARRIAGE_RETURN_LINE_FEED );
162- footerBuilder .append (BOUNDARY_SEPARATOR );
163- footerBuilder .append (boundary );
164- if (selectedParts .hasNext ()) {
165- footerBuilder .append (CARRIAGE_RETURN_LINE_FEED );
166- } else {
167- // Add boundary separator after last part indicating end
168- footerBuilder .append (BOUNDARY_SEPARATOR );
169- }
170-
171- return footerBuilder .toString ();
172- }
173-
174169 private record MultipartHttpContentType (String contentType ) implements HttpContentType {
175170 @ Override
176171 public String getContentType () {
@@ -180,8 +175,23 @@ public String getContentType() {
180175
181176 private record Part (
182177 String name ,
178+ String fileName ,
183179 HttpContentType httpContentType ,
184180 InputStream inputStream
185181 ) {
186182 }
183+
184+ private String sanitizeFileName (final String fileName ) {
185+ if (fileName == null || fileName .isBlank ()) {
186+ throw new IllegalArgumentException ("File Name required" );
187+ }
188+
189+ final String sanitized = fileName ;
190+ final Matcher fileNameMatcher = ALLOWED_NAME_PATTERN .matcher (sanitized );
191+ if (!fileNameMatcher .matches ()) {
192+ throw new IllegalArgumentException ("File Name contains characters outside of ASCII character set" );
193+ }
194+
195+ return sanitized ;
196+ }
187197}
0 commit comments