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}