001 /* 002 * Copyright (c) 2009 The openGion Project. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the specific language 014 * governing permissions and limitations under the License. 015 */ 016 package org.opengion.hayabusa.servlet.multipart; 017 018 import org.opengion.fukurou.util.Closer ; 019 020 import java.io.IOException; 021 022 import java.util.List; 023 import java.util.ArrayList; 024 import java.util.Locale ; 025 026 import javax.servlet.http.HttpServletRequest; 027 import javax.servlet.ServletInputStream; 028 029 /** 030 * ファイルア??ロード時のマルチパート???パ?サーです? 031 * 032 * @og.group そ?他機? 033 * 034 * @version 4.0 035 * @author Kazuhiko Hasegawa 036 * @since JDK5.0, 037 */ 038 public class MultipartParser { 039 private final ServletInputStream in; 040 private final String boundary; 041 private FilePart lastFilePart; 042 private final byte[] buf = new byte[8 * 1024]; 043 private static final String DEFAULT_ENCODING = "MS932"; 044 private String encoding = DEFAULT_ENCODING; 045 046 /** 047 * マルチパート???パ?サーオブジェクトを構築する?コンストラクター 048 * 049 * @og.rev 5.3.7.0 (2011/07/01) ?容量オーバ?時?エラーメ?ージ変更 050 * @og.rev 5.5.2.6 (2012/05/25) maxSize で?,また?マイナスで無制? 051 * 052 * @param req HttpServletRequestオブジェク? 053 * @param maxSize ?容?0,また?マイナスで無制? 054 * @throws IOException 入出力エラーが発生したと? 055 */ 056 public MultipartParser( final HttpServletRequest req, final int maxSize ) throws IOException { 057 String type = null; 058 String type1 = req.getHeader("Content-Type"); 059 String type2 = req.getContentType(); 060 System.out.println(type1); 061 System.out.println(type2); 062 if(type1 == null && type2 != null) { 063 type = type2; 064 } 065 else if(type2 == null && type1 != null) { 066 type = type1; 067 } 068 069 else if(type1 != null && type2 != null) { 070 type = (type1.length() > type2.length() ? type1 : type2); 071 } 072 073 if(type == null || 074 !type.toLowerCase(Locale.JAPAN).startsWith("multipart/form-data")) { 075 throw new IOException("Posted content type isn't multipart/form-data"); 076 } 077 078 int length = req.getContentLength(); 079 // 5.5.2.6 (2012/05/25) maxSize で?,また?マイナスで無制? 080 // if(length > maxSize) { 081 if( maxSize > 0 && length > maxSize ) { 082 // throw new IOException("Posted content length of " + length + 083 // " exceeds limit of " + maxSize); 084 throw new IOException("登録したファイルサイズが上限(" + ( maxSize / 1024 / 1024 ) + "MB)を越えて?す?" 085 + " 登録ファイル=" + ( length / 1024 / 1024 ) + "MB" ); // 5.3.7.0 (2011/07/01) 086 } 087 088 // 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser 089 String bound = extractBoundary(type); 090 if(bound == null) { 091 throw new IOException("Separation boundary was not specified"); 092 } 093 094 this.in = req.getInputStream(); 095 this.boundary = bound; 096 097 String line = readLine(); 098 if(line == null) { 099 throw new IOException("Corrupt form data: premature ending"); 100 } 101 102 if(!line.startsWith(boundary)) { 103 throw new IOException("Corrupt form data: no leading boundary: " + 104 line + " != " + boundary); 105 } 106 } 107 108 /** 109 * エンコードを設定します? 110 * 111 * @param encoding エンコー? 112 */ 113 public void setEncoding( final String encoding ) { 114 this.encoding = encoding; 115 } 116 117 /** 118 * 次のパ?トを読み取ります? 119 * 120 * @og.rev 3.5.6.2 (2004/07/05) ??の連結にStringBuilderを使用します? 121 * 122 * @return 次のパ?? 123 * @throws IOException 入出力エラーが発生したと? 124 */ 125 public Part readNextPart() throws IOException { 126 if(lastFilePart != null) { 127 Closer.ioClose( lastFilePart.getInputStream() ); // 4.0.0 (2006/01/31) close 処?の IOException を無? 128 lastFilePart = null; 129 } 130 131 List<String> headers = new ArrayList<String>(); 132 133 String line = readLine(); 134 if(line == null) { 135 return null; 136 } 137 else if(line.length() == 0) { 138 return null; 139 } 140 141 while (line != null && line.length() > 0) { 142 String nextLine = null; 143 boolean getNextLine = true; 144 StringBuilder buf = new StringBuilder( 100 ); 145 buf.append( line ); 146 while (getNextLine) { 147 nextLine = readLine(); 148 if(nextLine != null 149 && (nextLine.startsWith(" ") 150 || nextLine.startsWith("\t"))) { 151 buf.append( nextLine ); 152 } 153 else { 154 getNextLine = false; 155 } 156 } 157 158 headers.add(buf.toString()); 159 line = nextLine; 160 } 161 162 if(line == null) { 163 return null; 164 } 165 166 String name = null; 167 String filename = null; 168 String origname = null; 169 String contentType = "text/plain"; 170 171 for( String headerline : headers ) { 172 if(headerline.toLowerCase(Locale.JAPAN).startsWith("content-disposition:")) { 173 String[] dispInfo = extractDispositionInfo(headerline); 174 175 name = dispInfo[1]; 176 filename = dispInfo[2]; 177 origname = dispInfo[3]; 178 } 179 else if(headerline.toLowerCase(Locale.JAPAN).startsWith("content-type:")) { 180 String type = extractContentType(headerline); 181 if(type != null) { 182 contentType = type; 183 } 184 } 185 } 186 187 if(filename == null) { 188 return new ParamPart(name, in, boundary, encoding); 189 } 190 else { 191 if( "".equals( filename ) ) { 192 filename = null; 193 } 194 lastFilePart = new FilePart(name,in,boundary,contentType,filename,origname); 195 return lastFilePart; 196 } 197 } 198 199 /** 200 * ローカル変数「?」アクセス可能なフィールドを返します? 201 * 202 * @param line ?? 203 * 204 * @return ??? 205 * @see org.opengion.hayabusa.servlet.multipart.MultipartParser 206 */ 207 private String extractBoundary( final String line ) { 208 // 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser 209 int index = line.lastIndexOf("boundary="); 210 if(index == -1) { 211 return null; 212 } 213 String bound = line.substring(index + 9); 214 if(bound.charAt(0) == '"') { 215 index = bound.lastIndexOf('"'); 216 bound = bound.substring(1, index); 217 } 218 219 bound = "--" + bound; 220 221 return bound; 222 } 223 224 /** 225 * コン?????を返します? 226 * 227 * @param origline ??? 228 * 229 * @return コン?????配? 230 * @throws IOException 入出力エラーが発生したと? 231 */ 232 private String[] extractDispositionInfo( final String origline ) throws IOException { 233 String[] retval = new String[4]; 234 235 String line = origline.toLowerCase(Locale.JAPAN); 236 237 int start = line.indexOf("content-disposition: "); 238 int end = line.indexOf(';'); 239 if(start == -1 || end == -1) { 240 throw new IOException("Content disposition corrupt: " + origline); 241 } 242 String disposition = line.substring(start + 21, end); 243 if(!"form-data".equals(disposition)) { 244 throw new IOException("Invalid content disposition: " + disposition); 245 } 246 247 start = line.indexOf("name=\"", end); // start at last semicolon 248 end = line.indexOf("\"", start + 7); // skip name=\" 249 if(start == -1 || end == -1) { 250 throw new IOException("Content disposition corrupt: " + origline); 251 } 252 String name = origline.substring(start + 6, end); 253 254 String filename = null; 255 String origname = null; 256 start = line.indexOf("filename=\"", end + 2); // start after name 257 end = line.indexOf("\"", start + 10); // skip filename=\" 258 if(start != -1 && end != -1) { // note the != 259 filename = origline.substring(start + 10, end); 260 origname = filename; 261 int slash = 262 Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); 263 if(slash > -1) { 264 filename = filename.substring(slash + 1); // past last slash 265 } 266 } 267 268 retval[0] = disposition; 269 retval[1] = name; 270 retval[2] = filename; 271 retval[3] = origname; 272 return retval; 273 } 274 275 /** 276 * コン??イプ???を返します? 277 * 278 * @param origline ??? 279 * 280 * @return コン??イプ??? 281 * @throws IOException 入出力エラーが発生したと? 282 */ 283 private String extractContentType( final String origline ) throws IOException { 284 String contentType = null; 285 286 String line = origline.toLowerCase(Locale.JAPAN); 287 288 if(line.startsWith("content-type")) { 289 int start = line.indexOf(' '); 290 if(start == -1) { 291 throw new IOException("Content type corrupt: " + origline); 292 } 293 contentType = line.substring(start + 1); 294 } 295 else if(line.length() > 0) { // no content type, so should be empty 296 throw new IOException("Malformed line after disposition: " + origline); 297 } 298 299 return contentType; 300 } 301 302 /** 303 * 行を読み取ります? 304 * 305 * @return 読み取られた?行? 306 * @throws IOException 入出力エラーが発生したと? 307 */ 308 private String readLine() throws IOException { 309 StringBuilder sbuf = new StringBuilder(); 310 int result; 311 312 do { 313 result = in.readLine(buf, 0, buf.length); 314 if(result != -1) { 315 sbuf.append(new String(buf, 0, result, encoding)); 316 } 317 } while (result == buf.length); 318 319 if(sbuf.length() == 0) { 320 return null; 321 } 322 323 // 4.0.0 (2005/01/31) The method StringBuilder.setLength() should be avoided in favor of creating a new StringBuilder. 324 String rtn = sbuf.toString(); 325 int len = sbuf.length(); 326 if(len >= 2 && sbuf.charAt(len - 2) == '\r') { 327 rtn = rtn.substring(0,len - 2); 328 } 329 else if(len >= 1 && sbuf.charAt(len - 1) == '\n') { 330 rtn = rtn.substring(0,len - 1); 331 } 332 return rtn ; 333 } 334 }