    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                        // 暫定的なチェック：'*' は１箇所しか指定できません。
    189                        if( ad != filename.lastIndexOf( '*' ) ) {       // つまり、２個以上ある。
    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}