View Javadoc

1   // ========================================================================
2   // Copyright 1996-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  package org.mortbay.servlet;
15  
16  import java.io.BufferedInputStream;
17  import java.io.BufferedOutputStream;
18  import java.io.ByteArrayOutputStream;
19  import java.io.File;
20  import java.io.FileOutputStream;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.io.UnsupportedEncodingException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.StringTokenizer;
31  
32  import javax.servlet.Filter;
33  import javax.servlet.FilterChain;
34  import javax.servlet.FilterConfig;
35  import javax.servlet.ServletContext;
36  import javax.servlet.ServletException;
37  import javax.servlet.ServletRequest;
38  import javax.servlet.ServletResponse;
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpServletRequestWrapper;
41  
42  import org.mortbay.util.LazyList;
43  import org.mortbay.util.MultiMap;
44  import org.mortbay.util.StringUtil;
45  import org.mortbay.util.TypeUtil;
46  
47  /* ------------------------------------------------------------ */
48  /**
49   * Multipart Form Data Filter.
50   * <p>
51   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
52   * item.  Any files sent are stored to a tempary file and a File object added to the request 
53   * as an attribute.  All other values are made available via the normal getParameter API and
54   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
55   * 
56   * If the init paramter "delete" is set to "true", any files created will be deleted when the
57   * current request returns.
58   * 
59   * @author Greg Wilkins
60   * @author Jim Crossley
61   */
62  public class MultiPartFilter implements Filter
63  {
64      private final static String FILES ="org.mortbay.servlet.MultiPartFilter.files";
65      private File tempdir;
66      private boolean _deleteFiles;
67      private ServletContext _context;
68      private int _fileOutputBuffer = 0;
69  
70      /* ------------------------------------------------------------------------------- */
71      /**
72       * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
73       */
74      public void init(FilterConfig filterConfig) throws ServletException
75      {
76          tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
77          _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
78          String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
79          if(fileOutputBuffer!=null)
80              _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
81          _context=filterConfig.getServletContext();
82      }
83  
84      /* ------------------------------------------------------------------------------- */
85      /**
86       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
87       *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
88       */
89      public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) 
90          throws IOException, ServletException
91      {
92          HttpServletRequest srequest=(HttpServletRequest)request;
93          if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
94          {
95              chain.doFilter(request,response);
96              return;
97          }
98          
99          BufferedInputStream in = new BufferedInputStream(request.getInputStream());
100         String content_type=srequest.getContentType();
101         
102         // TODO - handle encodings
103         
104         String boundary="--"+value(content_type.substring(content_type.indexOf("boundary=")));
105         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
106         
107         MultiMap params = new MultiMap();
108         for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
109         {
110             Map.Entry entry=(Map.Entry)i.next();
111             Object value=entry.getValue();
112             if (value instanceof String[])
113                 params.addValues(entry.getKey(),(String[])value);
114             else
115                 params.add(entry.getKey(),value);
116         }
117         
118         try
119         {
120             // Get first boundary
121             byte[] bytes=TypeUtil.readLine(in);
122             String line=bytes==null?null:new String(bytes,"UTF-8");
123             if(line==null || !line.equals(boundary))
124             {
125                 throw new IOException("Missing initial multi part boundary");
126             }
127             
128             // Read each part
129             boolean lastPart=false;
130             String content_disposition=null;
131             while(!lastPart)
132             {
133                 while(true)
134                 {
135                     bytes=TypeUtil.readLine(in);
136                     // If blank line, end of part headers
137                     if(bytes==null || bytes.length==0)
138                         break;
139                     line=new String(bytes,"UTF-8");
140                     
141                     // place part header key and value in map
142                     int c=line.indexOf(':',0);
143                     if(c>0)
144                     {
145                         String key=line.substring(0,c).trim().toLowerCase();
146                         String value=line.substring(c+1,line.length()).trim();
147                         if(key.equals("content-disposition"))
148                             content_disposition=value;
149                     }
150                 }
151                 // Extract content-disposition
152                 boolean form_data=false;
153                 if(content_disposition==null)
154                 {
155                     throw new IOException("Missing content-disposition");
156                 }
157                 
158                 StringTokenizer tok=new StringTokenizer(content_disposition,";");
159                 String name=null;
160                 String filename=null;
161                 while(tok.hasMoreTokens())
162                 {
163                     String t=tok.nextToken().trim();
164                     String tl=t.toLowerCase();
165                     if(t.startsWith("form-data"))
166                         form_data=true;
167                     else if(tl.startsWith("name="))
168                         name=value(t);
169                     else if(tl.startsWith("filename="))
170                         filename=value(t);
171                 }
172                 
173                 // Check disposition
174                 if(!form_data)
175                 {
176                     continue;
177                 }
178                 
179                 //It is valid for reset and submit buttons to have an empty name.
180                 //If no name is supplied, the browser skips sending the info for that field.
181                 //However, if you supply the empty string as the name, the browser sends the
182                 //field, with name as the empty string. So, only continue this loop if we
183                 //have not yet seen a name field.
184                 if(name==null)
185                 {
186                     continue;
187                 }
188                 
189                 OutputStream out=null;
190                 File file=null;
191                 try
192                 {
193                     if (filename!=null && filename.length()>0)
194                     {
195                         file = File.createTempFile("MultiPart", "", tempdir);
196                         out = new FileOutputStream(file);
197                         if(_fileOutputBuffer>0)
198                             out = new BufferedOutputStream(out, _fileOutputBuffer);
199                         request.setAttribute(name,file);
200                         params.add(name, filename);
201                         
202                         if (_deleteFiles)
203                         {
204                             file.deleteOnExit();
205                             ArrayList files = (ArrayList)request.getAttribute(FILES);
206                             if (files==null)
207                             {
208                                 files=new ArrayList();
209                                 request.setAttribute(FILES,files);
210                             }
211                             files.add(file);
212                         }
213                         
214                     }
215                     else
216                         out=new ByteArrayOutputStream();
217                     
218                     int state=-2;
219                     int c;
220                     boolean cr=false;
221                     boolean lf=false;
222                     
223                     // loop for all lines`
224                     while(true)
225                     {
226                         int b=0;
227                         while((c=(state!=-2)?state:in.read())!=-1)
228                         {
229                             state=-2;
230                             // look for CR and/or LF
231                             if(c==13||c==10)
232                             {
233                                 if(c==13)
234                                     state=in.read();
235                                 break;
236                             }
237                             // look for boundary
238                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
239                                 b++;
240                             else
241                             {
242                                 // this is not a boundary
243                                 if(cr)
244                                     out.write(13);
245                                 if(lf)
246                                     out.write(10);
247                                 cr=lf=false;
248                                 if(b>0)
249                                     out.write(byteBoundary,0,b);
250                                 b=-1;
251                                 out.write(c);
252                             }
253                         }
254                         // check partial boundary
255                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
256                         {
257                             if(cr)
258                                 out.write(13);
259                             if(lf)
260                                 out.write(10);
261                             cr=lf=false;
262                             out.write(byteBoundary,0,b);
263                             b=-1;
264                         }
265                         // boundary match
266                         if(b>0||c==-1)
267                         {
268                             if(b==byteBoundary.length)
269                                 lastPart=true;
270                             if(state==10)
271                                 state=-2;
272                             break;
273                         }
274                         // handle CR LF
275                         if(cr)
276                             out.write(13);
277                         if(lf)
278                             out.write(10);
279                         cr=(c==13);
280                         lf=(c==10||state==10);
281                         if(state==10)
282                             state=-2;
283                     }
284                 }
285                 finally
286                 {
287                     out.close();
288                 }
289                 
290                 if (file==null)
291                 {
292                     bytes = ((ByteArrayOutputStream)out).toByteArray();
293                     params.add(name,bytes);
294                 }
295             }
296         
297             // handle request
298             chain.doFilter(new Wrapper(srequest,params),response);
299         }
300         finally
301         {
302             deleteFiles(request);
303         }
304     }
305 
306     private void deleteFiles(ServletRequest request)
307     {
308         ArrayList files = (ArrayList)request.getAttribute(FILES);
309         if (files!=null)
310         {
311             Iterator iter = files.iterator();
312             while (iter.hasNext())
313             {
314                 File file=(File)iter.next();
315                 try
316                 {
317                     file.delete();
318                 }
319                 catch(Exception e)
320                 {
321                     _context.log("failed to delete "+file,e);
322                 }
323             }
324         }
325     }
326     /* ------------------------------------------------------------ */
327     private String value(String nameEqualsValue)
328     {
329         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
330         int i=value.indexOf(';');
331         if(i>0)
332             value=value.substring(0,i);
333         if(value.startsWith("\""))
334         {
335             value=value.substring(1,value.indexOf('"',1));
336         }
337         else
338         {
339             i=value.indexOf(' ');
340             if(i>0)
341                 value=value.substring(0,i);
342         }
343         return value;
344     }
345 
346     /* ------------------------------------------------------------------------------- */
347     /**
348      * @see javax.servlet.Filter#destroy()
349      */
350     public void destroy()
351     {
352     }
353     
354     private static class Wrapper extends HttpServletRequestWrapper
355     {
356         String encoding="UTF-8";
357         MultiMap map;
358         
359         /* ------------------------------------------------------------------------------- */
360         /** Constructor.
361          * @param request
362          */
363         public Wrapper(HttpServletRequest request, MultiMap map)
364         {
365             super(request);
366             this.map=map;
367         }
368         
369         /* ------------------------------------------------------------------------------- */
370         /**
371          * @see javax.servlet.ServletRequest#getContentLength()
372          */
373         public int getContentLength()
374         {
375             return 0;
376         }
377         
378         /* ------------------------------------------------------------------------------- */
379         /**
380          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
381          */
382         public String getParameter(String name)
383         {
384             Object o=map.get(name);
385             if (!(o instanceof byte[]) && LazyList.size(o)>0)
386                 o=LazyList.get(o,0);
387             
388             if (o instanceof byte[])
389             {
390                 try
391                 {
392                     String s=new String((byte[])o,encoding);
393                     return s;
394                 }
395                 catch(Exception e)
396                 {
397                     e.printStackTrace();
398                 }
399             }
400             else if (o!=null)
401                 return String.valueOf(o);
402             return null;
403         }
404         
405         /* ------------------------------------------------------------------------------- */
406         /**
407          * @see javax.servlet.ServletRequest#getParameterMap()
408          */
409         public Map getParameterMap()
410         {
411             return Collections.unmodifiableMap(map.toStringArrayMap());
412         }
413         
414         /* ------------------------------------------------------------------------------- */
415         /**
416          * @see javax.servlet.ServletRequest#getParameterNames()
417          */
418         public Enumeration getParameterNames()
419         {
420             return Collections.enumeration(map.keySet());
421         }
422         
423         /* ------------------------------------------------------------------------------- */
424         /**
425          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
426          */
427         public String[] getParameterValues(String name)
428         {
429             List l=map.getValues(name);
430             if (l==null || l.size()==0)
431                 return new String[0];
432             String[] v = new String[l.size()];
433             for (int i=0;i<l.size();i++)
434             {
435                 Object o=l.get(i);
436                 if (o instanceof byte[])
437                 {
438                     try
439                     {
440                         v[i]=new String((byte[])o,encoding);
441                     }
442                     catch(Exception e)
443                     {
444                         e.printStackTrace();
445                     }
446                 }
447                 else if (o instanceof String)
448                     v[i]=(String)o;
449             }
450             return v;
451         }
452         
453         /* ------------------------------------------------------------------------------- */
454         /**
455          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
456          */
457         public void setCharacterEncoding(String enc) 
458             throws UnsupportedEncodingException
459         {
460             encoding=enc;
461         }
462     }
463 }