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    }