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

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

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

96.82
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 org.jspecify.annotations.Nullable;
21

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

31
import infra.lang.Assert;
32
import infra.util.StringUtils;
33

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

62
  private static final String EMPTY_STRING = "";
63

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

69
  private final Elements elements;
70

71
  private final CharSequence[] uniformElements;
72

73
  private int hashCode;
74

75
  private final @Nullable String[] string = new String[ToStringFormat.values().length];
5✔
76

77
  @Nullable
78
  private Boolean hasDashedElement;
79

80
  @Nullable
81
  private ConfigurationPropertyName systemEnvironmentLegacyName;
82

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

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

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

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

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

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

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

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

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

191
  private CharSequence convertToDashedElement(CharSequence element) {
192
    return convertElement(element, true, ElementsParser::isValidChar);
6✔
193
  }
194

195
  private CharSequence convertToUniformElement(CharSequence element) {
196
    return convertElement(element, true, (ch, i) -> ElementsParser.isAlphaNumeric(ch));
9✔
197
  }
198

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

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

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

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

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

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

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

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

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

324
  @Override
325
  public int compareTo(ConfigurationPropertyName other) {
326
    return compare(this, other);
5✔
327
  }
328

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

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

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

391
  private boolean toStringMatches(String s1, String s2) {
392
    return s1.hashCode() == s2.hashCode() && s1.equals(s2);
13!
393
  }
394

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

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

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

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

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

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

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

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

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

561
  @Override
562
  public String toString() {
563
    return toString(ToStringFormat.DEFAULT, false);
5✔
564
  }
565

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

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

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

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

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

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

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

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

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

682
  @SuppressWarnings("NullAway")
683
  private static Elements probablySingleElementOf(CharSequence name) {
684
    return elementsOf(name, false, 1);
5✔
685
  }
686

687
  @Nullable
688
  private static Elements elementsOf(@Nullable CharSequence name, boolean returnNullIfInvalid) {
689
    return elementsOf(name, returnNullIfInvalid, ElementsParser.DEFAULT_CAPACITY);
5✔
690
  }
691

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

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

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

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

770
  /**
771
   * The various forms that a non-indexed element value can take.
772
   */
773
  public enum Form {
3✔
774

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

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

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

811
  }
812

813
  /**
814
   * Allows access to the individual elements that make up the name. We store the
815
   * indexes in arrays rather than a list of object in order to conserve memory.
816
   */
