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.util.Set;
019import java.util.Locale;
020import java.util.Arrays;
021import java.util.concurrent.CopyOnWriteArraySet;
022
023import java.nio.file.Path;
024import java.nio.file.PathMatcher;
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                        // 暫定的なチェック:'*' は1箇所しか指定できません。
196                        if( ad != filename.lastIndexOf( '*' ) ) {       // つまり、2個以上ある。
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                                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 SimplifyBooleanReturns
222//                                              if( fname.startsWith( prefix ) && fname.endsWith( sufix ) ) { return true; }    // 両方成立が条件
223//                                              return false;
224                                                return fname.startsWith( prefix ) && fname.endsWith( sufix );           // 両方成立が条件
225                                        }
226                                );
227
228        //                      addStartsWith( prefix );                                // ゼロ文字列の場合は、true になります。
229        //                      addEndsWith(   sufix  );                                // ゼロ文字列の場合は、true になります。
230                        }
231                }
232
233                return this;
234        }
235
236        /**
237         * 指定されたパスがこのマッチャのパターンに一致するかどうかを示します。
238         *
239         * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
240         * 未登録の場合は、true が返され、評価されません。
241         * これは、#allMatch( Path ) と同じ結果を返します。
242         *
243         * @param       path 照合するパス
244         * @return      パスがこのマッチャのパターンに一致した場合にのみtrue
245         * @see         #allMatch( Path )
246         */
247        @Override       // PathMatcher
248        public boolean matches( final Path path ) {
249                return allMatch( path );
250        }
251
252        /**
253         * すべての要素が、条件を満たす場合にのみ、有効となります。
254         *
255         * 内部の PathMatcher が、すべて true を返す場合のみ、true を返します。
256         * 未登録の場合は、true が返され、評価されません。
257         * これは、#matches( Path ) と同じ結果を返します。
258         *
259         * @param       path 判定対象の Pathオブジェクト
260         * @return      内部の PathMatcher が、すべて true を返す場合のみ、true
261         * @see         #matches( Path )
262         */
263        public boolean allMatch( final Path path ) {
264                // stream().allMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
265                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().allMatch( pMch -> pMch.matches( path ) );
266
267                if( flag ) {
268                        LOGGER.debug( () -> "allMatch : Path=" + path );
269                }
270
271                return flag;
272        }
273
274        /**
275         * いずれかの要素が、条件を満たす場合に、有効となります。
276         *
277         * 内部の PathMatcher の、いずれかが、true を返す場合に、true を返します。
278         * 未登録の場合は、true が返され、評価されません。
279         * この動きは、Set#anyMatch(java.util.function.Predicate)とは異なりますので、ご注意ください。
280         *
281         * @param       path 判定対象の Pathオブジェクト
282         * @return      内部の PathMatcher の、いずれかが、true を返す場合に、true
283         */
284        public boolean anyMatch( final Path path ) {
285                // stream().anyMatch の場合は、Collectionが未登録時は、false が返る為、明示的に処理が必要です。
286                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().anyMatch( pMch -> pMch.matches( path ) );
287
288                if( flag ) {
289                        LOGGER.debug( () -> "anyMatch : Path=" + path );
290                }
291
292                return flag;
293        }
294
295        /**
296         * 一致する要素が、ひとつも存在しない場合に、有効となります。
297         *
298         * 内部の PathMatcher の要素のすべてに、false を返す場合に、true を返します。
299         * 未登録の場合は、true が返され、評価されません。
300         *
301         * @param       path 判定対象の Pathオブジェクト
302         * @return 内部の PathMatcher の要素のすべてに、false を返す場合に、true
303         */
304        public boolean noneMatch( final Path path ) {
305                // stream().noneMatch で、Collectionが未登録時も、true ですが、明示的に示しておきます。
306                final boolean flag = pathMchSet.isEmpty() || pathMchSet.stream().noneMatch( pMch -> pMch.matches( path ) );
307
308                if( flag ) {
309                        LOGGER.debug( () -> "noneMatch : Path=" + path );
310                }
311
312                return flag;
313        }
314}