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