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