Skip to content

Commit

Permalink
Merge 69105e9 into dbd64b8
Browse files Browse the repository at this point in the history
  • Loading branch information
yrkkap committed Jul 22, 2019
2 parents dbd64b8 + 69105e9 commit ce5f860
Show file tree
Hide file tree
Showing 12 changed files with 739 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/main/java/coresearch/cvurl/io/constant/HttpHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public final class HttpHeader {

public static final String CONTENT_TYPE = "Content-Type";

public static final String CONTENT_DISPOSITION = "Content-Disposition";

public static final String DATE = "Date";

public static final String DAV = "Dav";
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/coresearch/cvurl/io/constant/MultipartType.java
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()));
}
}
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 src/main/java/coresearch/cvurl/io/multipart/MultipartBody.java
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);
}
}

144 changes: 144 additions & 0 deletions src/main/java/coresearch/cvurl/io/multipart/Part.java
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;
}
}

Loading

0 comments on commit ce5f860

Please sign in to comment.