817
  private static class Elements {
818

819
    private static final int[] NO_POSITION = {};
3✔
820

821
    private static final ElementType[] NO_TYPE = {};
3✔
822

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

825
    public final CharSequence source;
826

827
    private final int size;
828

829
    private final int[] start;
830

831
    private final int[] end;
832

833
    private final ElementType[] type;
834

835
    private final int[] hashCode;
836

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

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

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

873
    Elements chop(int size) {
874
      CharSequence[] resolved = newResolved(0, size);
5✔
875
      return new Elements(this.source, size, this.start, this.end, this.type, this.hashCode, resolved);
16✔
876
    }
877

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

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

900
    CharSequence get(int index) {
901
      if (this.resolved != null) {
3✔
902
        CharSequence sequence = resolved[index];
5✔
903
        if (sequence != null) {
2✔
904
          return sequence;
2✔
905
        }
906
      }
907
      int start = this.start[index];
5✔
908
      int end = this.end[index];
5✔
909
      return this.source.subSequence(start, end);
6✔
910
    }
911

912
    int getLength(int index) {
913
      if (this.resolved != null) {
3✔
914
        CharSequence sequence = this.resolved[index];
5✔
915
        if (sequence != null) {
2✔
916
          return sequence.length();
3✔
917
        }
918
      }
919
      int start = this.start[index];
5✔
920
      int end = this.end[index];
5✔
921
      return end - start;
4✔
922
    }
923

924
    char charAt(int index, int charIndex) {
925
      if (this.resolved != null) {
3✔
926
        CharSequence sequence = this.resolved[index];
5✔
927
        if (sequence != null) {
2✔
928
          return sequence.charAt(charIndex);
4✔
929
        }
930
      }
931
      int start = this.start[index];
5✔
932
      return this.source.charAt(start + charIndex);
7✔
933
    }
934

935
    ElementType getType(int index) {
936
      return this.type[index];
5✔
937
    }
938

939
    int hashCode(int index) {
940
      int hashCode = this.hashCode[index];
5✔
941
      if (hashCode == 0) {
2✔
942
        boolean indexed = getType(index).isIndexed();
5✔
943
        int length = getLength(index);
4✔
944
        for (int i = 0; i < length; i++) {
7✔
945
          char ch = charAt(index, i);
5✔
946
          if (!indexed) {
2✔
947
            ch = Character.toLowerCase(ch);
3✔
948
          }
949
          if (ElementsParser.isAlphaNumeric(ch)) {
3✔
950
            hashCode = 31 * hashCode + ch;
6✔
951
          }
952
        }
953
        this.hashCode[index] = hashCode;
5✔
954
      }
955
      return hashCode;
2✔
956
    }
957

958
    /**
959
     * Returns if the element source can be used as a shortcut for an operation such
960
     * as {@code equals} or {@code toString}.
961
     *
962
     * @param requiredType the required type
963
     * @return {@code true} if all elements match at least one of the types
964
     */
965
    boolean canShortcutWithSource(ElementType requiredType) {
966
      return canShortcutWithSource(requiredType, requiredType);
5✔
967
    }
968

969
    /**
970
     * Returns if the element source can be used as a shortcut for an operation such
971
     * as {@code equals} or {@code toString}.
972
     *
973
     * @param requiredType the required type
974
     * @param alternativeType and alternative required type
975
     * @return {@code true} if all elements match at least one of the types
976
     */
977
    boolean canShortcutWithSource(ElementType requiredType, ElementType alternativeType) {
978
      if (this.resolved != null) {
3✔
979
        return false;
2✔
980
      }
981
      int size = this.size;
3✔
982
      int[] end = this.end;
3✔
983
      int[] start = this.start;
3✔
984
      ElementType[] thisTypes = this.type;
3✔
985
      for (int i = 0; i < size; i++) {
7✔
986
        ElementType type = thisTypes[i];
4✔
987
        if (type != requiredType && type != alternativeType) {
6✔
988
          return false;
2✔
989
        }
990
        if (i > 0 && end[i - 1] + 1 != start[i]) {
13!
991
          return false;
×
992
        }
993
      }
994
      return true;
2✔
995
    }
996

997
  }
998

999
  /**
1000
   * Main parsing logic used to convert a {@link CharSequence} to {@link Elements}.
1001
   */
