All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.signalfx.shaded.jetty.client.util.MultiPartContentProvider Maven / Gradle / Ivy

//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package com.signalfx.shaded.jetty.client.util;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;

import com.signalfx.shaded.jetty.client.AsyncContentProvider;
import com.signalfx.shaded.jetty.client.Synchronizable;
import com.signalfx.shaded.jetty.client.api.ContentProvider;
import com.signalfx.shaded.jetty.http.HttpField;
import com.signalfx.shaded.jetty.http.HttpFields;
import com.signalfx.shaded.jetty.http.HttpHeader;
import com.signalfx.shaded.jetty.io.RuntimeIOException;
import com.signalfx.shaded.jetty.util.Callback;
import com.signalfx.shaded.jetty.util.IO;
import com.signalfx.shaded.jetty.util.log.Log;
import com.signalfx.shaded.jetty.util.log.Logger;

/**
 * 

A {@link ContentProvider} for form uploads with the {@code "multipart/form-data"} * content type.

*

Example usage:

*
 * MultiPartContentProvider multiPart = new MultiPartContentProvider();
 * multiPart.addFieldPart("field", new StringContentProvider("foo"), null);
 * multiPart.addFilePart("icon", "img.png", new PathContentProvider(Paths.get("/tmp/img.png")), null);
 * multiPart.close();
 * ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
 *         .method(HttpMethod.POST)
 *         .content(multiPart)
 *         .send();
 * 
*

The above example would be the equivalent of submitting this form:

*
 * <form method="POST" enctype="multipart/form-data"  accept-charset="UTF-8">
 *     <input type="text" name="field" value="foo" />
 *     <input type="file" name="icon" />
 * </form>
 * 
*/ public class MultiPartContentProvider extends AbstractTypedContentProvider implements AsyncContentProvider, Closeable { private static final Logger LOG = Log.getLogger(MultiPartContentProvider.class); private static final byte[] COLON_SPACE_BYTES = new byte[]{':', ' '}; private static final byte[] CR_LF_BYTES = new byte[]{'\r', '\n'}; private final List parts = new ArrayList<>(); private final ByteBuffer firstBoundary; private final ByteBuffer middleBoundary; private final ByteBuffer onlyBoundary; private final ByteBuffer lastBoundary; private final AtomicBoolean closed = new AtomicBoolean(); private Listener listener; private long length = -1; public MultiPartContentProvider() { this(makeBoundary()); } public MultiPartContentProvider(String boundary) { super("multipart/form-data; boundary=" + boundary); String firstBoundaryLine = "--" + boundary + "\r\n"; this.firstBoundary = ByteBuffer.wrap(firstBoundaryLine.getBytes(StandardCharsets.US_ASCII)); String middleBoundaryLine = "\r\n" + firstBoundaryLine; this.middleBoundary = ByteBuffer.wrap(middleBoundaryLine.getBytes(StandardCharsets.US_ASCII)); String onlyBoundaryLine = "--" + boundary + "--\r\n"; this.onlyBoundary = ByteBuffer.wrap(onlyBoundaryLine.getBytes(StandardCharsets.US_ASCII)); String lastBoundaryLine = "\r\n" + onlyBoundaryLine; this.lastBoundary = ByteBuffer.wrap(lastBoundaryLine.getBytes(StandardCharsets.US_ASCII)); } private static String makeBoundary() { StringBuilder builder = new StringBuilder("JettyHttpClientBoundary"); builder.append(Long.toString(System.identityHashCode(builder), 36)); builder.append(Long.toString(System.identityHashCode(Thread.currentThread()), 36)); builder.append(Long.toString(System.nanoTime(), 36)); return builder.toString(); } /** *

Adds a field part with the given {@code name} as field name, and the given * {@code content} as part content.

*

The {@code Content-Type} of this part will be obtained from:

*
    *
  • the {@code Content-Type} header in the {@code fields} parameter; otherwise
  • *
  • the {@link com.signalfx.shaded.jetty.client.api.ContentProvider.Typed#getContentType()} method if the {@code content} parameter * implements {@link com.signalfx.shaded.jetty.client.api.ContentProvider.Typed}; otherwise
  • *
  • "text/plain"
  • *
* * @param name the part name * @param content the part content * @param fields the headers associated with this part */ public void addFieldPart(String name, ContentProvider content, HttpFields fields) { addPart(new Part(name, null, "text/plain", content, fields)); } /** *

Adds a file part with the given {@code name} as field name, the given * {@code fileName} as file name, and the given {@code content} as part content.

*

The {@code Content-Type} of this part will be obtained from:

*
    *
  • the {@code Content-Type} header in the {@code fields} parameter; otherwise
  • *
  • the {@link com.signalfx.shaded.jetty.client.api.ContentProvider.Typed#getContentType()} method if the {@code content} parameter * implements {@link com.signalfx.shaded.jetty.client.api.ContentProvider.Typed}; otherwise
  • *
  • "application/octet-stream"
  • *
* * @param name the part name * @param fileName the file name associated to this part * @param content the part content * @param fields the headers associated with this part */ public void addFilePart(String name, String fileName, ContentProvider content, HttpFields fields) { addPart(new Part(name, fileName, "application/octet-stream", content, fields)); } private void addPart(Part part) { parts.add(part); if (LOG.isDebugEnabled()) LOG.debug("Added {}", part); } @Override public void setListener(Listener listener) { this.listener = listener; if (closed.get()) this.length = calculateLength(); } private long calculateLength() { // Compute the length, if possible. if (parts.isEmpty()) { return onlyBoundary.remaining(); } else { long result = 0; for (int i = 0; i < parts.size(); ++i) { result += (i == 0) ? firstBoundary.remaining() : middleBoundary.remaining(); Part part = parts.get(i); long partLength = part.length; result += partLength; if (partLength < 0) { result = -1; break; } } if (result > 0) result += lastBoundary.remaining(); return result; } } @Override public long getLength() { return length; } @Override public Iterator iterator() { return new MultiPartIterator(); } @Override public void close() { closed.compareAndSet(false, true); } private static class Part { private final String name; private final String fileName; private final String contentType; private final ContentProvider content; private final HttpFields fields; private final ByteBuffer headers; private final long length; private Part(String name, String fileName, String contentType, ContentProvider content, HttpFields fields) { this.name = name; this.fileName = fileName; this.contentType = contentType; this.content = content; this.fields = fields; this.headers = headers(); this.length = content.getLength() < 0 ? -1 : headers.remaining() + content.getLength(); } private ByteBuffer headers() { try { // Compute the Content-Disposition. String contentDisposition = "Content-Disposition: form-data; name=\"" + name + "\""; if (fileName != null) contentDisposition += "; filename=\"" + fileName + "\""; contentDisposition += "\r\n"; // Compute the Content-Type. String contentType = fields == null ? null : fields.get(HttpHeader.CONTENT_TYPE); if (contentType == null) { if (content instanceof Typed) contentType = ((Typed)content).getContentType(); else contentType = this.contentType; } contentType = "Content-Type: " + contentType + "\r\n"; if (fields == null || fields.size() == 0) { String headers = contentDisposition; headers += contentType; headers += "\r\n"; return ByteBuffer.wrap(headers.getBytes(StandardCharsets.UTF_8)); } ByteArrayOutputStream buffer = new ByteArrayOutputStream((fields.size() + 1) * contentDisposition.length()); buffer.write(contentDisposition.getBytes(StandardCharsets.UTF_8)); buffer.write(contentType.getBytes(StandardCharsets.UTF_8)); for (HttpField field : fields) { if (HttpHeader.CONTENT_TYPE.equals(field.getHeader())) continue; buffer.write(field.getName().getBytes(StandardCharsets.US_ASCII)); buffer.write(COLON_SPACE_BYTES); String value = field.getValue(); if (value != null) buffer.write(value.getBytes(StandardCharsets.UTF_8)); buffer.write(CR_LF_BYTES); } buffer.write(CR_LF_BYTES); return ByteBuffer.wrap(buffer.toByteArray()); } catch (IOException x) { throw new RuntimeIOException(x); } } @Override public String toString() { return String.format("%s@%x[name=%s,fileName=%s,length=%d,headers=%s]", getClass().getSimpleName(), hashCode(), name, fileName, content.getLength(), fields); } } private class MultiPartIterator implements Iterator, Synchronizable, Callback, Closeable { private Iterator iterator; private int index; private State state = State.FIRST_BOUNDARY; @Override public boolean hasNext() { return state != State.COMPLETE; } @Override public ByteBuffer next() { while (true) { switch (state) { case FIRST_BOUNDARY: { if (parts.isEmpty()) { state = State.COMPLETE; return onlyBoundary.slice(); } else { state = State.HEADERS; return firstBoundary.slice(); } } case HEADERS: { Part part = parts.get(index); ContentProvider content = part.content; if (content instanceof AsyncContentProvider) ((AsyncContentProvider)content).setListener(listener); iterator = content.iterator(); state = State.CONTENT; return part.headers.slice(); } case CONTENT: { if (iterator.hasNext()) return iterator.next(); ++index; if (index < parts.size()) { state = State.MIDDLE_BOUNDARY; if (iterator instanceof Closeable) IO.close((Closeable)iterator); } else { state = State.LAST_BOUNDARY; } break; } case MIDDLE_BOUNDARY: { state = State.HEADERS; return middleBoundary.slice(); } case LAST_BOUNDARY: { state = State.COMPLETE; return lastBoundary.slice(); } case COMPLETE: { throw new NoSuchElementException(); } } } } @Override public Object getLock() { if (iterator instanceof Synchronizable) return ((Synchronizable)iterator).getLock(); return this; } @Override public void succeeded() { if (state == State.CONTENT && iterator instanceof Callback) ((Callback)iterator).succeeded(); } @Override public void failed(Throwable x) { if (state == State.CONTENT && iterator instanceof Callback) ((Callback)iterator).failed(x); } @Override public void close() throws IOException { if (iterator instanceof Closeable) ((Closeable)iterator).close(); } } private enum State { FIRST_BOUNDARY, HEADERS, CONTENT, MIDDLE_BOUNDARY, LAST_BOUNDARY, COMPLETE } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy