001/*
002 * Copyright (c) 2017 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.fileexec;
017
018import java.nio.file.Path;
019import java.nio.file.PathMatcher;
020
021import java.util.Set;
022import java.util.Locale;
023import java.util.Arrays;
024import java.util.concurrent.CopyOnWriteArraySet;
025
026/**
027 * PathMatcherSet は、ファイル監視を行うクラスで利用する、ファイルの選別(PathMatcher)を管理するクラスです。
028 *
029 *<pre>
030 * PathMatcherオブジェクトを複数持っており(Set)それらが、その、判定によって、
031 * イベントを起こすかどうか、フィルタリングします。
032 *
033 *</pre>
034 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
035 *
036 * @version  7.0
037 * @author   Kazuhiko Hasegawa
038 * @since    JDK1.8,
039 */
040public class PathMatcherSet implements PathMatcher {
041        private static final XLogger LOGGER= XLogger.getLogger( PathMatcherSet.class.getName() );               // ログ出力
042
043        // パスの照合操作を行うPathMatcher のSetオブジェクト
044        private final Set<PathMatcher> pathMchSet = new CopyOnWriteArraySet<>();                // 未設定のときは、すべてにマッチングさせる。(startメソッドで)
045
046        /**
047         * デフォルトコンストラクター
048         *
049         */
050        public PathMatcherSet() { super(); }
051
052        /**
053         * すべてのPathMatcherSet を、追加登録します。
054         *
055         * 引数が、null の場合は、登録しません。
056         *
057         * @param       pmSet パスの照合操作のパターン
058         * @return      このセットが変更された場合はtrue
059         */
060        public boolean addAll( final PathMatcherSet pmSet ) {
061                return pmSet != null && pathMchSet.addAll( pmSet.pathMchSet );
062        }
063
064        /**
065         * 内部の PathMatcherに、要素が含まれてい無い場合に、true を返します。
066         *
067         * @return      このセットに要素が1つも含まれていない場合はtrue
068         */
069        public boolean isEmpty() {
070                return pathMchSet.isEmpty();
071        }
072
073        /**
074         * すべての要素をセットから削除します。
075         *
076         */
077        public void clear() {
078                pathMchSet.clear();
079        }
080
081        /**
082         * PathMatcher を、追加登録します。
083         *
084         * 引数が、null の場合は、登録しません。
085         *
086         * @param       pathMch パスの照合操作のパターン
087         * @return      自分自身
088         * @see         java.nio.file.PathMatcher
089         * @see         #addStartsWith(String...)
090         * @see         #addEndsWith(String...)
091         */
092        public PathMatcherSet addPathMatcher( final PathMatcher pathMch ) {
093        //      LOGGER.debug( () -> "addPathMatcher : PathMatcher=" + pathMch );
094
095                if( pathMch != null ) {
096                        pathMchSet.add( pathMch );
097                }
098
099                return this;
100        }
101
102        /**
103         * 指定のパスが、指定の文字列と、先頭一致(startsWith) したパスのみ、有効とします。
104         *
105         * これは、#addPathMatcher(PathMatcher) の簡易指定版です。
106         * 指定の先頭一致(一般にはファイル名の先頭)のうち、ひとつでも一致すれば、true となります。
107         * 先頭文字列の判定には、大文字小文字の区別を行いません。
108         *
109         * @param       startKey パスの先頭一致のパターン
110         * @return      自分自身
111         * @see         #addPathMatcher(PathMatcher)
112         * @see         #addEndsWith(String...)
113         */
114        public PathMatcherSet addStartsWith( final String... startKey ) {
115                if( startKey != null ) {
116                        LOGGER.debug( () -> "addStartsWith : String[]=" + Arrays.toString ( startKey ) );
117
118                        pathMchSet.add(
119                                path -> {
120                                        // 大文字小文字の区別を行いません。
121                                        final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
122                                        for( final String key : startKey ) {
123                                                if( key == null || key.isEmpty() || fname.startsWith( key.toUpperCase(Locale.JAPAN) ) ) { return true; }
124                                        }
125                                        return false;
126                                }
127                        );
128                }
129
130                return this;
131        }
132
133        /**
134         * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、有効とします。
135         *
136         * これは、#addPathMatcher(PathMatcher) の簡易指定版です。
137         * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となります。
138         * 指定しない場合(null)は、すべて許可されたことになります。
139         * 終端文字列の判定には、大文字小文字の区別を行いません。
140         *
141         * @param       endKey パスの終端一致のパターン
142         * @return      自分自身
143         * @see         #addPathMatcher(PathMatcher)
144         * @see         #addStartsWith(String...)
145         */
146        public PathMatcherSet addEndsWith( final String... endKey ) {
147                if( endKey != null ) {
148                        LOGGER.debug( () -> "addEndsWith : String[]=" + Arrays.toString ( endKey ) );
149
150                        pathMchSet.add(
151                                path -> {
152                                        // 大文字小文字の区別を行いません。
153                                        final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
154                                        for( final String key : endKey ) {
155                                                if( key == null || key.isEmpty() || fname.endsWith( key.toUpperCase(Locale.JAPAN) ) ) { return true; }
156                                        }
157                                        return false;
158                                }
159                        );
160                }
161
162                return this;
163        }
164
165        /**
166         * 指定のパスが、指定の文字列と、あいまい条件で一致したパスのみ、有効とします。
167         *
168         * PREFIX*SUFIX 形式で、'*' を前後に、StartsWithとEndsWithに登録します。
169         * '*'は、一つしか使用できません。正規表現ではなく、簡易的なあいまい検索です。
170         * そのため、ファイル名の指定は、一つのみとします。
171         * '*' が存在しない場合は、先頭一致とします。
172         * 指定しない場合(null)は、すべて許可されたことになります。
173         * 終端文字列の判定には、大文字小文字の区別を行いません。
174         *
175         * @og.rev 6.8.1.5 (2017/09/08) ファイル名の'*'の処理の見直し
176         *
177         * @param       filename パスの一致のパターン
178         * @return      自分自身
179         * @see         #addStartsWith(String...)
180         * @see         #addEndsWith(String...)
181         */
182        public PathMatcherSet addFileName( final String filename ) {
183                if( filename != null && !filename.isEmpty() ) {
184                        LOGGER.debug( () -> "addFileName : filename=" + filename );
185
186                        final int ad = filename.indexOf( '*' );         // 分割するためのキーワード
187
188                        // 暫定的なチェック:'*' は1箇所しか指定できません。
189                        if( ad != filename.lastIndexOf( '*' ) ) {       // つまり、2個以上ある。
190                                // MSG2005 = 検索条件のファイル名の指定に、'*'を複数使用することは出来ません。 File=[{0}]
191                                MsgUtil.errPrintln( "MSG2005" , filename );
192                                return this;
193                        }
194
195                        if( ad < 0 ) {
196                                addStartsWith( filename );                                      // '*'が無い場合は、先頭一致で判定します。
197                        }
198                        else if( ad == 0 ) {
199                                addEndsWith( filename.substring( 1 ) );         // 先頭が、'*' の場合は、後方一致で判定します。
200                        }
201                        else if( ad == filename.length()-1 ) {
202                                addStartsWith( filename.substring( 0,filename.length()-1 ) );   // 最後が '*'の場合は、先頭一致で判定します。
203                        }
204                        else {
205                                final String prefix = filename.substring( 0,ad ).toUpperCase(Locale.JAPAN);
206                                final String sufix  = filename.substring( ad+1 ).toUpperCase(Locale.JAPAN);
207
208                                pathMchSet.add(
209                                        path -> {
210                                                // 大文字小文字の区別を行いません。
211                                                final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
212
213                                                if( fname.startsWith( prefix ) && fname.endsWith( sufix ) ) { return true; }    // 両方成立が条件
214                                                return false;
215                                        }
216                                );
217
218        //                      addStartsWith( prefix );                                // ゼロ文字列の場合は、true になります。
219        //                      addEndsWith(   sufix  );                                // ゼロ文字列の場合は、true になります。
220                        }
221                }
222
223                return this;
224        }
225
226        /**
227         * 指定されたパスがこのマッチャのパターンに一致するかどうかを示します。
228         *
229         * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
230         * 未登録の場合は、true が返され、評価されません。
231         * これは、#allMatch( Path ) と同じ結果を返します。
232         *
233         * @param       path 照合するパス
234         * @return      パスがこのマッチャのパターンに一致した場合にのみtrue
235         * @see         #allMatch( Path )
236         */
237        @Override
238        public boolean matches( final Path path ) {
239                return allMatch( path );
240        }
241
242        /**
243         * すべての要素が、条件を満たす場合にのみ、有効となります。
244         *
245         * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
246         * 未登録の場合は、true が返され、評価されません。
247         * これは、#matches( Path ) と同じ結果を返します。
248         *
249         * @param       path 判定対象の Pathオブジェクト
250         * @return      内部の PathMatcher が、すべて true を返す場合のみ、true
251         * @see         #matches( Path )
252         */
253        public boolean allMatch( final Path path ) {
254                // stream().allMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
255                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().allMatch( pMch -> pMch.matches( path ) );
256
257                LOGGER.debug( () -> "allMatch [" + flag + "] : Path=" + path );
258
259                return flag;
260        }
261
262        /**
263         * いずれかの要素が、条件を満たす場合に、有効となります。
264         *
265         * 内部の PathMatcher の、いずれかが、 true を返す場合に、true を返します。
266         * 未登録の場合は、true が返され、評価されません。
267         * この動きは、Set#anyMatch(java.util.function.Predicate)とは異なりますので、ご注意ください。
268         *
269         * @param       path 判定対象の Pathオブジェクト
270         * @return      内部の PathMatcher の、いずれかが、 true を返す場合に、true
271         */
272        public boolean anyMatch( final Path path ) {
273                // stream().anyMatch の場合は、Collectionが未登録時は、false が返る為、明示的に処理が必要です。
274                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().anyMatch( pMch -> pMch.matches( path ) );
275
276                LOGGER.debug( () -> "anyMatch [" + flag + "] : Path=" + path );
277
278                return flag;
279        }
280
281        /**
282         * 一致する要素が、ひとつも存在しない場合に、有効となります。
283         *
284         * 内部の PathMatcher の要素のすべてに、false を返す場合に、true を返します。
285         * 未登録の場合は、true が返され、評価されません。
286         *
287         * @param       path 判定対象の Pathオブジェクト
288         * @return 内部の PathMatcher の要素のすべてに、false を返す場合に、true
289         */
290        public boolean noneMatch( final Path path ) {
291                // stream().noneMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
292                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().noneMatch( pMch -> pMch.matches( path ) );
293
294                LOGGER.debug( () -> "noneMatch [" + flag + "] : Path=" + path );
295
296                return flag;
297        }
298
299        /** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
300        public static final String USAGE = "Usage: java jp.euromap.eu63.util.PathMatcherSet [dir] [-start ・・・・] [-end ・・・・]" ;
301
302        /**
303         * 引数に監視対象のフォルダをフィルターします。
304         *
305         * {@value #USAGE}
306         *
307         * @param       args    コマンド引数配列
308         */
309        public static void main( final String[] args ) {
310                // ********** 【整合性チェック】 **********
311                if( args.length < 1 ) {
312                        System.out.println( USAGE );
313                        return;
314                }
315
316                // ********** 【引数定義】 **********
317                Path    sPath   = new java.io.File( "." ).toPath();             // スキャンパス の初期値
318
319                String  startsWith      = null;         // ファイル先頭文字列の一致キー
320                String  endsWith        = null;         // ファイル終端文字列の一致キー
321
322                // ********** 【引数処理】 **********
323                for( int i=0; i<args.length; i++ ) {
324                        final String arg = args[i];
325
326                        if(      "-help" .equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
327                        else if( "-start".equalsIgnoreCase( arg ) ) { startsWith = args[++i]; }         // 記号を見つけたら、次の引数をセットします。
328                        else if( "-end"  .equalsIgnoreCase( arg ) ) { endsWith   = args[++i]; }         // 記号を見つけたら、次の引数をセットします。
329                        else {
330                                sPath = new java.io.File( arg ).toPath();
331                        }
332                }
333
334                // ********** 【本体処理】 **********
335                final PathMatcherSet pmSet = new PathMatcherSet();
336                pmSet.addStartsWith( startsWith );
337                pmSet.addEndsWith( endsWith );
338
339                try {
340                        java.nio.file.Files.walk( sPath )
341                                                                .filter(  path -> pmSet.allMatch( path ) )
342                                                                .forEach( path -> System.out.println( path ) );
343                }
344                catch( final  java.io.IOException ex ) {
345                        ex.printStackTrace();
346                }
347        }
348}