001package com.hfg.util.mime;
002
003import com.hfg.util.StringUtil;
004import com.hfg.util.io.InputStreamSegment;
005
006import javax.servlet.http.HttpServletRequest;
007import java.util.List;
008import java.util.ArrayList;
009import java.util.regex.Pattern;
010import java.util.regex.Matcher;
011import java.io.*;
012
013//------------------------------------------------------------------------------
014/**
015 Parser for multipart MIME (Multipurpose Internet Mail Extensions).
016 <p>
017 See <a href='http://www.ietf.org/rfc/rfc2045.txt'>[RFC2045]</a>
018 </p>
019 @author J. Alex Taylor, hairyfatguy.com
020 */
021//------------------------------------------------------------------------------
022// com.hfg XML/HTML Coding Library
023//
024// This library is free software; you can redistribute it and/or
025// modify it under the terms of the GNU Lesser General Public
026// License as published by the Free Software Foundation; either
027// version 2.1 of the License, or (at your option) any later version.
028//
029// This library is distributed in the hope that it will be useful,
030// but WITHOUT ANY WARRANTY; without even the implied warranty of
031// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
032// Lesser General Public License for more details.
033//
034// You should have received a copy of the GNU Lesser General Public
035// License along with this library; if not, write to the Free Software
036// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
037//
038// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
039// jataylor@hairyfatguy.com
040//------------------------------------------------------------------------------
041
042public class MultipartMimeParser
043{
044   //##########################################################################
045   // PRIVATE FIELDS
046   //##########################################################################
047
048   private String  mContentType;
049   private String  mBoundary;
050   private String  mLineSeparator;
051   private File    mFileCacheDir;
052   private boolean mEndIsHere = false;
053
054   private static final String CONTENT_TYPE        = "content-type:";
055   private static final String CONTENT_DISPOSITION = "content-disposition:";
056
057   private static Pattern sContentTypePattern = Pattern.compile("^\\s*(\\S+); boundary=(\\S+)$", Pattern.CASE_INSENSITIVE);
058
059   //##########################################################################
060   // PUBLIC METHODS
061   //##########################################################################
062
063   //--------------------------------------------------------------------------
064   public static boolean isMultipartMimeRequest(HttpServletRequest inRequest)
065         throws IOException
066   {
067      String contentTypeHeader = inRequest.getHeader("Content-Type");
068
069      return (contentTypeHeader != null && sContentTypePattern.matcher(contentTypeHeader).matches());
070   }
071
072   //--------------------------------------------------------------------------
073   public MultipartMimeParser setFileCacheDir(File inValue)
074   {
075      mFileCacheDir = inValue;
076      return this;
077   }
078
079   //--------------------------------------------------------------------------
080   public List<MimeEntity> parse(HttpServletRequest inRequest)
081         throws IOException
082   {
083      parseContentTypeFromHeader(inRequest);
084      return parse(inRequest.getInputStream());
085   }
086
087   //--------------------------------------------------------------------------
088   public List<MimeEntity> parse(InputStream inMimeStream)
089         throws IOException
090   {
091      return parse(inMimeStream, null);
092   }
093
094   //--------------------------------------------------------------------------
095   public List<MimeEntity> parse(InputStream inMimeStream, String inBoundary)
096         throws IOException
097   {
098      if (inBoundary != null) mBoundary = inBoundary;
099
100      List<MimeEntity> entities = new ArrayList<>();
101
102      SpecialBufferedInputStream reader = null;
103      try
104      {
105         reader = new SpecialBufferedInputStream(inMimeStream);
106
107         String line;
108
109         if (null == mBoundary)
110         {
111            do
112            {
113               line = reader.readLine();
114            }
115            while (line != null
116                  && !line.toLowerCase().startsWith(CONTENT_TYPE));
117
118            if (null == line)
119            {
120               throw new MimeException("No content-type line found!");
121            }
122
123            parseContentType(line.substring(line.indexOf(":")));
124         }
125
126         // Read to the first boundary
127         do
128         {
129            line = reader.readLine();
130         }
131         while (line != null
132                && !line.equals("--" + mBoundary));
133
134         if (null == line)
135         {
136            throw new MimeException("Problem detecting the initial MIME entity boundary!");
137         }
138
139         MimeEntity entity;
140         while ((entity = parseNextEntity(reader)) != null)
141         {
142            entities.add(entity);
143
144            line = reader.readLine();
145            if (line.equals("--")) break; // Hit the last boundary.
146         }
147      }
148      finally
149      {
150         if (reader != null) reader.close();
151      }
152
153      return entities;
154   }
155
156   //##########################################################################
157   // PRIVATE METHODS
158   //##########################################################################
159
160   //--------------------------------------------------------------------------
161   private void parseContentTypeFromHeader(HttpServletRequest inRequest)
162   {
163      parseContentType(inRequest.getHeader("Content-Type"));
164   }
165
166   //--------------------------------------------------------------------------
167   private void parseContentType(String inLine)
168   {
169      // Content-Type: multipart/form-data; boundary=---------------------------29772313742745
170
171      Matcher m = sContentTypePattern.matcher(inLine);
172
173      if (m.matches())
174      {
175         mContentType = m.group(1);
176         mBoundary    = m.group(2);
177      }
178      else
179      {
180         throw new MimeException("Failed to parse the content-type line '" + inLine + "'!");
181      }
182   }
183
184   //--------------------------------------------------------------------------
185   private MimeEntity parseNextEntity(SpecialBufferedInputStream inReader)
186         throws IOException
187   {
188      MimeEntity entity = new MimeEntity();
189
190      // Read the entity's content-disposition
191      readEntityHeader(entity, inReader);
192
193      // Read the entity's content
194      InputStreamSegment contentStream = new InputStreamSegment(inReader, getEntityEndMarker().getBytes());
195
196      if (mFileCacheDir != null
197          && entity.getContentDisposition().getFilename() != null)
198      {
199         String filename = entity.getContentDisposition().getFilename().trim();
200         File file = new File(mFileCacheDir, filename);
201         if (! mFileCacheDir.exists())
202         {
203            if (! mFileCacheDir.mkdirs())
204            {
205               throw new IOException("Couldn't create dir " + StringUtil.singleQuote(mFileCacheDir.getPath()) + " as user " + System.getProperty("user.name") + "!");
206            }
207         }
208
209         entity.setCachedContentFile(file);
210         streamSegmentToFile(contentStream, file);
211      }
212      else
213      {
214         entity.setContent(streamSegmentToString(contentStream));
215      }
216
217      return entity;
218   }
219
220   //---------------------------------------------------------------------------
221   private void readEntityHeader(MimeEntity inEntity, SpecialBufferedInputStream inReader)
222      throws IOException
223   {
224      String line;
225      while ((line = inReader.readLine()) != null
226             && line.length() > 0)
227      {
228         if (line.toLowerCase().startsWith(CONTENT_TYPE))
229         {
230            inEntity.setContentType(parseEntityContentTypeLine(line));
231         }
232         else if (line.toLowerCase().startsWith(CONTENT_DISPOSITION))
233         {
234            inEntity.setContentDisposition(parseEntityContentDispositionLine(line));
235         }
236      }
237
238      if (null == inEntity.getContentDisposition())
239      {
240         throw new MimeException("Mime entity without a Content-Disposition detected!");
241      }
242   }
243
244   //--------------------------------------------------------------------------
245   private MimeType parseEntityContentTypeLine(String inLine)
246   {
247      // Content-Type: image/png
248      MimeType contentType;
249
250      Pattern pattern = Pattern.compile("^\\s*Content-Type: (\\S+)$", Pattern.CASE_INSENSITIVE);
251
252      Matcher m = pattern.matcher(inLine);
253
254      if (m.matches())
255      {
256         contentType = MimeType.valueOf(m.group(1));
257      }
258      else
259      {
260         throw new MimeException("Failed to parse the entity content-type line '" + inLine + "'!");
261      }
262
263      return contentType;
264   }
265
266   //--------------------------------------------------------------------------
267   private MimeContentDisposition parseEntityContentDispositionLine(String inLine)
268   {
269      // Content-Disposition: form-data; name="action"
270      // Content-Disposition: form-data; name="upload_file"; filename="rss.png"
271      MimeContentDisposition contentDisposition = new MimeContentDisposition();
272
273      Pattern pattern = Pattern.compile("^\\s*Content-Disposition: (\\S+); name=\"([^\"]+)\"(?:; filename=\"([^\"]+)\")?$", Pattern.CASE_INSENSITIVE);
274
275      Matcher m = pattern.matcher(inLine);
276
277      if (m.matches())
278      {
279         contentDisposition.setType(m.group(1));
280         contentDisposition.setName(m.group(2));
281         if (m.group(3) != null)
282         {
283            contentDisposition.setFilename(m.group(3));
284         }
285      }
286      else
287      {
288         throw new MimeException("Failed to parse the entity content-disposition line '" + inLine + "'!");
289      }
290
291      return contentDisposition;
292   }
293
294   //---------------------------------------------------------------------------
295   private String getEntityEndMarker()
296   {
297      return mLineSeparator + "--" + mBoundary;
298   }
299
300   //---------------------------------------------------------------------------
301   /**
302    Reads the contents of the specified InputStreamSegment into a String.
303    */
304   private String streamSegmentToString(InputStreamSegment inStream)
305         throws IOException
306   {
307      StringBuilder buffer = new StringBuilder();
308
309      byte[] bytes = new byte[1024];
310      int bytesRead = 0;
311      while ((bytesRead = inStream.read(bytes)) != -1)
312      {
313         buffer.append(new String(bytes, 0, bytesRead));
314      }
315
316      return buffer.toString();
317   }
318
319   //---------------------------------------------------------------------------
320   /**
321    Reads the contents of the specified InputStreamSegment into a File.
322    */
323   private void streamSegmentToFile(InputStreamSegment inStream, File inFile)
324         throws IOException
325   {
326      OutputStream fileStream = null;
327      try
328      {
329         fileStream = new BufferedOutputStream(new FileOutputStream(inFile));
330         byte[] bytes = new byte[1024];
331         int bytesRead = 0;
332         while ((bytesRead = inStream.read(bytes)) != -1)
333         {
334            fileStream.write(bytes, 0, bytesRead);
335         }
336      }
337      finally
338      {
339         if (fileStream != null) fileStream.close();
340      }
341   }
342
343   //##########################################################################
344   // INNER CLASS
345   //##########################################################################
346
347   private class SpecialBufferedInputStream extends BufferedInputStream
348   {
349      //-----------------------------------------------------------------------
350      public SpecialBufferedInputStream(InputStream in)
351      {
352         super(in);
353      }
354
355      //-----------------------------------------------------------------------
356      public String readLine()
357            throws IOException
358      {
359         StringBuilder line = new StringBuilder();
360
361         if (!mEndIsHere)
362         {
363            do
364            {
365               int theChar = read();
366               if (-1 == theChar)
367               {
368                  mEndIsHere = true;
369                  break;
370               }
371
372               if (theChar != '\n' && theChar != '\r')
373               {
374                  line.append((char)theChar);
375               }
376               else
377               {
378                  if (null == mLineSeparator) mLineSeparator = (char) theChar + "";
379                  if (mLineSeparator.equals("\r\n")
380                        || (theChar == '\r' && pos < count && buf[pos] == '\n'))
381                  {
382                     read();
383                     if (mLineSeparator.length() == 1) mLineSeparator = "\r\n";
384                  }
385
386                  break;
387               }
388
389            } while (true);
390         }
391
392         return (mEndIsHere && 0 == line.length() ? null : line.toString());
393      }
394   }
395
396
397}