View Javadoc
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      /** A null cased string. */
49      public static final CasedString NULL = new CasedString(StringCase.KEBAB, CasedString.StringCase.NULL_SEGMENT);
50  
51      /**
52       * Creates a cased string by parsing the string argument for the specific case.
53       * @param stringCase the case of the string being parsed.
54       * @param string the string to parse.
55       */
56      public CasedString(final StringCase stringCase, final String string) {
57          this.segments = string == null ? CasedString.StringCase.NULL_SEGMENT : stringCase.getSegments(string.trim());
58          this.stringCase = stringCase;
59      }
60  
61      /**
62       * Creates a cased string of the specified case and segments
63       * @param stringCase the case of the string.
64       * @param segments the segments of the string.
65       */
66      public CasedString(final StringCase stringCase, final String[] segments) {
67          this.segments = segments.length == 0 ? StringCase.NULL_SEGMENT : segments;
68          this.stringCase = stringCase;
69      }
70  
71      /**
72       * Converts this cased string into another format.
73       * @param stringCase the desired format.
74       * @return the new CasedString.
75       */
76      public CasedString as(final StringCase stringCase) {
77          return stringCase.name.equals(this.stringCase.name) ? this : new CasedString(stringCase, copySegments());
78      }
79  
80      private String[] copySegments() {
81          return this.segments.length == 0 ? StringCase.NULL_SEGMENT : Arrays.copyOf(this.segments, this.segments.length);
82      }
83  
84      public boolean isNull() {
85          return segments.length == 0;
86      }
87  
88      /**
89       * Gets the segments of this cased string.
90       * @return the segments of this cased string.
91       */
92      public String[] getSegments() {
93          return this.segments;
94      }
95  
96      /**
97       * Generates a string from this cased string but with the desired case.
98       * @param stringCase the desired case.
99       * @return this cased string in the desired case.
100      */
101     public String toCase(final StringCase stringCase) {
102         return this.segments == CasedString.StringCase.NULL_SEGMENT ? null : stringCase.assemble(this.getSegments());
103     }
104 
105     @Override
106     public String toString() {
107         return this.toCase(this.stringCase);
108     }
109 
110     @Override
111     public boolean equals(final Object o) {
112         if (o == null || getClass() != o.getClass()) {
113             return false;
114         }
115         CasedString that = (CasedString) o;
116         return Objects.equals(stringCase, that.stringCase) && Objects.deepEquals(getSegments(), that.getSegments());
117     }
118 
119     @Override
120     public int hashCode() {
121         return Arrays.hashCode(getSegments());
122     }
123 
124     /**
125      * The definition of a String case.
126      */
127     public static final class StringCase {
128         /** The camel case. Example: "HelloWorld"*/
129         public static final StringCase CAMEL;
130         /** The pascal case. Example: "helloWorld" */
131         public static final StringCase PASCAL;
132         /** The Snake case. Example: "hello_world" */
133         public static final StringCase SNAKE;
134         /** The Kebab case. Example: "hello-world" */
135         public static final StringCase KEBAB;
136         /** The phrase case. Example: "hello world" */
137         public static final StringCase PHRASE;
138         /** The dot case. Example: "hello.world" */
139         public static final StringCase DOT;
140         /** The slash case. Example: "hello/world" */
141         public static final StringCase SLASH;
142         /** A marker for the parsing of a NULL string. */
143         static final String[] NULL_SEGMENT;
144         /** An empty segment marker. */
145         static final String[] EMPTY_SEGMENT;
146         /** The name of this case. */
147         private final String name;
148         /**
149          * The predicate that determines if a character is a spliter character. A splitter character
150          * is the character that signals the start of a new segment.
151          */
152         private final Predicate<Character> splitter;
153         /**
154          * If {@code true} the spliter character is preserved as part of the subsequent section otherwise,
155          * the spliter character is discarded.
156          */
157         private final boolean preserveSplit;
158         /** The function that converts segments into the String representation */
159         private final Function<String[], String> joiner;
160         /** A function to provide post-processing on the joined string */
161         private final UnaryOperator<String> postProcess;
162 
163         /**
164          * Constructs a StringCase
165          * @param name the name of the case.
166          * @param splitter the splitter to determine when to split a string.
167          * @param preserveSplit the preserveSplit flag.
168          * @param joiner the joiner to assemble the String from the segments.
169          */
170         public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner) {
171             this(name, splitter, preserveSplit, joiner, UnaryOperator.identity());
172         }
173 
174         /**
175          * Constructs a String case for the common cases where the delimiter is not preserved in the segments.
176          * @param name the name of the case.
177          * @param delimiter the delimiter between segments.
178          */
179         public StringCase(final String name, final char delimiter) {
180             this(name, c -> c == delimiter, false, simpleJoiner(delimiter));
181         }
182 
183         /**
184          * Constructs a StingCase.
185          * @param name the name of the string case.
186          * @param splitter the splitter to detect segments.
187          * @param preserveSplit the flag to preserve the splitter character.
188          * @param joiner the joiner to assemble a String from segments.
189          * @param postProcess the post-process applied to the segments after the splitter has created them.
190          */
191         public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner,
192                           final UnaryOperator<String> postProcess) {
193             this.name = name;
194             this.splitter = splitter;
195             this.preserveSplit = preserveSplit;
196             this.joiner = joiner;
197             this.postProcess = postProcess;
198         }
199 
200         /**
201          * A simple joiner that assembles a String from a collection of segments.
202          * Correctly handles the case where there are zero length segments.
203          * @param delimiter the delimiter to use between the segments.
204          * @return the assembled string.
205          */
206         public static Function<String[], String> simpleJoiner(final char delimiter) {
207             return s -> String.join(String.valueOf(delimiter), Arrays.stream(s).filter(Objects::nonNull).toArray(String[]::new));
208         }
209 
210         @Override
211         public String toString() {
212             return this.name;
213         }
214 
215         /**
216          * Assembles segments into a String.
217          * @param segments the segments to assemble.
218          * @return the complete String.
219          */
220         public String assemble(final String[] segments) {
221             return this.joiner.apply(segments);
222         }
223 
224         /**
225          * Parses a String into segments.
226          * @param string the string to parse
227          * @return the segments from the string.
228          */
229         public String[] getSegments(final String string) {
230             if (string == null) {
231                 return NULL_SEGMENT;
232             } else if (string.isEmpty()) {
233                 return EMPTY_SEGMENT;
234             } else {
235                 List<String> lst = new ArrayList<>();
236                 StringBuilder sb = new StringBuilder();
237 
238                 for (char c : string.toCharArray()) {
239                     if (this.splitter.test(c)) {
240                         lst.add(sb.toString());
241                         sb.setLength(0);
242                         if (this.preserveSplit) {
243                             sb.append(c);
244                         }
245                     } else {
246                         sb.append(c);
247                     }
248                 }
249 
250                 if (!sb.isEmpty()) {
251                     lst.add(sb.toString());
252                 }
253 
254                 return lst.stream().map(this.postProcess).filter(Objects::nonNull).toArray(String[]::new);
255             }
256         }
257         static {
258             CAMEL = new StringCase("CAMEL", Character::isUpperCase, true, CasedString.CAMEL_JOINER,
259                     x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
260             PASCAL = new StringCase("PASCAL", Character::isUpperCase, true, CasedString.CAMEL_JOINER.andThen(WordUtils::uncapitalize),
261                     x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
262             SNAKE = new StringCase("SNAKE", '_');
263             KEBAB = new StringCase("KEBAB", '-');
264             PHRASE = new StringCase("PHRASE", Character::isWhitespace, false, simpleJoiner(' '));
265             DOT = new StringCase("DOT", '.');
266             SLASH = new StringCase("SLASH", '/');
267             NULL_SEGMENT = new String[0];
268             EMPTY_SEGMENT = new String[]{""};
269         }
270     }
271 }