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}