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 */
016package org.opengion.hayabusa.servlet.multipart;
017
018import org.opengion.fukurou.util.Closer ;
019
020import java.io.IOException;
021
022import java.util.List;
023import java.util.ArrayList;
024import java.util.Locale ;
025
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.ServletInputStream;
028
029/**
030 * ファイルアップロード時のマルチパート処理のパーサーです。
031 *
032 * @og.group その他機能
033 *
034 * @version  4.0
035 * @author   Kazuhiko Hasegawa
036 * @since    JDK5.0,
037 */
038public 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 で、0,またはマイナスで無制限
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
061                if(type1 == null && type2 != null) {
062                        type = type2;
063                }
064                else if(type2 == null && type1 != null) {
065                        type = type1;
066                }
067
068                else if(type1 != null && type2 != null) {
069                        type = (type1.length() > type2.length() ? type1 : type2);
070                }
071
072                if(type == null ||
073                                !type.toLowerCase(Locale.JAPAN).startsWith("multipart/form-data")) {
074                        throw new IOException("Posted content type isn't multipart/form-data");
075                }
076
077                int length = req.getContentLength();
078                // 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限
079                if( maxSize > 0 && length > maxSize ) {
080                        throw new IOException("登録したファイルサイズが上限(" + ( maxSize / 1024 / 1024 ) + "MB)を越えています。"
081                                                                        + " 登録ファイル=" + ( length / 1024 / 1024 ) + "MB" ); // 5.3.7.0 (2011/07/01)
082                }
083
084                // 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
085                String bound = extractBoundary(type);
086                if(bound == null) {
087                        throw new IOException("Separation boundary was not specified");
088                }
089
090                this.in = req.getInputStream();
091                this.boundary = bound;
092
093                String line = readLine();
094                if(line == null) {
095                        throw new IOException("Corrupt form data: premature ending");
096                }
097
098                if(!line.startsWith(boundary)) {
099                        throw new IOException("Corrupt form data: no leading boundary: " +
100                                                                                                                line + " != " + boundary);
101                }
102        }
103
104        /**
105         * エンコードを設定します。
106         *
107         * @param  encoding エンコード
108         */
109        public void setEncoding( final String encoding ) {
110                 this.encoding = encoding;
111         }
112
113        /**
114         * 次のパートを読み取ります。
115         *
116         * @og.rev 3.5.6.2 (2004/07/05) 文字列の連結にStringBuilderを使用します。
117         *
118         * @return      次のパート
119         * @throws IOException 入出力エラーが発生したとき
120         */
121        public Part readNextPart() throws IOException {
122                if(lastFilePart != null) {
123                        Closer.ioClose( lastFilePart.getInputStream() );                // 4.0.0 (2006/01/31) close 処理時の IOException を無視
124                        lastFilePart = null;
125                }
126
127                List<String> headers = new ArrayList<String>();
128
129                String line = readLine();
130                if(line == null) {
131                        return null;
132                }
133                else if(line.length() == 0) {
134                        return null;
135                }
136
137                while (line != null && line.length() > 0) {
138                        String nextLine = null;
139                        boolean getNextLine = true;
140                        StringBuilder buf = new StringBuilder( 100 );
141                        buf.append( line );
142                        while (getNextLine) {
143                                nextLine = readLine();
144                                if(nextLine != null
145                                                && (nextLine.startsWith(" ")
146                                                || nextLine.startsWith("\t"))) {
147                                        buf.append( nextLine );
148                                }
149                                else {
150                                        getNextLine = false;
151                                }
152                        }
153
154                        headers.add(buf.toString());
155                        line = nextLine;
156                }
157
158                if(line == null) {
159                        return null;
160                }
161
162                String name = null;
163                String filename = null;
164                String origname = null;
165                String contentType = "text/plain";
166
167                for( String headerline : headers ) {
168                        if(headerline.toLowerCase(Locale.JAPAN).startsWith("content-disposition:")) {
169                                String[] dispInfo = extractDispositionInfo(headerline);
170
171                                name = dispInfo[1];
172                                filename = dispInfo[2];
173                                origname = dispInfo[3];
174                        }
175                        else if(headerline.toLowerCase(Locale.JAPAN).startsWith("content-type:")) {
176                                String type = extractContentType(headerline);
177                                if(type != null) {
178                                        contentType = type;
179                                }
180                        }
181                }
182
183                if(filename == null) {
184                        return new ParamPart(name, in, boundary, encoding);
185                }
186                else {
187                        if( "".equals( filename ) ) {
188                                filename = null;
189                        }
190                        lastFilePart = new FilePart(name,in,boundary,contentType,filename,origname);
191                        return lastFilePart;
192                }
193        }
194
195        /**
196         * ローカル変数「境界」アクセス可能なフィールドを返します。
197         *
198         * @param       line    1行
199         *
200         * @return      境界文字列
201         * @see         org.opengion.hayabusa.servlet.multipart.MultipartParser
202         */
203        private String extractBoundary( final String line ) {
204                // 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
205                int index = line.lastIndexOf("boundary=");
206                if(index == -1) {
207                        return null;
208                }
209                String bound = line.substring(index + 9);
210                if(bound.charAt(0) == '"') {
211                        index = bound.lastIndexOf('"');
212                        bound = bound.substring(1, index);
213                }
214
215                bound = "--" + bound;
216
217                return bound;
218        }
219
220        /**
221         * コンテンツの情報を返します。
222         *
223         * @param       origline        元の行
224         *
225         * @return      コンテンツの情報配列
226         * @throws IOException 入出力エラーが発生したとき
227         */
228        private String[] extractDispositionInfo( final String origline ) throws IOException {
229                String[] retval = new String[4];
230
231                String line = origline.toLowerCase(Locale.JAPAN);
232
233                int start = line.indexOf("content-disposition: ");
234                int end = line.indexOf(';');
235                if(start == -1 || end == -1) {
236                        throw new IOException("Content disposition corrupt: " + origline);
237                }
238                String disposition = line.substring(start + 21, end);
239                if(!"form-data".equals(disposition)) {
240                        throw new IOException("Invalid content disposition: " + disposition);
241                }
242
243                start = line.indexOf("name=\"", end);   // start at last semicolon
244                end = line.indexOf("\"", start + 7);     // skip name=\"
245                if(start == -1 || end == -1) {
246                        throw new IOException("Content disposition corrupt: " + origline);
247                }
248                String name = origline.substring(start + 6, end);
249
250                String filename = null;
251                String origname = null;
252                start = line.indexOf("filename=\"", end + 2);   // start after name
253                end = line.indexOf("\"", start + 10);                                   // skip filename=\"
254                if(start != -1 && end != -1) {                                                          // note the !=
255                        filename = origline.substring(start + 10, end);
256                        origname = filename;
257                        int slash =
258                                Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
259                        if(slash > -1) {
260                                filename = filename.substring(slash + 1);       // past last slash
261                        }
262                }
263
264                retval[0] = disposition;
265                retval[1] = name;
266                retval[2] = filename;
267                retval[3] = origname;
268                return retval;
269        }
270
271        /**
272         * コンテンツタイプの情報を返します。
273         *
274         * @param       origline        元の行
275         *
276         * @return      コンテンツタイプの情報
277         * @throws IOException 入出力エラーが発生したとき
278         */
279        private String extractContentType( final String origline ) throws IOException {
280                String contentType = null;
281
282                String line = origline.toLowerCase(Locale.JAPAN);
283
284                if(line.startsWith("content-type")) {
285                        int start = line.indexOf(' ');
286                        if(start == -1) {
287                                throw new IOException("Content type corrupt: " + origline);
288                        }
289                        contentType = line.substring(start + 1);
290                }
291                else if(line.length() > 0) {    // no content type, so should be empty
292                        throw new IOException("Malformed line after disposition: " + origline);
293                }
294
295                return contentType;
296        }
297
298        /**
299         * 行を読み取ります。
300         *
301         * @return      読み取られた1行分
302         * @throws IOException 入出力エラーが発生したとき
303         */
304        private String readLine() throws IOException {
305                StringBuilder sbuf = new StringBuilder();
306                int result;
307
308                do {
309                        result = in.readLine(buf, 0, buf.length);
310                        if(result != -1) {
311                                sbuf.append(new String(buf, 0, result, encoding));
312                        }
313                } while (result == buf.length);
314
315                if(sbuf.length() == 0) {
316                        return null;
317                }
318
319                // 4.0.0 (2005/01/31) The method StringBuilder.setLength() should be avoided in favor of creating a new StringBuilder.
320                String rtn = sbuf.toString();
321                int len = sbuf.length();
322                if(len >= 2 && sbuf.charAt(len - 2) == '\r') {
323                        rtn = rtn.substring(0,len - 2);
324                }
325                else if(len >= 1 && sbuf.charAt(len - 1) == '\n') {
326                        rtn = rtn.substring(0,len - 1);
327                }
328                return rtn ;
329        }
330}