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