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.fukurou.util;
017
018import java.io.File;
019import java.io.InputStream;
020// import java.io.FileInputStream;                                                                      // 8.5.4.2 (2024/01/12)
021import java.io.BufferedInputStream;
022import java.io.FileNotFoundException;
023// import java.io.FileOutputStream;                                                                     // 8.5.4.2 (2024/01/12)
024import java.io.BufferedOutputStream;
025import java.io.IOException;
026import java.util.ArrayList;
027import java.util.List;
028import java.nio.file.Files;                                                                                     // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
029
030import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
031import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
032import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
033// import org.apache.commons.compress.utils.IOUtils;
034import org.apache.commons.io.IOUtils;                                                           // 8.5.7.0 (2024/03/29) compress.utils.IOUtils#copy ⇒ io.IOUtils#copy
035
036import org.opengion.fukurou.system.OgRuntimeException ;                         // 6.4.2.0 (2016/01/29)
037import org.opengion.fukurou.system.Closer;                                                      // 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system
038
039/**
040 * ZipArchive.java は、ZIPファイルの解凍・圧縮を行うためのUtilクラスです。
041 *
042 * zipファイルで、圧縮時のファイルのエンコードを指定できるようにします。
043 * ファイルをZIPにするには、java.util.zipパッケージ を利用するのが一般的です。
044 * ところが、ファイル名にUTF-8文字エンコーディングを利用する為、Windowsの世界では
045 * これを取り扱うアプリケーションも少ないため、文字化けして見えてしまいます。
046 * これを解決するには、エンコードが指定できるアーカイバを使用する必要があります。
047 * 有名どころでは、ant.jar に含まれる、org.apache.tools.zip と、Apache Commons の
048 * org.apache.commons.compress です。
049 * org.apache.tools.zip は、java.util.zip とほぼ同じ扱い方、クラス名が使えるので
050 * 既存のアプリケーションを作り変えるには、最適です。
051 * openGion では、アーカイバ専用ということで、org.apache.commons.compress を
052 * 採用します。
053 *
054 * @og.group ユーティリティ
055 * @og.rev 6.0.0.0 (2014/04/11) org.apache.commons.compress パッケージの利用(日本語ファイル名対応)
056 *
057 * @version  6.0
058 * @author   Kazuhiko Hasegawa
059 * @since    JDK5.0,
060 */
061public final class ZipArchive {
062
063        /**
064         * デフォルトコンストラクターをprivateにして、
065         * オブジェクトの生成をさせないようにする。
066         */
067        private ZipArchive() {}         // 8.5.4.2 (2024/01/12) PMD 7.0.0 UnnecessarySemicolon
068
069        /**
070         * エンコードに、Windows-31J を指定した、ZIPファイルの解凍処理を行います。
071         * 引数にフォルダ(targetPath)に指定されたZIPファイル(zipFile)を解凍します。
072         * 解凍先のファイルが存在する場合でも、上書きされますので注意下さい。
073         *
074         * @og.rev 5.7.1.2 (2013/12/20) org.apache.commons.compress パッケージの利用(日本語ファイル名対応)
075         *
076         * @param targetPath 解凍先のフォルダ
077         * @param zipFile 解凍するZIPファイル
078         *
079         * @return 解凍されたZIPファイルの一覧
080         * @og.rtnNotNull
081         */
082        public static List<File> unCompress( final File targetPath , final File zipFile ) {
083                return unCompress( targetPath,zipFile,"Windows-31J" );
084        }
085
086        /**
087         * エンコードを指定した、ZIPファイルの解凍処理を行います。
088         * 引数にフォルダ(targetPath)に指定されたZIPファイル(zipFile)を解凍します。
089         * 解凍先のファイルが存在する場合でも、上書きされますので注意下さい。
090         *
091         * 解凍途中のエラーは、エラー出力に出力するだけで、処理は止めません。
092         *
093         * @og.rev 4.1.0.2 (2008/02/01) 新規追加
094         * @og.rev 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
095         * @og.rev 4.3.3.3 (2008/10/22) mkdirsする前に存在チェック
096         * @og.rev 5.1.9.0 (2010/08/01) 更新時刻の設定
097         * @og.rev 5.7.1.2 (2013/12/20) org.apache.commons.compress パッケージの利用(日本語ファイル名対応)
098         * @og.rev 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
099         *
100         * @param targetPath 解凍先のフォルダ
101         * @param zipFile 解凍するZIPファイル
102         * @param encording ファイルのエンコード(Windows環境では、"Windows-31J" を指定します)
103         *
104         * @return 解凍されたZIPファイルの一覧
105         * @og.rtnNotNull
106         */
107        public static List<File> unCompress( final File targetPath, final File zipFile, final String encording ) {
108                final List<File> list = new ArrayList<>();
109
110                // 解凍先フォルダの末尾が'/'又は'\'でなければ区切り文字を挿入
111        //      String tmpPrefix = targetPath;
112        //      if( File.separatorChar != targetPath.charAt( targetPath.length() - 1 ) ) {
113        //              tmpPrefix = tmpPrefix + File.separator;
114        //      }
115
116                ZipArchiveInputStream zis = null;
117                File tmpFile = null;
118        //      String fileName = null;
119
120                try {
121                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
122//                      zis = new ZipArchiveInputStream( new BufferedInputStream( new FileInputStream( zipFile ) ) ,encording );
123                        zis = new ZipArchiveInputStream( new BufferedInputStream( Files.newInputStream(zipFile.toPath())) ,encording );
124
125                        ZipArchiveEntry entry = zis.getNextEntry();                     // 8.5.5.1 (2024/02/29) commons-compress-1.25.0.jar 対応
126//                      while( ( entry = zis.getNextZipEntry() ) != null ) {
127                        while( entry != null ) {                                                        // 8.5.5.1 (2024/02/29) 代入と判定を分離します。
128        //                      fileName = tmpPrefix + entry.getName().replace( '/', File.separatorChar );
129                                tmpFile = new File( targetPath,entry.getName() );
130                                list.add( tmpFile );
131
132                                // ディレクトリの場合は、自身を含むディレクトリを作成
133                                if( entry.isDirectory() ) {
134                                        if( !tmpFile.exists() && !tmpFile.mkdirs() ) {
135                                                final String errMsg = "ディレクトリ作成に失敗しました。[ファイル名=" + tmpFile + "]";
136                                                System.err.println( errMsg );
137                                                continue;
138                                        }
139                                }
140                                // ファイルの場合は、自身の親となるディレクトリを作成
141                                else {
142                                        // 4.3.3.3 (2008/10/22) 作成する前に存在チェック
143                                        if( !tmpFile.getParentFile().exists() && !tmpFile.getParentFile().mkdirs() ) {
144                                                final String errMsg = "親ディレクトリ作成に失敗しました。[ファイル名=" + tmpFile + "]";
145                                                System.err.println( errMsg );
146                                                continue;
147                                        }
148
149                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
150//                                      final BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream( tmpFile ) );
151//                                      try {
152                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
153//                                      try ( BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream( tmpFile ) ) ) {
154                                        try ( BufferedOutputStream out = new BufferedOutputStream( Files.newOutputStream(tmpFile.toPath())) ) {
155                                                IOUtils.copy( zis,out );
156                                        }
157                                        catch( final IOException zex ) {
158                                                final String errMsg = "ZIPファイルの作成(copy)に失敗しました。[ファイル名=" + tmpFile + "]";
159                                                System.err.println( errMsg );
160                                                continue;
161                                        }
162//                                      finally {
163//                                              Closer.ioClose( out );
164//                                      }
165                                }
166                                // 5.1.9.0 (2010/08/01) 更新時刻の設定
167                                final long lastTime = entry.getTime();
168                                if( lastTime >= 0 && !tmpFile.setLastModified( lastTime ) ) {
169                                        final String errMsg = "ZIP更新時刻の設定に失敗しました。[ファイル名=" + tmpFile + "]";
170                                        System.err.println( errMsg );
171                                }
172                                entry = zis.getNextEntry();                             // 8.5.5.1 (2024/02/29) 代入と判定を分離します。
173                        }
174                }
175                catch( final FileNotFoundException ex ) {
176                        final String errMsg = "解凍ファイルが作成できません。[ファイル名=" + tmpFile + "]";
177                        throw new OgRuntimeException( errMsg, ex );
178                }
179                catch( final IOException ex ) {
180                        final String errMsg = "ZIPファイルの解凍に失敗しました。[ファイル名=" + tmpFile + "]";
181                        throw new OgRuntimeException( errMsg, ex );
182                }
183                finally {
184                        Closer.ioClose( zis );
185                }
186
187                return list;
188        }
189
190        /**
191         * 引数に指定されたファイル又はフィルダ内に存在するファイルをZIPファイルに圧縮します。
192         * 圧縮レベルはデフォルトのDEFAULT_COMPRESSIONです。
193         * 圧縮ファイルのエントリー情報として本来は、圧縮前後のファイルサイズ、変更日時、CRCを登録する
194         * 必要がありますが、ここでは高速化のため、設定していません。(特に圧縮後ファイルサイズの取得は、
195         * 非常に不可がかかる。)
196         * このため、一部のアーカイバでは正しく解凍できない可能性があります。
197         * 既にZIPファイルが存在する場合でも、上書きされますので注意下さい。
198         *
199         * @og.rev 4.1.0.2 (2008/02/01) 新規追加
200         * @og.rev 5.7.1.2 (2013/12/20) org.apache.commons.compress パッケージの利用(日本語ファイル名対応)
201         *
202         * @param files 圧縮対象のファイル配列
203         * @param zipFile ZIPファイル名
204         *
205         * @return ZIPファイルのエントリーファイル名一覧
206         * @og.rtnNotNull
207         */
208        public static List<File> compress( final File[] files, final File zipFile ) {
209                return compress( files,zipFile,"Windows-31J" );
210        }
211
212        /**
213         * 引数に指定されたファイル又はフィルダ内に存在するファイルをZIPファイルに圧縮します。
214         * 圧縮レベルはデフォルトのDEFAULT_COMPRESSIONです。
215         * 圧縮ファイルのエントリー情報として本来は、圧縮前後のファイルサイズ、変更日時、CRCを登録する
216         * 必要がありますが、ここでは高速化のため、設定していません。(特に圧縮後ファイルサイズの取得は、
217         * 非常に不可がかかる。)
218         * このため、一部のアーカイバでは正しく解凍できない可能性があります。
219         * 既にZIPファイルが存在する場合でも、上書きされますので注意下さい。
220         *
221         * @og.rev 5.7.1.2 (2013/12/20) org.apache.commons.compress パッケージの利用(日本語ファイル名対応)
222         * @og.rev 6.3.9.0 (2015/11/06) 1行にまとめる。
223         *
224         * @param dir 圧縮対象のディレクトリか、ファイル
225         * @param zipFile ZIPファイル名
226         *
227         * @return ZIPファイルのエントリーファイル名一覧
228         * @og.rtnNotNull
229         */
230        public static List<File> compress( final File dir, final File zipFile ) {
231
232                final File[] files = dir.isDirectory() ? dir.listFiles() : new File[] { dir } ;         // ※ files は null もありうる。
233                return compress( files,zipFile,"Windows-31J" );
234        }
235
236        /**
237         * 引数に指定されたファイル又はフィルダ内に存在するファイルをZIPファイルに圧縮します。
238         * 圧縮レベルはデフォルトのDEFAULT_COMPRESSIONです。
239         * 圧縮ファイルのエントリー情報として本来は、圧縮前後のファイルサイズ、変更日時、CRCを登録する
240         * 必要がありますが、ここでは高速化のため、設定していません。(特に圧縮後ファイルサイズの取得は、
241         * 非常に不可がかかる。)
242         * このため、一部のアーカイバでは正しく解凍できない可能性があります。
243         * 既にZIPファイルが存在する場合でも、上書きされますので注意下さい。
244         *
245         * @og.rev 4.1.0.2 (2008/02/01) 新規追加
246         * @og.rev 5.7.1.2 (2013/12/20) org.apache.commons.compress パッケージの利用(日本語ファイル名対応)
247         * @og.rev 6.3.9.0 (2015/11/06) null になっている可能性があるメソッドの戻り値のnullチェックを追加。
248         *
249         * @param files 圧縮対象のファイル配列
250         * @param zipFile ZIPファイル名
251         * @param encording ファイルのエンコード(Windows環境では、"Windows-31J" を指定します)
252         *
253         * @return ZIPファイルのエントリーファイル名一覧
254         * @og.rtnNotNull
255         */
256        public static List<File> compress( final File[] files, final File zipFile, final String encording ) {
257                final List<File> list = new ArrayList<>();
258
259                if( files != null && files.length > 0 && zipFile != null ) {
260                        ZipArchiveOutputStream zos = null;
261
262                        try {
263                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
264//                              zos = new ZipArchiveOutputStream( new BufferedOutputStream ( new FileOutputStream( zipFile ) ) );
265                                zos = new ZipArchiveOutputStream( new BufferedOutputStream ( Files.newOutputStream(zipFile.toPath()) ) );
266                                if( encording != null ) {
267                                        zos.setEncoding( encording );           // "Windows-31J"
268                                }
269
270                                // ZIP圧縮処理を行います
271                                addZipEntry( list, zos, "" , files );   // 開始フォルダは、空文字列とします。
272                        }
273                        catch( final FileNotFoundException ex ) {
274                                final String errMsg = "ZIPファイルが見つかりません。[ファイル名=" + zipFile + "]";
275                                throw new OgRuntimeException( errMsg, ex );
276                        }
277                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
278                        catch( final IOException ex ) {
279                                final String errMsg = "ファイルのオープン中に入出力エラーが発生しました。"
280                                                                + "File=[" + zipFile + "] , encode=[" + encording + "]" ;
281                                throw new OgRuntimeException( errMsg,ex );
282                        }
283                        finally {
284                                Closer.ioClose( zos );
285                //              zos.finish();
286                //              zos.flush();
287                //              zos.close();
288                        }
289                }
290
291                return list;
292        }
293
294        /**
295         * ZIP圧縮処理を行います。
296         * 引数に指定されたFileオブジェクトがディレクトリであれば再帰的に呼び出し、
297         * 下層のファイルをエントリーします。但し、そのディレクトリ自身が空である場合は、
298         * ディレクトリをエントリー情報として設定します。
299         *
300         * @og.rev 4.1.0.2 (2008/02/01) 新規追加
301         * @og.rev 5.1.9.0 (2010/08/01) 更新時刻の設定、BufferedInputStream のスコープを小さくする。
302         * @og.rev 6.8.0.0 (2017/06/02) アーカイブするファイルのタイムスタンプをセットする。
303         * @og.rev 7.2.9.5 (2020/11/28) 削除してすぐに実行すると、存在しないファイルリストが渡される場合がある。
304         * @og.rev 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
305         *
306         * @param list ZIPファイルのエントリーファイル名一覧
307         * @param zos ZIP用OutputStream
308         * @param prefix 圧縮時のフォルダ
309         * @param files 圧縮対象のファイル配列(可変長引数)
310         */
311        private static void addZipEntry( final List<File> list, final ZipArchiveOutputStream zos, final String prefix, final File... files ) {
312                File tmpFile = null;
313                try {
314                        for( final File fi : files ) {
315                                tmpFile = fi;                           // エラー時のファイル
316                                if( !fi.exists() ) { continue; }                        // 7.2.9.5 (2020/11/28)
317
318                                list.add( fi );
319                                if( fi.isDirectory() ) {
320                                        final String entryName = prefix + fi.getName() + "/" ;
321                                        final ZipArchiveEntry zae = new ZipArchiveEntry( entryName );
322                                        zae.setTime( fi.lastModified() );                                                                       // 6.8.0.0 (2017/06/02) ※ 効いてなさそう
323                                        zos.putArchiveEntry( zae );
324                                        zos.closeArchiveEntry();
325
326                                        addZipEntry( list, zos, entryName, fi.listFiles() );
327                                }
328                                else {
329                                        final String entryName = prefix + fi.getName() ;
330                                        final ZipArchiveEntry zae = new ZipArchiveEntry( entryName );
331                                        zae.setTime( fi.lastModified() );                                                                       // 6.8.0.0 (2017/06/02)
332                                        zos.putArchiveEntry( zae );
333                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応
334//                                      final InputStream is = new BufferedInputStream( new FileInputStream(fi) );
335//                                      IOUtils.copy( is,zos );
336//                                      zos.closeArchiveEntry();
337//                                      Closer.ioClose( is );
338                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidFileStream 対応
339//                                      try ( InputStream is = new BufferedInputStream( new FileInputStream(fi) ) ) {
340                                        try ( InputStream is = new BufferedInputStream( Files.newInputStream(fi.toPath()) ) ) {
341                                                IOUtils.copy( is,zos );
342                                                zos.closeArchiveEntry();
343                                        }
344                                }
345                        }
346                }
347                catch( final FileNotFoundException ex ) {
348                        final String errMsg = "圧縮対象のファイルが見つかりません。[ファイル名=" + tmpFile + "]";
349                        throw new OgRuntimeException( errMsg, ex );
350                }
351                catch( final IOException ex ) {
352                        final String errMsg = "ZIP圧縮に失敗しました。[ファイル名=" + tmpFile + "]";
353                        throw new OgRuntimeException( errMsg, ex );
354                }
355        }
356
357        /**
358         * ファイルの圧縮または解凍を行います。
359         *
360         * @og.rev 4.1.0.2 (2008/02/01) 新規追加
361         * @og.rev 5.9.21.1 (2017/06/12) 結果出力追加
362         *
363         * Usage: java org.opengion.fukurou.util.ZipArchive comp|uncomp targetPath zipFileName
364         * 第1引数 : comp:圧縮 uncomp:解凍
365         * 第2引数 : ZIPファイル名
366         * 第3引数 : 圧縮時:圧縮対象のファイル又はフォルダ 解凍時:解凍先のフォルダ
367         *
368         * @param args パラメータ
369         */
370        public static void main( final String[] args ) {
371                final String usage = "Usage: java org.opengion.fukurou.util.ZipArchive comp|uncomp targetPath zipFileName";
372
373                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
374                if( args.length < 3 || !"comp".equalsIgnoreCase( args[0] ) && !"uncomp".equalsIgnoreCase( args[0] ) ) {
375                        System.out.println( usage );
376                        return;
377                }
378
379                // 開始時間
380                final long start = System.currentTimeMillis();
381
382                List<File> list = null;
383                final File tgtFile = new File(args[1]);
384                final File zipFile = new File(args[2]);
385                if( "comp".equalsIgnoreCase( args[0] ) ) {
386                        list = compress( tgtFile, zipFile );
387                }
388                else if( "uncomp".equalsIgnoreCase( args[0] ) ) {
389                        list = unCompress( tgtFile, zipFile );
390                }
391
392                if( list != null ) {
393                        // 処理時間を表示
394                        System.out.println( "処理時間 : " + ( System.currentTimeMillis() - start ) + "(ms)" );
395                        // 結果を表示
396                        for( final File fileName : list ) {
397                                System.out.println( fileName );
398                        }
399                }
400                else{
401                        System.out.println( "list is null." );          // 5.9.21.1 (2017/06/16)
402                }
403        }
404}