001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.commons.io.res.util;
020
021import java.util.ArrayList;
022import java.util.List;
023import java.util.StringTokenizer;
024
025import lucee.commons.lang.StringUtil;
026
027
028/**
029 * a WildcardPattern that accepts a comma- (or semi-colon-) separated value of patterns, e.g. "*.gif, *.jpg, *.jpeg, *.png"
030 * and an optional isExclude boolean value which negates the results of the default implementation
031 * 
032 * also, lines 31 - 35 allow to set isExclude to true by passing a pattern whose first character is an exclamation point '!'
033 * 
034 * @author Igal
035 */
036public class WildcardPattern {
037
038    private final String pattern;
039    private final boolean isInclude;
040    
041    private final List<ParsedPattern> patterns;
042    
043    /**
044     * 
045     * @param pattern - the wildcard pattern, or a comma/semi-colon separated value of wildcard patterns
046     * @param isCaseSensitive - if true, does a case-sensitive matching
047     * @param isExclude - if true, the filter becomes an Exclude filter so that only items that do not match the pattern are accepted
048     */
049    public WildcardPattern( String pattern, boolean isCaseSensitive, boolean isExclude, String delimiters ) {
050
051        if ( pattern.charAt( 0 ) == '!' ) {             // set isExclude to true if the first char of pattern is an exclamation point '!'
052                
053                pattern = pattern.substring( 1 );
054                isExclude = true;
055        }
056        
057        this.pattern = pattern;
058        this.isInclude = !isExclude;
059
060        StringTokenizer tokenizer = new StringTokenizer( pattern, !StringUtil.isEmpty(delimiters, true) ? delimiters : "|" );
061        
062        patterns = new ArrayList<ParsedPattern>();
063        
064        while ( tokenizer.hasMoreTokens() ) {
065            
066            String token = tokenizer.nextToken().trim();
067            
068            if ( !token.isEmpty() )
069                patterns.add( new ParsedPattern( token, isCaseSensitive ) );
070        }
071    }
072    
073    
074    /** calls this( pattern, isCaseSensitive, false, delimiters ); */
075    public WildcardPattern( String pattern, boolean isCaseSensitive, String delimiters ) {
076    
077        this( pattern, isCaseSensitive, false, delimiters );
078    }
079    
080    
081    public boolean isMatch( String input ) {
082        
083        for ( ParsedPattern pp : this.patterns ) {
084
085            if (pp.isMatch(input))
086                return isInclude;
087        }
088        
089        return !isInclude;
090    }
091    
092    
093    public String toString() {
094        
095        return "WildcardPattern: " + pattern;
096    }
097
098    public static class ParsedPattern {
099
100        public final static String MATCH_ANY = "*";
101        public final static String MATCH_ONE = "?";
102
103            private String[] parts;
104        private final boolean isCaseSensitive;
105
106
107        public ParsedPattern( String pattern, boolean isCaseSensitive ) {
108
109            this.isCaseSensitive = isCaseSensitive;
110
111            if (!isCaseSensitive)
112                pattern = pattern.toLowerCase();
113
114                List<String> lsp = new ArrayList<String>();
115
116            int len = pattern.length();
117            int subStart = 0;
118
119            for (int i=subStart; i<len; i++) {
120
121                char c = pattern.charAt( i );
122
123                if (c == '*' || c == '?') {
124
125                    if (i > subStart)
126                        lsp.add( pattern.substring( subStart, i ) );
127
128                    lsp.add(c == '*' ? MATCH_ANY : MATCH_ONE);
129                    subStart = i + 1;
130                }
131            }
132
133            if (len > subStart)
134                lsp.add(pattern.substring(subStart));
135
136                this.parts = lsp.toArray(new String[ lsp.size()] );
137        }
138
139
140        /** calls this( pattern, false, false ); */
141        public ParsedPattern( String pattern ) {
142
143            this( pattern, false );
144        }
145
146
147        /** tests if the input string matches the pattern */
148        public boolean isMatch( String input ) {
149
150            if ( !isCaseSensitive )
151                input = input.toLowerCase();
152
153                if (parts.length == 1)
154                        return ( parts[0] == MATCH_ANY || parts[0].equals(input) );
155
156                if (parts.length == 2) {
157
158                                if (parts[0] == MATCH_ANY)
159                                        return input.endsWith( parts[1] );
160
161                        if (parts[ parts.length - 1 ] == MATCH_ANY)
162                                return input.startsWith( parts[0] );
163                }
164
165            int pos = 0;
166            int len = input.length();
167
168            boolean doMatchAny = false;
169
170            for (String part : parts) {
171
172                if ( part == MATCH_ANY ) {
173
174                    doMatchAny = true;
175                    continue;
176                }
177
178                if ( part == MATCH_ONE ) {
179
180                    doMatchAny = false;
181                    pos++;
182                    continue;
183                }
184
185                int ix = input.indexOf( part, pos );
186
187                if ( ix == -1 )
188                    return false;
189
190                if ( !doMatchAny && ix != pos )
191                    return false;
192
193                pos = ix + part.length();
194                doMatchAny = false;
195            }
196
197            if ( (parts[ parts.length - 1 ] != MATCH_ANY) && (len != pos) )        // if pattern doesn't end with * then we shouldn't have any more characters in input
198                return false;
199
200            return true;
201        }
202
203
204        @Override
205        public String toString() {
206
207            StringBuilder sb = new StringBuilder();
208
209            for (String s : parts)
210                sb.append( s );
211
212            return sb.toString();
213        }
214    }
215}
216