001/*
002 * Copyright (c) 2010-2026 Mark Allen, Norbert Bartels.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.restfb.request;
023
024import java.io.Closeable;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.net.http.HttpRequest.BodyPublisher;
029import java.nio.charset.StandardCharsets;
030import java.util.List;
031
032import com.restfb.BinaryAttachment;
033import com.restfb.util.StringUtils;
034
035/**
036 * Writes multipart/form-data requests to a temporary file and exposes them as a BodyPublisher.
037 */
038public class MultipartFormBodyPublisher implements Closeable {
039
040  private static final byte[] MULTIPART_CARRIAGE_RETURN_AND_NEWLINE = "\r\n".getBytes(StandardCharsets.UTF_8);
041  private static final byte[] MULTIPART_TWO_HYPHENS = "--".getBytes(StandardCharsets.UTF_8);
042
043  private final TempFileBodyPublisher tempFileBodyPublisher;
044  private final OutputStream outputStream;
045  private final String boundary;
046  private final int bufferSize;
047
048  public MultipartFormBodyPublisher(String boundary, int bufferSize) throws IOException {
049    this.boundary = boundary;
050    this.bufferSize = bufferSize;
051    tempFileBodyPublisher = new TempFileBodyPublisher();
052    outputStream = tempFileBodyPublisher.outputStream();
053  }
054
055  public void addAttachments(List<BinaryAttachment> attachments) throws IOException {
056    for (BinaryAttachment attachment : attachments) {
057      addAttachment(attachment);
058    }
059  }
060
061  private void addAttachment(BinaryAttachment attachment) throws IOException {
062    StringBuilder headers = new StringBuilder();
063    headers.append(new String(MULTIPART_TWO_HYPHENS, StandardCharsets.UTF_8)).append(boundary)
064      .append(new String(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE, StandardCharsets.UTF_8))
065      .append("Content-Disposition: form-data; name=\"").append(attachment.getFormFieldName()).append("\"; filename=\"")
066      .append(attachment.getFilename()).append("\"")
067      .append(new String(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE, StandardCharsets.UTF_8)).append("Content-Type: ")
068      .append(attachment.getContentType())
069      .append(new String(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE, StandardCharsets.UTF_8))
070      .append(new String(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE, StandardCharsets.UTF_8));
071
072    outputStream.write(headers.toString().getBytes(StringUtils.ENCODING_CHARSET));
073    try (InputStream data = attachment.getData()) {
074      write(data, outputStream);
075    }
076    outputStream.write((new String(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE, StandardCharsets.UTF_8)
077        + new String(MULTIPART_TWO_HYPHENS, StandardCharsets.UTF_8) + boundary
078        + new String(MULTIPART_TWO_HYPHENS, StandardCharsets.UTF_8)
079        + new String(MULTIPART_CARRIAGE_RETURN_AND_NEWLINE, StandardCharsets.UTF_8))
080      .getBytes(StringUtils.ENCODING_CHARSET));
081  }
082
083  private void write(InputStream source, OutputStream destination) throws IOException {
084    byte[] buffer = new byte[bufferSize];
085    int read;
086    while ((read = source.read(buffer)) > 0) {
087      destination.write(buffer, 0, read);
088    }
089  }
090
091  public BodyPublisher build() throws IOException {
092    outputStream.flush();
093    outputStream.close();
094    return tempFileBodyPublisher.build();
095  }
096
097  @Override
098  public void close() throws IOException {
099    tempFileBodyPublisher.close();
100  }
101}