View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3;
18  
19  import static org.apache.commons.lang3.LangAssertions.assertIndexOutOfBoundsException;
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  import static org.junit.jupiter.params.provider.Arguments.arguments;
28  
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.Modifier;
31  import java.nio.CharBuffer;
32  import java.util.Random;
33  import java.util.stream.IntStream;
34  import java.util.stream.Stream;
35  
36  import org.junit.jupiter.api.Test;
37  import org.junit.jupiter.params.ParameterizedTest;
38  import org.junit.jupiter.params.provider.Arguments;
39  import org.junit.jupiter.params.provider.MethodSource;
40  
41  /**
42   * Tests CharSequenceUtils
43   */
44  class CharSequenceUtilsTest extends AbstractLangTest {
45  
46      private abstract static class RunTest {
47  
48          abstract boolean invoke();
49  
50          void run(final TestData data, final String id) {
51              if (data.throwable != null) {
52                  assertThrows(data.throwable, this::invoke, id + " Expected " + data.throwable);
53              } else {
54                  final boolean stringCheck = invoke();
55                  assertEquals(data.expected, stringCheck, id + " Failed test " + data);
56              }
57          }
58  
59      }
60  
61      static class TestData {
62          final String source;
63          final boolean ignoreCase;
64          final int toffset;
65          final String other;
66          final int ooffset;
67          final int len;
68          final boolean expected;
69          final Class<? extends Throwable> throwable;
70  
71          TestData(final String source, final boolean ignoreCase, final int toffset, final String other, final int ooffset, final int len,
72                  final boolean expected) {
73              this.source = source;
74              this.ignoreCase = ignoreCase;
75              this.toffset = toffset;
76              this.other = other;
77              this.ooffset = ooffset;
78              this.len = len;
79              this.expected = expected;
80              this.throwable = null;
81          }
82  
83          TestData(final String source, final boolean ignoreCase, final int toffset, final String other, final int ooffset, final int len,
84                  final Class<? extends Throwable> throwable) {
85              this.source = source;
86              this.ignoreCase = ignoreCase;
87              this.toffset = toffset;
88              this.other = other;
89              this.ooffset = ooffset;
90              this.len = len;
91              this.expected = false;
92              this.throwable = throwable;
93          }
94  
95          @Override
96          public String toString() {
97              final StringBuilder sb = new StringBuilder();
98              sb.append(source).append("[").append(toffset).append("]");
99              sb.append(ignoreCase ? " caseblind " : " samecase ");
100             sb.append(other).append("[").append(ooffset).append("]");
101             sb.append(" ").append(len).append(" => ");
102             if (throwable != null) {
103                 sb.append(throwable);
104             } else {
105                 sb.append(expected);
106             }
107             return sb.toString();
108         }
109     }
110 
111     static class WrapperString implements CharSequence {
112         private final CharSequence inner;
113 
114         WrapperString(final CharSequence inner) {
115             this.inner = inner;
116         }
117 
118         @Override
119         public char charAt(final int index) {
120             return inner.charAt(index);
121         }
122 
123         @Override
124         public IntStream chars() {
125             return inner.chars();
126         }
127 
128         @Override
129         public IntStream codePoints() {
130             return inner.codePoints();
131         }
132 
133         @Override
134         public int length() {
135             return inner.length();
136         }
137 
138         @Override
139         public CharSequence subSequence(final int start, final int end) {
140             return inner.subSequence(start, end);
141         }
142 
143         @Override
144         public String toString() {
145             return inner.toString();
146         }
147     }
148 
149     private static final TestData[] TEST_DATA = {
150             // @formatter:off
151             //           Source  IgnoreCase Offset Other  Offset Length Result
152             new TestData("",     true,      -1,     "",    -1,    -1,    false),
153             new TestData("",     true,       0,     "",     0,     1,    false),
154             new TestData("a",    true,       0,     "abc",  0,     0,    true),
155             new TestData("a",    true,       0,     "abc",  0,     1,    true),
156             new TestData("a",    true,       0,     null,   0,     0,    NullPointerException.class),
157             new TestData(null,   true,       0,     null,   0,     0,    NullPointerException.class),
158             new TestData(null,   true,       0,     "",     0,     0,    NullPointerException.class),
159             new TestData("Abc",  true,       0,     "abc",  0,     3,    true),
160             new TestData("Abc",  false,      0,     "abc",  0,     3,    false),
161             new TestData("Abc",  true,       1,     "abc",  1,     2,    true),
162             new TestData("Abc",  false,      1,     "abc",  1,     2,    true),
163             new TestData("Abcd", true,       1,     "abcD", 1,     2,    true),
164             new TestData("Abcd", false,      1,     "abcD", 1,     2,    true),
165             // @formatter:on
166     };
167 
168     private static void assertRegionMatchesParity(final String source, final boolean ignoreCase, final int toffset, final String other,
169             final int ooffset, final int len) {
170         // String is the reference: whatever the running JDK does for String, every CharSequence type must match.
171         final boolean expected = source.regionMatches(ignoreCase, toffset, other, ooffset, len);
172         final CharSequence[] sources = {source, new StringBuilder(source), new StringBuffer(source), CharBuffer.wrap(source)};
173         for (final CharSequence cs : sources) {
174             assertEquals(expected, CharSequenceUtils.regionMatches(cs, ignoreCase, toffset, other, ooffset, len),
175                     cs.getClass().getSimpleName() + " differs from String for " + source + " vs " + other);
176         }
177     }
178 
179     static Stream<Arguments> lastIndexWithStandardCharSequence() {
180         // @formatter:off
181         return Stream.of(
182             arguments("abc", "b", 2, 1),
183             arguments(new StringBuilder("abc"), "b", 2, 1),
184             arguments(new StringBuffer("abc"), "b", 2, 1),
185             arguments("abc", new StringBuilder("b"), 2, 1),
186             arguments(new StringBuilder("abc"), new StringBuilder("b"), 2, 1),
187             arguments(new StringBuffer("abc"), new StringBuffer("b"), 2, 1),
188             arguments(new StringBuilder("abc"), new StringBuffer("b"), 2, 1)
189         );
190         // @formatter:on
191     }
192 
193     @Test
194     void testConstructor() {
195         assertNotNull(new CharSequenceUtils());
196         final Constructor<?>[] cons = CharSequenceUtils.class.getDeclaredConstructors();
197         assertEquals(1, cons.length);
198         assertTrue(Modifier.isPublic(cons[0].getModifiers()));
199         assertTrue(Modifier.isPublic(CharSequenceUtils.class.getModifiers()));
200         assertFalse(Modifier.isFinal(CharSequenceUtils.class.getModifiers()));
201     }
202 
203     @ParameterizedTest
204     @MethodSource("lastIndexWithStandardCharSequence")
205     void testLastIndexOfWithDifferentCharSequences(final CharSequence cs, final CharSequence search, final int start, final int expected) {
206         assertEquals(expected, CharSequenceUtils.lastIndexOf(cs, search, start));
207     }
208 
209     /**
210      * Tests that the direct-scan fallback in {@link CharSequenceUtils#indexOf(CharSequence, CharSequence, int)} (taken by CharSequence
211      * types without a dedicated dispatch branch, such as {@code org.apache.commons.lang3.text.StrBuilder}) matches
212      * {@link String#indexOf(String, int)} semantics exactly and never materializes the searched sequence via {@code toString()}.
213      */
214     @Test
215     void testNewIndexOf() {
216         testNewIndexOfSingle("808087847-1321060740-635567660180086727-925755305", "-1321060740-635567660");
217         testNewIndexOfSingle("", "");
218         testNewIndexOfSingle("1", "");
219         testNewIndexOfSingle("", "1");
220         testNewIndexOfSingle("1", "1");
221         testNewIndexOfSingle("11", "1");
222         testNewIndexOfSingle("1", "11");
223         testNewIndexOfSingle("apache", "a");
224         testNewIndexOfSingle("apache", "p");
225         testNewIndexOfSingle("apache", "e");
226         testNewIndexOfSingle("apache", "x");
227         testNewIndexOfSingle("oraoraoraora", "r");
228         testNewIndexOfSingle("mudamudamudamuda", "d");
229         testNewIndexOfSingle("junk-ststarting", "starting");
230         // The searched sequence must not be copied by the fallback.
231         final CharSequence noToString = new WrapperString("hello world") {
232             @Override
233             public String toString() {
234                 throw new AssertionError("cs.toString() must not be called by indexOf");
235             }
236         };
237         assertEquals(6, CharSequenceUtils.indexOf(noToString, "world", 0));
238         assertEquals(-1, CharSequenceUtils.indexOf(noToString, "worlds", 0));
239     }
240 
241     private void testNewIndexOfSingle(final CharSequence a, final CharSequence b) {
242         final int maxa = Math.max(a.length(), b.length());
243         for (int i = -maxa - 10; i <= maxa + 10; i++) {
244             testNewIndexOfSingle(a, b, i);
245         }
246         testNewIndexOfSingle(a, b, Integer.MIN_VALUE);
247         testNewIndexOfSingle(a, b, Integer.MAX_VALUE);
248     }
249 
250     private void testNewIndexOfSingle(final CharSequence a, final CharSequence b, final int start) {
251         testNewIndexOfSingleSingle(a, b, start);
252         testNewIndexOfSingleSingle(b, a, start);
253     }
254 
255     private void testNewIndexOfSingleSingle(final CharSequence a, final CharSequence b, final int start) {
256         assertEquals(a.toString().indexOf(b.toString(), start),
257                 CharSequenceUtils.indexOf(new WrapperString(a.toString()), new WrapperString(b.toString()), start),
258                 "testNewIndexOf fails! original : " + a + " seg : " + b + " start : " + start);
259     }
260 
261     @Test
262     void testNewLastIndexOf() {
263         testNewLastIndexOfSingle("808087847-1321060740-635567660180086727-925755305", "-1321060740-635567660", 21);
264         testNewLastIndexOfSingle("", "");
265         testNewLastIndexOfSingle("1", "");
266         testNewLastIndexOfSingle("", "1");
267         testNewLastIndexOfSingle("1", "1");
268         testNewLastIndexOfSingle("11", "1");
269         testNewLastIndexOfSingle("1", "11");
270 
271         testNewLastIndexOfSingle("apache", "a");
272         testNewLastIndexOfSingle("apache", "p");
273         testNewLastIndexOfSingle("apache", "e");
274         testNewLastIndexOfSingle("apache", "x");
275         testNewLastIndexOfSingle("oraoraoraora", "r");
276         testNewLastIndexOfSingle("mudamudamudamuda", "d");
277         // There is a route through checkLaterThan1#checkLaterThan1
278         // which only gets touched if there is a two letter (or more) partial match
279         // (in this case "st") earlier in the searched string.
280         testNewLastIndexOfSingle("junk-ststarting", "starting");
281 
282         final Random random = new Random();
283         final StringBuilder seg = new StringBuilder();
284         while (seg.length() <= CharSequenceUtils.TO_STRING_LIMIT) {
285             seg.append(random.nextInt());
286         }
287         StringBuilder original = new StringBuilder(seg);
288         testNewLastIndexOfSingle(original, seg);
289         for (int i = 0; i < 100; i++) {
290             if (random.nextDouble() < 0.5) {
291                 original.append(random.nextInt() % 10);
292             } else {
293                 original = new StringBuilder().append(random.nextInt() % 100).append(original);
294             }
295             testNewLastIndexOfSingle(original, seg);
296         }
297     }
298 
299     private void testNewLastIndexOfSingle(final CharSequence a, final CharSequence b) {
300         final int maxa = Math.max(a.length(), b.length());
301         for (int i = -maxa - 10; i <= maxa + 10; i++) {
302             testNewLastIndexOfSingle(a, b, i);
303         }
304         testNewLastIndexOfSingle(a, b, Integer.MIN_VALUE);
305         testNewLastIndexOfSingle(a, b, Integer.MAX_VALUE);
306     }
307 
308     private void testNewLastIndexOfSingle(final CharSequence a, final CharSequence b, final int start) {
309         testNewLastIndexOfSingleSingle(a, b, start);
310         testNewLastIndexOfSingleSingle(b, a, start);
311     }
312 
313     private void testNewLastIndexOfSingleSingle(final CharSequence a, final CharSequence b, final int start) {
314         assertEquals(a.toString().lastIndexOf(b.toString(), start),
315                 CharSequenceUtils.lastIndexOf(new WrapperString(a.toString()), new WrapperString(b.toString()), start),
316                 "testNewLastIndexOf fails! original : " + a + " seg : " + b + " start : " + start);
317     }
318 
319     @Test
320     void testRegionMatches() {
321         for (final TestData data : TEST_DATA) {
322             new RunTest() {
323                 @Override
324                 boolean invoke() {
325                     return data.source.regionMatches(data.ignoreCase, data.toffset, data.other, data.ooffset, data.len);
326                 }
327             }.run(data, "String");
328             new RunTest() {
329                 @Override
330                 boolean invoke() {
331                     return CharSequenceUtils.regionMatches(data.source, data.ignoreCase, data.toffset, data.other, data.ooffset, data.len);
332                 }
333             }.run(data, "CSString");
334             new RunTest() {
335                 @Override
336                 boolean invoke() {
337                     return CharSequenceUtils.regionMatches(new StringBuilder(data.source), data.ignoreCase, data.toffset, data.other, data.ooffset, data.len);
338                 }
339             }.run(data, "CSNonString");
340         }
341     }
342 
343     /**
344      * A supplementary code point split across a surrogate pair must fold the same way for every {@link CharSequence}
345      * type that it does for {@link String} on the running JDK. {@link String#regionMatches(boolean, int, String, int, int)}
346      * only folds such a code point from Java 9 on, so these rows are checked against {@link String} itself rather than a
347      * fixed result: {@link String}, {@link StringBuilder}, {@link StringBuffer} and {@link CharBuffer} all have to agree.
348      * Deseret CAPITAL LONG I (U+10400) folds to SMALL LONG I (U+10428).
349      */
350     @Test
351     void testRegionMatchesSupplementaryCaseFold() {
352         assertRegionMatchesParity("\uD801\uDC00", true, 0, "\uD801\uDC28", 0, 2);
353         assertRegionMatchesParity("\uD801\uDC00", false, 0, "\uD801\uDC28", 0, 2);
354         assertRegionMatchesParity("\uD801\uDC28", true, 0, "\uD801\uDC00", 0, 2);
355         assertRegionMatchesParity("x\uD801\uDC00", true, 1, "\uD801\uDC28", 0, 2);
356         assertRegionMatchesParity("\uD801\uDC00", true, 0, "\uD801\uDC29", 0, 2);
357     }
358 
359     @Test
360     void testSubSequence() {
361         //
362         // null input
363         //
364         assertNull(CharSequenceUtils.subSequence(null, -1));
365         assertNull(CharSequenceUtils.subSequence(null, 0));
366         assertNull(CharSequenceUtils.subSequence(null, 1));
367         //
368         // non-null input
369         //
370         assertEquals(StringUtils.EMPTY, CharSequenceUtils.subSequence(StringUtils.EMPTY, 0));
371         assertEquals("012", CharSequenceUtils.subSequence("012", 0));
372         assertEquals("12", CharSequenceUtils.subSequence("012", 1));
373         assertEquals("2", CharSequenceUtils.subSequence("012", 2));
374         assertEquals(StringUtils.EMPTY, CharSequenceUtils.subSequence("012", 3));
375     }
376 
377     @Test
378     void testSubSequenceNegativeStart() {
379         assertIndexOutOfBoundsException(() -> CharSequenceUtils.subSequence(StringUtils.EMPTY, -1));
380     }
381 
382     @Test
383     void testSubSequenceTooLong() {
384         assertIndexOutOfBoundsException(() -> CharSequenceUtils.subSequence(StringUtils.EMPTY, 1));
385     }
386 
387     @Test
388     void testToCharArray() {
389         final StringBuilder builder = new StringBuilder("abcdefg");
390         final char[] expected = builder.toString().toCharArray();
391         assertArrayEquals(expected, CharSequenceUtils.toCharArray(builder));
392         assertArrayEquals(expected, CharSequenceUtils.toCharArray(builder.toString()));
393         assertArrayEquals(ArrayUtils.EMPTY_CHAR_ARRAY, CharSequenceUtils.toCharArray(null));
394     }
395 }