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}