001    // ========================================================================
002    // Copyright (c) 1996 Mort Bay Consulting Pty. Ltd. All rights reserved.
003    // $Id: MultiPartRequest.java,v 1.6 2004/03/29 12:52:09 laurent Exp $
004    // ------------------------------------------------------------------------
005    
006    package org.objectweb.jac.aspects.gui.web;
007    
008    import java.io.ByteArrayInputStream;
009    import java.io.ByteArrayOutputStream;
010    import java.io.IOException;
011    import java.io.InputStream;
012    import java.io.UnsupportedEncodingException;
013    import java.util.Hashtable;
014    import java.util.List;
015    import java.util.StringTokenizer;
016    import javax.servlet.http.HttpServletRequest;
017    import org.apache.log4j.Logger;
018    import org.mortbay.http.HttpFields;
019    import org.mortbay.util.Code;
020    import org.mortbay.util.LineInput;
021    import org.mortbay.util.MultiMap;
022    import org.mortbay.util.StringUtil;
023    import org.objectweb.jac.util.ExtArrays;
024    
025    /* ------------------------------------------------------------ */
026    /** Multipart Form Data request.
027     * <p>
028     * This class decodes the multipart/form-data stream sent by
029     * a HTML form that uses a file input item.
030     *
031     * <p><h4>Usage</h4>
032     * Each part of the form data is named from the HTML form and
033     * is available either via getString(name) or getInputStream(name).
034     * Furthermore the MIME parameters and filename can be requested for
035     * each part.
036     * <pre>
037     * </pre>
038     *
039     * @version $Id: MultiPartRequest.java,v 1.6 2004/03/29 12:52:09 laurent Exp $
040     * @author  Greg Wilkins
041     * @author  Jim Crossley
042     */
043    public class MultiPartRequest
044    {
045        static Logger logger = Logger.getLogger("web.servlet");
046    
047        /* ------------------------------------------------------------ */
048        HttpServletRequest _request;
049        LineInput _in;
050        String _boundary;
051        byte[] _byteBoundary;
052        MultiMap _partMap = new MultiMap(10);
053        int _char = -2;
054        boolean _lastPart = false;
055        String _enc = StringUtil.__ISO_8859_1;
056    
057        /* ------------------------------------------------------------ */
058        /** Constructor. 
059         * @param request The request containing a multipart/form-data
060         * request
061         * @param enc the encoding of characters
062         * @exception IOException IOException
063         */
064        public MultiPartRequest(HttpServletRequest request, String enc)
065            throws IOException
066        {
067            _request=request;
068            _enc = enc;
069            String content_type = request.getHeader(HttpFields.__ContentType);
070            if (!content_type.startsWith("multipart/form-data"))
071                throw new IOException("Not multipart/form-data request");
072    
073            Code.debug("Multipart content type = ",content_type);
074            
075            _in = new LineInput(request.getInputStream());
076            
077            // Extract boundary string
078            _boundary="--"+
079                value(content_type.substring(content_type.indexOf("boundary=")));
080            
081            Code.debug("Boundary=",_boundary);
082            _byteBoundary= (_boundary+"--").getBytes(StringUtil.__ISO_8859_1);
083            
084            loadAllParts();
085        }
086    
087        protected Part getPart(String name) {
088           Object part = _partMap.get(name);
089           if (part instanceof List)
090              return (Part)((List)part).get(0);
091           else
092              return (Part)part;
093        }
094    
095        /* ------------------------------------------------------------ */
096        /** Get the part names.
097         * @return an array of part names
098         */
099        public String[] getPartNames()
100        {
101            return (String[]) _partMap.keySet().toArray(ExtArrays.emptyStringArray);
102        }
103        
104        /* ------------------------------------------------------------ */
105        /** Check if a named part is present 
106         * @param name The part
107         * @return true if it was included 
108         */
109        public boolean contains(String name)
110        {
111            Part part = (Part)_partMap.get(name);
112            return (part!=null);
113        }
114        
115        /* ------------------------------------------------------------ */
116        /** Get the data of a part as a string.
117         * @param name The part name 
118         * @return The part data
119         */
120        public String getString(String name)
121        {
122            List part = (List)_partMap.getValues(name);
123            if (part==null)
124                return null;
125            return decodeString(((Part)part.get(0))._data);
126        }
127        
128        /* ------------------------------------------------------------ */
129        /** 
130         * @param name The part name 
131         * @return The parts data
132         */
133        public String[] getStrings(String name)
134        {
135            List parts = (List)_partMap.getValues(name);
136            if (parts==null)
137                return null;
138            String[] strings = new String[parts.size()];
139            for (int i=0; i<strings.length; i++) {
140                strings[i] = decodeString(((Part)parts.get(i))._data);
141            }
142            return strings;
143        }
144    
145        protected String decodeString(byte[] bytes) {
146            try {
147                return new String(bytes,_enc);
148            } catch (UnsupportedEncodingException e) {
149                logger.error("Unsupported encoding: "+_enc);
150                return new String(bytes);
151            }        
152        }
153        
154        /* ------------------------------------------------------------ */
155        /** Get the data of a part as a stream.
156         * @param name The part name 
157         * @return Stream providing the part data
158         */
159        public InputStream getInputStream(String name)
160        {
161            Part part = getPart(name);
162            if (part==null)
163                return null;
164            else 
165               return new ByteArrayInputStream(part._data);
166        }
167    
168        public InputStream[] getInputStreams(String name) 
169        {
170            List parts = (List)_partMap.getValues(name);
171            if (parts==null)
172                return null;
173            InputStream[] streams = new InputStream[parts.size()];
174            for (int i=0; i<streams.length; i++) {
175                streams[i] = new ByteArrayInputStream(((Part)parts.get(i))._data);
176            }
177            return streams;
178        }
179    
180        /* ------------------------------------------------------------ */
181        /** Get the MIME parameters associated with a part.
182         * @param name The part name 
183         * @return Hashtable of parameters
184         */
185        public Hashtable getParams(String name)
186        {
187            List part = (List)_partMap.getValues(name);
188            if (part==null)
189                return null;
190            return ((Part)part.get(0))._headers;
191        }
192    
193        public Hashtable[] getMultipleParams(String name) 
194        {
195            List parts = (List)_partMap.getValues(name);
196            if (parts==null)
197                return null;
198            Hashtable[] params = new Hashtable[parts.size()];
199            for (int i=0; i<params.length; i++) {
200                params[i] = ((Part)parts.get(i))._headers;
201            }
202            return params;
203        }
204    
205        /* ------------------------------------------------------------ */
206        /** Get any file name associated with a part.
207         * @param name The part name 
208         * @return The filename
209         */
210        public String getFilename(String name)
211        {
212            List part = (List)_partMap.getValues(name);
213            if (part==null)
214                return null;
215            return ((Part)part.get(0))._filename;
216        }
217    
218        public String[] getFilenames(String name) 
219        {
220            List parts = (List)_partMap.getValues(name);
221            if (parts==null)
222                return null;
223            String[] filenames = new String[parts.size()];
224            for (int i=0; i<filenames.length; i++) {
225                filenames[i] = ((Part)parts.get(i))._filename;
226            }
227            return filenames;
228        }
229    
230        /* ------------------------------------------------------------ */
231        private void loadAllParts()
232            throws IOException
233        {
234            // Get first boundary
235            String line = _in.readLine();
236            if (!line.equals(_boundary))
237            {
238                Code.warning(line);
239                throw new IOException("Missing initial multi part boundary");
240            }
241            
242            // Read each part
243            while (!_lastPart)
244            {
245                // Read Part headers
246                Part part = new Part();
247                
248                String content_disposition=null;
249                while ((line=_in.readLine())!=null)
250                {
251                    // If blank line, end of part headers
252                    if (line.length()==0)
253                        break;
254    
255                    Code.debug("LINE=",line);
256                    
257                    // place part header key and value in map
258                    int c = line.indexOf(':',0);
259                    if (c>0)
260                    {
261                        String key = line.substring(0,c).trim().toLowerCase();
262                        String value = line.substring(c+1,line.length()).trim();
263                        String ev = (String) part._headers.get(key);
264                        part._headers.put(key,(ev!=null)?(ev+';'+value):value);
265                        Code.debug(key,": ",value);
266                        if (key.equals("content-disposition"))
267                            content_disposition=value;
268                    }
269                }
270    
271                // Extract content-disposition
272                boolean form_data=false;
273                if (content_disposition==null)
274                {
275                    throw new IOException("Missing content-disposition");
276                }
277                
278                StringTokenizer tok =
279                    new StringTokenizer(content_disposition,";");
280                while (tok.hasMoreTokens())
281                {
282                    String t = tok.nextToken().trim();
283                    String tl = t.toLowerCase();
284                    if (t.startsWith("form-data"))
285                        form_data=true;
286                    else if (tl.startsWith("name="))
287                        part._name=value(t);
288                    else if (tl.startsWith("filename="))
289                        part._filename=value(t);
290                }
291    
292                // Check disposition
293                if (!form_data)
294                {
295                    Code.warning("Non form-data part in multipart/form-data");
296                    continue;
297                }
298                if (part._name==null || part._name.length()==0)
299                {
300                    Code.warning("Part with no name in multipart/form-data");
301                    continue;
302                }
303                Code.debug("name=",part._name);
304                Code.debug("filename=",part._filename);
305                _partMap.add(part._name,part);
306                part._data=readBytes();
307            }       
308        }
309    
310        /* ------------------------------------------------------------ */
311        private byte[] readBytes()
312            throws IOException
313        {
314            ByteArrayOutputStream baos = new ByteArrayOutputStream();
315    
316            int c;
317            boolean cr=false;
318            boolean lf=false;
319            
320            // loop for all lines`
321            while (true)
322            {
323                int b=0;
324                while ((c=(_char!=-2)?_char:_in.read())!=-1)
325                {
326                    _char=-2;
327    
328                    // look for CR and/or LF
329                    if (c==13 || c==10)
330                    {
331                        if (c==13) _char=_in.read();
332                        break;
333                    }
334    
335                    // look for boundary
336                    if (b>=0 && b<_byteBoundary.length && c==_byteBoundary[b])
337                        b++;
338                    else
339                    {
340                        // this is not a boundary
341                        if (cr) baos.write(13);
342                        if (lf) baos.write(10);
343                        cr=lf=false;
344                        
345                        if (b>0)
346                            baos.write(_byteBoundary,0,b);
347                        b=-1;
348                      
349                        baos.write(c);
350                    }
351                }
352    
353                // check partial boundary
354                if ((b>0 && b<_byteBoundary.length-2) ||
355                    (b==_byteBoundary.length-1))
356                {
357                    if (cr) baos.write(13);
358                    if (lf) baos.write(10);
359                    cr=lf=false;
360                    baos.write(_byteBoundary,0,b);
361                    b=-1;
362                }
363                
364                // boundary match
365                if (b>0 || c==-1)
366                {
367                    if (b==_byteBoundary.length)
368                        _lastPart=true;
369                    if (_char==10) _char=-2;
370                    break;
371                }
372                
373                // handle CR LF
374                if (cr) baos.write(13);
375                if (lf) baos.write(10);
376                cr=(c==13);
377                lf=(c==10 || _char==10);
378                if (_char==10) _char=-2;  
379            }
380            if (Code.verbose()) Code.debug(baos.toString());
381            return baos.toByteArray();
382        }
383        
384        
385        /* ------------------------------------------------------------ */
386        private String value(String nameEqualsValue)
387        {   
388            String value =
389                nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
390            
391            int i=value.indexOf(';');
392            if (i>0)
393                value=value.substring(0,i);
394            if (value.startsWith("\""))
395            {
396                value=value.substring(1,value.indexOf('"',1));
397            }
398            
399            else
400            {
401                i=value.indexOf(' ');
402                if (i>0)
403                    value=value.substring(0,i);
404            }
405            return value;
406        }
407        
408        /* ------------------------------------------------------------ */
409        private class Part
410        {
411            String _name=null;
412            String _filename=null;
413            Hashtable _headers= new Hashtable(10);
414            byte[] _data=null;
415        }    
416    };