1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.lang3;
18
19 import java.io.Serializable;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.LinkedHashSet;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.stream.Stream;
26
27 /**
28 * A set of characters.
29 *
30 * <p>
31 * Instances are immutable, but instances of subclasses may not be.
32 * </p>
33 *
34 * <p>
35 * #ThreadSafe#
36 * </p>
37 *
38 * @since 1.0
39 */
40 public class CharSet implements Serializable {
41
42 /**
43 * Required for serialization support. Lang version 2.0.
44 *
45 * @see java.io.Serializable
46 */
47 private static final long serialVersionUID = 5947847346149275958L;
48
49 /**
50 * A CharSet defining no characters.
51 *
52 * @since 2.0
53 */
54 public static final CharSet EMPTY = new CharSet((String) null);
55
56 /**
57 * A CharSet defining ASCII alphabetic characters "a-zA-Z".
58 *
59 * @since 2.0
60 */
61 public static final CharSet ASCII_ALPHA = new CharSet("a-zA-Z");
62
63 /**
64 * A CharSet defining ASCII alphabetic characters "a-z".
65 *
66 * @since 2.0
67 */
68 public static final CharSet ASCII_ALPHA_LOWER = new CharSet("a-z");
69
70 /**
71 * A CharSet defining ASCII alphabetic characters "A-Z".
72 *
73 * @since 2.0
74 */
75 public static final CharSet ASCII_ALPHA_UPPER = new CharSet("A-Z");
76
77 /**
78 * A CharSet defining ASCII alphabetic characters "0-9".
79 *
80 * @since 2.0
81 */
82 public static final CharSet ASCII_NUMERIC = new CharSet("0-9");
83
84 /**
85 * A Map of the common cases used in the factory.
86 * <p>
87 * Subclasses can add more common patterns if desired.
88 * </p>
89 *
90 * @since 2.0
91 */
92 protected static final Map<String, CharSet> COMMON = Collections.synchronizedMap(new HashMap<>());
93
94 static {
95 COMMON.put(null, EMPTY);
96 COMMON.put(StringUtils.EMPTY, EMPTY);
97 COMMON.put("a-zA-Z", ASCII_ALPHA);
98 COMMON.put("A-Za-z", ASCII_ALPHA);
99 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 }