001/* 002 * Copyright 2012-2018 the original author or authors. 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, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.context.properties.source; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.List; 023import java.util.Map; 024import java.util.function.Function; 025 026import org.springframework.util.Assert; 027 028/** 029 * A configuration property name composed of elements separated by dots. User created 030 * names may contain the characters "{@code a-z}" "{@code 0-9}") and "{@code -}", they 031 * must be lower-case and must start with an alpha-numeric character. The "{@code -}" is 032 * used purely for formatting, i.e. "{@code foo-bar}" and "{@code foobar}" are considered 033 * equivalent. 034 * <p> 035 * The "{@code [}" and "{@code ]}" characters may be used to indicate an associative 036 * index(i.e. a {@link Map} key or a {@link Collection} index. Indexes names are not 037 * restricted and are considered case-sensitive. 038 * <p> 039 * Here are some typical examples: 040 * <ul> 041 * <li>{@code spring.main.banner-mode}</li> 042 * <li>{@code server.hosts[0].name}</li> 043 * <li>{@code log[org.springboot].level}</li> 044 * </ul> 045 * 046 * @author Phillip Webb 047 * @author Madhura Bhave 048 * @since 2.0.0 049 * @see #of(CharSequence) 050 * @see ConfigurationPropertySource 051 */ 052public final class ConfigurationPropertyName 053 implements Comparable<ConfigurationPropertyName> { 054 055 private static final String EMPTY_STRING = ""; 056 057 /** 058 * An empty {@link ConfigurationPropertyName}. 059 */ 060 public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName( 061 Elements.EMPTY); 062 063 private Elements elements; 064 065 private final CharSequence[] uniformElements; 066 067 private String string; 068 069 private ConfigurationPropertyName(Elements elements) { 070 this.elements = elements; 071 this.uniformElements = new CharSequence[elements.getSize()]; 072 } 073 074 /** 075 * Returns {@code true} if this {@link ConfigurationPropertyName} is empty. 076 * @return {@code true} if the name is empty 077 */ 078 public boolean isEmpty() { 079 return this.elements.getSize() == 0; 080 } 081 082 /** 083 * Return if the last element in the name is indexed. 084 * @return {@code true} if the last element is indexed 085 */ 086 public boolean isLastElementIndexed() { 087 int size = getNumberOfElements(); 088 return (size > 0 && isIndexed(size - 1)); 089 } 090 091 /** 092 * Return if the element in the name is indexed. 093 * @param elementIndex the index of the element 094 * @return {@code true} if the element is indexed 095 */ 096 boolean isIndexed(int elementIndex) { 097 return this.elements.getType(elementIndex).isIndexed(); 098 } 099 100 /** 101 * Return if the element in the name is indexed and numeric. 102 * @param elementIndex the index of the element 103 * @return {@code true} if the element is indexed and numeric 104 */ 105 public boolean isNumericIndex(int elementIndex) { 106 return this.elements.getType(elementIndex) == ElementType.NUMERICALLY_INDEXED; 107 } 108 109 /** 110 * Return the last element in the name in the given form. 111 * @param form the form to return 112 * @return the last element 113 */ 114 public String getLastElement(Form form) { 115 int size = getNumberOfElements(); 116 return (size != 0) ? getElement(size - 1, form) : EMPTY_STRING; 117 } 118 119 /** 120 * Return an element in the name in the given form. 121 * @param elementIndex the element index 122 * @param form the form to return 123 * @return the last element 124 */ 125 public String getElement(int elementIndex, Form form) { 126 CharSequence element = this.elements.get(elementIndex); 127 ElementType type = this.elements.getType(elementIndex); 128 if (type.isIndexed()) { 129 return element.toString(); 130 } 131 if (form == Form.ORIGINAL) { 132 if (type != ElementType.NON_UNIFORM) { 133 return element.toString(); 134 } 135 return convertToOriginalForm(element).toString(); 136 } 137 if (form == Form.DASHED) { 138 if (type == ElementType.UNIFORM || type == ElementType.DASHED) { 139 return element.toString(); 140 } 141 return convertToDashedElement(element).toString(); 142 } 143 CharSequence uniformElement = this.uniformElements[elementIndex]; 144 if (uniformElement == null) { 145 uniformElement = (type != ElementType.UNIFORM) 146 ? convertToUniformElement(element) : element; 147 this.uniformElements[elementIndex] = uniformElement.toString(); 148 } 149 return uniformElement.toString(); 150 } 151 152 private CharSequence convertToOriginalForm(CharSequence element) { 153 return convertElement(element, false, (ch, i) -> ch == '_' 154 || ElementsParser.isValidChar(Character.toLowerCase(ch), i)); 155 } 156 157 private CharSequence convertToDashedElement(CharSequence element) { 158 return convertElement(element, true, ElementsParser::isValidChar); 159 } 160 161 private CharSequence convertToUniformElement(CharSequence element) { 162 return convertElement(element, true, 163 (ch, i) -> ElementsParser.isAlphaNumeric(ch)); 164 } 165 166 private CharSequence convertElement(CharSequence element, boolean lowercase, 167 ElementCharPredicate filter) { 168 StringBuilder result = new StringBuilder(element.length()); 169 for (int i = 0; i < element.length(); i++) { 170 char ch = lowercase ? Character.toLowerCase(element.charAt(i)) 171 : element.charAt(i); 172 if (filter.test(ch, i)) { 173 result.append(ch); 174 } 175 } 176 return result; 177 } 178 179 /** 180 * Return the total number of elements in the name. 181 * @return the number of elements 182 */ 183 public int getNumberOfElements() { 184 return this.elements.getSize(); 185 } 186 187 /** 188 * Create a new {@link ConfigurationPropertyName} by appending the given element 189 * value. 190 * @param elementValue the single element value to append 191 * @return a new {@link ConfigurationPropertyName} 192 * @throws InvalidConfigurationPropertyNameException if elementValue is not valid 193 */ 194 public ConfigurationPropertyName append(String elementValue) { 195 if (elementValue == null) { 196 return this; 197 } 198 Elements additionalElements = of(elementValue).elements; 199 return new ConfigurationPropertyName(this.elements.append(additionalElements)); 200 } 201 202 /** 203 * Return a new {@link ConfigurationPropertyName} by chopping this name to the given 204 * {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return 205 * {@code foo}. 206 * @param size the size to chop 207 * @return the chopped name 208 */ 209 public ConfigurationPropertyName chop(int size) { 210 if (size >= getNumberOfElements()) { 211 return this; 212 } 213 return new ConfigurationPropertyName(this.elements.chop(size)); 214 } 215 216 /** 217 * Returns {@code true} if this element is an immediate parent of the specified name. 218 * @param name the name to check 219 * @return {@code true} if this name is an ancestor 220 */ 221 public boolean isParentOf(ConfigurationPropertyName name) { 222 Assert.notNull(name, "Name must not be null"); 223 if (this.getNumberOfElements() != name.getNumberOfElements() - 1) { 224 return false; 225 } 226 return isAncestorOf(name); 227 } 228 229 /** 230 * Returns {@code true} if this element is an ancestor (immediate or nested parent) of 231 * the specified name. 232 * @param name the name to check 233 * @return {@code true} if this name is an ancestor 234 */ 235 public boolean isAncestorOf(ConfigurationPropertyName name) { 236 Assert.notNull(name, "Name must not be null"); 237 if (this.getNumberOfElements() >= name.getNumberOfElements()) { 238 return false; 239 } 240 for (int i = 0; i < this.elements.getSize(); i++) { 241 if (!elementEquals(this.elements, name.elements, i)) { 242 return false; 243 } 244 } 245 return true; 246 } 247 248 @Override 249 public int compareTo(ConfigurationPropertyName other) { 250 return compare(this, other); 251 } 252 253 private int compare(ConfigurationPropertyName n1, ConfigurationPropertyName n2) { 254 int l1 = n1.getNumberOfElements(); 255 int l2 = n2.getNumberOfElements(); 256 int i1 = 0; 257 int i2 = 0; 258 while (i1 < l1 || i2 < l2) { 259 try { 260 ElementType type1 = (i1 < l1) ? n1.elements.getType(i1) : null; 261 ElementType type2 = (i2 < l2) ? n2.elements.getType(i2) : null; 262 String e1 = (i1 < l1) ? n1.getElement(i1++, Form.UNIFORM) : null; 263 String e2 = (i2 < l2) ? n2.getElement(i2++, Form.UNIFORM) : null; 264 int result = compare(e1, type1, e2, type2); 265 if (result != 0) { 266 return result; 267 } 268 } 269 catch (ArrayIndexOutOfBoundsException ex) { 270 throw new RuntimeException(ex); 271 } 272 } 273 return 0; 274 } 275 276 private int compare(String e1, ElementType type1, String e2, ElementType type2) { 277 if (e1 == null) { 278 return -1; 279 } 280 if (e2 == null) { 281 return 1; 282 } 283 int result = Boolean.compare(type2.isIndexed(), type1.isIndexed()); 284 if (result != 0) { 285 return result; 286 } 287 if (type1 == ElementType.NUMERICALLY_INDEXED 288 && type2 == ElementType.NUMERICALLY_INDEXED) { 289 long v1 = Long.parseLong(e1); 290 long v2 = Long.parseLong(e2); 291 return Long.compare(v1, v2); 292 } 293 return e1.compareTo(e2); 294 } 295 296 @Override 297 public boolean equals(Object obj) { 298 if (obj == this) { 299 return true; 300 } 301 if (obj == null || obj.getClass() != getClass()) { 302 return false; 303 } 304 ConfigurationPropertyName other = (ConfigurationPropertyName) obj; 305 if (getNumberOfElements() != other.getNumberOfElements()) { 306 return false; 307 } 308 if (this.elements.canShortcutWithSource(ElementType.UNIFORM) 309 && other.elements.canShortcutWithSource(ElementType.UNIFORM)) { 310 return toString().equals(other.toString()); 311 } 312 for (int i = 0; i < this.elements.getSize(); i++) { 313 if (!elementEquals(this.elements, other.elements, i)) { 314 return false; 315 } 316 } 317 return true; 318 } 319 320 private boolean elementEquals(Elements e1, Elements e2, int i) { 321 int l1 = e1.getLength(i); 322 int l2 = e2.getLength(i); 323 boolean indexed1 = e1.getType(i).isIndexed(); 324 boolean indexed2 = e2.getType(i).isIndexed(); 325 int i1 = 0; 326 int i2 = 0; 327 while (i1 < l1) { 328 if (i2 >= l2) { 329 return false; 330 } 331 char ch1 = indexed1 ? e1.charAt(i, i1) 332 : Character.toLowerCase(e1.charAt(i, i1)); 333 char ch2 = indexed2 ? e2.charAt(i, i2) 334 : Character.toLowerCase(e2.charAt(i, i2)); 335 if (!indexed1 && !ElementsParser.isAlphaNumeric(ch1)) { 336 i1++; 337 } 338 else if (!indexed2 && !ElementsParser.isAlphaNumeric(ch2)) { 339 i2++; 340 } 341 else if (ch1 != ch2) { 342 return false; 343 } 344 else { 345 i1++; 346 i2++; 347 } 348 } 349 while (i2 < l2) { 350 char ch2 = Character.toLowerCase(e2.charAt(i, i2++)); 351 if (indexed2 || ElementsParser.isAlphaNumeric(ch2)) { 352 return false; 353 } 354 } 355 return true; 356 } 357 358 @Override 359 public int hashCode() { 360 return 0; 361 } 362 363 @Override 364 public String toString() { 365 if (this.string == null) { 366 this.string = buildToString(); 367 } 368 return this.string; 369 } 370 371 private String buildToString() { 372 if (this.elements.canShortcutWithSource(ElementType.UNIFORM, 373 ElementType.DASHED)) { 374 return this.elements.getSource().toString(); 375 } 376 StringBuilder result = new StringBuilder(); 377 for (int i = 0; i < getNumberOfElements(); i++) { 378 boolean indexed = isIndexed(i); 379 if (result.length() > 0 && !indexed) { 380 result.append('.'); 381 } 382 if (indexed) { 383 result.append("["); 384 result.append(getElement(i, Form.ORIGINAL)); 385 result.append("]"); 386 } 387 else { 388 result.append(getElement(i, Form.DASHED)); 389 } 390 } 391 return result.toString(); 392 } 393 394 /** 395 * Returns if the given name is valid. If this method returns {@code true} then the 396 * name may be used with {@link #of(CharSequence)} without throwing an exception. 397 * @param name the name to test 398 * @return {@code true} if the name is valid 399 */ 400 public static boolean isValid(CharSequence name) { 401 return of(name, true) != null; 402 } 403 404 /** 405 * Return a {@link ConfigurationPropertyName} for the specified string. 406 * @param name the source name 407 * @return a {@link ConfigurationPropertyName} instance 408 * @throws InvalidConfigurationPropertyNameException if the name is not valid 409 */ 410 public static ConfigurationPropertyName of(CharSequence name) { 411 return of(name, false); 412 } 413 414 /** 415 * Return a {@link ConfigurationPropertyName} for the specified string. 416 * @param name the source name 417 * @param returnNullIfInvalid if null should be returned if the name is not valid 418 * @return a {@link ConfigurationPropertyName} instance 419 * @throws InvalidConfigurationPropertyNameException if the name is not valid and 420 * {@code returnNullIfInvalid} is {@code false} 421 */ 422 static ConfigurationPropertyName of(CharSequence name, boolean returnNullIfInvalid) { 423 if (name == null) { 424 Assert.isTrue(returnNullIfInvalid, "Name must not be null"); 425 return null; 426 } 427 if (name.length() == 0) { 428 return EMPTY; 429 } 430 if (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.') { 431 if (returnNullIfInvalid) { 432 return null; 433 } 434 throw new InvalidConfigurationPropertyNameException(name, 435 Collections.singletonList('.')); 436 } 437 Elements elements = new ElementsParser(name, '.').parse(); 438 for (int i = 0; i < elements.getSize(); i++) { 439 if (elements.getType(i) == ElementType.NON_UNIFORM) { 440 if (returnNullIfInvalid) { 441 return null; 442 } 443 throw new InvalidConfigurationPropertyNameException(name, 444 getInvalidChars(elements, i)); 445 } 446 } 447 return new ConfigurationPropertyName(elements); 448 } 449 450 private static List<Character> getInvalidChars(Elements elements, int index) { 451 List<Character> invalidChars = new ArrayList<>(); 452 for (int charIndex = 0; charIndex < elements.getLength(index); charIndex++) { 453 char ch = elements.charAt(index, charIndex); 454 if (!ElementsParser.isValidChar(ch, charIndex)) { 455 invalidChars.add(ch); 456 } 457 } 458 return invalidChars; 459 } 460 461 /** 462 * Create a {@link ConfigurationPropertyName} by adapting the given source. See 463 * {@link #adapt(CharSequence, char, Function)} for details. 464 * @param name the name to parse 465 * @param separator the separator used to split the name 466 * @return a {@link ConfigurationPropertyName} 467 */ 468 static ConfigurationPropertyName adapt(CharSequence name, char separator) { 469 return adapt(name, separator, null); 470 } 471 472 /** 473 * Create a {@link ConfigurationPropertyName} by adapting the given source. The name 474 * is split into elements around the given {@code separator}. This method is more 475 * lenient than {@link #of} in that it allows mixed case names and '{@code _}' 476 * characters. Other invalid characters are stripped out during parsing. 477 * <p> 478 * The {@code elementValueProcessor} function may be used if additional processing is 479 * required on the extracted element values. 480 * @param name the name to parse 481 * @param separator the separator used to split the name 482 * @param elementValueProcessor a function to process element values 483 * @return a {@link ConfigurationPropertyName} 484 */ 485 static ConfigurationPropertyName adapt(CharSequence name, char separator, 486 Function<CharSequence, CharSequence> elementValueProcessor) { 487 Assert.notNull(name, "Name must not be null"); 488 if (name.length() == 0) { 489 return EMPTY; 490 } 491 Elements elements = new ElementsParser(name, separator) 492 .parse(elementValueProcessor); 493 if (elements.getSize() == 0) { 494 return EMPTY; 495 } 496 return new ConfigurationPropertyName(elements); 497 } 498 499 /** 500 * The various forms that a non-indexed element value can take. 501 */ 502 public enum Form { 503 504 /** 505 * The original form as specified when the name was created or adapted. For 506 * example: 507 * <ul> 508 * <li>"{@code foo-bar}" = "{@code foo-bar}"</li> 509 * <li>"{@code fooBar}" = "{@code fooBar}"</li> 510 * <li>"{@code foo_bar}" = "{@code foo_bar}"</li> 511 * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> 512 * </ul> 513 */ 514 ORIGINAL, 515 516 /** 517 * The dashed configuration form (used for toString; lower-case with only 518 * alphanumeric characters and dashes). 519 * <ul> 520 * <li>"{@code foo-bar}" = "{@code foo-bar}"</li> 521 * <li>"{@code fooBar}" = "{@code foobar}"</li> 522 * <li>"{@code foo_bar}" = "{@code foobar}"</li> 523 * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> 524 * </ul> 525 */ 526 DASHED, 527 528 /** 529 * The uniform configuration form (used for equals/hashCode; lower-case with only 530 * alphanumeric characters). 531 * <ul> 532 * <li>"{@code foo-bar}" = "{@code foobar}"</li> 533 * <li>"{@code fooBar}" = "{@code foobar}"</li> 534 * <li>"{@code foo_bar}" = "{@code foobar}"</li> 535 * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li> 536 * </ul> 537 */ 538 UNIFORM 539 540 } 541 542 /** 543 * Allows access to the individual elements that make up the name. We store the 544 * indexes in arrays rather than a list of object in order to conserve memory. 545 */ 546 private static class Elements { 547 548 private static final int[] NO_POSITION = {}; 549 550 private static final ElementType[] NO_TYPE = {}; 551 552 public static final Elements EMPTY = new Elements("", 0, NO_POSITION, NO_POSITION, 553 NO_TYPE, null); 554 555 private final CharSequence source; 556 557 private final int size; 558 559 private final int[] start; 560 561 private final int[] end; 562 563 private final ElementType[] type; 564 565 /** 566 * Contains any resolved elements or can be {@code null} if there aren't any. 567 * Resolved elements allow us to modify the element values in some way (or example 568 * when adapting with a mapping function, or when append has been called). Note 569 * that this array is not used as a cache, in fact, when it's not null then 570 * {@link #canShortcutWithSource} will always return false which may hurt 571 * performance. 572 */ 573 private final CharSequence[] resolved; 574 575 Elements(CharSequence source, int size, int[] start, int[] end, 576 ElementType[] type, CharSequence[] resolved) { 577 super(); 578 this.source = source; 579 this.size = size; 580 this.start = start; 581 this.end = end; 582 this.type = type; 583 this.resolved = resolved; 584 } 585 586 public Elements append(Elements additional) { 587 Assert.isTrue(additional.getSize() == 1, () -> "Element value '" 588 + additional.getSource() + "' must be a single item"); 589 ElementType[] type = new ElementType[this.size + 1]; 590 System.arraycopy(this.type, 0, type, 0, this.size); 591 type[this.size] = additional.type[0]; 592 CharSequence[] resolved = newResolved(this.size + 1); 593 resolved[this.size] = additional.get(0); 594 return new Elements(this.source, this.size + 1, this.start, this.end, type, 595 resolved); 596 } 597 598 public Elements chop(int size) { 599 CharSequence[] resolved = newResolved(size); 600 return new Elements(this.source, size, this.start, this.end, this.type, 601 resolved); 602 } 603 604 private CharSequence[] newResolved(int size) { 605 CharSequence[] resolved = new CharSequence[size]; 606 if (this.resolved != null) { 607 System.arraycopy(this.resolved, 0, resolved, 0, 608 Math.min(size, this.size)); 609 } 610 return resolved; 611 } 612 613 public int getSize() { 614 return this.size; 615 } 616 617 public CharSequence get(int index) { 618 if (this.resolved != null && this.resolved[index] != null) { 619 return this.resolved[index]; 620 } 621 int start = this.start[index]; 622 int end = this.end[index]; 623 return this.source.subSequence(start, end); 624 } 625 626 public int getLength(int index) { 627 if (this.resolved != null && this.resolved[index] != null) { 628 return this.resolved[index].length(); 629 } 630 int start = this.start[index]; 631 int end = this.end[index]; 632 return end - start; 633 } 634 635 public char charAt(int index, int charIndex) { 636 if (this.resolved != null && this.resolved[index] != null) { 637 return this.resolved[index].charAt(charIndex); 638 } 639 int start = this.start[index]; 640 return this.source.charAt(start + charIndex); 641 } 642 643 public ElementType getType(int index) { 644 return this.type[index]; 645 } 646 647 public CharSequence getSource() { 648 return this.source; 649 } 650 651 /** 652 * Returns if the element source can be used as a shortcut for an operation such 653 * as {@code equals} or {@code toString}. 654 * @param requiredType the required type 655 * @return {@code true} if all elements match at least one of the types 656 */ 657 public boolean canShortcutWithSource(ElementType requiredType) { 658 return canShortcutWithSource(requiredType, requiredType); 659 } 660 661 /** 662 * Returns if the element source can be used as a shortcut for an operation such 663 * as {@code equals} or {@code toString}. 664 * @param requiredType the required type 665 * @param alternativeType and alternative required type 666 * @return {@code true} if all elements match at least one of the types 667 */ 668 public boolean canShortcutWithSource(ElementType requiredType, 669 ElementType alternativeType) { 670 if (this.resolved != null) { 671 return false; 672 } 673 for (int i = 0; i < this.size; i++) { 674 ElementType type = this.type[i]; 675 if (type != requiredType && type != alternativeType) { 676 return false; 677 } 678 if (i > 0 && this.end[i - 1] + 1 != this.start[i]) { 679 return false; 680 } 681 } 682 return true; 683 } 684 685 } 686 687 /** 688 * Main parsing logic used to convert a {@link CharSequence} to {@link Elements}. 689 */ 690 private static class ElementsParser { 691 692 private static final int DEFAULT_CAPACITY = 6; 693 694 private final CharSequence source; 695 696 private final char separator; 697 698 private int size; 699 700 private int[] start; 701 702 private int[] end; 703 704 private ElementType[] type; 705 706 private CharSequence[] resolved; 707 708 ElementsParser(CharSequence source, char separator) { 709 this(source, separator, DEFAULT_CAPACITY); 710 } 711 712 ElementsParser(CharSequence source, char separator, int capacity) { 713 this.source = source; 714 this.separator = separator; 715 this.start = new int[capacity]; 716 this.end = new int[capacity]; 717 this.type = new ElementType[capacity]; 718 } 719 720 public Elements parse() { 721 return parse(null); 722 } 723 724 public Elements parse(Function<CharSequence, CharSequence> valueProcessor) { 725 int length = this.source.length(); 726 int openBracketCount = 0; 727 int start = 0; 728 ElementType type = ElementType.EMPTY; 729 for (int i = 0; i < length; i++) { 730 char ch = this.source.charAt(i); 731 if (ch == '[') { 732 if (openBracketCount == 0) { 733 add(start, i, type, valueProcessor); 734 start = i + 1; 735 type = ElementType.NUMERICALLY_INDEXED; 736 } 737 openBracketCount++; 738 } 739 else if (ch == ']') { 740 openBracketCount--; 741 if (openBracketCount == 0) { 742 add(start, i, type, valueProcessor); 743 start = i + 1; 744 type = ElementType.EMPTY; 745 } 746 } 747 else if (!type.isIndexed() && ch == this.separator) { 748 add(start, i, type, valueProcessor); 749 start = i + 1; 750 type = ElementType.EMPTY; 751 } 752 else { 753 type = updateType(type, ch, i - start); 754 } 755 } 756 if (openBracketCount != 0) { 757 type = ElementType.NON_UNIFORM; 758 } 759 add(start, length, type, valueProcessor); 760 return new Elements(this.source, this.size, this.start, this.end, this.type, 761 this.resolved); 762 } 763 764 private ElementType updateType(ElementType existingType, char ch, int index) { 765 if (existingType.isIndexed()) { 766 if (existingType == ElementType.NUMERICALLY_INDEXED && !isNumeric(ch)) { 767 return ElementType.INDEXED; 768 } 769 return existingType; 770 } 771 if (existingType == ElementType.EMPTY && isValidChar(ch, index)) { 772 return (index == 0) ? ElementType.UNIFORM : ElementType.NON_UNIFORM; 773 } 774 if (existingType == ElementType.UNIFORM && ch == '-') { 775 return ElementType.DASHED; 776 } 777 if (!isValidChar(ch, index)) { 778 if (existingType == ElementType.EMPTY 779 && !isValidChar(Character.toLowerCase(ch), index)) { 780 return ElementType.EMPTY; 781 } 782 return ElementType.NON_UNIFORM; 783 } 784 return existingType; 785 } 786 787 private void add(int start, int end, ElementType type, 788 Function<CharSequence, CharSequence> valueProcessor) { 789 if ((end - start) < 1 || type == ElementType.EMPTY) { 790 return; 791 } 792 if (this.start.length <= end) { 793 this.start = expand(this.start); 794 this.end = expand(this.end); 795 this.type = expand(this.type); 796 this.resolved = expand(this.resolved); 797 } 798 if (valueProcessor != null) { 799 if (this.resolved == null) { 800 this.resolved = new CharSequence[this.start.length]; 801 } 802 CharSequence resolved = valueProcessor 803 .apply(this.source.subSequence(start, end)); 804 Elements resolvedElements = new ElementsParser(resolved, '.').parse(); 805 Assert.state(resolvedElements.getSize() == 1, 806 "Resolved element must not contain multiple elements"); 807 this.resolved[this.size] = resolvedElements.get(0); 808 type = resolvedElements.getType(0); 809 } 810 this.start[this.size] = start; 811 this.end[this.size] = end; 812 this.type[this.size] = type; 813 this.size++; 814 } 815 816 private int[] expand(int[] src) { 817 int[] dest = new int[src.length + DEFAULT_CAPACITY]; 818 System.arraycopy(src, 0, dest, 0, src.length); 819 return dest; 820 } 821 822 private ElementType[] expand(ElementType[] src) { 823 ElementType[] dest = new ElementType[src.length + DEFAULT_CAPACITY]; 824 System.arraycopy(src, 0, dest, 0, src.length); 825 return dest; 826 } 827 828 private CharSequence[] expand(CharSequence[] src) { 829 if (src == null) { 830 return null; 831 } 832 CharSequence[] dest = new CharSequence[src.length + DEFAULT_CAPACITY]; 833 System.arraycopy(src, 0, dest, 0, src.length); 834 return dest; 835 } 836 837 public static boolean isValidChar(char ch, int index) { 838 return isAlpha(ch) || isNumeric(ch) || (index != 0 && ch == '-'); 839 } 840 841 public static boolean isAlphaNumeric(char ch) { 842 return isAlpha(ch) || isNumeric(ch); 843 } 844 845 private static boolean isAlpha(char ch) { 846 return ch >= 'a' && ch <= 'z'; 847 } 848 849 private static boolean isNumeric(char ch) { 850 return ch >= '0' && ch <= '9'; 851 } 852 853 } 854 855 /** 856 * The various types of element that we can detect. 857 */ 858 private enum ElementType { 859 860 /** 861 * The element is logically empty (contains no valid chars). 862 */ 863 EMPTY(false), 864 865 /** 866 * The element is a uniform name (a-z, 0-9, no dashes, lowercase). 867 */ 868 UNIFORM(false), 869 870 /** 871 * The element is almost uniform, but it contains (but does not start with) at 872 * least one dash. 873 */ 874 DASHED(false), 875 876 /** 877 * The element contains non uniform characters and will need to be converted. 878 */ 879 NON_UNIFORM(false), 880 881 /** 882 * The element is non-numerically indexed. 883 */ 884 INDEXED(true), 885 886 /** 887 * The element is numerically indexed. 888 */ 889 NUMERICALLY_INDEXED(true); 890 891 private final boolean indexed; 892 893 ElementType(boolean indexed) { 894 this.indexed = indexed; 895 } 896 897 public boolean isIndexed() { 898 return this.indexed; 899 } 900 901 } 902 903 /** 904 * Predicate used to filter element chars. 905 */ 906 private interface ElementCharPredicate { 907 908 boolean test(char ch, int index); 909 910 } 911 912}