Source code: com/jcorporate/expresso/core/misc/upload/Uploader.java
1 /* ====================================================================
2 * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
3 *
4 * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 *
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in
15 * the documentation and/or other materials provided with the
16 * distribution.
17 *
18 * 3. The end-user documentation included with the redistribution,
19 * if any, must include the following acknowledgment:
20 * "This product includes software developed by Jcorporate Ltd.
21 * (http://www.jcorporate.com/)."
22 * Alternately, this acknowledgment may appear in the software itself,
23 * if and wherever such third-party acknowledgments normally appear.
24 *
25 * 4. "Jcorporate" and product names such as "Expresso" must
26 * not be used to endorse or promote products derived from this
27 * software without prior written permission. For written permission,
28 * please contact info@jcorporate.com.
29 *
30 * 5. Products derived from this software may not be called "Expresso",
31 * or other Jcorporate product names; nor may "Expresso" or other
32 * Jcorporate product names appear in their name, without prior
33 * written permission of Jcorporate Ltd.
34 *
35 * 6. No product derived from this software may compete in the same
36 * market space, i.e. framework, without prior written permission
37 * of Jcorporate Ltd. For written permission, please contact
38 * partners@jcorporate.com.
39 *
40 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
41 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
42 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
43 * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
44 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
45 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
46 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
47 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
48 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
49 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
50 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
51 * SUCH DAMAGE.
52 * ====================================================================
53 *
54 * This software consists of voluntary contributions made by many
55 * individuals on behalf of the Jcorporate Ltd. Contributions back
56 * to the project(s) are encouraged when you make modifications.
57 * Please send them to support@jcorporate.com. For more information
58 * on Jcorporate Ltd. and its products, please see
59 * <http://www.jcorporate.com/>.
60 *
61 * Portions of this software are based upon other open source
62 * products and are subject to their respective licenses.
63 */
64
65 package com.jcorporate.expresso.core.misc.upload;
66
67 import com.jcorporate.expresso.core.controller.ControllerException;
68 import com.jcorporate.expresso.core.db.DBException;
69 import com.jcorporate.expresso.core.misc.StringUtil;
70 import com.jcorporate.expresso.services.dbobj.Setup;
71 import org.apache.log4j.Logger;
72 import org.apache.struts.action.ActionMapping;
73 import org.apache.struts.action.ActionServlet;
74 import org.apache.struts.upload.MultipartRequestHandler;
75
76 import javax.servlet.ServletException;
77 import javax.servlet.http.HttpServletRequest;
78 import java.io.IOException;
79 import java.io.InputStream;
80 import java.io.OutputStream;
81 import java.util.Enumeration;
82 import java.util.Hashtable;
83
84
85 /**
86 * <p> Files will be stored in temporary disk storage
87 * <p/>
88 * <p>This implementation of {@link Uploader} handles multiple
89 * files per single html widget, sent using multipar/mixed encoding
90 * type, as specified by RFC 1867. Use {@link
91 * org.apache.turbine.util.ParameterParser#getFileItems(String)} to
92 * acquire an array of {@link
93 * org.apache.turbine.util.upload.FileItem}s associated with given
94 * html widget.
95 *
96 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
97 */
98 public class Uploader
99 implements MultipartRequestHandler {
100
101 /**
102 * A maximum lenght of a single header line that will be
103 * parsed. (1024 bytes).
104 */
105 public static final int MAX_HEADER_SIZE = 1024;
106
107 /**
108 * Stores parsed headers as key - value pairs.
109 */
110 private Hashtable headers;
111 private DefaultParameterParser myParser = null;
112 private ActionMapping myMapping = null;
113 private ActionServlet myActionServlet = null;
114 private static Logger log = Logger.getLogger(Uploader.class);
115
116 public Uploader() {
117
118 }
119
120 /**
121 * Processes an <a href="http://rf.cx/rfc1867.html">RFC
122 * 1867</a> compliant <code>multipart/form-data</code> stream.
123 *
124 * @param req The servlet request to be parsed.
125 * @param params The ParameterParser instance to insert form
126 * fields into.
127 * @param path The location where the files should be stored.
128 * @throws ControllerException If there are problems reading/parsing
129 * the request or storing files.
130 */
131 public void parseRequest(HttpServletRequest req, ParameterParser params,
132 String path)
133 throws ControllerException {
134 log.debug("Parse request begins");
135
136 String contentType = req.getHeader("Content-type");
137
138 if (!contentType.startsWith("multipart/form-data")) {
139 throw new ControllerException("Request doesn't contain multipart/form-data stream");
140 }
141
142 int requestSize = req.getContentLength();
143
144 if (requestSize == -1) {
145 throw new ControllerException("Request was rejected because it's size is unknown");
146 }
147 try {
148 byte[] boundary = contentType.substring(contentType.indexOf("boundary=") + 9).getBytes();
149 InputStream input = req.getInputStream();
150 MultipartStream multi = new MultipartStream(input, boundary);
151 boolean nextPart = multi.skipPreamble();
152
153 while (nextPart) {
154 parseHeaders(multi.readHeaders());
155
156 String fieldName = getFieldName();
157
158 if (fieldName != null) {
159 String subContentType = getHeader("Content-type");
160
161 if (subContentType != null &&
162 subContentType.startsWith("multipart/mixed")) {
163
164 // Multiple files.
165 byte[] subBoundary = subContentType.substring(subContentType.indexOf("boundary=") + 9).getBytes();
166 multi.setBoundary(subBoundary);
167
168 boolean nextSubPart = multi.skipPreamble();
169
170 while (nextSubPart) {
171 parseHeaders(multi.readHeaders());
172
173 if (getFileName() != null) {
174 FileItem item = createItem(path, requestSize,
175 true);
176 OutputStream ops = item.getOutputStream();
177 multi.readBodyData(ops);
178 ops.close();
179 params.append(getFieldName(), item);
180 } else {
181
182 // Ignore anything but files inside
183 // multipart/mixed.
184 multi.discardBodyData();
185 }
186
187 nextSubPart = multi.readBoundary();
188 }
189
190 multi.setBoundary(boundary);
191 } else {
192 if (getFileName() != null) {
193
194 // A single file.
195 FileItem item = createItem(path, requestSize, true);
196 OutputStream ops = item.getOutputStream();
197 multi.readBodyData(ops);
198 ops.close();
199 params.append(getFieldName(), item);
200 log.debug("Read a file " + getFileName());
201 } else {
202
203 // A form field.
204
205 FileItem item = createItem(path, requestSize,
206 false);
207 OutputStream ops = item.getOutputStream();
208 multi.readBodyData(ops);
209 ops.close();
210
211 String fieldData = new String(item.get());
212 params.append(getFieldName(), fieldData);
213 log.debug("Read a Field:" + getFieldName() +
214 ", value:" + fieldData);
215 }
216 }
217 } else {
218
219 // Skip this part.
220 multi.discardBodyData();
221 }
222
223 nextPart = multi.readBoundary();
224 }
225 } catch (IOException e) {
226 log.error("I/O Exception parsing upload", e);
227 throw new ControllerException("Processing of multipart/form-data request failed",
228 e);
229 }
230
231 log.debug("Finished parsing");
232 }
233
234 /**
235 * <p> Retrieves field name from 'Content-disposition' header.
236 *
237 * @return A String with the field name for the current
238 * <code>encapsulation</code>.
239 */
240 protected String getFieldName() {
241 String cd = getHeader("Content-disposition");
242
243 if (cd == null || !cd.startsWith("form-data")) {
244 return null;
245 }
246
247 int start = cd.indexOf("name=\"");
248 int end = cd.indexOf('"', start + 6);
249
250 if (start == -1 || end == -1) {
251 return null;
252 }
253
254 return cd.substring(start + 6, end);
255 }
256
257 /**
258 * <p> Retrieves file name from 'Content-disposition' header.
259 *
260 * @return A String with the file name for the current
261 * <code>encapsulation</code>.
262 */
263 protected String getFileName() {
264 String cd = getHeader("Content-disposition");
265
266 if (log.isDebugEnabled()) {
267 log.debug("Disposition says " + cd);
268 }
269 if (!cd.startsWith("form-data") && !cd.startsWith("attachment")) {
270 return null;
271 }
272
273 int start = cd.indexOf("filename=\"");
274
275 /* Find the next quote */
276 int end = cd.indexOf('"', start + 10);
277
278 if (start == -1 || end == -1 || ((start + 10) == end)) {
279 return null;
280 }
281
282 String str = cd.substring(start + 10, end).trim();
283
284 if (str.length() == 0) {
285 return null;
286 } else {
287 if (log.isDebugEnabled()) {
288 log.debug("Got a filename '" + str + "'");
289 }
290
291 return str;
292 }
293 }
294
295 /**
296 * <p> Creates a new instance of a FileItem.
297 *
298 * @param path The path for the FileItem.
299 * @param requestSize The size of the request.
300 * @return A newly created <code>FileItem</code>.
301 */
302 protected FileItem createItem(String path, int requestSize,
303 boolean storeAsFile) {
304 return FileItem.newInstance(path, getFileName(),
305 getHeader("Content-type"), requestSize,
306 storeAsFile);
307 }
308
309 /**
310 * <p> Parses the <code>header-part</code> and stores as key -
311 * value pairs.
312 * <p/>
313 * <p> If there are multiple headers of the same names, the name
314 * will map to a comma-separated list containing the values.
315 *
316 * @param headerPart The <code>header-part</code> of the current
317 * <code>encapsulation</code>.
318 */
319 protected void parseHeaders(String headerPart) {
320 if (headers == null) {
321 headers = new Hashtable();
322 } else {
323 headers.clear();
324 }
325
326 char[] buffer = new char[MAX_HEADER_SIZE];
327 boolean done = false;
328 int j = 0;
329 int i;
330 String header;
331 String headerName;
332 String headerValue;
333
334 try {
335 while (!done) {
336 i = 0;
337
338 // Copy a single line of characters into the buffer,
339 // omitting trailing CRLF.
340 while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n') {
341 buffer[i++] = headerPart.charAt(j++);
342 }
343
344 header = new String(buffer, 0, i - 2);
345
346 if (header.equals("")) {
347 done = true;
348 } else {
349 if (header.indexOf(':') == -1) {
350
351 // This header line is malformed, skip it.
352 continue;
353 }
354
355 headerName = header.substring(0, header.indexOf(':')).trim().toLowerCase();
356 headerValue = header.substring(header.indexOf(':') + 1).trim();
357
358 if (headers.get(headerName) != null) {
359
360 // More that one heder of that name exists,
361 // append to the list.
362 headers.put(headerName,
363 (String) headers.get(headerName) + "," +
364 headerValue);
365 } else {
366 headers.put(headerName, headerValue);
367 }
368 }
369 }
370 } catch (IndexOutOfBoundsException e) {
371
372 // Headers were malformed. continue with all that was
373 // parsed.
374 }
375 }
376
377 /**
378 * <p> Returns a header with specified name.
379 *
380 * @param name The name of the header to fetch.
381 * @return The value of specified header, or a comma-separated
382 * list if there were multiple headers of that name.
383 */
384 protected String getHeader(String name) {
385 return (String) headers.get(name.toLowerCase());
386 }
387 /*** Below here are the methods required by the Struts MultipartRequestHandler interface */
388 /**
389 * Convienience method to set a reference to a working
390 * ActionServlet instance.
391 */
392 public void setServlet(ActionServlet servlet) {
393 myActionServlet = servlet;
394 }
395
396 /**
397 * Convienience method to set a reference to a working
398 * ActionMapping instance.
399 */
400 public void setMapping(ActionMapping mapping) {
401 myMapping = mapping;
402 }
403
404 /**
405 * Get the ActionServlet instance
406 */
407 public ActionServlet getServlet() {
408 return myActionServlet;
409 }
410
411 /**
412 * Get the ActionMapping instance for this request
413 */
414 public ActionMapping getMapping() {
415 return myMapping;
416 }
417
418 /**
419 * After constructed, this is the first method called on
420 * by ActionServlet. Use this method for all your
421 * data-parsing of the ServletInputStream in the request
422 *
423 * @throws ServletException thrown if something goes wrong
424 */
425 public void handleRequest(HttpServletRequest request)
426 throws ServletException {
427 myParser = new DefaultParameterParser();
428
429 String tempDir = null;
430
431 try {
432 tempDir = Setup.getValueRequired("default", "TempDir");
433 } catch (DBException de) {
434 log.error(de);
435 throw new ServletException("Unable to get temp dir:" +
436 de.getMessage());
437 }
438 try {
439 if (log.isDebugEnabled()) {
440 log.debug("About to parse request - tempDir is " + tempDir);
441 log.debug("Username in request is '" +
442 StringUtil.notNull((String) request.getAttribute("UserName")));
443 }
444
445 parseRequest(request, myParser, tempDir);
446 } catch (ControllerException ce) {
447 log.error(ce);
448 throw new ServletException(ce.getMessage());
449 }
450 }
451
452 /**
453 * This method is called on to retrieve all the text
454 * input elements of the request.
455 *
456 * @return A Hashtable where the keys and values are the names and values of the request input parameters
457 */
458 public Hashtable getTextElements() {
459 Hashtable textElements = new Hashtable();
460 String oneKey = null;
461
462 for (Enumeration ee = myParser.keys(); ee.hasMoreElements();) {
463 oneKey = (String) ee.nextElement();
464
465 if (!myParser.hasFileItem(oneKey)) {
466 textElements.put(oneKey, StringUtil.notNull(myParser.get(oneKey)));
467 }
468 }
469
470 return textElements;
471 }
472
473 /**
474 * This method is called on to retrieve all the FormFile
475 * input elements of the request.
476 *
477 * @return A Hashtable where the keys are the input names of the files and the values are FormFile objects
478 * @see org.apache.struts.upload.FormFile
479 */
480 public Hashtable getFileElements() {
481 Hashtable fileElements = new Hashtable();
482 String oneKey = null;
483
484 for (Enumeration ee = myParser.keys(); ee.hasMoreElements();) {
485 oneKey = (String) ee.nextElement();
486
487 if (myParser.hasFileItem(oneKey)) {
488 fileElements.put(oneKey, myParser.getFileItem(oneKey));
489 }
490 }
491
492 return fileElements;
493 }
494
495 /**
496 * This method returns all elements of a multipart request.
497 *
498 * @return A Hashtable where the keys are input names and values are either Strings or FormFiles
499 */
500 public Hashtable getAllElements() {
501 if (myParser == null) {
502 throw new IllegalArgumentException("Parser not set");
503 }
504
505 Hashtable allElements = new Hashtable();
506 String oneKey = null;
507
508 for (Enumeration ee = myParser.keys(); ee.hasMoreElements();) {
509 oneKey = (String) ee.nextElement();
510
511 Object o = myParser.get(oneKey);
512
513 if (o != null) {
514 allElements.put(oneKey, o);
515 }
516 }
517
518 return allElements;
519 }
520
521 /**
522 * This method is called on when there's some sort of problem
523 * and the form post needs to be rolled back. Providers
524 * should remove any FormFiles used to hold information
525 * by setting them to null and also physically delete
526 * them if the implementation calls for writing directly
527 * to disk.
528 * NOTE: Currently implemented but not automatically
529 * supported, ActionForm implementors must call rollback()
530 * manually for rolling back file uploads.
531 */
532 public void rollback() {
533
534 /* Not supported with this Uloader class */
535 }
536
537 /**
538 * This method is called on when a successful form post
539 * has been made. Some implementations will use this
540 * to destroy temporary files or write to a database
541 * or something of that nature
542 */
543 public void finish() {
544
545 /* Not used here */
546 }
547
548 }