2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* Portions Copyrighted 2007 Sun Microsystems, Inc.
29
package org.netbeans.lib.uihandler;
32
* A utility class to handle content of multipart/form-data type used in form uploads.
34
* Parses and provides accessor functions to extract the form fields and the uploaded
35
* file content parts separated by a boundary string.
36
* See http://www.ietf.org/rfc/rfc1867.txt.
39
import java.io.BufferedOutputStream;
40
import java.io.ByteArrayOutputStream;
42
import java.io.FileOutputStream;
43
import java.io.FilterInputStream;
44
import java.io.IOException;
45
import java.io.InputStream;
46
import java.io.OutputStream;
47
import java.util.Enumeration;
48
import java.util.Hashtable;
49
import java.util.HashMap;
50
import java.util.Vector;
51
import java.util.zip.GZIPInputStream;
53
public class MultiPartHandler {
54
public interface InputFacade {
55
public int readLine(byte[] arr, int off, int len) throws IOException;
56
public InputStream getInputStream();
59
public interface RequestFacade {
60
public int getContentLength();
61
public String getContentType();
62
public InputFacade getInput() throws IOException;
65
private static final int DEFAULT_MAX_UPLOAD_SIZE = 1024 * 1024; // 1Mb
67
protected Hashtable<String,Vector<String>> formFields = new Hashtable<String,Vector<String>>();
68
Hashtable<String,OneUpload> uploadFiles = new Hashtable<String,OneUpload>();
70
/** servlet request */
71
private RequestFacade req;
73
/** input stream to read parts from */
74
private InputFacade in;
76
/** MIME boundary that delimits parts */
77
private String boundary;
79
/** buffer for readLine method */
80
private byte[] buf = new byte[8 * 1024];
82
/** upload directory */
83
private File uploadDir;
85
/** encoding used for the from fields */
86
private String fieldEncoding = "ISO-8859-1";
89
/*private static StringManager localStrings =
90
StringManager.getManager( OneUpload.class );*/
93
* Instantiate a new multipart handler with default
95
public MultiPartHandler(RequestFacade request,
96
String tmpDirectory) throws IOException {
97
this(request, tmpDirectory, DEFAULT_MAX_UPLOAD_SIZE, "ISO-8859-1");
100
public MultiPartHandler(RequestFacade request,
102
int maxUploadSize) throws IOException {
103
this(request, tmpDirectory, maxUploadSize, "ISO-8859-1");
107
* Instantiate a new OneUpload to handle the given request,
108
* saving any uploaded files to the given directory and limiting the
109
* upload size to maxUploadSize.
111
* An IOException is thrown when the request content-type doesn't match
112
* with multipart/form-data or if the upload size exceeds the given limit.
114
* call parseMultipartUpload() to parse various parts of the posted data and then
115
* call getParameter(), getParameters(), getParameterNames() and getParameterValues()
116
* functions to access form field names and their values.
117
* call getFile(), getFileType() to access uploaded file and its content-type.
119
public MultiPartHandler(RequestFacade request,
122
String fieldEncoding) throws IOException {
124
// Ensure we are passed legal arguments
125
if (request == null) {
126
//String msg = localStrings.getString( "admin.server.gui.servlet.request_cannot_be_null" );
127
throw new IllegalArgumentException( "request is null" );
129
if (tmpDirectory == null) {
130
//String msg = localStrings.getString( "admin.server.gui.servlet.tmpdirectory_cannot_be_null" );
131
throw new IllegalArgumentException( "tmp Dir is null" );
133
if (maxUploadSize <= 0) {
134
//String msg = localStrings.getString( "admin.server.gui.servlet.maxpostsize_must_be_positive" );
135
throw new IllegalArgumentException( "Max size is < 0" );
138
// Ensure that the directory exists and is writable (this should be a temp directory)
139
uploadDir = new File(tmpDirectory);
140
if (!uploadDir.isDirectory()) {
141
//String msg = localStrings.getString( "admin.server.gui.servlet.not_directory", tmpDirectory );
142
throw new IllegalArgumentException( "Not a Directory" );
144
if (!uploadDir.canWrite()) {
145
//String msg = localStrings.getString( "admin.server.gui.servlet.not_writable", tmpDirectory );
146
throw new IllegalArgumentException("write protected" );
149
int length = request.getContentLength();
150
//commented this code to remove the restriction on the file upload size.
151
/*if (length > maxUploadSize) {
152
//String msg = localStrings.getString( "admin.server.gui.servlet.posted_content_length_exceeds_limit", new Integer(length), new Integer(maxUploadSize) );
153
throw new IOException( msg );
155
// Check the content type to make sure it's "multipart/form-data"
156
String type = request.getContentType();
158
!type.toLowerCase().startsWith("multipart/form-data")) {
159
//String msg = localStrings.getString( "admin.server.gui.servlet.posted_content_type_not_multipart" );
160
throw new IOException( "type null" );
163
// Check the content length to prevent denial of service attacks
164
this.fieldEncoding = fieldEncoding;
168
/* parseMultipartUpload:
170
* This function parses the multipart/form-data and throws an IOException
171
* if there's any problem reading or parsing the request or if the posted
172
* content is larger than the maximum permissible size.
174
public void parseMultipartUpload()
176
// setup the initial buffered input stream, boundary string that separates
177
// various parts in the stream.
178
startMultipartParse();
180
HashMap partHeaders = parsePartHeaders();
181
while (partHeaders != null) {
183
String fieldName = (String)partHeaders.get("fieldName");
184
String fileName = (String)partHeaders.get("fileName");
186
if (fileName != null) {
187
// This is a file upload part
188
if (fileName.equals("")) {
189
fileName = null; // empty filename, probably an "empty" file param
192
if (fileName != null) {
193
// a filename was actually specified
194
String content = (String)partHeaders.get("content-type");
195
fileName = saveUploadFile(fileName, content);
197
uploadFiles.put(fieldName,
199
uploadDir.toString(),
206
uploadFiles.put(fieldName, new OneUpload(null, null, null));
210
// this is a parameters list part
211
byte[] valueBytes = parseFormFieldBytes();
212
String value = new String(valueBytes, fieldEncoding);
214
Vector<String> existingValues = formFields.get(fieldName);
215
if (existingValues == null) {
216
existingValues = new Vector<String>();
217
formFields.put(fieldName, existingValues);
219
existingValues.addElement(value);
223
partHeaders = parsePartHeaders();
227
private void startMultipartParse()
229
// Get the boundary string; it's included in the content type.
230
// Should look something like "------------------------12012133613061"
231
String boundary = parseBoundary(req.getContentType());
232
if (boundary == null) {
233
//String msg = localStrings.getString( "admin.server.gui.servlet.separation_boundary_not_specified" );
234
throw new IOException( "boundary is nul" );
237
this.in = req.getInput();
238
this.boundary = boundary;
240
// Read the first line, should be the first boundary
241
String line = readLine();
243
//String msg = localStrings.getString( "admin.server.gui.servlet.corrupt_form_data_premature_ending" );
244
throw new IOException( "line is null" );
247
// Verify that the line is the boundary
248
if (!line.startsWith(boundary)) {
249
//String msg = localStrings.getString( "admin.server.gui.servlet.corrupt_form_data_no_leading_boundary", line, boundary );
250
throw new IOException( "not start with boundary" );
255
* parse the headers of the individual part; they look like this:
256
* Content-Disposition: form-data; name="field1"; filename="file1.txt"
257
* Content-Type: type/subtype
258
* Content-Transfer-Encoding: binary
260
private HashMap parsePartHeaders() throws IOException {
261
HashMap<String,String> partHeaders = new HashMap<String,String>();
263
Vector<String> headers = new Vector<String>();
264
String line = readLine();
266
// No parts left, we're done
269
else if (line.length() == 0) {
270
// IE4 on Mac sends an empty line at the end; treat that as the end.
273
headers.addElement(line);
275
// Read the following header lines we hit an empty line
276
while ((line = readLine()) != null && (line.length() > 0)) {
277
headers.addElement(line);
280
// If we got a null above, it's the end
285
// default part content type (rfc1867)
286
partHeaders.put("content-type", "text/plain");
288
Enumeration ee = headers.elements();
289
while (ee.hasMoreElements()) {
290
String headerline = (String) ee.nextElement();
292
if (headerline.toLowerCase().startsWith("content-disposition:")) {
293
// Parse the content-disposition line
294
parseContentDisposition(headerline, partHeaders);
296
else if (headerline.toLowerCase().startsWith("content-type:")) {
297
// Get the content type, or null if none specified
298
parseContentType(headerline, partHeaders);
306
* parses and returns the boundary token from a line.
308
private String parseBoundary(String line) {
309
// Use lastIndexOf() because IE 4.01 on Win98 has been known to send the
310
// "boundary=" string multiple times.
311
int index = line.lastIndexOf("boundary=");
315
String boundary = line.substring(index + 9); // 9 for "boundary="
316
if (boundary.charAt(0) == '"') {
317
// The boundary is enclosed in quotes, strip them
318
index = boundary.lastIndexOf('"');
319
boundary = boundary.substring(1, index);
322
// The real boundary is always preceeded by an extra "--"
323
boundary = "--" + boundary;
329
* parses and returns content-disposition header and stores the values
330
* in the partHeaders.
332
* throws IOException if the line is malformatted.
334
private void parseContentDisposition(String line, HashMap<String,String> partHeaders)
337
// Convert the line to a lowercase string without the ending \r\n
338
// Keep the original line for error messages and for variable names.
339
String origline = line;
340
line = origline.toLowerCase();
342
// Get the content disposition, should be "form-data"
343
int start = line.indexOf("content-disposition: ");
344
int end = line.indexOf(";");
345
if (start == -1 || end == -1) {
346
//String msg = localStrings.getString( "admin.server.gui.servlet.content_disposition_corrupt", origline );
347
throw new IOException( "end reached" );
349
String disposition = line.substring(start + 21, end);
350
if (!disposition.equals("form-data")) {
351
//String msg = localStrings.getString( "admin.server.gui.servlet.invalid_content_disposition", disposition );
352
throw new IOException( "fome-data not match" );
355
// Get the field name
356
start = line.indexOf("name=\"", end); // start at last semicolon
357
end = line.indexOf("\"", start + 7); // skip name=\"
358
if (start == -1 || end == -1) {
359
//String msg = localStrings.getString( "admin.server.gui.servlet.content_disposition_corrupt", origline );
360
throw new IOException( "data corrupt" );
363
String name = origline.substring(start + 6, end);
365
// Get the fileName, if given
366
String fileName = null;
367
String origFileName = null;
368
start = line.indexOf("filename=\"", end + 2); // start after name
369
end = line.indexOf("\"", start + 10); // skip filename=\"
371
if (start != -1 && end != -1) { // note the !=
372
fileName = origline.substring(start + 10, end);
373
origFileName = fileName;
374
// The filename may contain a full path. Cut to just the filename.
376
Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
378
fileName = fileName.substring(slash + 1); // past last slash
382
// fill in the part parameters map: disposition, name, filename
383
// empty fileName denotes no file posted!
384
partHeaders.put("disposition", disposition);
385
partHeaders.put("fieldName", name);
386
partHeaders.put("fileName", fileName);
387
partHeaders.put("filePath", origFileName);
391
* parse and returns the content type from a line, or null if the
394
private void parseContentType(String line, HashMap<String,String> partHeaders)
396
String contentType = null;
398
// Convert the line to a lowercase string
399
String origline = line;
400
line = origline.toLowerCase();
402
// Get the content type, if any
403
if (line.startsWith("content-type")) {
404
int start = line.indexOf(" ");
407
//String msg = localStrings.getString( "admin.server.gui.servlet.corrupt_content_type", origline );
408
throw new IOException( "no start" );
410
contentType = line.substring(start + 1);
412
partHeaders.put("content-type", contentType);
414
else if (line.length() != 0) { // no content type, so should be empty
415
//String msg = localStrings.getString( "admin.server.gui.servlet.malformed_line_after_disposition", origline );
416
throw new IOException( "length 0" );
420
/** parse contents of a form field parameter; uses the encoding set by the user
422
private byte[] parseFormFieldBytes() throws IOException {
424
// Copy the part's contents into a byte array
425
MultipartInputStream pis = new MultipartInputStream(in, boundary);
427
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
428
byte[] buf = new byte[128];
430
while ((read = pis.read(buf)) != -1) {
431
baos.write(buf, 0, read);
436
// get the value bytes
437
return baos.toByteArray();
441
* Read the next line of input.
443
* @return a String containing the next line of input from the stream,
444
* or null to indicate the end of the stream.
445
* @exception IOException if an input or output exception has occurred.
447
private String readLine() throws IOException {
448
StringBuffer sbuf = new StringBuffer();
453
result = in.readLine(buf, 0, buf.length); // does +=
455
sbuf.append(new String(buf, 0, result, "ISO-8859-1"));
457
} while (result == buf.length); // loop only if the buffer was filled
459
if (sbuf.length() == 0) {
460
return null; // nothing read, must be at the end of stream
463
// Cut off the trailing \n or \r\n
464
// It should always be \r\n but IE5 sometimes does just \n
465
int len = sbuf.length();
466
if (len >= 2 && sbuf.charAt(len - 2) == '\r') {
467
sbuf.setLength(len - 2); // cut \r\n
470
sbuf.setLength(len - 1); // cut \n
472
return sbuf.toString();
476
* Write this file part to the specified directory.
478
private String saveUploadFile(String fileName, String content)
482
OutputStream fileOut = null;
485
// Only do something if this part contains a file
486
File file = new File(uploadDir, fileName);
487
for (int i = 0; file.exists(); i++) {
488
if (!file.exists()) {
491
file = new File(uploadDir, fileName + "." + i);
493
fileName = file.getName();
495
fileOut = new BufferedOutputStream(new FileOutputStream(file));
497
byte[] buf = new byte[8 * 1024];
499
InputStream partInput;
500
if (content.equals("x-application/gzip")) { // NOI18N
501
// sending from NetBeans UI Gestures Collector
502
partInput = in.getInputStream();
504
/** input stream containing file data */
505
partInput = new MultipartInputStream(in, boundary);
507
while((numBytes = partInput.read(buf)) != -1) {
508
fileOut.write(buf, 0, numBytes);
514
if (fileOut != null) fileOut.close();
522
* Returns the names of all the parameters as an Enumeration of
523
* Strings. It returns an empty Enumeration if there are no parameters.
526
public Enumeration getParameterNames() {
527
return formFields.keys();
531
* Returns the names of all the uploaded files as an Enumeration of
532
* Strings. It returns an empty Enumeration if there are no uploaded
533
* files. Each file name is the name specified by the form, not by
537
public Enumeration getFileNames() {
538
return uploadFiles.keys();
542
* Returns the value of the named parameter as a String, or null if
543
* the parameter was not sent or was sent without a value. The value
544
* is guaranteed to be in its normal, decoded form. If the parameter
545
* has multiple values, only the last one is returned (for backward
546
* compatibility). For parameters with multiple values, it's possible
547
* the last "value" may be null.
550
public String getParameter(String name) {
552
Vector values = (Vector)formFields.get(name);
553
if (values == null || values.size() == 0) {
556
String value = (String)values.elementAt(values.size() - 1);
559
catch (Exception e) {
565
* Returns the values of the named parameter as a String array, or null if
566
* the parameter was not sent. The array has one entry for each parameter
567
* field sent. If any field was sent without a value that entry is stored
568
* in the array as a null. The values are guaranteed to be in their
569
* normal, decoded form. A single value is returned as a one-element array.
572
public String[] getParameterValues(String name) {
574
Vector values = (Vector)formFields.get(name);
575
if (values == null || values.size() == 0) {
578
String[] valuesArray = new String[values.size()];
579
values.copyInto(valuesArray);
582
catch (Exception e) {
588
* Returns the filesystem name of the specified file, or null if the
589
* file was not included in the upload. A filesystem name is the name
590
* specified by the user. It is also the name under which the file is
594
public String getFileName(String name) {
596
OneUpload file = uploadFiles.get(name);
597
return file.getFileName(); // may be null
599
catch (Exception e) {
605
* Returns the content type of the specified file (as supplied by the
606
* client browser), or null if the file was not included in the upload.
609
public String getFileType(String name) {
611
OneUpload file = uploadFiles.get(name);
612
return file.getFileType(); // may be null
614
catch (Exception e) {
620
* Returns a File object for the specified file saved on the server's
621
* filesystem, or null if the file was not included in the upload.
624
public File getFile(String name) {
626
OneUpload file = uploadFiles.get(name);
627
return file.getFile(); // may be null
629
catch (Exception e) {
635
* close the multi-part form handler
637
public void close() throws IOException {
646
/** A class to hold information about an uploaded file. */
647
private static class OneUpload {
650
private String filename;
653
OneUpload(String dir, String filename, String type) {
655
this.filename = filename;
659
public String getFileType() {
663
public String getFileName() {
667
public File getFile() {
668
if (dir == null || filename == null) {
671
return new File(dir + File.separator + filename);
677
* providing access to a single MIME part contained with in which ends with
678
* the boundary specified. It uses buffering to provide maximum performance.
681
private static class MultipartInputStream extends FilterInputStream {
682
/** boundary which "ends" the stream */
683
private String boundary;
686
private byte [] buf = new byte[64*1024]; // 64k
688
/** number of bytes we've read into the buffer */
691
/** current position in the buffer */
694
/** flag that indicates if we have encountered the boundary */
697
/** associated facade */
698
private MultiPartHandler.InputFacade facade;
700
// i18n StringManager
701
/*private static StringManager localStrings =
702
StringManager.getManager( MultipartInputStream.class );*/
705
* Instantiate a MultipartInputStream which stops at the specified
706
* boundary from an underlying ServletInputStream.
709
MultipartInputStream(MultiPartHandler.InputFacade in,
710
String boundary) throws IOException {
711
super(in.getInputStream());
712
this.boundary = boundary;
717
* Fill up our buffer from the underlying input stream, and check for the
718
* boundary that signifies end-of-file. Users of this method must ensure
719
* that they leave exactly 2 characters in the buffer before calling this
720
* method (except the first time), so that we may only use these characters
721
* if a boundary is not found in the first line read.
723
* @exception IOException if an I/O error occurs.
725
private void fill() throws IOException
730
// as long as we are not just starting up
733
// if the caller left the requisite amount spare in the buffer
734
if (count - pos == 2) {
735
// copy it back to the start of the buffer
736
System.arraycopy(buf, pos, buf, 0, count - pos);
740
// should never happen, but just in case
741
//String msg = localStrings.getString( "admin.server.gui.servlet.fill_detected_illegal_buffer_state" );
742
throw new IllegalStateException( "should never happen" );
746
// try and fill the entire buffer, starting at count, line by line
747
// but never read so close to the end that we might split a boundary
749
int maxRead = buf.length - boundary.length();
750
while (count < maxRead) {
752
read = facade.readLine(buf, count, buf.length - count);
753
// check for eof and boundary
755
//String msg = localStrings.getString( "admin.server.gui.servlet.unexpected_end_part" );
756
throw new IOException( "read is -1" );
758
if (read >= boundary.length()) {
760
for (int i=0; i < boundary.length(); i++) {
761
if (boundary.charAt(i) != buf[count + i]) {
778
* See the general contract of the read method of InputStream.
779
* Returns -1 (end of file) when the MIME boundary of this part is encountered.
781
* throws IOException if an I/O error occurs.
783
public int read() throws IOException {
784
if (count - pos <= 2) {
786
if (count - pos <= 2) {
790
return buf[pos++] & 0xff;
794
* See the general contract of the read method of InputStream.
796
* Returns -1 (end of file) when the MIME boundary of this part
799
* throws IOException if an I/O error occurs.
801
public int read(byte b[]) throws IOException {
802
return read(b, 0, b.length);
806
* See the general contract of the read method of InputStream.
808
* Returns -1 (end of file) when the MIME boundary of this part is encountered.
810
* throws IOException if an I/O error occurs.
812
public int read(byte b[], int off, int len) throws IOException
819
int avail = count - pos - 2;
822
avail = count - pos - 2;
827
int copy = Math.min(len, avail);
828
System.arraycopy(buf, pos, b, off, copy);
832
while (total < len) {
834
avail = count - pos - 2;
838
copy = Math.min(len - total, avail);
839
System.arraycopy(buf, pos, b, off + total, copy);
847
* Returns the number of bytes that can be read from this input stream
848
* without blocking. This is a standard InputStream idiom
849
* to deal with buffering gracefully, and is not same as the length of the
850
* part arriving in this stream.
852
* throws IOException if an I/O error occurs.
854
public int available() throws IOException {
855
int avail = (count - pos - 2) + in.available();
856
// Never return a negative value
857
return (avail < 0 ? 0 : avail);
861
* Closes this input stream and releases any system resources
862
* associated with the stream. This method will read any unread data
863
* in the MIME part so that the next part starts an an expected place in
864
* the parent InputStream.
866
* throws IOException if an I/O error occurs.
868
public void close() throws IOException {
870
while (read(buf, 0, buf.length) != -1)