forked from apache/nifi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NIFI-13661 Added Multipart Form Data Builder to web-client-api
This closes apache#9183 Signed-off-by: dan-s1 <dstieg1@gmail.com>
- Loading branch information
1 parent
fa6e55f
commit 6013b93
Showing
5 changed files
with
479 additions
and
0 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
...ons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpContentType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.apache.nifi.web.client.api; | ||
|
||
/** | ||
* Content Type value for HTTP headers | ||
*/ | ||
public interface HttpContentType { | ||
/** | ||
* Get Content Type value for HTTP header | ||
* | ||
* @return Content Type | ||
*/ | ||
String getContentType(); | ||
} |
58 changes: 58 additions & 0 deletions
58
...ient-api/src/main/java/org/apache/nifi/web/client/api/MultipartFormDataStreamBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.apache.nifi.web.client.api; | ||
|
||
import java.io.InputStream; | ||
|
||
/** | ||
* Multipart Form Data Stream Builder supports construction of an Input Stream with form-data sections according to RFC 7578 | ||
*/ | ||
public interface MultipartFormDataStreamBuilder { | ||
/** | ||
* Build Input Stream based on current component elements | ||
* | ||
* @return Input Stream | ||
*/ | ||
InputStream build(); | ||
|
||
/** | ||
* Get Content-Type Header value containing multipart/form-data with boundary | ||
* | ||
* @return Multipart HTTP Content-Type | ||
*/ | ||
HttpContentType getHttpContentType(); | ||
|
||
/** | ||
* Add Part using specified Name with Content-Type and Stream | ||
* | ||
* @param name Name field of part to be added | ||
* @param httpContentType Content-Type of part to be added | ||
* @param inputStream Stream content of part to be added | ||
* @return Builder | ||
*/ | ||
MultipartFormDataStreamBuilder addPart(String name, HttpContentType httpContentType, InputStream inputStream); | ||
|
||
/** | ||
* Add Part using specified Name with Content-Type and byte array | ||
* | ||
* @param name Name field of part to be added | ||
* @param httpContentType Content-Type of part to be added | ||
* @param bytes Byte array content of part to be added | ||
* @return Builder | ||
*/ | ||
MultipartFormDataStreamBuilder addPart(String name, HttpContentType httpContentType, byte[] bytes); | ||
} |
48 changes: 48 additions & 0 deletions
48
...-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpContentType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.apache.nifi.web.client.api; | ||
|
||
/** | ||
* Enumeration of standard registered Content Types applicable to most HTTP requests and responses | ||
*/ | ||
public enum StandardHttpContentType implements HttpContentType { | ||
/** Defined in RFC 8259 */ | ||
APPLICATION_JSON("application/json"), | ||
|
||
/** Defined in RFC 2046 */ | ||
APPLICATION_OCTET_STREAM("application/octet-stream"), | ||
|
||
/** Defined in RFF 7303 */ | ||
APPLICATION_XML("application/xml"), | ||
|
||
/** Defined according to W3C */ | ||
TEXT_HTML("text/html"), | ||
|
||
/** Defined in RFC 2046 */ | ||
TEXT_PLAIN("text/plain"); | ||
|
||
private final String contentType; | ||
|
||
StandardHttpContentType(final String contentType) { | ||
this.contentType = contentType; | ||
} | ||
|
||
@Override | ||
public String getContentType() { | ||
return contentType; | ||
} | ||
} |
187 changes: 187 additions & 0 deletions
187
.../src/main/java/org/apache/nifi/web/client/api/StandardMultipartFormDataStreamBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.apache.nifi.web.client.api; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.InputStream; | ||
import java.io.SequenceInputStream; | ||
import java.nio.charset.Charset; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.Enumeration; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.UUID; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* Standard implementation of Multipart Form Data Stream Builder supporting form-data as described in RFC 7578 | ||
*/ | ||
public class StandardMultipartFormDataStreamBuilder implements MultipartFormDataStreamBuilder { | ||
private static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition: form-data; name=\"%s\""; | ||
|
||
private static final String CONTENT_TYPE_HEADER = "Content-Type: %s"; | ||
|
||
private static final Pattern ALLOWED_NAME_PATTERN = Pattern.compile("^\\p{ASCII}+$"); | ||
|
||
private static final String CARRIAGE_RETURN_LINE_FEED = "\r\n"; | ||
|
||
private static final String BOUNDARY_SEPARATOR = "--"; | ||
|
||
private static final String BOUNDARY_FORMAT = "FormDataBoundary-%s"; | ||
|
||
private static final String MULTIPART_FORM_DATA_FORMAT = "multipart/form-data; boundary=\"%s\""; | ||
|
||
private static final Charset HEADERS_CHARACTER_SET = StandardCharsets.US_ASCII; | ||
|
||
private final String boundary = BOUNDARY_FORMAT.formatted(UUID.randomUUID()); | ||
|
||
private final List<Part> parts = new ArrayList<>(); | ||
|
||
/** | ||
* Build Sequence Input Stream from collection of Form Data Parts formatted with boundaries | ||
* | ||
* @return Input Stream | ||
*/ | ||
@Override | ||
public InputStream build() { | ||
if (parts.isEmpty()) { | ||
throw new IllegalStateException("Parts required"); | ||
} | ||
|
||
final List<InputStream> partInputStreams = new ArrayList<>(); | ||
|
||
final Iterator<Part> selectedParts = parts.iterator(); | ||
while (selectedParts.hasNext()) { | ||
final Part part = selectedParts.next(); | ||
final String footer = getFooter(selectedParts); | ||
|
||
final InputStream partInputStream = getPartInputStream(part, footer); | ||
partInputStreams.add(partInputStream); | ||
} | ||
|
||
final Enumeration<InputStream> enumeratedPartInputStreams = Collections.enumeration(partInputStreams); | ||
return new SequenceInputStream(enumeratedPartInputStreams); | ||
} | ||
|
||
/** | ||
* Get Content-Type Header value containing multipart/form-data with boundary | ||
* | ||
* @return Multipart HTTP Content-Type | ||
*/ | ||
@Override | ||
public HttpContentType getHttpContentType() { | ||
final String contentType = MULTIPART_FORM_DATA_FORMAT.formatted(boundary); | ||
return new MultipartHttpContentType(contentType); | ||
} | ||
|
||
/** | ||
* Add Part with field name and stream source | ||
* | ||
* @param name Name field of part to be added | ||
* @param httpContentType Content-Type of part to be added | ||
* @param inputStream Stream content of part to be added | ||
* @return Builder | ||
*/ | ||
@Override | ||
public MultipartFormDataStreamBuilder addPart(final String name, final HttpContentType httpContentType, final InputStream inputStream) { | ||
Objects.requireNonNull(name, "Name required"); | ||
Objects.requireNonNull(httpContentType, "Content Type required"); | ||
Objects.requireNonNull(inputStream, "Input Stream required"); | ||
|
||
final Matcher nameMatcher = ALLOWED_NAME_PATTERN.matcher(name); | ||
if (nameMatcher.matches()) { | ||
final Part part = new Part(name, httpContentType, inputStream); | ||
parts.add(part); | ||
} else { | ||
throw new IllegalArgumentException("Name contains characters outside of ASCII character set"); | ||
} | ||
|
||
return this; | ||
} | ||
|
||
/** | ||
* Add Part with field name and byte array source | ||
* | ||
* @param name Name field of part to be added | ||
* @param httpContentType Content-Type of part to be added | ||
* @param bytes Byte array content of part to be added | ||
* @return Builder | ||
*/ | ||
@Override | ||
public MultipartFormDataStreamBuilder addPart(final String name, final HttpContentType httpContentType, final byte[] bytes) { | ||
Objects.requireNonNull(bytes, "Byte Array required"); | ||
final InputStream inputStream = new ByteArrayInputStream(bytes); | ||
return addPart(name, httpContentType, inputStream); | ||
} | ||
|
||
private InputStream getPartInputStream(final Part part, final String footer) { | ||
final String partHeaders = getPartHeaders(part); | ||
final InputStream headersInputStream = new ByteArrayInputStream(partHeaders.getBytes(HEADERS_CHARACTER_SET)); | ||
final InputStream footerInputStream = new ByteArrayInputStream(footer.getBytes(HEADERS_CHARACTER_SET)); | ||
final Enumeration<InputStream> inputStreams = Collections.enumeration(List.of(headersInputStream, part.inputStream, footerInputStream)); | ||
return new SequenceInputStream(inputStreams); | ||
} | ||
|
||
private String getPartHeaders(final Part part) { | ||
final StringBuilder headersBuilder = new StringBuilder(); | ||
|
||
final String contentDispositionHeader = CONTENT_DISPOSITION_HEADER.formatted(part.name); | ||
headersBuilder.append(contentDispositionHeader); | ||
headersBuilder.append(CARRIAGE_RETURN_LINE_FEED); | ||
|
||
final String contentType = part.httpContentType.getContentType(); | ||
final String contentTypeHeader = CONTENT_TYPE_HEADER.formatted(contentType); | ||
headersBuilder.append(contentTypeHeader); | ||
headersBuilder.append(CARRIAGE_RETURN_LINE_FEED); | ||
|
||
headersBuilder.append(CARRIAGE_RETURN_LINE_FEED); | ||
return headersBuilder.toString(); | ||
} | ||
|
||
private String getFooter(final Iterator<Part> selectedParts) { | ||
final StringBuilder footerBuilder = new StringBuilder(); | ||
footerBuilder.append(CARRIAGE_RETURN_LINE_FEED); | ||
footerBuilder.append(BOUNDARY_SEPARATOR); | ||
footerBuilder.append(boundary); | ||
if (selectedParts.hasNext()) { | ||
footerBuilder.append(CARRIAGE_RETURN_LINE_FEED); | ||
} else { | ||
// Add boundary separator after last part indicating end | ||
footerBuilder.append(BOUNDARY_SEPARATOR); | ||
} | ||
|
||
return footerBuilder.toString(); | ||
} | ||
|
||
private record MultipartHttpContentType(String contentType) implements HttpContentType { | ||
@Override | ||
public String getContentType() { | ||
return contentType; | ||
} | ||
} | ||
|
||
private record Part( | ||
String name, | ||
HttpContentType httpContentType, | ||
InputStream inputStream | ||
) { | ||
} | ||
} |
Oops, something went wrong.