    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.getSimpleName() );         // ログ出力
    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         * @og.rev 7.2.9.4 (2020/11/20) spotbugs:null になっている可能性があるメソッドの戻り値を利用している
    110         *
    111         * @param       startKey パスの先頭一致のパターン
    112         * @return      自分自身
    113         * @see         #addPathMatcher(PathMatcher)
    114         * @see         #addEndsWith(String...)
    115         */
    116        public PathMatcherSet addStartsWith( final String... startKey ) {
    117                if( startKey != null ) {
    118                        LOGGER.debug( () -> "addStartsWith : String[]=" + Arrays.toString ( startKey ) );
    119
    120                        pathMchSet.add(
    121                                path -> {
    122                                        // 大文字小文字の区別を行いません。
    123//                                      final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
    124                                        final String fname = FileUtil.pathFileName( path ).toUpperCase(Locale.JAPAN);           // 7.2.9.4 (2020/11/20) Path.getFileName().toString()
    125                                        for( final String key : startKey ) {
    126                                                if( key == null || key.isEmpty() || fname.startsWith( key.toUpperCase(Locale.JAPAN) ) ) { return true; }
    127                                        }
    128                                        return false;
    129                                }
    130                        );
    131                }
    132
    133                return this;
    134        }
    135
    136        /**
    137         * 指定のパスが、指定の文字列と、終端一致(endsWith) したパスのみ、有効とします。
    138         *
    139         * これは、#addPathMatcher(PathMatcher) の簡易指定版です。
    140         * 指定の終端文字列(一般には拡張子)のうち、ひとつでも一致すれば、true となります。
    141         * 指定しない場合(null)は、すべて許可されたことになります。
    142         * 終端文字列の判定には、大文字小文字の区別を行いません。
    143         *
    144         * @og.rev 7.2.9.4 (2020/11/20) spotbugs:null になっている可能性があるメソッドの戻り値を利用している
    145         *
    146         * @param       endKey パスの終端一致のパターン
    147         * @return      自分自身
    148         * @see         #addPathMatcher(PathMatcher)
    149         * @see         #addStartsWith(String...)
    150         */
    151        public PathMatcherSet addEndsWith( final String... endKey ) {
    152                if( endKey != null ) {
    153                        LOGGER.debug( () -> "addEndsWith : String[]=" + Arrays.toString ( endKey ) );
    154
    155                        pathMchSet.add(
    156                                path -> {
    157                                        // 大文字小文字の区別を行いません。
    158//                                      final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
    159                                        final String fname = FileUtil.pathFileName( path ).toUpperCase(Locale.JAPAN);                   // 7.2.9.4 (2020/11/20) Path.getFileName().toString()
    160                                        for( final String key : endKey ) {
    161                                                if( key == null || key.isEmpty() || fname.endsWith( key.toUpperCase(Locale.JAPAN) ) ) { return true; }
    162                                        }
    163                                        return false;
    164                                }
    165                        );
    166                }
    167
    168                return this;
    169        }
    170
    171        /**
    172         * 指定のパスが、指定の文字列と、あいまい条件で一致したパスのみ、有効とします。
    173         *
    174         * PREFIX*SUFIX 形式で、'*' を前後に、StartsWithとEndsWithに登録します。
    175         * '*'は、一つしか使用できません。正規表現ではなく、簡易的なあいまい検索です。
    176         * そのため、ﾌｧｲﾙ名の指定は、一つのみとします。
    177         * '*' が存在しない場合は、先頭一致とします。
    178         * 指定しない場合(null)は、すべて許可されたことになります。
    179         * 終端文字列の判定には、大文字小文字の区別を行いません。
    180         *
    181         * @og.rev 6.8.1.5 (2017/09/08) ﾌｧｲﾙ名の'*'の処理の見直し
    182         * @og.rev 7.2.9.4 (2020/11/20) spotbugs:null になっている可能性があるメソッドの戻り値を利用している
    183         *
    184         * @param       filename パスの一致のパターン
    185         * @return      自分自身
    186         * @see         #addStartsWith(String...)
    187         * @see         #addEndsWith(String...)
    188         */
    189        public PathMatcherSet addFileName( final String filename ) {
    190                if( filename != null && !filename.isEmpty() ) {
    191                        LOGGER.debug( () -> "addFileName : filename=" + filename );
    192
    193                        final int ad = filename.indexOf( '*' );         // 分割するためのキーワード
    194
    195                        // 暫定的なチェック：'*' は１箇所しか指定できません。
    196                        if( ad != filename.lastIndexOf( '*' ) ) {       // つまり、２個以上ある。
    197                                // MSG3005 = 検索条件のﾌｧｲﾙ名の指定に、'*'を複数使用することは出来ません。 File=[{0}]
    198                                MsgUtil.errPrintln( "MSG3005" , filename );
    199                                return this;
    200                        }
    201
    202                        if( ad < 0 ) {                                                                  // '*'が無い場合は、先頭一致で判定します。
    203                                addStartsWith( filename );
    204                        }
    205                        else if( ad == 0 ) {                                                    // 先頭が、'*' の場合は、後方一致で判定します。
    206                                addEndsWith( filename.substring( 1 ) );
    207                        }
    208                        else if( ad == filename.length()-1 ) {                  // 最後が '*'の場合は、先頭一致で判定します。
    209                                addStartsWith( filename.substring( 0,filename.length()-1 ) );
    210                        }
    211                        else {
    212                                final String prefix = filename.substring( 0,ad ).toUpperCase(Locale.JAPAN);
    213                                final String sufix  = filename.substring( ad+1 ).toUpperCase(Locale.JAPAN);
    214
    215                                pathMchSet.add(
    216                                        path -> {
    217                                                // 大文字小文字の区別を行いません。
    218//                                              final String fname = path.getFileName().toString().toUpperCase(Locale.JAPAN);
    219                                                final String fname = FileUtil.pathFileName( path ).toUpperCase(Locale.JAPAN);   // 7.2.9.4 (2020/11/20) Path.getFileName().toString()
    220
    221                                                if( fname.startsWith( prefix ) && fname.endsWith( sufix ) ) { return true; }    // 両方成立が条件
    222                                                return false;
    223                                        }
    224                                );
    225
    226        //                      addStartsWith( prefix );                                // ゼロ文字列の場合は、true になります。
    227        //                      addEndsWith(   sufix  );                                // ゼロ文字列の場合は、true になります。
    228                        }
    229                }
    230
    231                return this;
    232        }
    233
    234        /**
    235         * 指定されたパスがこのマッチャのパターンに一致するかどうかを示します。
    236         *
    237         * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
    238         * 未登録の場合は、true が返され、評価されません。
    239         * これは、#allMatch( Path ) と同じ結果を返します。
    240         *
    241         * @param       path 照合するパス
    242         * @return      パスがこのマッチャのパターンに一致した場合にのみtrue
    243         * @see         #allMatch( Path )
    244         */
    245        @Override       // PathMatcher
    246        public boolean matches( final Path path ) {
    247                return allMatch( path );
    248        }
    249
    250        /**
    251         * すべての要素が、条件を満たす場合にのみ、有効となります。
    252         *
    253         * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
    254         * 未登録の場合は、true が返され、評価されません。
    255         * これは、#matches( Path ) と同じ結果を返します。
    256         *
    257         * @param       path 判定対象の Pathオブジェクト
    258         * @return      内部の PathMatcher が、すべて true を返す場合のみ、true
    259         * @see         #matches( Path )
    260         */
    261        public boolean allMatch( final Path path ) {
    262                // stream().allMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
    263                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().allMatch( pMch -> pMch.matches( path ) );
    264
    265                if( flag ) {
    266                        LOGGER.debug( () -> "allMatch : Path=" + path );
    267                }
    268
    269                return flag;
    270        }
    271
    272        /**
    273         * いずれかの要素が、条件を満たす場合に、有効となります。
    274         *
    275         * 内部の PathMatcher の、いずれかが、 true を返す場合に、true を返します。
    276         * 未登録の場合は、true が返され、評価されません。
    277         * この動きは、Set#anyMatch(java.util.function.Predicate)とは異なりますので、ご注意ください。
    278         *
    279         * @param       path 判定対象の Pathオブジェクト
    280         * @return      内部の PathMatcher の、いずれかが、 true を返す場合に、true
    281         */
    282        public boolean anyMatch( final Path path ) {
    283                // stream().anyMatch の場合は、Collectionが未登録時は、false が返る為、明示的に処理が必要です。
    284                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().anyMatch( pMch -> pMch.matches( path ) );
    285
    286                if( flag ) {
    287                        LOGGER.debug( () -> "anyMatch : Path=" + path );
    288                }
    289
    290                return flag;
    291        }
    292
    293        /**
    294         * 一致する要素が、ひとつも存在しない場合に、有効となります。
    295         *
    296         * 内部の PathMatcher の要素のすべてに、false を返す場合に、true を返します。
    297         * 未登録の場合は、true が返され、評価されません。
    298         *
    299         * @param       path 判定対象の Pathオブジェクト
    300         * @return 内部の PathMatcher の要素のすべてに、false を返す場合に、true
    301         */
    302        public boolean noneMatch( final Path path ) {
    303                // stream().noneMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
    304                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().noneMatch( pMch -> pMch.matches( path ) );
    305
    306                if( flag ) {
    307                        LOGGER.debug( () -> "noneMatch : Path=" + path );
    308                }
    309
    310                return flag;
    311        }
    312}