diff --git a/core/src/main/java/org/jclouds/http/MultipartForm.java b/core/src/main/java/org/jclouds/http/MultipartForm.java new file mode 100644 index 0000000000..69470248d7 --- /dev/null +++ b/core/src/main/java/org/jclouds/http/MultipartForm.java @@ -0,0 +1,129 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Map.Entry; + +import org.apache.commons.io.IOUtils; +import org.jclouds.util.InputStreamChain; + +import com.google.common.collect.Multimap; + +/** + * + * @author Adrian Cole + */ +public class MultipartForm { + private static final String rn = "\r\n"; + private static final String dd = "--"; + + private final InputStreamChain chain; + private long size; + + public MultipartForm(String boundary, Part... parts) { + String boundaryrn = boundary + rn; + chain = new InputStreamChain(); + for (Part part : parts) { + addHeaders(boundaryrn, part); + addData(part); + } + addFooter(boundary); + } + + private void addData(Part part) { + chain.addInputStream(part.getData()); + chain.addAsInputStream(rn); + size += part.getSize() + rn.length(); + } + + private void addHeaders(String boundaryrn, Part part) { + StringBuilder builder = new StringBuilder(dd).append(boundaryrn); + for (Entry entry : part.getHeaders().entries()) { + String header = String.format("%s: %s%s", entry.getKey(), entry.getValue(), rn); + builder.append(header); + } + builder.append(rn); + chain.addAsInputStream(builder.toString()); + size += builder.length(); + } + + private void addFooter(String boundary) { + String end = dd + boundary + dd + rn; + chain.addAsInputStream(end); + size += end.length(); + } + + public MultipartForm(Part... parts) { + this("__redrose__", parts); + } + + public static class Part { + private final Multimap headers; + private final InputStream data; + private final long size; + + public Part(Multimap headers, InputStream data, long size) { + this.headers = headers; + this.data = data; + this.size = size; + } + + public Part(Multimap headers, String data) { + this(headers, IOUtils.toInputStream(data), data.length()); + } + + public Part(Multimap headers, File data) throws FileNotFoundException { + this(headers, new FileInputStream(data), data.length()); + } + + public Part(Multimap headers, byte[] data) { + this(headers, new ByteArrayInputStream(data), data.length); + } + + public Multimap getHeaders() { + return headers; + } + + public InputStream getData() { + return data; + } + + public long getSize() { + return size; + } + } + + public long getSize() { + return size; + } + + public InputStream getData() { + return chain; + } +} diff --git a/core/src/main/java/org/jclouds/util/InputStreamChain.java b/core/src/main/java/org/jclouds/util/InputStreamChain.java new file mode 100644 index 0000000000..681be89ee9 --- /dev/null +++ b/core/src/main/java/org/jclouds/util/InputStreamChain.java @@ -0,0 +1,139 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; + +import org.apache.commons.io.IOUtils; + +/** + * {@link InputStream} implementation that allows chaining of various streams for seamless + * sequential reading + * + * @author Adrian Cole + * @author Tomas Varaneckas + */ +public class InputStreamChain extends InputStream { + + /** + * Input stream chain + */ + private final LinkedList streams = new LinkedList(); + + /** + * Currently active stream + */ + private InputStream current; + + /** + * Constructor with an initial stream + * + * @param first + * Initial InputStream + */ + public InputStreamChain(InputStream... inputStreams) { + for (InputStream stream : inputStreams) { + addInputStream(stream); + } + } + + /** + * Adds input stream to the end of chain + * + * @param stream + * InputStream to add to chain + * @return instance of self (for fluent calls) + */ + public InputStreamChain addInputStream(final InputStream stream) { + streams.addLast(stream); + if (current == null) { + current = streams.removeFirst(); + } + return this; + } + + /** + * Adds a String to the end of chain + * + * @param value + * String to add to the chain + * @return instance of self (for fluent calls) + */ + public InputStreamChain addAsInputStream(final String value) { + return addInputStream(IOUtils.toInputStream(value)); + } + + @Override + public int read() throws IOException { + int bit = current.read(); + if (bit == -1 && streams.size() > 0) { + try { + current.close(); + } catch (final IOException e) { + // replace this with a call to logging facility + e.printStackTrace(); + } + current = streams.removeFirst(); + bit = read(); + } + return bit; + } + + @Override + public int available() throws IOException { + int available = current.available(); + for (InputStream stream : streams) { + available += stream.available(); + } + return available; + } + + @Override + public void close() throws IOException { + current.close(); + } + + @Override + public boolean markSupported() { + return current.markSupported(); + } + + @Override + public synchronized void mark(int i) { + current.mark(i); + } + + @Override + public synchronized void reset() throws IOException { + current.reset(); + } + + @Override + public long skip(long l) throws IOException { + return current.skip(l); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/jclouds/http/MultipartFormTest.java b/core/src/test/java/org/jclouds/http/MultipartFormTest.java new file mode 100644 index 0000000000..32e07cc80d --- /dev/null +++ b/core/src/test/java/org/jclouds/http/MultipartFormTest.java @@ -0,0 +1,95 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import org.jclouds.http.MultipartForm.Part; +import org.jclouds.util.Utils; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMultimap; + +/** + * Tests parsing of a request + * + * @author Adrian Cole + */ +@Test(testName = "http.MultipartFormTest") +public class MultipartFormTest { + String boundary = "------------------------------c88555ffd14e"; + + public void testSinglePart() throws IOException { + + StringBuilder builder = new StringBuilder(); + addData(boundary, "hello", builder); + builder.append("--").append(boundary).append("--").append("\r\n"); + String expects = builder.toString(); + assertEquals(expects.length(), 197); + + MultipartForm multipartForm = new MultipartForm(boundary, newPart("hello")); + + assertEquals(Utils.toStringAndClose(multipartForm.getData()), expects); + assertEquals(multipartForm.getSize(), 197); + } + + private Part newPart(String data) { + return new MultipartForm.Part(ImmutableMultimap.of("Content-Disposition", + "form-data; name=\"file\"; filename=\"testfile.txt\"", HttpHeaders.CONTENT_TYPE, + MediaType.TEXT_PLAIN), data); + } + + private void addData(String boundary, String data, StringBuilder builder) { + builder.append("--").append(boundary).append("\r\n"); + builder.append("Content-Disposition").append(": ").append( + "form-data; name=\"file\"; filename=\"testfile.txt\"").append("\r\n"); + builder.append("Content-Type").append(": ").append("text/plain").append("\r\n"); + builder.append("\r\n"); + builder.append(data).append("\r\n"); + } + + public void testMultipleParts() throws IOException { + + StringBuilder builder = new StringBuilder("--"); + addData(boundary, "hello", builder); + addData(boundary, "goodbye", builder); + + builder.append(boundary).append("--").append("\r\n"); + String expects = builder.toString(); + + assertEquals(expects.length(), 348); + + MultipartForm multipartForm = new MultipartForm(boundary, newPart("hello"), + newPart("goodbye")); + + assertEquals(Utils.toStringAndClose(multipartForm.getData()), expects); + assertEquals(multipartForm.getSize(), 348); + } + +}