1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.rat.utils;
20
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Objects;
26 import java.util.function.Function;
27 import java.util.function.Predicate;
28 import java.util.function.UnaryOperator;
29
30 import org.apache.commons.lang3.StringUtils;
31 import org.apache.commons.text.WordUtils;
32
33 /**
34 * Handles converting from one string case to another (e.g. camel case to snake case).
35 * @since 0.17
36 */
37 public final class CasedString {
38 /** The segments of the cased string. */
39 private final String[] segments;
40 /** The case of the string as parsed. */
41 private final StringCase stringCase;
42 /** A joiner used for the pascal and camel cases. */
43 private static final Function<String[], String> CAMEL_JOINER = strings -> {
44 StringBuilder sb = new StringBuilder();
45 Arrays.stream(strings).map(s -> s == null ? "" : s).forEach(token -> sb.append(WordUtils.capitalize(token.toLowerCase(Locale.ROOT))));
46 return sb.toString();
47 };
48
49 /**
50 * Creates a cased string by parsing the string argument for the specific case.
51 * @param stringCase the case of the string being parsed.
52 * @param string the string to parse.
53 */
54 public CasedString(final StringCase stringCase, final String string) {
55 this.segments = string == null ? CasedString.StringCase.NULL_SEGMENT : stringCase.getSegments(string.trim());
56 this.stringCase = stringCase;
57 }
58
59 /**
60 * Creates a cased string of the specified case and segments
61 * @param stringCase the case of the string.
62 * @param segments the segments of the string.
63 */
64 public CasedString(final StringCase stringCase, final String[] segments) {
65 this.segments = segments;
66 this.stringCase = stringCase;
67 }
68
69 /**
70 * Converts this cased string into another format.
71 * @param stringCase the desired format.
72 * @return the new CasedString.
73 */
74 public CasedString as(final StringCase stringCase) {
75 return stringCase.name.equals(this.stringCase.name) ? this : new CasedString(stringCase, Arrays.copyOf(this.segments, this.segments.length));
76 }
77
78 /**
79 * Gets the segments of this cased string.
80 * @return the segments of this cased string.
81 */
82 public String[] getSegments() {
83 return this.segments;
84 }
85
86 /**
87 * Generates a string from this cased string but with the desired case.
88 * @param stringCase the desired case.
89 * @return this cased string in the desired case.
90 */
91 public String toCase(final StringCase stringCase) {
92 return this.segments == CasedString.StringCase.NULL_SEGMENT ? null : stringCase.assemble(this.getSegments());
93 }
94
95 @Override
96 public String toString() {
97 return this.toCase(this.stringCase);
98 }
99
100 @Override
101 public boolean equals(final Object o) {
102 if (o == null || getClass() != o.getClass()) {
103 return false;
104 }
105 CasedString that = (CasedString) o;
106 return Objects.deepEquals(getSegments(), that.getSegments()) && Objects.equals(stringCase, that.stringCase);
107 }
108
109 @Override
110 public int hashCode() {
111 return Arrays.hashCode(getSegments());
112 }
113
114 /**
115 * The definition of a String case.
116 */
117 public static final class StringCase {
118 /** The camel case. Example: "HelloWorld"*/
119 public static final StringCase CAMEL;
120 /** The pascal case. Example: "helloWorld" */
121 public static final StringCase PASCAL;
122 /** The Snake case. Example: "hello_world" */
123 public static final StringCase SNAKE;
124 /** The Kebab case. Example: "hello-world" */
125 public static final StringCase KEBAB;
126 /** The phrase case. Example: "hello world" */
127 public static final StringCase PHRASE;
128 /** The dot case. Example: "hello.world" */
129 public static final StringCase DOT;
130 /** The slash case. Example: "hello/world" */
131 public static final StringCase SLASH;
132 /** A marker for the parsing of a NULL string. */
133 static final String[] NULL_SEGMENT;
134 /** An empty segment marker. */
135 static final String[] EMPTY_SEGMENT;
136 /** The name of this case. */
137 private final String name;
138 /**
139 * The predicate that determines if a character is a spliter character. A splitter character
140 * is the character that signals the start of a new segment.
141 */
142 private final Predicate<Character> splitter;
143 /**
144 * If {@code true} the spliter character is preserved as part of the subsequent section otherwise,
145 * the spliter character is discarded.
146 */
147 private final boolean preserveSplit;
148 /** The function that converts segments into the String representation */
149 private final Function<String[], String> joiner;
150 /** A function to provide post-processing on the joined string */
151 private final UnaryOperator<String> postProcess;
152
153 /**
154 * Constructs a StringCase
155 * @param name the name of the case.
156 * @param splitter the splitter to determine when to split a string.
157 * @param preserveSplit the preserveSplit flag.
158 * @param joiner the joiner to assemble the String from the segments.
159 */
160 public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner) {
161 this(name, splitter, preserveSplit, joiner, UnaryOperator.identity());
162 }
163
164 /**
165 * Constructs a String case for the common cases where the delimiter is not preserved in the segments.
166 * @param name the name of the case.
167 * @param delimiter the delimiter between segments.
168 */
169 public StringCase(final String name, final char delimiter) {
170 this(name, c -> c == delimiter, false, simpleJoiner(delimiter));
171 }
172
173 /**
174 * Constructs a StingCase.
175 * @param name the name of the string case.
176 * @param splitter the splitter to detect segments.
177 * @param preserveSplit the flag to preserve the splitter character.
178 * @param joiner the joiner to assemble a String from segments.
179 * @param postProcess the post-process applied to the segments after the splitter has created them.
180 */
181 public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner,
182 final UnaryOperator<String> postProcess) {
183 this.name = name;
184 this.splitter = splitter;
185 this.preserveSplit = preserveSplit;
186 this.joiner = joiner;
187 this.postProcess = postProcess;
188 }
189
190 /**
191 * A simple joiner that assembles a String from a collection of segments.
192 * Correctly handles the case where there are zero length segments.
193 * @param delimiter the delimiter to use between the segments.
194 * @return the assembled string.
195 */
196 public static Function<String[], String> simpleJoiner(final char delimiter) {
197 return s -> String.join(String.valueOf(delimiter), Arrays.stream(s).filter(Objects::nonNull).toArray(String[]::new));
198 }
199
200 @Override
201 public String toString() {
202 return this.name;
203 }
204
205 /**
206 * Assembles segments into a String.
207 * @param segments the segments to assemble.
208 * @return the complete String.
209 */
210 public String assemble(final String[] segments) {
211 return this.joiner.apply(segments);
212 }
213
214 /**
215 * Parses a String into segments.
216 * @param string the string to parse
217 * @return the segments from the string.
218 */
219 public String[] getSegments(final String string) {
220 if (string == null) {
221 return NULL_SEGMENT;
222 } else if (string.isEmpty()) {
223 return EMPTY_SEGMENT;
224 } else {
225 List<String> lst = new ArrayList<>();
226 StringBuilder sb = new StringBuilder();
227
228 for (char c : string.toCharArray()) {
229 if (this.splitter.test(c)) {
230 lst.add(sb.toString());
231 sb.setLength(0);
232 if (this.preserveSplit) {
233 sb.append(c);
234 }
235 } else {
236 sb.append(c);
237 }
238 }
239
240 if (!sb.isEmpty()) {
241 lst.add(sb.toString());
242 }
243
244 return lst.stream().map(this.postProcess).filter(Objects::nonNull).toArray(String[]::new);
245 }
246 }
247 static {
248 CAMEL = new StringCase("CAMEL", Character::isUpperCase, true, CasedString.CAMEL_JOINER,
249 x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
250 PASCAL = new StringCase("PASCAL", Character::isUpperCase, true, CasedString.CAMEL_JOINER.andThen(WordUtils::uncapitalize),
251 x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
252 SNAKE = new StringCase("SNAKE", '_');
253 KEBAB = new StringCase("KEBAB", '-');
254 PHRASE = new StringCase("PHRASE", Character::isWhitespace, false, simpleJoiner(' '));
255 DOT = new StringCase("DOT", '.');
256 SLASH = new StringCase("SLASH", '/');
257 NULL_SEGMENT = new String[0];
258 EMPTY_SEGMENT = new String[]{""};
259 }
260 }
261 }