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 }