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; 017 018import org.opengion.hayabusa.common.HybsSystem; 019import org.opengion.hayabusa.servlet.multipart.MultipartParser; 020import org.opengion.hayabusa.servlet.multipart.Part; 021import org.opengion.hayabusa.servlet.multipart.FilePart; 022import org.opengion.hayabusa.servlet.multipart.ParamPart; 023import org.opengion.fukurou.util.ZipArchive; // 6.0.0.0 (2014/04/11) zip 対応 024 025import java.io.File; 026import java.io.IOException; 027import java.util.Map; 028import java.util.TreeMap; 029import java.util.List; 030import java.util.ArrayList; 031import java.util.Set; 032import java.util.Random ; 033import java.util.concurrent.atomic.AtomicInteger; // 5.5.2.6 (2012/05/25) findbugs対応 034import javax.servlet.http.HttpServletRequest; 035 036/** 037 * ファイルをサーバーにアップロードする場合に使用されるマルチパート処理サーブレットです。 038 * 039 * 通常のファイルアップロード時の、form で使用する、enctype="multipart/form-data" 040 * を指定した場合の、他のリクエスト情報も、取り出すことが可能です。 041 * 042 * ファイルをアップロード後に、指定のファイル名に変更する機能があります。 043 * file 登録ダイアログで指定した name に、"_NEW" という名称を付けたリクエスト値を 044 * ファイルのアップロードと同時に送信することで、この名前にファイルを付け替えます。 045 * また、アップロード後のファイル名は、name 指定の名称で、取り出せます。 046 * クライアントから登録したオリジナルのファイル名は、name に、"_ORG" という名称 047 * で取り出すことが可能です。 048 * 049 * maxPostSize : 最大転送サイズ(Byte)を指定します。 0,またはマイナスで無制限です。 050 * useBackup : ファイルアップロード時に、すでに同名のファイルが存在した場合に、 051 * バックアップ処理(renameTo)するかどうか[true/false]を指定します(初期値:false) 052 * 053 * ファイルアップロード時に、アップロード先に、同名のファイルが存在した場合は、既存機能は、そのまま 054 * 置き換えていましたが、簡易バージョンアップ機能として、useBackup="true" を指定すると、既存のファイルを 055 * リネームして、バックアップファイルを作成します。 056 * バックアップファイルは、アップロードフォルダを基準として、_backup/ファイル名.拡張子_処理時刻のlong値.拡張子 になります。 057 * オリジナルのファイル名(拡張子付)を残したまま、"_処理時刻のlong値" を追加し、さらに、オリジナルの拡張子を追加します。 058 * バックアップファイルの形式は指定できません。 059 * 060 * 5.7.1.2 (2013/12/20) zip 対応 061 * filename 属性に、".zip" の拡張子のファイル名を指定した場合は、アップロードされた一連のファイルを 062 * ZIP圧縮します。これは、アップロード後の処理になります。 063 * ZIP圧縮のオリジナルファイルは、そのまま残ります。 064 * なお、ZIPファイルは、useBackup属性を true に設定しても、無関係に、上書きされます。 065 * 066 * @og.group その他機能 067 * 068 * @version 4.0 069 * @author Kazuhiko Hasegawa 070 * @since JDK5.0, 071 */ 072public final class MultipartRequest { 073 private static AtomicInteger dumyNewFileCnt = new AtomicInteger(1); // 5.5.2.6 (2012/05/25) findbugs対応 074 075 private static String RANDOM_KEY = new Random().nextInt( Integer.MAX_VALUE ) + "_" ; // 5.6.5.3 (2013/06/28) アップロード時のダミーファイル名をもう少しだけランダムにする。 076 077 private final Map<String,List<String>> parameters = new TreeMap<String,List<String>>(); // 5.6.5.2 (2013/06/21) ソートします。 078 079 // 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 080 private final List<UploadedFile> files = new ArrayList<UploadedFile>(); // 5.7.1.1 (2013/12/13) HTML5対応 081 082 /** 083 * MultipartRequest オブジェクトを構築します。 084 * 085 * 引数として、ファイルアップロード時の保存フォルダ、最大サイズ、エンコード、 086 * 新しいファイル名などを指定できます。新しいファイル名は、アップロードされる 087 * ファイルが一つだけの場合に使用できます。複数のファイルを同時に変更したい 088 * 場合は、アップロードルールにのっとり、リクエストパラメータで指定してください。 089 * 090 * HTML5 では、ファイルアップロード時に、multiple 属性(inputタグのtype="file")を 091 * 付ける事で、ファイルを複数選択できます。 092 * その場合は、inputのname属性は、一つなので、_NEW による名前の書き換えはできません。 093 * 094 * @og.rev 3.8.1.3A (2006/01/30) 新ファイル名にオリジナルファイル名の拡張子をセットします 095 * @og.rev 4.0.0.0 (2007/11/28) メソッドの戻り値をチェックします。 096 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応。staticフィールドへの書き込みに、AtomicInteger を利用します。 097 * @og.rev 5.6.5.3 (2013/06/28) useBackup引数追加 098 * @og.rev 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 099 * @og.rev 5.7.1.2 (2013/12/20) zip 対応 100 * @og.rev 5.7.4.3 (2014/03/28) zip 対応復活。inputFilename のリクエスト変数処理追加 101 * 102 * @param request HttpServletRequestオブジェクト 103 * @param saveDirectory ファイルアップロードがあった場合の保存フォルダ名 104 * @param maxPostSize ファイルアップロード時の最大ファイルサイズ(Byte)0,またはマイナスで無制限 105 * @param encoding ファイルのエンコード 106 * @param inputFilename アップロードされたファイルの新しい名前 107 * @param useBackup ファイルアップロード時に、バックアップ処理するかどうか[true/false]を指定 108 * @throws IOException 入出力エラーが発生したとき 109 */ 110 public MultipartRequest(final HttpServletRequest request, 111 final String saveDirectory, 112 final int maxPostSize, 113 final String encoding, 114 final String inputFilename, 115 final boolean useBackup ) throws IOException { // 5.6.5.3 (2013/06/28) 追加 116 117 if(request == null) { 118 throw new IllegalArgumentException("request cannot be null"); 119 } 120 121 if(saveDirectory == null) { 122 throw new IllegalArgumentException("saveDirectory cannot be null"); 123 } 124 // 5.5.2.6 (2012/05/25) 0,またはマイナスで無制限 125 // Save the dir 126 File dir = new File(saveDirectory); 127 128 // Check saveDirectory is truly a directory 129 if(!dir.isDirectory()) { 130 throw new IllegalArgumentException("Not a directory: " + saveDirectory); 131 } 132 133 // Check saveDirectory is writable 134 if(!dir.canWrite()) { 135 throw new IllegalArgumentException("Not writable: " + saveDirectory); 136 } 137 138 // Parse the incoming multipart, storing files in the dir provided, 139 // and populate the meta objects which describe what we found 140 MultipartParser parser = new MultipartParser(request, maxPostSize); 141 if(encoding != null) { 142 parser.setEncoding(encoding); 143 } 144 145 // 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 146 Part part; 147 while ((part = parser.readNextPart()) != null) { 148 String name = part.getName(); 149 if( part.isParam() && part instanceof ParamPart ) { 150 ParamPart paramPart = (ParamPart)part; 151 String value = paramPart.getStringValue(); 152 List<String> existingValues = parameters.get(name); 153 if(existingValues == null) { 154 existingValues = new ArrayList<String>(); 155 parameters.put(name, existingValues); 156 } 157 existingValues.add(value); 158 } 159 else if( part.isFile() && part instanceof FilePart ) { 160 FilePart filePart = (FilePart)part; 161 String orgName = filePart.getFilename(); // 5.7.1.1 (2013/12/13) 判りやすいように変数名変更 162 if(orgName != null) { 163 // 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 164 // 同一 name で、複数ファイルを扱う必要があります。 165 // 3.8.1.2 (2005/12/19) 仮ファイルでセーブする。 166 String uniqKey = RANDOM_KEY + dumyNewFileCnt.getAndIncrement() ; // 5.6.5.3 (2013/06/28) アップロード時のダミーファイル名をもう少しだけランダムにする。 167 filePart.setFilename( uniqKey ); 168 filePart.writeTo(dir); 169 170 // 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 171 files.add( new UploadedFile( 172 uniqKey, // 5.7.1.1 (2013/12/13) 順番変更 173 dir.toString(), 174 name, // 5.7.1.1 (2013/12/13) 項目追加 175 orgName, 176 filePart.getContentType())); 177 178 } 179 } 180 else { 181 String errMsg = "Partオブジェクトが、ParamPartでもFilePartでもありません。" 182 + " class=[" + part.getClass() + "]"; 183 throw new RuntimeException( errMsg ); 184 } 185 } 186 187 // 5.7.4.3 (2014/03/28) inputFilename は、リクエスト変数が使えるようにします。 188 String filename = getReqParamFileName( inputFilename ) ; 189 190 // 3.5.6.5 (2004/08/09) 登録後にファイルをリネームします。 191 // 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 192 int size = files.size(); 193 194 // 5.7.1.2 (2013/12/20) zip 対応 195 File[] tgtFiles = new File[size]; 196 boolean isZip = filename != null && filename.endsWith( ".zip" ) ; 197 198 for( int i=0; i<size; i++ ) { 199 UploadedFile upFile = files.get(i); 200 String name = upFile.getName(); // 5.7.1.1 (2013/12/13) 201 202 String newName = isZip ? null : filename ; 203 if( newName == null && name != null ) { 204 int adrs = name.lastIndexOf( HybsSystem.JOINT_STRING ); // カラム__行番号 の __ の位置 205 if( adrs < 0 ) { 206 newName = getParameter( name + "_NEW" ); 207 } 208 else { 209 newName = getParameter( name.substring( 0,adrs ) + "_NEW" + name.substring( adrs ) ); 210 } 211 } 212 213 // 5.7.1.1 (2013/12/13) UploadedFile 内で処理するように変更 214 tgtFiles[i] = upFile.renameTo( newName,useBackup ); 215 216 } 217 // 5.7.1.2 (2013/12/20) zip 対応 218 // 6.0.0.0 (2014/04/11) 一旦保留にしていましたが、復活します。 219 if( isZip ) { 220 File zipFile = new File( saveDirectory,filename ); 221 ZipArchive.compress( tgtFiles,zipFile ); 222 } 223 } 224 225 /** 226 * リクエストパラメータの名前配列を取得します。 227 * 228 * @return リクエストパラメータの名前配列 229 */ 230 public String[] getParameterNames() { 231 Set<String> keyset = parameters.keySet(); 232 return keyset.toArray( new String[keyset.size()] ); 233 } 234 235 /** 236 * ファイルアップロードされたファイル群のファイル配列を取得します。 237 * 238 * @og.rev 5.7.1.1 (2013/12/13) HTML5 ファイルアップロードの複数選択(multiple)対応 239 * 240 * @return アップロードされたファイル群 241 */ 242 public UploadedFile[] getUploadedFile() { 243 return files.toArray( new UploadedFile[files.size()] ); 244 } 245 246 /** 247 * 指定の名前のリクエストパラメータの値を取得します。 248 * 249 * 複数存在する場合は、一番最後の値を返します。 250 * 251 * @param name リクエストパラメータ名 252 * 253 * @return パラメータの値 254 */ 255 public String getParameter( final String name ) { 256 List<String> values = parameters.get(name); 257 if( values == null || values.isEmpty() ) { 258 return null; 259 } 260 return values.get(values.size() - 1); 261 } 262 263 /** 264 * 指定の名前のリクエストパラメータの値を配列型式で取得します。 265 * 266 * @og.rev 5.3.2.0 (2011/02/01) 新規作成 267 * 268 * @param name リクエストパラメータ名 269 * 270 * @return パラメータの値配列 271 */ 272 public String[] getParameters( final String name ) { 273 List<String> values = parameters.get(name); 274 if( values == null || values.isEmpty() ) { 275 return null; 276 } 277 return values.toArray( new String[values.size()] ); 278 } 279 280 /** 281 * 指定の名前のリクエストパラメータの値を配列(int)型式で取得します。 282 * 283 * @og.rev 5.3.2.0 (2011/02/01) 新規作成 284 * @og.rev 5.3.6.0 (2011/06/01) 配列値が""の場合にNumberFormatExceptionが発生するバグを修正 285 * 286 * @param name リクエストパラメータ名 287 * 288 * @return パラメータの値配列 289 */ 290 public int[] getIntParameters( final String name ) { 291 List<String> values = parameters.get(name); 292 if( values == null || values.isEmpty() ) { 293 return null; 294 } 295 296 // 5.3.6.0 (2011/06/01) ゼロストリング("")はint変換対象から予め除外する 297 List<Integer> intVals = new ArrayList<Integer>(); 298 for( int i=0; i<values.size(); i++ ) { 299 String str = values.get(i); 300 if( str != null && str.length() > 0 ) { 301 intVals.add( Integer.parseInt( str ) ); 302 } 303 } 304 if( intVals.isEmpty() ) { 305 return null; 306 } 307 308 int[] rtn = new int[intVals.size()]; 309 for( int i=0; i<intVals.size(); i++ ) { 310 rtn[i] = intVals.get(i).intValue(); 311 } 312 313 return rtn; 314 } 315 316 /** 317 * 指定の名前の ファイル名のリクエスト変数処理を行います。 318 * 319 * filename 属性のみ、{@XXXX} のリクエスト変数が使えるようにします。 320 * 321 * @og.rev 5.7.4.3 (2014/03/28) 新規追加 322 * 323 * @param fname ファイル名 324 * @return リクエスト変数を処理したファイル名 325 */ 326 private String getReqParamFileName( final String fname ) { 327 328 String rtn = fname ; 329 if( fname != null ) { 330 StringBuilder filename = new StringBuilder( fname ) ; 331 int st = filename.indexOf( "{@" ); 332 while( st >= 0 ) { 333 int ed = filename.indexOf( "}",st ); 334 if( ed < 0 ) { 335 String errMsg = "{@XXXX} の対応関係が取れていません。" 336 + " filename=[" + fname + "]"; 337 throw new RuntimeException( errMsg ); 338 } 339 String key = filename.substring( st+2,ed ); // "}" は切り出し対象外にする。 340 String val = getParameter( key ); 341 filename.replace( st,ed+1,val ); // "}" を含めて置換したいので、ed+1 342 // 次の "{@" を探す。開始は置換文字数が不明なので、st から始める。 343 st = filename.indexOf( "{@",st ); 344 } 345 rtn = filename.toString(); 346 } 347 return rtn ; 348 } 349}