-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
739 additions
and
3 deletions.
There are no files selected for viewing
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
15 changes: 15 additions & 0 deletions
15
src/main/java/coresearch/cvurl/io/constant/MultipartType.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,15 @@ | ||
package coresearch.cvurl.io.constant; | ||
|
||
import static java.lang.String.format; | ||
|
||
public class MultipartType { | ||
public static final String MIXED = "mixed"; | ||
public static final String FORM = "form-data"; | ||
public static final String ALTERNATIVE = "alternative"; | ||
public static final String DIGEST = "digest"; | ||
public static final String PARALLEL = "parallel"; | ||
|
||
private MultipartType() { | ||
throw new IllegalStateException(format("Creating of class %s is forbidden", MultipartType.class.getName())); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/main/java/coresearch/cvurl/io/exception/MultipartFileFormException.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,24 @@ | ||
package coresearch.cvurl.io.exception; | ||
|
||
/** | ||
* Thrown when {@link java.io.IOException} happen while reading part content file. | ||
*/ | ||
public class MultipartFileFormException extends RuntimeException { | ||
|
||
/** | ||
* Constructs a new exception with the specified detail message and | ||
* cause. <p>Note that the detail message associated with | ||
* {@code cause} is <i>not</i> automatically incorporated in | ||
* this runtime exception's detail message. | ||
* | ||
* @param message the detail message (which is saved for later retrieval | ||
* by the {@link #getMessage()} method). | ||
* @param cause the cause (which is saved for later retrieval by the | ||
* {@link #getCause()} method). (A {@code null} value is | ||
* permitted, and indicates that the cause is nonexistent or | ||
* unknown.) | ||
*/ | ||
public MultipartFileFormException(String message, Throwable cause) { | ||
super(message, cause); | ||
} | ||
} |
181 changes: 181 additions & 0 deletions
181
src/main/java/coresearch/cvurl/io/multipart/MultipartBody.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,181 @@ | ||
package coresearch.cvurl.io.multipart; | ||
|
||
import coresearch.cvurl.io.constant.HttpHeader; | ||
import coresearch.cvurl.io.constant.MultipartType; | ||
import coresearch.cvurl.io.exception.MultipartFileFormException; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.UUID; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import static coresearch.cvurl.io.util.Validation.notNullParam; | ||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
|
||
/** | ||
* Class for building multipart request body. | ||
*/ | ||
public class MultipartBody { | ||
|
||
private static final String CONTENT_DISPOSITION_TEMPLATE = "form-data; name=\"%s\""; | ||
private static final String CONTENT_DISPOSITION_WITH_FILENAME_TEMPLATE = CONTENT_DISPOSITION_TEMPLATE + "; filename=\"%s\""; | ||
private static final String BOUNDARY_DELIMITER = "--"; | ||
|
||
private String boundary; | ||
private String multipartType; | ||
private List<Part> parts; | ||
|
||
private MultipartBody(String boundary, String multipartType, List<Part> parts) { | ||
this.boundary = boundary; | ||
this.multipartType = multipartType; | ||
this.parts = parts; | ||
} | ||
|
||
/** | ||
* Creates new instance of {@link MultipartBody} with randomly generated boundary. | ||
* | ||
* @return new instance of {@link MultipartBody} | ||
*/ | ||
public static MultipartBody create() { | ||
return create(UUID.randomUUID().toString()); | ||
} | ||
|
||
/** | ||
* Creates new instance of {@link MultipartBody} with provided boundary. | ||
* | ||
* @return new instance of {@link MultipartBody} | ||
*/ | ||
public static MultipartBody create(String boundary) { | ||
notNullParam(boundary, "boundary"); | ||
|
||
return new MultipartBody(boundary, MultipartType.MIXED, new ArrayList<>()); | ||
} | ||
|
||
/** | ||
* Generate multipart body as byte array later to be used by {@link coresearch.cvurl.io.request.RequestWithBodyBuilder} | ||
* | ||
* @return list of byte arrays | ||
*/ | ||
public List<byte[]> asByteArrays() { | ||
var result = parts.stream() | ||
.flatMap(part -> (Stream<byte[]>) part.asByteArrays(boundary).stream()) | ||
.collect(Collectors.toList()); | ||
result.add((BOUNDARY_DELIMITER + boundary + BOUNDARY_DELIMITER).getBytes(UTF_8)); | ||
return result; | ||
} | ||
|
||
/** | ||
* Returns multipart type of {@link MultipartBody} | ||
* | ||
* @return multipart type | ||
*/ | ||
public String getMultipartType() { | ||
return multipartType; | ||
} | ||
|
||
/** | ||
* Returns boundary of {@link MultipartBody} | ||
* | ||
* @return boundary | ||
*/ | ||
public String getBoundary() { | ||
return boundary; | ||
} | ||
|
||
/** | ||
* Sets multipart type of {@link MultipartBody} | ||
* | ||
* @param multipartType multipart type to be set. | ||
* @return this {@link MultipartBody} | ||
*/ | ||
public MultipartBody type(String multipartType) { | ||
notNullParam(multipartType, "multipartType"); | ||
|
||
this.multipartType = multipartType; | ||
return this; | ||
} | ||
|
||
/** | ||
* Add a part to the body. | ||
* | ||
* @param part part to add | ||
* @return this {@link MultipartBody} | ||
*/ | ||
public MultipartBody part(Part part) { | ||
notNullParam(part, "part"); | ||
|
||
this.parts.add(part); | ||
return this; | ||
} | ||
|
||
/** | ||
* Add a form data part to the body with provided part name. | ||
* | ||
* @param name part name | ||
* @param part part to add | ||
* @return this {@link MultipartBody} | ||
*/ | ||
public MultipartBody formPart(String name, Part part) { | ||
notNullParam(name, "name"); | ||
notNullParam(part, "part"); | ||
|
||
this.parts.add(part.header(HttpHeader.CONTENT_DISPOSITION, getContentDispositionHeader(name))); | ||
return this; | ||
} | ||
|
||
/** | ||
* Add a file form data part to the body with provided part name.Use name of provided file as value for filename field | ||
* If Content-type is not previously set detect content-type from file. | ||
* | ||
* @param name part name | ||
* @param part part to add | ||
* @return this {@link MultipartBody} | ||
*/ | ||
public MultipartBody formPart(String name, PartWithFileContent part) { | ||
notNullParam(name, "name"); | ||
notNullParam(part, "part"); | ||
|
||
return formPart(name, part.getFilePath().getFileName().toString(), part); | ||
} | ||
|
||
/** | ||
* Add a file form data part to the body with provided part name.Use provided filename as value for filename field | ||
* If Content-type is not previously set detect content-type from file. | ||
* | ||
* @param name part name | ||
* @param filename value of filename field | ||
* @param part part to add | ||
* @return this {@link MultipartBody} | ||
*/ | ||
public MultipartBody formPart(String name, String filename, PartWithFileContent part) { | ||
notNullParam(name, "name"); | ||
notNullParam(filename, "filename"); | ||
notNullParam(part, "part"); | ||
|
||
var path = part.getFilePath(); | ||
part.header(HttpHeader.CONTENT_DISPOSITION, getContentDispositionHeader(name, filename)); | ||
|
||
if (!part.isContentTypeSet()) { | ||
try { | ||
part.contentType(Files.probeContentType(path)); | ||
} catch (IOException e) { | ||
throw new MultipartFileFormException(e.getMessage(), e.getCause()); | ||
} | ||
} | ||
|
||
this.parts.add(part); | ||
return this; | ||
} | ||
|
||
private String getContentDispositionHeader(String name) { | ||
return String.format(CONTENT_DISPOSITION_TEMPLATE, name); | ||
} | ||
|
||
private String getContentDispositionHeader(String name, String filename) { | ||
return String.format(CONTENT_DISPOSITION_WITH_FILENAME_TEMPLATE, name, filename); | ||
} | ||
} | ||
|
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,144 @@ | ||
package coresearch.cvurl.io.multipart; | ||
|
||
import coresearch.cvurl.io.constant.HttpHeader; | ||
import coresearch.cvurl.io.exception.MultipartFileFormException; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import static coresearch.cvurl.io.util.Validation.notNullParam; | ||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
import static java.util.stream.Collectors.joining; | ||
import static java.util.stream.Collectors.toMap; | ||
|
||
/** | ||
* Represent part of multipart data. | ||
*/ | ||
public class Part<T extends Part<T>> { | ||
public static final String CRLF = "\r\n"; | ||
public static final String BOUNDARY_DELIMITER = "--"; | ||
private Map<String, String> headers; | ||
private byte[] content; | ||
|
||
protected Part(byte[] content) { | ||
this.headers = new HashMap<>(); | ||
this.content = content; | ||
} | ||
|
||
/** | ||
* Creates new instance of {@link Part} | ||
* @param content content part | ||
* @return this {@link Part} | ||
*/ | ||
public static Part of(byte[] content) { | ||
notNullParam(content, "content"); | ||
|
||
return new Part(content); | ||
} | ||
|
||
/** | ||
* Creates new instance of {@link Part} | ||
* @param content content part | ||
* @return this {@link Part} | ||
*/ | ||
public static Part of(String content) { | ||
notNullParam(content, "content"); | ||
|
||
return new Part(content.getBytes()); | ||
} | ||
|
||
/** | ||
* Creates new instance of {@link Part} using file from provided filePath | ||
* Throws {@link MultipartFileFormException} in case {@link IOException} happens | ||
* while reading from the file. | ||
* @param filePath path to file that will be used as content. | ||
* @return this {@link Part} | ||
*/ | ||
public static PartWithFileContent of(Path filePath) { | ||
notNullParam(filePath, "filePath"); | ||
|
||
try { | ||
return new PartWithFileContent(filePath, Files.readAllBytes(filePath)); | ||
} catch (IOException e) { | ||
throw new MultipartFileFormException(e.getMessage(), e); | ||
} | ||
} | ||
|
||
/** | ||
* Add a header to the part. | ||
* | ||
* @param name header name | ||
* @param value header value | ||
* @return this {@link Part} | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public T header(String name, String value) { | ||
notNullParam(name, "key"); | ||
notNullParam(value, "value"); | ||
|
||
this.headers.put(name.toLowerCase(), value); | ||
return (T) this; | ||
} | ||
|
||
/** | ||
* Add headers to the part. | ||
* | ||
* @param headers headers to add | ||
* @return this {@link Part} | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public T headers(Map<String, String> headers) { | ||
notNullParam(headers, "headers"); | ||
|
||
this.headers.putAll(headers | ||
.entrySet() | ||
.stream() | ||
.collect(toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue))); | ||
|
||
return (T) this; | ||
} | ||
|
||
/** | ||
* Set a content-type header of the part. | ||
* | ||
* @param mimeType value to be set as content-type header. | ||
* @return this {@link Part} | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public T contentType(String mimeType) { | ||
notNullParam(mimeType, "mimeType"); | ||
|
||
header(HttpHeader.CONTENT_TYPE, mimeType); | ||
return (T) this; | ||
} | ||
|
||
boolean isContentTypeSet() { | ||
return this.headers.containsKey(HttpHeader.CONTENT_TYPE.toLowerCase()); | ||
} | ||
|
||
List<byte[]> asByteArrays(String boundary) { | ||
var result = new ArrayList<byte[]>(); | ||
|
||
result.add((BOUNDARY_DELIMITER + boundary + CRLF).getBytes()); | ||
|
||
if (!headers.isEmpty()) { | ||
result.add(headers.entrySet() | ||
.stream() | ||
.map(entry -> entry.getKey() + ":" + entry.getValue()) | ||
.collect(joining(CRLF, "", CRLF)) | ||
.getBytes(UTF_8)); | ||
} | ||
|
||
result.add(CRLF.getBytes()); | ||
result.add(content); | ||
result.add(CRLF.getBytes(UTF_8)); | ||
|
||
return result; | ||
} | ||
} | ||
|
Oops, something went wrong.