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