1002
  private static class ElementsParser {
1003

1004
    private static final int DEFAULT_CAPACITY = 6;
1005

1006
    private final CharSequence source;
1007

1008
    private final char separator;
1009

1010
    private int size;
1011

1012
    private int[] start;
1013

1014
    private int[] end;
1015

1016
    private ElementType[] type;
1017

1018
    private CharSequence @Nullable [] resolved;
1019

1020
    ElementsParser(CharSequence source, char separator) {
1021
      this(source, separator, DEFAULT_CAPACITY);
5✔
1022
    }
1✔
1023

1024
    ElementsParser(CharSequence source, char separator, int capacity) {
2✔
1025
      this.source = source;
3✔
1026
      this.separator = separator;
3✔
1027
      this.start = new int[capacity];
4✔
1028
      this.end = new int[capacity];
4✔
1029
      this.type = new ElementType[capacity];
4✔
1030
    }
1✔
1031

1032
    Elements parse() {
1033
      return parse(null);
4✔
1034
    }
1035

1036
    Elements parse(@Nullable Function<CharSequence, CharSequence> valueProcessor) {
1037
      int length = this.source.length();
4✔
1038
      int openBracketCount = 0;
2✔
1039
      int start = 0;
2✔
1040
      ElementType type = ElementType.EMPTY;
2✔
1041
      for (int i = 0; i < length; i++) {
7✔
1042
        char ch = this.source.charAt(i);
5✔
1043
        if (ch == '[') {
3✔
1044
          if (openBracketCount == 0) {
2✔
1045
            add(start, i, type, valueProcessor);
6✔
1046
            start = i + 1;
4✔
1047
            type = ElementType.NUMERICALLY_INDEXED;
2✔
1048
          }
1049
          openBracketCount++;
2✔
1050
        }
1051
        else if (ch == ']') {
3✔
1052
          openBracketCount--;
1✔
1053
          if (openBracketCount == 0) {
2✔
1054
            add(start, i, type, valueProcessor);
6✔
1055
            start = i + 1;
4✔
1056
            type = ElementType.EMPTY;
3✔
1057
          }
1058
        }
1059
        else if (!type.isIndexed() && ch == this.separator) {
7✔
1060
          add(start, i, type, valueProcessor);
6✔
1061
          start = i + 1;
4✔
1062
          type = ElementType.EMPTY;
3✔
1063
        }
1064
        else {
1065
          type = updateType(type, ch, i - start);
8✔
1066
        }
1067
      }
1068
      if (openBracketCount != 0) {
2✔
1069
        type = ElementType.NON_UNIFORM;
2✔
1070
      }
1071
      add(start, length, type, valueProcessor);
6✔
1072
      return new Elements(this.source, this.size, this.start, this.end, this.type, null, this.resolved);
17✔
1073
    }
1074

1075
    private ElementType updateType(ElementType existingType, char ch, int index) {
1076
      if (existingType.isIndexed()) {
3✔
1077
        if (existingType == ElementType.NUMERICALLY_INDEXED && !isNumeric(ch)) {
6✔
1078
          return ElementType.INDEXED;
2✔
1079
        }
1080
        return existingType;
2✔
1081
      }
1082
      if (existingType == ElementType.EMPTY && isValidChar(ch, index)) {
7✔
1083
        return (index == 0) ? ElementType.UNIFORM : ElementType.NON_UNIFORM;
6✔
1084
      }
1085
      if (existingType == ElementType.UNIFORM && ch == '-') {
6✔
1086
        return ElementType.DASHED;
2✔
1087
      }
1088
      if (!isValidChar(ch, index)) {
4✔
1089
        if (existingType == ElementType.EMPTY && !isValidChar(Character.toLowerCase(ch), index)) {
8✔
1090
          return ElementType.EMPTY;
2✔
1091
        }
1092
        return ElementType.NON_UNIFORM;
2✔
1093
      }
1094
      return existingType;
2✔
1095
    }
1096

1097
    private void add(int start, int end, ElementType type,
1098
            @Nullable Function<CharSequence, CharSequence> valueProcessor) {
1099
      if ((end - start) < 1 || type == ElementType.EMPTY) {
8✔
1100
        return;
1✔
1101
      }
1102
      if (this.start.length == this.size) {
6✔
1103
        this.start = expand(this.start);
6✔
1104
        this.end = expand(this.end);
6✔
1105
        this.type = expand(this.type);
6✔
1106
        this.resolved = expand(this.resolved);
6✔
1107
      }
1108
      if (valueProcessor != null) {
2✔
1109
        if (this.resolved == null) {
3✔
1110
          this.resolved = new CharSequence[this.start.length];
6✔
1111
        }
1112
        CharSequence resolved = valueProcessor.apply(this.source.subSequence(start, end));
9✔
1113
        Elements resolvedElements = new ElementsParser(resolved, '.').parse();
7✔
1114
        Assert.state(resolvedElements.size == 1, "Resolved element must not contain multiple elements");
9✔
1115
        this.resolved[this.size] = resolvedElements.get(0);
8✔
1116
        type = resolvedElements.getType(0);
4✔
1117
      }
1118
      this.start[this.size] = start;
6✔
1119
      this.end[this.size] = end;
6✔
1120
      this.type[this.size] = type;
6✔
1121
      this.size++;
6✔
1122
    }
1✔
1123

1124
    private int[] expand(int[] src) {
1125
      int[] dest = new int[src.length + DEFAULT_CAPACITY];
6✔
1126
      System.arraycopy(src, 0, dest, 0, src.length);
7✔
1127
      return dest;
2✔
1128
    }
1129

1130
    private ElementType[] expand(ElementType[] src) {
1131
      ElementType[] dest = new ElementType[src.length + DEFAULT_CAPACITY];
6✔
1132
      System.arraycopy(src, 0, dest, 0, src.length);
7✔
1133
      return dest;
2✔
1134
    }
1135

1136
    @Nullable
1137
    private CharSequence[] expand(@Nullable CharSequence[] src) {
1138
      if (src == null) {
2!
1139
        return null;
2✔
1140
      }
1141
      CharSequence[] dest = new CharSequence[src.length + DEFAULT_CAPACITY];
×
1142
      System.arraycopy(src, 0, dest, 0, src.length);
×
1143
      return dest;
×
1144
    }
1145

1146
    static boolean isValidChar(char ch, int index) {
1147
      return isAlpha(ch) || isNumeric(ch) || (index != 0 && ch == '-');
15✔
1148
    }
1149

1150
    static boolean isAlphaNumeric(char ch) {
1151
      return isAlpha(ch) || isNumeric(ch);
10✔
1152
    }
1153

1154
    private static boolean isAlpha(char ch) {
1155
      return ch >= 'a' && ch <= 'z';
10!
1156
    }
1157

1158
    private static boolean isNumeric(char ch) {
1159
      return ch >= '0' && ch <= '9';
10✔
1160
    }
1161

1162
  }
