001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3;
018
019import java.io.Serializable;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.LinkedHashSet;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Stream;
026
027/**
028 * A set of characters.
029 *
030 * <p>
031 * Instances are immutable, but instances of subclasses may not be.
032 * </p>
033 *
034 * <p>
035 * #ThreadSafe#
036 * </p>
037 *
038 * @since 1.0
039 */
040public class CharSet implements Serializable {
041
042    /**
043     * Required for serialization support. Lang version 2.0.
044     *
045     * @see java.io.Serializable
046     */
047    private static final long serialVersionUID = 5947847346149275958L;
048
049    /**
050     * A CharSet defining no characters.
051     *
052     * @since 2.0
053     */
054    public static final CharSet EMPTY = new CharSet((String) null);
055
056    /**
057     * A CharSet defining ASCII alphabetic characters "a-zA-Z".
058     *
059     * @since 2.0
060     */
061    public static final CharSet ASCII_ALPHA = new CharSet("a-zA-Z");
062
063    /**
064     * A CharSet defining ASCII alphabetic characters "a-z".
065     *
066     * @since 2.0
067     */
068    public static final CharSet ASCII_ALPHA_LOWER = new CharSet("a-z");
069
070    /**
071     * A CharSet defining ASCII alphabetic characters "A-Z".
072     *
073     * @since 2.0
074     */
075    public static final CharSet ASCII_ALPHA_UPPER = new CharSet("A-Z");
076
077    /**
078     * A CharSet defining ASCII alphabetic characters "0-9".
079     *
080     * @since 2.0
081     */
082    public static final CharSet ASCII_NUMERIC = new CharSet("0-9");
083
084    /**
085     * A Map of the common cases used in the factory.
086     * <p>
087     * Subclasses can add more common patterns if desired.
088     * </p>
089     *
090     * @since 2.0
091     */
092    protected static final Map<String, CharSet> COMMON = Collections.synchronizedMap(new HashMap<>());
093
094    static {
095        COMMON.put(null, EMPTY);
096        COMMON.put(StringUtils.EMPTY, EMPTY);
097        COMMON.put("a-zA-Z", ASCII_ALPHA);
098        COMMON.put("A-Za-z", ASCII_ALPHA);
099        COMMON.put("a-z", ASCII_ALPHA_LOWER);
100        COMMON.put("A-Z", ASCII_ALPHA_UPPER);
101        COMMON.put("0-9", ASCII_NUMERIC);
102    }
103
104    /**
105     * Gets a new CharSet using the syntax described below.
106     *
107     * <ul>
108     *  <li>{@code null} or empty string ("")
109     * - set containing no characters</li>
110     *  <li>Single character, such as "a"
111     *  - set containing just that character</li>
112     *  <li>Multi character, such as "a-e"
113     *  - set containing characters from one character to the other</li>
114     *  <li>Negated, such as "^a" or "^a-e"
115     *  - set containing all characters except those defined</li>
116     *  <li>Combinations, such as "abe-g"
117     *  - set containing all the characters from the individual sets</li>
118     * </ul>
119     *
120     * <p>
121     * The matching order is:
122     * </p>
123     * <ol>
124     *  <li>Negated multi character range, such as "^a-e"</li>
125     *  <li>Ordinary multi character range, such as "a-e"</li>
126     *  <li>Negated single character, such as "^a"</li>
127     *  <li>Ordinary single character, such as "a"</li>
128     * </ol>
129     *
130     * <p>
131     * Matching works left to right. Once a match is found the
132     * search starts again from the next character.
133     * </p>
134     *
135     * <p>
136     * If the same range is defined twice using the same syntax, only
137     * one range will be kept.
138     * Thus, "a-ca-c" creates only one range of "a-c".
139     * </p>
140     *
141     * <p>
142     * If the start and end of a range are in the wrong order,
143     * they are reversed. Thus "a-e" is the same as "e-a".
144     * As a result, "a-ee-a" would create only one range,
145     * as the "a-e" and "e-a" are the same.
146     * </p>
147     *
148     * <p>
149     * The set of characters represented is the union of the specified ranges.
150     * </p>
151     *
152     * <p>
153     * There are two ways to add a literal negation character ({@code ^}):
154     * </p>
155     * <ul>
156     *     <li>As the last character in a string, e.g. {@code CharSet.getInstance("a-z^")}</li>
157     *     <li>As a separate element, e.g. {@code CharSet.getInstance("^", "a-z")}</li>
158     * </ul>
159     *
160     * <p>
161     * Examples using the negation character:
162     * </p>
163     * <pre>
164     *     CharSet.getInstance("^a-c").contains('a') = false
165     *     CharSet.getInstance("^a-c").contains('d') = true
166     *     CharSet.getInstance("^^a-c").contains('a') = true // (only '^' is negated)
167     *     CharSet.getInstance("^^a-c").contains('^') = false
168     *     CharSet.getInstance("^a-cd-f").contains('d') = true
169     *     CharSet.getInstance("a-c^").contains('^') = true
170     *     CharSet.getInstance("^", "a-c").contains('^') = true
171     * </pre>
172     *
173     * <p>
174     * All CharSet objects returned by this method will be immutable.
175     * </p>
176     *
177     * @param setStrs  Strings to merge into the set, may be null.
178     * @return A CharSet instance.
179     * @since 2.4
180     */
181    public static CharSet getInstance(final String... setStrs) {
182        if (setStrs == null) {
183            return EMPTY;
184        }
185        if (setStrs.length == 1) {
186            final CharSet common = COMMON.get(setStrs[0]);
187            if (common != null) {
188                return common;
189            }
190        }
191        return new CharSet(setStrs);
192    }
193
194    /** The set of CharRange objects. */
195    private final Set<CharRange> set = Collections.synchronizedSet(new LinkedHashSet<>());
196
197    /**
198     * Lock object for synchronizing access.
199     */
200    private final Serializable lock = new SerializableObject();
201
202    /**
203     * Constructs a new CharSet using the set syntax.
204     * Each string is merged in with the set.
205     *
206     * @param set  Strings to merge into the initial set.
207     * @throws NullPointerException Thrown if set is {@code null}.
208     */
209    protected CharSet(final String... set) {
210        Stream.of(set).forEach(this::add);
211    }
212
213    /**
214     * Add a set definition string to the {@link CharSet}.
215     *
216     * @param str  set definition string
217     */
218    protected void add(final String str) {
219        if (str == null) {
220            return;
221        }
222        final int len = str.length();
223        int pos = 0;
224        while (pos < len) {
225            final int remainder = len - pos;
226            if (remainder >= 4 && str.charAt(pos) == '^' && str.charAt(pos + 2) == '-') {
227                // negated range
228                set.add(CharRange.isNotIn(str.charAt(pos + 1), str.charAt(pos + 3)));
229                pos += 4;
230            } else if (remainder >= 3 && str.charAt(pos + 1) == '-') {
231                // range
232                set.add(CharRange.isIn(str.charAt(pos), str.charAt(pos + 2)));
233                pos += 3;
234            } else if (remainder >= 2 && str.charAt(pos) == '^') {
235                // negated char
236                set.add(CharRange.isNot(str.charAt(pos + 1)));
237                pos += 2;
238            } else {
239                // char
240                set.add(CharRange.is(str.charAt(pos)));
241                pos += 1;
242            }
243        }
244    }
245
246    /**
247     * Tests whether this {@link CharSet} contain the specified character {@code ch}.
248     * <p>
249     * Examples using the negation character:
250     * </p>
251     * <pre>
252     *     CharSet.getInstance("^a-c").contains('a') = false
253     *     CharSet.getInstance("^a-c").contains('d') = true
254     *     CharSet.getInstance("^^a-c").contains('a') = true // (only '^' is negated)
255     *     CharSet.getInstance("^^a-c").contains('^') = false
256     *     CharSet.getInstance("^a-cd-f").contains('d') = true
257     *     CharSet.getInstance("a-c^").contains('^') = true
258     *     CharSet.getInstance("^", "a-c").contains('^') = true
259     * </pre>
260     *
261     * @param ch The character to check.
262     * @return {@code true} if the set contains the characters.
263     */
264    public boolean contains(final char ch) {
265        synchronized (lock) {
266            return set.stream().anyMatch(range -> range.contains(ch));
267        }
268    }
269
270    /**
271     * Compares two {@link CharSet} objects, returning true if they represent
272     * exactly the same set of characters defined in the same way.
273     *
274     * <p>
275     * The two sets {@code abc} and {@code a-c} are <em>not</em>
276     * equal according to this method.
277     * </p>
278     *
279     * @param obj  The object to compare.
280     * @return true if equal.
281     * @since 2.0
282     */
283    @Override
284    public boolean equals(final Object obj) {
285        if (obj == this) {
286            return true;
287        }
288        if (!(obj instanceof CharSet)) {
289            return false;
290        }
291        final CharSet other = (CharSet) obj;
292        return set.equals(other.set);
293    }
294
295    /**
296     * Gets the set of character ranges.
297     * <p>
298     * Package private for testing.
299     * </p>
300     *
301     * @return The set of character ranges.
302     */
303    Set<CharRange> getCharRanges() {
304        return set;
305    }
306
307    /**
308     * Gets a hash code compatible with the equals method.
309     *
310     * @return A suitable hash code.
311     * @since 2.0
312     */
313    @Override
314    public int hashCode() {
315        return 89 + set.hashCode();
316    }
317
318    /**
319     * Gets a string representation of the set.
320     *
321     * @return string representation of the set.
322     */
323    @Override
324    public String toString() {
325        return set.toString();
326    }
327
328}