dev.fitko.fitconnect.api.domain.model.attachment.Attachment Maven / Gradle / Ivy
Show all versions of client Show documentation
package dev.fitko.fitconnect.api.domain.model.attachment;
import dev.fitko.fitconnect.api.config.chunking.AttachmentChunkingConfig;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.exceptions.client.FitConnectAttachmentException;
import dev.fitko.fitconnect.api.exceptions.client.FitConnectSenderException;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.UUID;
import static dev.fitko.fitconnect.api.config.chunking.AttachmentChunkingConfig.TEMP_BUFFERED_FILE_PREFIX;
/**
* This class represents an attachment with data payload and some metadata. The data can come from two sources:
*
* - a file path
* - in memory data stored in a byte array
*
* This is because we cannot buffer all data in memory for large attachments payloads.
*
* For attachment payloads that won't fit into memory (java byte arrays have a max. size of 2^32-1 byte == 2 GB.) use:
*
* - {@link #fromLargeAttachment(Path, String)}
* - {@link #fromLargeAttachment(InputStream, String)}
*
* Use all other creator methods for data that can be stored in memory.
*
* Large attachments with a file path will be chunked automatically.
* For further chunking options see {@link AttachmentChunkingConfig}
*/
public final class Attachment {
private final Path dataFile;
private final byte[] inMemoryData;
private UUID attachmentId;
private final String fileName;
private final String description;
private final String mimeType;
private final Purpose purpose;
/**
* Creates an attachment and reads the content from a given path into memory.
*
* @param filePath path of the attachment file
* @param mimeType mime-type of the attachment
* @throws FitConnectSenderException if the file path could not be read
*/
public static Attachment fromPath(final Path filePath, final String mimeType) throws FitConnectSenderException {
return fromPath(filePath, mimeType, null, null);
}
/**
* Creates an attachment and reads the content from a given path into memory.
*
* @param filePath path of the attachment file
* @param mimeType mime-type of the attachment
* @param fileName name of the attachment file
* @param description description of the attachment file
* @throws FitConnectSenderException if the file path could not be read
*/
public static Attachment fromPath(final Path filePath, final String mimeType, final String fileName, final String description) throws FitConnectSenderException {
try {
return new Attachment(Files.readAllBytes(filePath), mimeType, fileName, description, Purpose.ATTACHMENT);
} catch (IOException e) {
throw new FitConnectSenderException("Reading attachment from path '" + filePath + "' failed", e);
}
}
/**
* Creates an attachment and reads the content from an input-stream into memory.
*
* @param inputStream stream of the attachment data
* @param mimeType mime type of the provided attachment data
* @throws FitConnectSenderException if the input-stream could not be read
*/
public static Attachment fromInputStream(final InputStream inputStream, final String mimeType) throws FitConnectSenderException {
return fromInputStream(inputStream, mimeType, null, null);
}
/**
* Creates an attachment and reads the content from an input-stream into memory.
*
* @param inputStream stream of the attachment data
* @param mimeType mime type of the provided attachment data
* @param fileName name of the attachment file
* @param description description of the attachment file
* @throws FitConnectSenderException if the input-stream could not be read
*/
public static Attachment fromInputStream(final InputStream inputStream, final String mimeType, final String fileName, final String description) throws FitConnectSenderException {
try {
return new Attachment(inputStream.readAllBytes(), mimeType, fileName, description, Purpose.ATTACHMENT);
} catch (IOException e) {
throw new FitConnectSenderException(e.getMessage(), e);
}
}
/**
* Creates an attachment and reads the content from a byte-array into memory.
*
* @param content data of the attachment as byte[]
* @param mimeType mime type of the provided attachment data
*/
public static Attachment fromByteArray(final byte[] content, final String mimeType) {
return fromByteArray(content, mimeType, null, null);
}
/**
* Creates an attachment and reads the content from a byte-array into memory.
*
* @param content data of the attachment as byte[]
* @param fileName name of the attachment file
* @param mimeType mime type of the provided attachment data
* @param description description of the attachment file
*/
public static Attachment fromByteArray(final byte[] content, final String mimeType, final String fileName, final String description) {
return new Attachment(content, mimeType, fileName, description, Purpose.ATTACHMENT);
}
/**
* Creates an attachment and reads the content from a given string into memory. The content will be read with UTF-8 encoding.
* Note: If you don't use an SDK to retrieve the submission you may have to decode the string with UTF-8
*
* @param content data of the attachment as byte[]
* @param mimeType mime type of the provided attachment data
* @deprecated This method is no longer acceptable since it can lead to character encoding issues when used incorrectly, e.g. when an entire file is passed as string.
* Use {@link Attachment#fromByteArray(byte[], String)}} instead.
*/
@Deprecated(since = "3.0.0", forRemoval = true)
public static Attachment fromString(final String content, final String mimeType) {
return fromString(content, mimeType, null, null);
}
/**
* Creates an attachment and reads the content from a given string into memory. The content will be read with UTF-8 encoding.
*
Note: If you don't use an SDK to retrieve the submission you may have to decode the string with UTF-8
*
* @param content data of the attachment as string
* @param fileName name of the attachment file
* @param mimeType mime type of the provided attachment data
* @param description description of the attachment file
* @deprecated This method is no longer acceptable since it can lead to character encoding issues when used incorrectly, e.g. when an entire file is passed as string.
* Use {@link Attachment#fromByteArray(byte[], String)}} instead.
*/
@Deprecated(since = "3.0.0", forRemoval = true)
public static Attachment fromString(final String content, final String mimeType, final String fileName, final String description) {
return new Attachment(content.getBytes(StandardCharsets.UTF_8), mimeType, fileName, description, Purpose.ATTACHMENT);
}
/**
* Creates an attachment for a file that does not fit into memory and will be stored as a file/path reference.
* This type of attachment will be chunked automatically.
*
* @param filePath path of the attachment file
* @param mimeType mime-type of the attachment
* @throws FitConnectSenderException if the file path could not be read
* @see AttachmentChunkingConfig
*/
public static Attachment fromLargeAttachment(final Path filePath, final String mimeType) throws FitConnectSenderException {
return fromLargeAttachment(filePath, mimeType, null, null);
}
/**
* Creates an attachment for a file that does not fit into memory and will be stored as a file/path reference.
* This type of attachment will be chunked automatically.
*
* @param filePath path of the attachment file
* @param mimeType mime-type of the attachment
* @param fileName name of the attachment file
* @param description description of the attachment file
* @throws FitConnectSenderException if the file path could not be read
* @see AttachmentChunkingConfig
*/
public static Attachment fromLargeAttachment(final Path filePath, final String mimeType, final String fileName, final String description) throws FitConnectSenderException {
return new Attachment(filePath, mimeType, fileName, description, Purpose.ATTACHMENT);
}
/**
* Creates an attachment for an input-stream that does not fit into memory and will be stored as a file/path reference.
* This type of attachment will be chunked automatically.
*
* @param inputStream stream of the attachment file
* @param mimeType mime-type of the attachment
* @throws FitConnectSenderException if the stream could not be read or buffered in filesystem
* @see AttachmentChunkingConfig
*/
public static Attachment fromLargeAttachment(final InputStream inputStream, final String mimeType) throws FitConnectSenderException {
return fromLargeAttachment(inputStream, mimeType, null, null);
}
/**
* Creates an attachment for an input-stream that does not fit into memory and will be stored as a file/path reference.
* This type of attachment will be chunked automatically.
*
* @param inputStream stream of the attachment file
* @param mimeType mime-type of the attachment
* @param fileName name of the attachment file
* @param description description of the attachment file
* @throws FitConnectSenderException if the stream could not be read or buffered in filesystem
* @see AttachmentChunkingConfig
*/
public static Attachment fromLargeAttachment(final InputStream inputStream, final String mimeType, final String fileName, final String description) throws FitConnectSenderException {
final Path tempFile = bufferStreamToFileSystem(inputStream);
return fromLargeAttachment(tempFile, mimeType, fileName, description);
}
/**
* Creates an attachment for submission data that is too large to be transferred in the submission metadata.
* This type of attachment will be chunked automatically.
*
* HINT: Only one attachment with {@link Purpose#DATA} is allowed per submission!
*
* @param inputStream stream of the submission data
* @param mimeType mime-type of the submission data (json/xml)
* @throws FitConnectAttachmentException if the stream could not be read or buffered in filesystem
*/
public static Attachment fromSubmissionData(InputStream inputStream, MimeType mimeType) {
final Path tempFile = bufferStreamToFileSystem(inputStream);
var filename = "data." + mimeType.getExtension();
return new Attachment(null, null, tempFile, mimeType.value(), filename, "submission data as attachment", Purpose.DATA);
}
/**
* Gets the attachment content for in-memory data and large attachment files as byte[].
* Be aware that large attachments might not fit into memory. Use {@link #getDataAsInputStream()} instead.
*
* @return byte array of the attachment content
* @see #isInMemoryAttachment()
* @see #isLargeAttachment()
*/
public byte[] getDataAsBytes() {
if (isInMemoryAttachment()) {
return inMemoryData;
}
try (FileInputStream fileInputStream = new FileInputStream(dataFile.toFile())) {
return fileInputStream.readAllBytes();
} catch (final IOException e) {
throw new FitConnectSenderException(e.getMessage(), e);
}
}
/**
* Gets the attachment content as input-stream.
*
* @return input-stream of the attachment content
* @throws FitConnectAttachmentException if creating an input-stream from large attachment files fails
*/
public InputStream getDataAsInputStream() {
if (isInMemoryAttachment()) {
return new ByteArrayInputStream(inMemoryData);
}
try {
return Files.newInputStream(dataFile);
} catch (IOException e) {
throw new FitConnectAttachmentException(e.getMessage(), e);
}
}
/**
* Gets the in-memory attachment content as string.
*
* Be aware that the attachments data might not fit onto memory and use {@link #getDataAsInputStream()} instead.
*
* @param encoding charset the string should be encoded with
* @return string of the attachments content.
*/
public String getDataAsString(final Charset encoding) {
return new String(getDataAsBytes(), encoding);
}
/**
* Gets the in-memory attachment content as string with a default UTF-8 encoding.
*
* Be aware that the attachments data might not fit onto memory and use {@link #getDataAsInputStream()} instead.
*
* @return utf-8 encoded string of the attachments content.
*/
public String getDataAsString() {
return getDataAsString(StandardCharsets.UTF_8);
}
/**
* Gets the file path for large attachment data that is stored in file system.
*
* @return Path to the large attachment file
*/
public Path getLargeAttachmentFilePath() {
return dataFile;
}
/**
* Get the attachment id.
*
* @return attachment id as UUID
*/
public UUID getAttachmentId() {
return attachmentId;
}
/**
* Gets the filename of the attachment. This filed is optional so it might be null.
*
* @return filename as string, null if not present
*/
public String getFileName() {
return fileName;
}
/**
* Gets the description of the attachment. This filed is optional so it might be null.
*
* @return description as string, null if not present
*/
public String getDescription() {
return description;
}
/**
* Gets the mimetype of the attachments content.
*
* @return mimetype as string.
*/
public String getMimeType() {
return mimeType;
}
/**
* Gets the purpose of the attachment.
*
* @return {@link Purpose} .
*/
public Purpose getPurpose() {
return purpose;
}
/**
* Checks if the attachment payload is stored in memory or as a file path for large attachments.
*
* @return true | false
*/
public boolean isInMemoryAttachment() {
return inMemoryData != null && dataFile == null;
}
/**
* Checks if the attachment payload is stored as file and not in memory.
*
* @return true | false
*/
public boolean isLargeAttachment() {
return !isInMemoryAttachment();
}
private Attachment(final byte[] inMemoryData, final String mimeType, final String fileName, final String description, Purpose purpose) {
this(null, inMemoryData, null, mimeType, fileName, description, purpose);
}
private Attachment(final Path dataFile, final String mimeType, final String fileName, final String description, Purpose purpose) {
this(null, null, dataFile, mimeType, fileName, description, purpose);
}
public Attachment(UUID attachmentId, final byte[] inMemoryData, final Path dataFile, final String mimeType, final String fileName, final String description, Purpose purpose) {
this.inMemoryData = inMemoryData;
this.dataFile = dataFile;
this.attachmentId = attachmentId;
this.fileName = fileName != null ? getBaseNameFromPath(fileName) : UUID.randomUUID().toString(); // prevent maliciously injected filePaths
this.mimeType = mimeType;
this.description = description;
this.purpose = purpose;
}
/**
* Write an input-stream to a temp file path to be used as attachment payload.
*
* @param inputStream the stream that is written to the filesystem
* @return a {@link Path} to the
* @throws FitConnectAttachmentException if attachment stream could not be written to temp file
* @see AttachmentChunkingConfig#TEMP_BUFFERED_FILE_PREFIX
*/
private static Path bufferStreamToFileSystem(InputStream inputStream) {
try {
final Path tempFile = Files.createTempFile(TEMP_BUFFERED_FILE_PREFIX, ".tmp");
try (OutputStream os = new FileOutputStream(tempFile.toFile())) {
inputStream.transferTo(os);
}
return tempFile;
} catch (IOException e) {
throw new FitConnectAttachmentException(e.getMessage(), e);
}
}
private static String getBaseNameFromPath(final String fileName) {
try {
return Path.of(fileName).getFileName().toString();
} catch (final InvalidPathException e) {
throw new FitConnectSenderException("Reading filename '" + fileName + "' failed ", e);
}
}
}