• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 15794111114

21 Jun 2025 08:54AM UTC coverage: 81.755% (+0.008%) from 81.747%
15794111114

Pull #286

github

web-flow
Merge e7915e795 into 8470e463d
Pull Request #286: 开发分支

59270 of 77451 branches covered (76.53%)

Branch coverage included in aggregate %.

140365 of 166736 relevant lines covered (84.18%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.8
today-context/src/main/java/infra/context/properties/source/ConfigurationPropertyName.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.properties.source;
19

20
import java.util.ArrayList;
21
import java.util.Collection;
22
import java.util.Collections;
23
import java.util.List;
24
import java.util.Locale;
25
import java.util.Map;
26
import java.util.function.Function;
27
import java.util.function.IntFunction;
28

29
import infra.lang.Assert;
30
import infra.lang.Nullable;
31
import infra.util.StringUtils;
32

33
/**
34
 * A configuration property name composed of elements separated by dots. User created
35
 * names may contain the characters "{@code a-z}" "{@code 0-9}") and "{@code -}", they
36
 * must be lower-case and must start with an alpha-numeric character. The "{@code -}" is
37
 * used purely for formatting, i.e. "{@code foo-bar}" and "{@code foobar}" are considered
38
 * equivalent.
39
 * <p>
40
 * The "{@code [}" and "{@code ]}" characters may be used to indicate an associative
41
 * index(i.e. a {@link Map} key or a {@link Collection} index). Indexes names are not
42
 * restricted and are considered case-sensitive.
43
 * <p>
44
 * Here are some typical examples:
45
 * <ul>
46
 * <li>{@code app.main.banner-mode}</li>
47
 * <li>{@code server.hosts[0].name}</li>
48
 * <li>{@code log[infra].level}</li>
49
 * </ul>
50
 *
51
 * @author Phillip Webb
52
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
53
 * @author Madhura Bhave
54
 * @see #of(CharSequence)
55
 * @see ConfigurationPropertySource
56
 * @since 4.0
57
 */
58
public final class ConfigurationPropertyName implements Comparable<ConfigurationPropertyName> {
59

60
  private static final String EMPTY_STRING = "";
61

62
  /**
63
   * An empty {@link ConfigurationPropertyName}.
64
   */
65
  public static final ConfigurationPropertyName EMPTY = new ConfigurationPropertyName(Elements.EMPTY);
6✔
66

67
  private final Elements elements;
68

69
  private final CharSequence[] uniformElements;
70

71
  private int hashCode;
72

73
  private final String[] string = new String[ToStringFormat.values().length];
5✔
74

75
  @Nullable
76
  private Boolean hasDashedElement;
77

78
  @Nullable
79
  private ConfigurationPropertyName systemEnvironmentLegacyName;
80

81
  private ConfigurationPropertyName(Elements elements) {
2✔
82
    this.elements = elements;
3✔
83
    this.uniformElements = new CharSequence[elements.size];
5✔
84
  }
1✔
85

86
  /**
87
   * Returns {@code true} if this {@link ConfigurationPropertyName} is empty.
88
   *
89
   * @return {@code true} if the name is empty
90
   */
91
  public boolean isEmpty() {
92
    return this.elements.size == 0;
8✔
93
  }
94

95
  /**
96
   * Return if the last element in the name is indexed.
97
   *
98
   * @return {@code true} if the last element is indexed
99
   */
100
  public boolean isLastElementIndexed() {
101
    int size = getNumberOfElements();
3✔
102
    return (size > 0 && isIndexed(size - 1));
12!
103
  }
104

105
  /**
106
   * Return {@code true} if any element in the name is indexed.
107
   *
108
   * @return if the element has one or more indexed elements
109
   */
110
  public boolean hasIndexedElement() {
111
    for (int i = 0; i < getNumberOfElements(); i++) {
8✔
112
      if (isIndexed(i)) {
4✔
113
        return true;
2✔
114
      }
115
    }
116
    return false;
2✔
117
  }
118

119
  /**
120
   * Return if the element in the name is indexed.
121
   *
122
   * @param elementIndex the index of the element
123
   * @return {@code true} if the element is indexed
124
   */
125
  boolean isIndexed(int elementIndex) {
126
    return this.elements.getType(elementIndex).isIndexed();
6✔
127
  }
128

129
  /**
130
   * Return if the element in the name is indexed and numeric.
131
   *
132
   * @param elementIndex the index of the element
133
   * @return {@code true} if the element is indexed and numeric
134
   */
135
  public boolean isNumericIndex(int elementIndex) {
136
    return this.elements.getType(elementIndex) == ElementType.NUMERICALLY_INDEXED;
10✔
137
  }
138

139
  /**
140
   * Return the last element in the name in the given form.
141
   *
142
   * @param form the form to return
143
   * @return the last element
144
   */
145
  public String getLastElement(Form form) {
146
    int size = getNumberOfElements();
3✔
147
    return (size != 0) ? getElement(size - 1, form) : EMPTY_STRING;
11✔
148
  }
149

150
  /**
151
   * Return an element in the name in the given form.
152
   *
153
   * @param elementIndex the element index
154
   * @param form the form to return
155
   * @return the last element
156
   */
157
  public String getElement(int elementIndex, Form form) {
158
    CharSequence element = this.elements.get(elementIndex);
5✔
159
    ElementType type = this.elements.getType(elementIndex);
5✔
160
    if (type.isIndexed()) {
3✔
161
      return element.toString();
3✔
162
    }
163
    if (form == Form.ORIGINAL) {
3✔
164
      if (type != ElementType.NON_UNIFORM) {
3✔
165
        return element.toString();
3✔
166
      }
167
      return convertToOriginalForm(element).toString();
5✔
168
    }
169
    if (form == Form.DASHED) {
3✔
170
      if (type == ElementType.UNIFORM || type == ElementType.DASHED) {
6✔
171
        return element.toString();
3✔
172
      }
173
      return convertToDashedElement(element).toString();
5✔
174
    }
175
    CharSequence uniformElement = this.uniformElements[elementIndex];
5✔
176
    if (uniformElement == null) {
2✔
177
      uniformElement = (type != ElementType.UNIFORM) ? convertToUniformElement(element) : element;
9✔
178
      this.uniformElements[elementIndex] = uniformElement.toString();
6✔
179
    }
180
    return uniformElement.toString();
3✔
181
  }
182

183
  private CharSequence convertToOriginalForm(CharSequence element) {
184
    return convertElement(element, false,
6✔
185
            (ch, i) -> ch == '_' || ElementsParser.isValidChar(Character.toLowerCase(ch), i));
12✔
186
  }
187

188
  private CharSequence convertToDashedElement(CharSequence element) {
189
    return convertElement(element, true, ElementsParser::isValidChar);
6✔
190
  }
191

192
  private CharSequence convertToUniformElement(CharSequence element) {
193
    return convertElement(element, true, (ch, i) -> ElementsParser.isAlphaNumeric(ch));
9✔
194
  }
195

196
  private CharSequence convertElement(CharSequence element, boolean lowercase, ElementCharPredicate filter) {
197
    int length = element.length();
3✔
198
    StringBuilder result = new StringBuilder(length);
5✔
199
    for (int i = 0; i < length; i++) {
7✔
200
      char ch = lowercase ? Character.toLowerCase(element.charAt(i)) : element.charAt(i);
11✔
201
      if (filter.test(ch, i)) {
5✔
202
        result.append(ch);
4✔
203
      }
204
    }
205
    return result;
2✔
206
  }
207

208
  /**
209
   * Return the total number of elements in the name.
210
   *
211
   * @return the number of elements
212
   */
213
  public int getNumberOfElements() {
214
    return this.elements.size;
4✔
215
  }
216

217
  /**
218
   * Create a new {@link ConfigurationPropertyName} by appending the given suffix.
219
   *
220
   * @param suffix the elements to append
221
   * @return a new {@link ConfigurationPropertyName}
222
   * @throws InvalidConfigurationPropertyNameException if the result is not valid
223
   */
224
  public ConfigurationPropertyName append(String suffix) {
225
    if (StringUtils.isEmpty(suffix)) {
3✔
226
      return this;
2✔
227
    }
228
    Elements additionalElements = probablySingleElementOf(suffix);
3✔
229
    return new ConfigurationPropertyName(this.elements.append(additionalElements));
8✔
230
  }
231

232
  /**
233
   * Create a new {@link ConfigurationPropertyName} by appending the given suffix.
234
   *
235
   * @param suffix the elements to append
236
   * @return a new {@link ConfigurationPropertyName}
237
   */
238
  public ConfigurationPropertyName append(@Nullable ConfigurationPropertyName suffix) {
239
    if (suffix == null) {
2✔
240
      return this;
2✔
241
    }
242
    return new ConfigurationPropertyName(this.elements.append(suffix.elements));
9✔
243
  }
244

245
  /**
246
   * Return the parent of this {@link ConfigurationPropertyName} or
247
   * {@link ConfigurationPropertyName#EMPTY} if there is no parent.
248
   *
249
   * @return the parent name
250
   */
251
  public ConfigurationPropertyName getParent() {
252
    int numberOfElements = getNumberOfElements();
3✔
253
    return numberOfElements <= 1 ? EMPTY : chop(numberOfElements - 1);
11✔
254
  }
255

256
  /**
257
   * Return a new {@link ConfigurationPropertyName} by chopping this name to the given
258
   * {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return
259
   * {@code foo}.
260
   *
261
   * @param size the size to chop
262
   * @return the chopped name
263
   */
264
  public ConfigurationPropertyName chop(int size) {
265
    if (size >= getNumberOfElements()) {
4✔
266
      return this;
2✔
267
    }
268
    return new ConfigurationPropertyName(this.elements.chop(size));
8✔
269
  }
270

271
  /**
272
   * Return a new {@link ConfigurationPropertyName} by based on this name offset by
273
   * specific element index. For example, {@code chop(1)} on the name {@code foo.bar}
274
   * will return {@code bar}.
275
   *
276
   * @param offset the element offset
277
   * @return the sub name
278
   */
279
  public ConfigurationPropertyName subName(int offset) {
280
    if (offset == 0) {
2✔
281
      return this;
2✔
282
    }
283
    if (offset == getNumberOfElements()) {
4✔
284
      return EMPTY;
2✔
285
    }
286
    if (offset < 0 || offset > getNumberOfElements()) {
6✔
287
      throw new IndexOutOfBoundsException("Offset: " + offset + ", NumberOfElements: " + getNumberOfElements());
8✔
288
    }
289
    return new ConfigurationPropertyName(this.elements.subElements(offset));
8✔
290
  }
291

292
  /**
293
   * Returns {@code true} if this element is an immediate parent of the specified name.
294
   *
295
   * @param name the name to check
296
   * @return {@code true} if this name is an ancestor
297
   */
298
  public boolean isParentOf(ConfigurationPropertyName name) {
299
    Assert.notNull(name, "Name is required");
3✔
300
    if (getNumberOfElements() != name.getNumberOfElements() - 1) {
7✔
301
      return false;
2✔
302
    }
303
    return isAncestorOf(name);
4✔
304
  }
305

306
  /**
307
   * Returns {@code true} if this element is an ancestor (immediate or nested parent) of
308
   * the specified name.
309
   *
310
   * @param name the name to check
311
   * @return {@code true} if this name is an ancestor
312
   */
313
  public boolean isAncestorOf(ConfigurationPropertyName name) {
314
    Assert.notNull(name, "Name is required");
3✔
315
    if (getNumberOfElements() >= name.getNumberOfElements()) {
5✔
316
      return false;
2✔
317
    }
318
    return endsWithElementsEqualTo(name);
4✔
319
  }
320

321
  @Override
322
  public int compareTo(ConfigurationPropertyName other) {
323
    return compare(this, other);
5✔
324
  }
325

326
  private int compare(ConfigurationPropertyName n1, ConfigurationPropertyName n2) {
327
    int l1 = n1.getNumberOfElements();
3✔
328
    int l2 = n2.getNumberOfElements();
3✔
329
    int i1 = 0;
2✔
330
    int i2 = 0;
2✔
331
    while (i1 < l1 || i2 < l2) {
6✔
332
      ElementType type1 = (i1 < l1) ? n1.elements.getType(i1) : null;
10✔
333
      ElementType type2 = (i2 < l2) ? n2.elements.getType(i2) : null;
9!
334
      String e1 = (i1 < l1) ? n1.getElement(i1++, Form.UNIFORM) : null;
11✔
335
      String e2 = (i2 < l2) ? n2.getElement(i2++, Form.UNIFORM) : null;
10!
336
      int result = compare(e1, type1, e2, type2);
7✔
337
      if (result != 0) {
2✔
338
        return result;
2✔
339
      }
340
    }
1✔
341
    return 0;
2✔
342
  }
343

344
  private int compare(@Nullable String e1, ElementType type1, @Nullable String e2, ElementType type2) {
345
    if (e1 == null) {
2✔
346
      return -1;
2✔
347
    }
348
    if (e2 == null) {
2!
349
      return 1;
×
350
    }
351
    int result = Boolean.compare(type2.isIndexed(), type1.isIndexed());
6✔
352
    if (result != 0) {
2✔
353
      return result;
2✔
354
    }
355
    if (type1 == ElementType.NUMERICALLY_INDEXED && type2 == ElementType.NUMERICALLY_INDEXED) {
6!
356
      long v1 = Long.parseLong(e1);
3✔
357
      long v2 = Long.parseLong(e2);
3✔
358
      return Long.compare(v1, v2);
4✔
359
    }
360
    return e1.compareTo(e2);
4✔
361
  }
362

363
  @Override
364
  public boolean equals(Object obj) {
365
    if (obj == this) {
3✔
366
      return true;
2✔
367
    }
368
    if (obj == null || obj.getClass() != getClass()) {
7!
369
      return false;
×
370
    }
371
    ConfigurationPropertyName other = (ConfigurationPropertyName) obj;
3✔
372
    if (getNumberOfElements() != other.getNumberOfElements()) {
5✔
373
      return false;
2✔
374
    }
375
    if (this.elements.canShortcutWithSource(ElementType.UNIFORM)
8✔
376
            && other.elements.canShortcutWithSource(ElementType.UNIFORM)) {
2✔
377
      return toString().equals(other.toString());
6✔
378
    }
379
    if (hashCode() != other.hashCode()) {
5✔
380
      return false;
2✔
381
    }
382
    if (toStringMatches(toString(), other.toString())) {
7✔
383
      return true;
2✔
384
    }
385
    return endsWithElementsEqualTo(other);
4✔
386
  }
387

388
  private boolean toStringMatches(String s1, String s2) {
389
    return s1.hashCode() == s2.hashCode() && s1.equals(s2);
13!
390
  }
391

392
  private boolean endsWithElementsEqualTo(ConfigurationPropertyName name) {
393
    for (int i = this.elements.size - 1; i >= 0; i--) {
10✔
394
      if (elementDiffers(this.elements, name.elements, i)) {
8✔
395
        return false;
2✔
396
      }
397
    }
398
    return true;
2✔
399
  }
400

401
  private boolean elementDiffers(Elements e1, Elements e2, int i) {
402
    ElementType type1 = e1.getType(i);
4✔
403
    ElementType type2 = e2.getType(i);
4✔
404
    if (type1.allowsFastEqualityCheck() && type2.allowsFastEqualityCheck()) {
6✔
405
      return !fastElementEquals(e1, e2, i);
10✔
406
    }
407
    if (type1.allowsDashIgnoringEqualityCheck() && type2.allowsDashIgnoringEqualityCheck()) {
6✔
408
      return !dashIgnoringElementEquals(e1, e2, i);
10✔
409
    }
410
    return !defaultElementEquals(e1, e2, i);
10✔
411
  }
412

413
  private boolean fastElementEquals(Elements e1, Elements e2, int i) {
414
    int length1 = e1.getLength(i);
4✔
415
    int length2 = e2.getLength(i);
4✔
416
    if (length1 == length2) {
3✔
417
      int i1 = 0;
2✔
418
      while (length1-- != 0) {
3✔
419
        char ch1 = e1.charAt(i, i1);
5✔
420
        char ch2 = e2.charAt(i, i1);
5✔
421
        if (ch1 != ch2) {
3✔
422
          return false;
2✔
423
        }
424
        i1++;
1✔
425
      }
1✔
426
      return true;
2✔
427
    }
428
    return false;
2✔
429
  }
430

431
  private boolean dashIgnoringElementEquals(Elements e1, Elements e2, int i) {
432
    int l1 = e1.getLength(i);
4✔
433
    int l2 = e2.getLength(i);
4✔
434
    int i1 = 0;
2✔
435
    int i2 = 0;
2✔
436
    while (i1 < l1) {
3✔
437
      if (i2 >= l2) {
3✔
438
        return remainderIsDashes(e1, i, i1);
6✔
439
      }
440
      char ch1 = e1.charAt(i, i1);
5✔
441
      char ch2 = e2.charAt(i, i2);
5✔
442
      if (ch1 == '-') {
3✔
443
        i1++;
2✔
444
      }
445
      else if (ch2 == '-') {
3✔
446
        i2++;
2✔
447
      }
448
      else if (ch1 != ch2) {
3✔
449
        return false;
2✔
450
      }
451
      else {
452
        i1++;
1✔
453
        i2++;
1✔
454
      }
455
    }
1✔
456
    if (i2 < l2) {
3✔
457
      if (e2.getType(i).isIndexed()) {
5!
458
        return false;
×
459
      }
460
      do {
461
        char ch2 = e2.charAt(i, i2++);
6✔
462
        if (ch2 != '-') {
3!
463
          return false;
×
464
        }
465
      }
466
      while (i2 < l2);
3✔
467
    }
468
    return true;
2✔
469
  }
470

471
  private boolean defaultElementEquals(Elements e1, Elements e2, int i) {
472
    int l1 = e1.getLength(i);
4✔
473
    int l2 = e2.getLength(i);
4✔
474
    boolean indexed1 = e1.getType(i).isIndexed();
5✔
475
    boolean indexed2 = e2.getType(i).isIndexed();
5✔
476
    int i1 = 0;
2✔
477
    int i2 = 0;
2✔
478
    while (i1 < l1) {
3✔
479
      if (i2 >= l2) {
3✔
480
        return remainderIsNotAlphanumeric(e1, i, i1);
6✔
481
      }
482
      char ch1 = indexed1 ? e1.charAt(i, i1) : Character.toLowerCase(e1.charAt(i, i1));
13✔
483
      char ch2 = indexed2 ? e2.charAt(i, i2) : Character.toLowerCase(e2.charAt(i, i2));
13✔
484
      if (!indexed1 && !ElementsParser.isAlphaNumeric(ch1)) {
5✔
485
        i1++;
2✔
486
      }
487
      else if (!indexed2 && !ElementsParser.isAlphaNumeric(ch2)) {
5✔
488
        i2++;
2✔
489
      }
490
      else if (ch1 != ch2) {
3✔
491
        return false;
2✔
492
      }
493
      else {
494
        i1++;
1✔
495
        i2++;
1✔
496
      }
497
    }
1✔
498
    if (i2 < l2) {
3✔
499
      return remainderIsNotAlphanumeric(e2, i, i2);
6✔
500
    }
501
    return true;
2✔
502
  }
503

504
  private boolean remainderIsNotAlphanumeric(Elements elements, int element, int index) {
505
    if (elements.getType(element).isIndexed()) {
5✔
506
      return false;
2✔
507
    }
508
    int length = elements.getLength(element);
4✔
509
    do {
510
      char c = Character.toLowerCase(elements.charAt(element, index++));
7✔
511
      if (ElementsParser.isAlphaNumeric(c)) {
3✔
512
        return false;
2✔
513
      }
514
    }
515
    while (index < length);
3!
516
    return true;
×
517
  }
518

519
  private boolean remainderIsDashes(Elements elements, int element, int index) {
520
    if (elements.getType(element).isIndexed()) {
5!
521
      return false;
×
522
    }
523
    int length = elements.getLength(element);
4✔
524
    do {
525
      char c = elements.charAt(element, index++);
6✔
526
      if (c != '-') {
3✔
527
        return false;
2✔
528
      }
529
    }
530
    while (index < length);
3✔
531
    return true;
2✔
532
  }
533

534
  @Override
535
  public int hashCode() {
536
    int hashCode = this.hashCode;
3✔
537
    Elements elements = this.elements;
3✔
538
    if (hashCode == 0 && elements.size != 0) {
5✔
539
      for (int elementIndex = 0; elementIndex < elements.size; elementIndex++) {
8✔
540
        hashCode = 31 * hashCode + elements.hashCode(elementIndex);
8✔
541
      }
542
      this.hashCode = hashCode;
3✔
543
    }
544
    return hashCode;
2✔
545
  }
546

547
  @Nullable
548
  ConfigurationPropertyName asSystemEnvironmentLegacyName() {
549
    ConfigurationPropertyName name = this.systemEnvironmentLegacyName;
3✔
550
    if (name == null) {
2✔
551
      name = ConfigurationPropertyName
4✔
552
              .ofIfValid(buildSimpleToString('.', (i) -> getElement(i, Form.DASHED).replace('-', '.')));
11✔
553
      this.systemEnvironmentLegacyName = (name != null) ? name : EMPTY;
6!
554
    }
555
    return name != EMPTY ? name : null;
6!
556
  }
557

558
  @Override
559
  public String toString() {
560
    return toString(ToStringFormat.DEFAULT, false);
5✔
561
  }
562

563
  String toString(ToStringFormat format, boolean upperCase) {
564
    String string = this.string[format.ordinal()];
6✔
565
    if (string == null) {
2✔
566
      string = buildToString(format);
4✔
567
      this.string[format.ordinal()] = string;
6✔
568
    }
569
    return (!upperCase) ? string : string.toUpperCase(Locale.ENGLISH);
8✔
570
  }
571

572
  private String buildToString(ToStringFormat format) {
573
    return switch (format) {
6✔
574
      case DEFAULT -> buildDefaultToString();
3✔
575
      case SYSTEM_ENVIRONMENT -> buildSimpleToString('_', i -> getElement(i, Form.UNIFORM));
11✔
576
      case LEGACY_SYSTEM_ENVIRONMENT -> buildSimpleToString('_', i -> getElement(i, Form.ORIGINAL).replace('-', '_'));
13✔
577
    };
578
  }
579

580
  private String buildDefaultToString() {
581
    if (this.elements.canShortcutWithSource(ElementType.UNIFORM, ElementType.DASHED)) {
6✔
582
      return this.elements.source.toString();
5✔
583
    }
584
    int elements = getNumberOfElements();
3✔
585
    StringBuilder result = new StringBuilder(elements * 8);
7✔
586
    for (int i = 0; i < elements; i++) {
7✔
587
      boolean indexed = isIndexed(i);
4✔
588
      if (!result.isEmpty() && !indexed) {
5✔
589
        result.append('.');
4✔
590
      }
591
      if (indexed) {
2✔
592
        result.append('[');
4✔
593
        result.append(getElement(i, Form.ORIGINAL));
7✔
594
        result.append(']');
5✔
595
      }
596
      else {
597
        result.append(getElement(i, Form.DASHED));
7✔
598
      }
599
    }
600
    return result.toString();
3✔
601
  }
602

603
  private String buildSimpleToString(char joinChar, IntFunction<String> elementConverter) {
604
    StringBuilder result = new StringBuilder();
4✔
605
    for (int i = 0; i < getNumberOfElements(); i++) {
8✔
606
      if (!result.isEmpty()) {
3✔
607
        result.append(joinChar);
4✔
608
      }
609
      result.append(elementConverter.apply(i));
7✔
610
    }
611
    return result.toString();
3✔
612
  }
613

614
  boolean hasDashedElement() {
615
    Boolean hasDashedElement = this.hasDashedElement;
3✔
616
    if (hasDashedElement != null) {
2✔
617
      return hasDashedElement;
3✔
618
    }
619
    for (int i = 0; i < getNumberOfElements(); i++) {
8✔
620
      if (getElement(i, Form.DASHED).indexOf('-') != -1) {
8✔
621
        this.hasDashedElement = true;
4✔
622
        return true;
2✔
623
      }
624
    }
625
    this.hasDashedElement = false;
4✔
626
    return false;
2✔
627
  }
628

629
  /**
630
   * Returns if the given name is valid. If this method returns {@code true} then the
631
   * name may be used with {@link #of(CharSequence)} without throwing an exception.
632
   *
633
   * @param name the name to test
634
   * @return {@code true} if the name is valid
635
   */
636
  public static boolean isValid(CharSequence name) {
637
    return of(name, true) != null;
8✔
638
  }
639

640
  /**
641
   * Return a {@link ConfigurationPropertyName} for the specified string.
642
   *
643
   * @param name the source name
644
   * @return a {@link ConfigurationPropertyName} instance
645
   * @throws InvalidConfigurationPropertyNameException if the name is not valid
646
   */
647
  public static ConfigurationPropertyName of(CharSequence name) {
648
    return of(name, false);
4✔
649
  }
650

651
  /**
652
   * Return a {@link ConfigurationPropertyName} for the specified string or {@code null}
653
   * if the name is not valid.
654
   *
655
   * @param name the source name
656
   * @return a {@link ConfigurationPropertyName} instance
657
   */
658
  @Nullable
659
  public static ConfigurationPropertyName ofIfValid(CharSequence name) {
660
    return of(name, true);
4✔
661
  }
662

663
  /**
664
   * Return a {@link ConfigurationPropertyName} for the specified string.
665
   *
666
   * @param name the source name
667
   * @param returnNullIfInvalid if null should be returned if the name is not valid
668
   * @return a {@link ConfigurationPropertyName} instance
669
   * @throws InvalidConfigurationPropertyNameException if the name is not valid and
670
   * {@code returnNullIfInvalid} is {@code false}
671
   */
672
  @Nullable
673
  static ConfigurationPropertyName of(@Nullable CharSequence name, boolean returnNullIfInvalid) {
674
    Elements elements = elementsOf(name, returnNullIfInvalid);
4✔
675
    return (elements != null) ? new ConfigurationPropertyName(elements) : null;
9✔
676
  }
677

678
  private static Elements probablySingleElementOf(CharSequence name) {
679
    return elementsOf(name, false, 1);
5✔
680
  }
681

682
  @Nullable
683
  private static Elements elementsOf(@Nullable CharSequence name, boolean returnNullIfInvalid) {
684
    return elementsOf(name, returnNullIfInvalid, ElementsParser.DEFAULT_CAPACITY);
5✔
685
  }
686

687
  @Nullable
688
  private static Elements elementsOf(@Nullable CharSequence name, boolean returnNullIfInvalid, int parserCapacity) {
689
    if (name == null) {
2✔
690
      Assert.isTrue(returnNullIfInvalid, "Name is required");
3✔
691
      return null;
2✔
692
    }
693
    if (name.isEmpty()) {
3✔
694
      return Elements.EMPTY;
2✔
695
    }
696
    if (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.') {
13✔
697
      if (returnNullIfInvalid) {
2✔
698
        return null;
2✔
699
      }
700
      throw new InvalidConfigurationPropertyNameException(name, Collections.singletonList('.'));
8✔
701
    }
702
    Elements elements = new ElementsParser(name, '.', parserCapacity).parse();
8✔
703
    for (int i = 0; i < elements.size; i++) {
8✔
704
      if (elements.getType(i) == ElementType.NON_UNIFORM) {
5✔
705
        if (returnNullIfInvalid) {
2✔
706
          return null;
2✔
707
        }
708
        throw new InvalidConfigurationPropertyNameException(name, getInvalidChars(elements, i));
8✔
709
      }
710
    }
711
    return elements;
2✔
712
  }
713

714
  private static List<Character> getInvalidChars(Elements elements, int index) {
715
    int length = elements.getLength(index);
4✔
716
    var invalidChars = new ArrayList<Character>();
4✔
717
    for (int charIndex = 0; charIndex < length; charIndex++) {
7✔
718
      char ch = elements.charAt(index, charIndex);
5✔
719
      if (!ElementsParser.isValidChar(ch, charIndex)) {
4✔
720
        invalidChars.add(ch);
5✔
721
      }
722
    }
723
    return invalidChars;
2✔
724
  }
725

726
  /**
727
   * Create a {@link ConfigurationPropertyName} by adapting the given source. See
728
   * {@link #adapt(CharSequence, char, Function)} for details.
729
   *
730
   * @param name the name to parse
731
   * @param separator the separator used to split the name
732
   * @return a {@link ConfigurationPropertyName}
733
   */
734
  public static ConfigurationPropertyName adapt(CharSequence name, char separator) {
735
    return adapt(name, separator, null);
5✔
736
  }
737

738
  /**
739
   * Create a {@link ConfigurationPropertyName} by adapting the given source. The name
740
   * is split into elements around the given {@code separator}. This method is more
741
   * lenient than {@link #of} in that it allows mixed case names and '{@code _}'
742
   * characters. Other invalid characters are stripped out during parsing.
743
   * <p>
744
   * The {@code elementValueProcessor} function may be used if additional processing is
745
   * required on the extracted element values.
746
   *
747
   * @param name the name to parse
748
   * @param separator the separator used to split the name
749
   * @param elementValueProcessor a function to process element values
750
   * @return a {@link ConfigurationPropertyName}
751
   */
752
  static ConfigurationPropertyName adapt(CharSequence name, char separator,
753
          @Nullable Function<CharSequence, CharSequence> elementValueProcessor) {
754
    Assert.notNull(name, "Name is required");
3✔
755
    if (name.isEmpty()) {
3✔
756
      return EMPTY;
2✔
757
    }
758
    Elements elements = new ElementsParser(name, separator).parse(elementValueProcessor);
8✔
759
    if (elements.size == 0) {
3✔
760
      return EMPTY;
2✔
761
    }
762
    return new ConfigurationPropertyName(elements);
5✔
763
  }
764

765
  /**
766
   * The various forms that a non-indexed element value can take.
767
   */
768
  public enum Form {
3✔
769

770
    /**
771
     * The original form as specified when the name was created or adapted. For
772
     * example:
773
     * <ul>
774
     * <li>"{@code foo-bar}" = "{@code foo-bar}"</li>
775
     * <li>"{@code fooBar}" = "{@code fooBar}"</li>
776
     * <li>"{@code foo_bar}" = "{@code foo_bar}"</li>
777
     * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
778
     * </ul>
779
     */
780
    ORIGINAL,
6✔
781

782
    /**
783
     * The dashed configuration form (used for toString; lower-case with only
784
     * alphanumeric characters and dashes).
785
     * <ul>
786
     * <li>"{@code foo-bar}" = "{@code foo-bar}"</li>
787
     * <li>"{@code fooBar}" = "{@code foobar}"</li>
788
     * <li>"{@code foo_bar}" = "{@code foobar}"</li>
789
     * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
790
     * </ul>
791
     */
792
    DASHED,
6✔
793

794
    /**
795
     * The uniform configuration form (used for equals/hashCode; lower-case with only
796
     * alphanumeric characters).
797
     * <ul>
798
     * <li>"{@code foo-bar}" = "{@code foobar}"</li>
799
     * <li>"{@code fooBar}" = "{@code foobar}"</li>
800
     * <li>"{@code foo_bar}" = "{@code foobar}"</li>
801
     * <li>"{@code [Foo.bar]}" = "{@code Foo.bar}"</li>
802
     * </ul>
803
     */
804
    UNIFORM
6✔
805

806
  }
807

808
  /**
809
   * Allows access to the individual elements that make up the name. We store the
810
   * indexes in arrays rather than a list of object in order to conserve memory.
811
   */
812
  private static class Elements {
813

814
    private static final int[] NO_POSITION = {};
3✔
815

816
    private static final ElementType[] NO_TYPE = {};
3✔
817

818
    public static final Elements EMPTY = new Elements("", 0, NO_POSITION, NO_POSITION, NO_TYPE, null, null);
12✔
819

820
    public final CharSequence source;
821

822
    private final int size;
823

824
    private final int[] start;
825

826
    private final int[] end;
827

828
    private final ElementType[] type;
829

830
    private final int[] hashCode;
831

832
    /**
833
     * Contains any resolved elements or can be {@code null} if there aren't any.
834
     * Resolved elements allow us to modify the element values in some way (or example
835
     * when adapting with a mapping function, or when append has been called). Note
836
     * that this array is not used as a cache, in fact, when it's not null then
837
     * {@link #canShortcutWithSource} will always return false which may hurt
838
     * performance.
839
     */
840
    @Nullable
841
    private final CharSequence[] resolved;
842

843
    Elements(CharSequence source, int size, int[] start, int[] end,
844
            ElementType[] type, @Nullable int[] hashCode, @Nullable CharSequence[] resolved) {
2✔
845
      this.source = source;
3✔
846
      this.size = size;
3✔
847
      this.start = start;
3✔
848
      this.end = end;
3✔
849
      this.type = type;
3✔
850
      this.hashCode = hashCode != null ? hashCode : new int[size];
8✔
851
      this.resolved = resolved;
3✔
852
    }
1✔
853

854
    Elements append(Elements additional) {
855
      int size = this.size + additional.size;
6✔
856
      ElementType[] type = new ElementType[size];
3✔
857
      int[] hashCode = new int[size];
3✔
858
      System.arraycopy(this.type, 0, type, 0, this.size);
8✔
859
      System.arraycopy(additional.type, 0, type, this.size, additional.size);
9✔
860
      System.arraycopy(this.hashCode, 0, hashCode, 0, this.size);
8✔
861
      System.arraycopy(additional.hashCode, 0, hashCode, this.size, additional.size);
9✔
862
      CharSequence[] resolved = newResolved(0, size);
5✔
863
      for (int i = 0; i < additional.size; i++) {
8✔
864
        resolved[this.size + i] = additional.get(i);
9✔
865
      }
866
      return new Elements(this.source, size, this.start, this.end, type, hashCode, resolved);
14✔
867
    }
868

869
    Elements chop(int size) {
870
      CharSequence[] resolved = newResolved(0, size);
5✔
871
      return new Elements(this.source, size, this.start, this.end, this.type, this.hashCode, resolved);
16✔
872
    }
873

874
    Elements subElements(int offset) {
875
      int size = this.size - offset;
5✔
876
      CharSequence[] resolved = newResolved(offset, size);
5✔
877
      int[] start = new int[size];
3✔
878
      System.arraycopy(this.start, offset, start, 0, size);
7✔
879
      int[] end = new int[size];
3✔
880
      System.arraycopy(this.end, offset, end, 0, size);
7✔
881
      ElementType[] type = new ElementType[size];
3✔
882
      System.arraycopy(this.type, offset, type, 0, size);
7✔
883
      int[] hashCode = new int[size];
3✔
884
      System.arraycopy(this.hashCode, offset, hashCode, 0, size);
7✔
885
      return new Elements(this.source, size, start, end, type, hashCode, resolved);
12✔
886
    }
887

888
    private CharSequence[] newResolved(int offset, int size) {
889
      CharSequence[] resolved = new CharSequence[size];
3✔
890
      if (this.resolved != null) {
3✔
891
        System.arraycopy(this.resolved, offset, resolved, 0, Math.min(size, this.size));
10✔
892
      }
893
      return resolved;
2✔
894
    }
895

896
    CharSequence get(int index) {
897
      if (this.resolved != null && this.resolved[index] != null) {
8✔
898
        return this.resolved[index];
5✔
899
      }
900
      int start = this.start[index];
5✔
901
      int end = this.end[index];
5✔
902
      return this.source.subSequence(start, end);
6✔
903
    }
904

905
    int getLength(int index) {
906
      if (this.resolved != null && this.resolved[index] != null) {
8✔
907
        return this.resolved[index].length();
6✔
908
      }
909
      int start = this.start[index];
5✔
910
      int end = this.end[index];
5✔
911
      return end - start;
4✔
912
    }
913

914
    char charAt(int index, int charIndex) {
915
      if (this.resolved != null && this.resolved[index] != null) {
8✔
916
        return this.resolved[index].charAt(charIndex);
7✔
917
      }
918
      int start = this.start[index];
5✔
919
      return this.source.charAt(start + charIndex);
7✔
920
    }
921

922
    ElementType getType(int index) {
923
      return this.type[index];
5✔
924
    }
925

926
    int hashCode(int index) {
927
      int hashCode = this.hashCode[index];
5✔
928
      if (hashCode == 0) {
2✔
929
        boolean indexed = getType(index).isIndexed();
5✔
930
        int length = getLength(index);
4✔
931
        for (int i = 0; i < length; i++) {
7✔
932
          char ch = charAt(index, i);
5✔
933
          if (!indexed) {
2✔
934
            ch = Character.toLowerCase(ch);
3✔
935
          }
936
          if (ElementsParser.isAlphaNumeric(ch)) {
3✔
937
            hashCode = 31 * hashCode + ch;
6✔
938
          }
939
        }
940
        this.hashCode[index] = hashCode;
5✔
941
      }
942
      return hashCode;
2✔
943
    }
944

945
    /**
946
     * Returns if the element source can be used as a shortcut for an operation such
947
     * as {@code equals} or {@code toString}.
948
     *
949
     * @param requiredType the required type
950
     * @return {@code true} if all elements match at least one of the types
951
     */
952
    boolean canShortcutWithSource(ElementType requiredType) {
953
      return canShortcutWithSource(requiredType, requiredType);
5✔
954
    }
955

956
    /**
957
     * Returns if the element source can be used as a shortcut for an operation such
958
     * as {@code equals} or {@code toString}.
959
     *
960
     * @param requiredType the required type
961
     * @param alternativeType and alternative required type
962
     * @return {@code true} if all elements match at least one of the types
963
     */
964
    boolean canShortcutWithSource(ElementType requiredType, ElementType alternativeType) {
965
      if (this.resolved != null) {
3✔
966
        return false;
2✔
967
      }
968
      int size = this.size;
3✔
969
      int[] end = this.end;
3✔
970
      int[] start = this.start;
3✔
971
      ElementType[] thisTypes = this.type;
3✔
972
      for (int i = 0; i < size; i++) {
7✔
973
        ElementType type = thisTypes[i];
4✔
974
        if (type != requiredType && type != alternativeType) {
6✔
975
          return false;
2✔
976
        }
977
        if (i > 0 && end[i - 1] + 1 != start[i]) {
13!
978
          return false;
×
979
        }
980
      }
981
      return true;
2✔
982
    }
983

984
  }
985

986
  /**
987
   * Main parsing logic used to convert a {@link CharSequence} to {@link Elements}.
988
   */
989
  private static class ElementsParser {
990

991
    private static final int DEFAULT_CAPACITY = 6;
992

993
    private final CharSequence source;
994

995
    private final char separator;
996

997
    private int size;
998

999
    private int[] start;
1000

1001
    private int[] end;
1002

1003
    private ElementType[] type;
1004

1005
    @Nullable
1006
    private CharSequence[] resolved;
1007

1008
    ElementsParser(CharSequence source, char separator) {
1009
      this(source, separator, DEFAULT_CAPACITY);
5✔
1010
    }
1✔
1011

1012
    ElementsParser(CharSequence source, char separator, int capacity) {
2✔
1013
      this.source = source;
3✔
1014
      this.separator = separator;
3✔
1015
      this.start = new int[capacity];
4✔
1016
      this.end = new int[capacity];
4✔
1017
      this.type = new ElementType[capacity];
4✔
1018
    }
1✔
1019

1020
    Elements parse() {
1021
      return parse(null);
4✔
1022
    }
1023

1024
    Elements parse(@Nullable Function<CharSequence, CharSequence> valueProcessor) {
1025
      int length = this.source.length();
4✔
1026
      int openBracketCount = 0;
2✔
1027
      int start = 0;
2✔
1028
      ElementType type = ElementType.EMPTY;
2✔
1029
      for (int i = 0; i < length; i++) {
7✔
1030
        char ch = this.source.charAt(i);
5✔
1031
        if (ch == '[') {
3✔
1032
          if (openBracketCount == 0) {
2✔
1033
            add(start, i, type, valueProcessor);
6✔
1034
            start = i + 1;
4✔
1035
            type = ElementType.NUMERICALLY_INDEXED;
2✔
1036
          }
1037
          openBracketCount++;
2✔
1038
        }
1039
        else if (ch == ']') {
3✔
1040
          openBracketCount--;
1✔
1041
          if (openBracketCount == 0) {
2✔
1042
            add(start, i, type, valueProcessor);
6✔
1043
            start = i + 1;
4✔
1044
            type = ElementType.EMPTY;
3✔
1045
          }
1046
        }
1047
        else if (!type.isIndexed() && ch == this.separator) {
7✔
1048
          add(start, i, type, valueProcessor);
6✔
1049
          start = i + 1;
4✔
1050
          type = ElementType.EMPTY;
3✔
1051
        }
1052
        else {
1053
          type = updateType(type, ch, i - start);
8✔
1054
        }
1055
      }
1056
      if (openBracketCount != 0) {
2✔
1057
        type = ElementType.NON_UNIFORM;
2✔
1058
      }
1059
      add(start, length, type, valueProcessor);
6✔
1060
      return new Elements(this.source, this.size, this.start, this.end, this.type, null, this.resolved);
17✔
1061
    }
1062

1063
    private ElementType updateType(ElementType existingType, char ch, int index) {
1064
      if (existingType.isIndexed()) {
3✔
1065
        if (existingType == ElementType.NUMERICALLY_INDEXED && !isNumeric(ch)) {
6✔
1066
          return ElementType.INDEXED;
2✔
1067
        }
1068
        return existingType;
2✔
1069
      }
1070
      if (existingType == ElementType.EMPTY && isValidChar(ch, index)) {
7✔
1071
        return (index == 0) ? ElementType.UNIFORM : ElementType.NON_UNIFORM;
6✔
1072
      }
1073
      if (existingType == ElementType.UNIFORM && ch == '-') {
6✔
1074
        return ElementType.DASHED;
2✔
1075
      }
1076
      if (!isValidChar(ch, index)) {
4✔
1077
        if (existingType == ElementType.EMPTY && !isValidChar(Character.toLowerCase(ch), index)) {
8✔
1078
          return ElementType.EMPTY;
2✔
1079
        }
1080
        return ElementType.NON_UNIFORM;
2✔
1081
      }
1082
      return existingType;
2✔
1083
    }
1084

1085
    private void add(int start, int end, ElementType type,
1086
            @Nullable Function<CharSequence, CharSequence> valueProcessor) {
1087
      if ((end - start) < 1 || type == ElementType.EMPTY) {
8✔
1088
        return;
1✔
1089
      }
1090
      if (this.start.length == this.size) {
6✔
1091
        this.start = expand(this.start);
6✔
1092
        this.end = expand(this.end);
6✔
1093
        this.type = expand(this.type);
6✔
1094
        this.resolved = expand(this.resolved);
6✔
1095
      }
1096
      if (valueProcessor != null) {
2✔
1097
        if (this.resolved == null) {
3✔
1098
          this.resolved = new CharSequence[this.start.length];
6✔
1099
        }
1100
        CharSequence resolved = valueProcessor.apply(this.source.subSequence(start, end));
9✔
1101
        Elements resolvedElements = new ElementsParser(resolved, '.').parse();
7✔
1102
        Assert.state(resolvedElements.size == 1, "Resolved element must not contain multiple elements");
9✔
1103
        this.resolved[this.size] = resolvedElements.get(0);
8✔
1104
        type = resolvedElements.getType(0);
4✔
1105
      }
1106
      this.start[this.size] = start;
6✔
1107
      this.end[this.size] = end;
6✔
1108
      this.type[this.size] = type;
6✔
1109
      this.size++;
6✔
1110
    }
1✔
1111

1112
    private int[] expand(int[] src) {
1113
      int[] dest = new int[src.length + DEFAULT_CAPACITY];
6✔
1114
      System.arraycopy(src, 0, dest, 0, src.length);
7✔
1115
      return dest;
2✔
1116
    }
1117

1118
    private ElementType[] expand(ElementType[] src) {
1119
      ElementType[] dest = new ElementType[src.length + DEFAULT_CAPACITY];
6✔
1120
      System.arraycopy(src, 0, dest, 0, src.length);
7✔
1121
      return dest;
2✔
1122
    }
1123

1124
    @Nullable
1125
    private CharSequence[] expand(@Nullable CharSequence[] src) {
1126
      if (src == null) {
2!
1127
        return null;
2✔
1128
      }
1129
      CharSequence[] dest = new CharSequence[src.length + DEFAULT_CAPACITY];
×
1130
      System.arraycopy(src, 0, dest, 0, src.length);
×
1131
      return dest;
×
1132
    }
1133

1134
    static boolean isValidChar(char ch, int index) {
1135
      return isAlpha(ch) || isNumeric(ch) || (index != 0 && ch == '-');
15✔
1136
    }
1137

1138
    static boolean isAlphaNumeric(char ch) {
1139
      return isAlpha(ch) || isNumeric(ch);
10✔
1140
    }
1141

1142
    private static boolean isAlpha(char ch) {
1143
      return ch >= 'a' && ch <= 'z';
10!
1144
    }
1145

1146
    private static boolean isNumeric(char ch) {
1147
      return ch >= '0' && ch <= '9';
10✔
1148
    }
1149

1150
  }
1151

1152
  /**
1153
   * The various types of element that we can detect.
1154
   */
1155
  private enum ElementType {
3✔
1156

1157
    /**
1158
     * The element is logically empty (contains no valid chars).
1159
     */
1160
    EMPTY(false),
7✔
1161

1162
    /**
1163
     * The element is a uniform name (a-z, 0-9, no dashes, lowercase).
1164
     */
1165
    UNIFORM(false),
7✔
1166

1167
    /**
1168
     * The element is almost uniform, but it contains (but does not start with) at
1169
     * least one dash.
1170
     */
1171
    DASHED(false),
7✔
1172

1173
    /**
1174
     * The element contains non-uniform characters and will need to be converted.
1175
     */
1176
    NON_UNIFORM(false),
7✔
1177

1178
    /**
1179
     * The element is non-numerically indexed.
1180
     */
1181
    INDEXED(true),
7✔
1182

1183
    /**
1184
     * The element is numerically indexed.
1185
     */
1186
    NUMERICALLY_INDEXED(true);
7✔
1187

1188
    private final boolean indexed;
1189

1190
    ElementType(boolean indexed) {
4✔
1191
      this.indexed = indexed;
3✔
1192
    }
1✔
1193

1194
    public boolean isIndexed() {
1195
      return this.indexed;
3✔
1196
    }
1197

1198
    public boolean allowsFastEqualityCheck() {
1199
      return this == UNIFORM || this == NUMERICALLY_INDEXED;
10✔
1200
    }
1201

1202
    public boolean allowsDashIgnoringEqualityCheck() {
1203
      return allowsFastEqualityCheck() || this == DASHED;
10✔
1204
    }
1205

1206
  }
1207

1208
  /**
1209
   * Predicate used to filter element chars.
1210
   */
1211
  private interface ElementCharPredicate {
1212

1213
    boolean test(char ch, int index);
1214

1215
  }
1216

1217
  /**
1218
   * Formats for {@code toString}.
1219
   */
1220
  enum ToStringFormat {
3✔
1221

1222
    DEFAULT, SYSTEM_ENVIRONMENT, LEGACY_SYSTEM_ENVIRONMENT
18✔
1223

1224
  }
1225

1226
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc