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