1163

1164
  /**
1165
   * The various types of element that we can detect.
1166
   */
1167
  private enum ElementType {
3✔
1168

1169
    /**
1170
     * The element is logically empty (contains no valid chars).
1171
     */
1172
    EMPTY(false),
7✔
1173

1174
    /**
1175
     * The element is a uniform name (a-z, 0-9, no dashes, lowercase).
1176
     */
1177
    UNIFORM(false),
7✔
1178

1179
    /**
1180
     * The element is almost uniform, but it contains (but does not start with) at
1181
     * least one dash.
1182
     */
1183
    DASHED(false),
7✔
1184

1185
    /**
1186
     * The element contains non-uniform characters and will need to be converted.
1187
     */
1188
    NON_UNIFORM(false),
7✔
1189

1190
    /**
1191
     * The element is non-numerically indexed.
1192
     */
1193
    INDEXED(true),
7✔
1194

1195
    /**
1196
     * The element is numerically indexed.
1197
     */
1198
    NUMERICALLY_INDEXED(true);
7✔
1199

1200
    private final boolean indexed;
1201

1202
    ElementType(boolean indexed) {
4✔
1203
      this.indexed = indexed;
3✔
1204
    }
1✔
1205

1206
    public boolean isIndexed() {
1207
      return this.indexed;
3✔
1208
    }
1209

1210
    public boolean allowsFastEqualityCheck() {
1211
      return this == UNIFORM || this == NUMERICALLY_INDEXED;
10✔
1212
    }
1213

1214
    public boolean allowsDashIgnoringEqualityCheck() {
1215
      return allowsFastEqualityCheck() || this == DASHED;
10✔
1216
    }
1217

1218
  }
1219

1220
  /**
1221
   * Predicate used to filter element chars.
1222
   */
1223
  private interface ElementCharPredicate {
1224

1225
    boolean test(char ch, int index);
1226

1227
  }
1228

1229
  /**
1230
   * Formats for {@code toString}.
1231
   */
1232
  enum ToStringFormat {
3✔
1233

1234
    DEFAULT, SYSTEM_ENVIRONMENT, LEGACY_SYSTEM_ENVIRONMENT
18✔
1235

1236
  }
1237

1238
}
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