GriffonNameUtils.java
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2008-2017 the original author or authors.
005  *
006  * Licensed under the Apache License, Version 2.0 (the "License");
007  * you may not use this file except in compliance with the License.
008  * You may obtain a copy of the License at
009  *
010  *     http://www.apache.org/licenses/LICENSE-2.0
011  *
012  * Unless required by applicable law or agreed to in writing, software
013  * distributed under the License is distributed on an "AS IS" BASIS,
014  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015  * See the License for the specific language governing permissions and
016  * limitations under the License.
017  */
018 package griffon.util;
019 
020 import javax.annotation.Nonnull;
021 import javax.annotation.Nullable;
022 import java.util.ArrayList;
023 import java.util.Arrays;
024 import java.util.Iterator;
025 import java.util.List;
026 import java.util.Locale;
027 
028 /**
029  * Contains utility methods for converting between different name types,
030  * for example from class names -> property names and vice-versa. The
031  * key aspect of this class is that it has no dependencies outside the
032  * JDK!
033  */
034 public class GriffonNameUtils {
035     private static final String PROPERTY_SET_PREFIX = "set";
036     private static final String PROPERTY_GET_PREFIX = "get";
037 
038     private static final String[] KEYWORDS = new String[]{
039         "abstract",
040         "assert",
041         "as",
042         "break",
043         "case",
044         "catch",
045         "class",
046         "const",
047         "continue",
048         "default",
049         "do",
050         "else",
051         "enum",
052         "extends",
053         "final",
054         "finally",
055         "for",
056         "goto",
057         "if",
058         "implements",
059         "import",
060         "in",
061         "instanceof",
062         "interface",
063         "native",
064         "new",
065         "package",
066         "private",
067         "protected",
068         "public",
069         "return",
070         "static",
071         "strictfp",
072         "super",
073         "switch",
074         "synchronized",
075         "this",
076         "throw",
077         "throws",
078         "transient",
079         "try",
080         "void",
081         "volatile",
082         "while"
083     };
084 
085     private GriffonNameUtils() {
086         // prevent instantiation
087     }
088 
089     /**
090      * Finds out if the given String is a Java/Groovy keyword.
091      *
092      @param str The String to test
093      *
094      @return <tt>true</tt> if the given String is a keyword, false otherwise
095      */
096     public static boolean isKeyword(String str) {
097         return isNotBlank(str&& Arrays.binarySearch(KEYWORDS, str.toLowerCase(Locale.ENGLISH)) > -1;
098     }
099 
100     /**
101      * Capitalizes a String (makes the first char uppercase) taking care
102      * of blank strings and single character strings.
103      *
104      @param str The String to be capitalized
105      *
106      @return Capitalized version of the target string if it is not blank
107      */
108     public static String capitalize(String str) {
109         if (isBlank(str)) { return str; }
110         if (str.length() == 1) { return str.toUpperCase()}
111         return str.substring(01).toUpperCase(Locale.ENGLISH+ str.substring(1);
112     }
113 
114     /**
115      * Uncapitalizes a String (makes the first char lowercase) taking care
116      * of blank strings and single character strings.
117      *
118      @param str The String to be uncapitalized
119      *
120      @return Uncapitalized version of the target string if it is not blank
121      */
122     public static String uncapitalize(String str) {
123         if (isBlank(str)) { return str; }
124         if (str.length() == 1) { return String.valueOf(Character.toLowerCase(str.charAt(0)))}
125         return Character.toLowerCase(str.charAt(0)) + str.substring(1);
126     }
127 
128     /**
129      * Retrieves the name of a setter for the specified property name
130      *
131      @param propertyName The property name
132      *
133      @return The setter equivalent
134      */
135     public static String getSetterName(String propertyName) {
136         return PROPERTY_SET_PREFIX + capitalize(propertyName);
137     }
138 
139     /**
140      * Calculate the name for a getter method to retrieve the specified property
141      *
142      @param propertyName The property name
143      *
144      @return The name for the getter method for this property, if it were to exist, i.e. getConstraints
145      */
146     public static String getGetterName(String propertyName) {
147         return PROPERTY_GET_PREFIX + capitalize(propertyName);
148     }
149 
150     /**
151      * Returns the class name for the given logical name and trailing name. For example "person" and "Controller" would evaluate to "PersonController"
152      *
153      @param logicalName  The logical name
154      @param trailingName The trailing name
155      *
156      @return The class name
157      */
158     public static String getClassName(String logicalName, String trailingName) {
159         if (isBlank(logicalName)) {
160             throw new IllegalArgumentException("Argument [logicalName] must not be null or blank");
161         }
162 
163         String className = capitalize(logicalName);
164         if (trailingName != null) {
165             className = className + trailingName;
166         }
167         return className;
168     }
169 
170     /**
171      * Returns the class name representation of the given name
172      *
173      @param name The name to convert
174      *
175      @return The property name representation
176      */
177     public static String getClassNameRepresentation(String name) {
178         StringBuilder buf = new StringBuilder();
179         if (name != null && name.length() 0) {
180             String[] tokens = name.split("[^\\w\\d]");
181             for (String token1 : tokens) {
182                 String token = token1.trim();
183                 buf.append(capitalize(token));
184             }
185         }
186 
187         return buf.toString();
188     }
189 
190     /**
191      * Converts foo-bar into FooBar. Empty and null strings are returned
192      * as-is.
193      *
194      @param name The lower case hyphen separated name
195      *
196      @return The class name equivalent.
197      */
198     public static String getClassNameForLowerCaseHyphenSeparatedName(String name) {
199         // Handle null and empty strings.
200         if (isBlank(name)) { return name; }
201 
202         if (name.indexOf('-'> -1) {
203             StringBuilder buf = new StringBuilder();
204             String[] tokens = name.split("-");
205             for (String token : tokens) {
206                 if (token == null || token.length() == 0) { continue}
207                 buf.append(capitalize(token));
208             }
209             return buf.toString();
210         }
211 
212         return capitalize(name);
213     }
214 
215     /**
216      * Retrieves the logical class name of a Griffon artifact given the Griffon class
217      * and a specified trailing name
218      *
219      @param clazz        The class
220      @param trailingName The trailing name such as "Controller" or "TagLib"
221      *
222      @return The logical class name
223      */
224     public static String getLogicalName(Class<?> clazz, String trailingName) {
225         return getLogicalName(clazz.getName(), trailingName);
226     }
227 
228     /**
229      * Retrieves the logical name of the class without the trailing name
230      *
231      @param name         The name of the class
232      @param trailingName The trailing name
233      *
234      @return The logical name
235      */
236     public static String getLogicalName(String name, String trailingName) {
237         if (isNotBlank(name&& isNotBlank(trailingName)) {
238             String shortName = getShortName(name);
239             if (shortName.endsWith(trailingName)) {
240                 return shortName.substring(0, shortName.length() - trailingName.length());
241             }
242         }
243         return name;
244     }
245 
246     public static String getLogicalPropertyName(String className, String trailingName) {
247         if (isNotBlank(className&& isNotBlank(trailingName&& className.length() == trailingName.length() && className.endsWith(trailingName)) {
248             return className.substring(01).toLowerCase();
249         }
250         return getLogicalName(getPropertyName(className), trailingName);
251     }
252 
253     /**
254      * Shorter version of getPropertyNameRepresentation
255      *
256      @param name The name to convert
257      *
258      @return The property name version
259      */
260     public static String getPropertyName(String name) {
261         return getPropertyNameRepresentation(name);
262     }
263 
264     /**
265      * Shorter version of getPropertyNameRepresentation
266      *
267      @param clazz The clazz to convert
268      *
269      @return The property name version
270      */
271     public static String getPropertyName(Class<?> clazz) {
272         return getPropertyNameRepresentation(clazz);
273     }
274 
275     /**
276      * Returns the property name equivalent for the specified class
277      *
278      @param targetClass The class to get the property name for
279      *
280      @return A property name representation of the class name (eg. MyClass becomes myClass)
281      */
282     public static String getPropertyNameRepresentation(Class<?> targetClass) {
283         String shortName = getShortName(targetClass);
284         return getPropertyNameRepresentation(shortName);
285     }
286 
287     /**
288      * Returns the property name representation of the given name
289      *
290      @param name The name to convert
291      *
292      @return The property name representation
293      */
294     public static String getPropertyNameRepresentation(String name) {
295         if (isBlank(name)) { return name; }
296         // Strip any package from the name.
297         int pos = name.lastIndexOf('.');
298         if (pos != -1) {
299             name = name.substring(pos + 1);
300         }
301 
302         // Check whether the name begins with two upper case letters.
303         if (name.length() && Character.isUpperCase(name.charAt(0)) && Character.isUpperCase(name.charAt(1))) {
304             return name;
305         }
306 
307         String propertyName = name.substring(01).toLowerCase(Locale.ENGLISH+ name.substring(1);
308         if (propertyName.indexOf(' '> -1) {
309             propertyName = propertyName.replaceAll("\\s""");
310         }
311         return propertyName;
312     }
313 
314     /**
315      * Converts foo-bar into fooBar
316      *
317      @param name The lower case hyphen separated name
318      *
319      @return The property name equivalent
320      */
321     public static String getPropertyNameForLowerCaseHyphenSeparatedName(String name) {
322         return getPropertyName(getClassNameForLowerCaseHyphenSeparatedName(name));
323     }
324 
325     /**
326      * Returns the class name without the package prefix
327      *
328      @param targetClass The class to get a short name for
329      *
330      @return The short name of the class
331      */
332     public static String getShortName(Class<?> targetClass) {
333         String className = targetClass.getName();
334         return getShortName(className);
335     }
336 
337     /**
338      * Returns the class name without the package prefix
339      *
340      @param className The class name to get a short name for
341      *
342      @return The short name of the class
343      */
344     public static String getShortName(String className) {
345         if (isBlank(className)) { return className; }
346         int i = className.lastIndexOf('.');
347         if (i > -1) {
348             className = className.substring(i + 1, className.length());
349         }
350         return className;
351     }
352 
353     /**
354      * Converts a property name into its natural language equivalent eg ('firstName' becomes 'First Name')
355      *
356      @param name The property name to convert
357      *
358      @return The converted property name
359      */
360     public static String getNaturalName(String name) {
361         name = getShortName(name);
362         if (isBlank(name)) { return name; }
363         List<String> words = new ArrayList<>();
364         int i = 0;
365         char[] chars = name.toCharArray();
366         for (char c : chars) {
367             String w;
368             if (i >= words.size()) {
369                 w = "";
370                 words.add(i, w);
371             else {
372                 w = words.get(i);
373             }
374 
375             if (Character.isLowerCase(c|| Character.isDigit(c)) {
376                 if (Character.isLowerCase(c&& w.length() == 0) {
377                     c = Character.toUpperCase(c);
378                 else if (w.length() && Character.isUpperCase(w.charAt(w.length() 1))) {
379                     w = "";
380                     words.add(++i, w);
381                 }
382 
383                 words.set(i, w + c);
384             else if (Character.isUpperCase(c)) {
385                 if ((i == && w.length() == 0|| Character.isUpperCase(w.charAt(w.length() 1))) {
386                     words.set(i, w + c);
387                 else {
388                     words.add(++i, String.valueOf(c));
389                 }
390             }
391         }
392 
393         StringBuilder buf = new StringBuilder();
394         for (Iterator<String> j = words.iterator(); j.hasNext()) {
395             String word = j.next();
396             buf.append(word);
397             if (j.hasNext()) {
398                 buf.append(' ');
399             }
400         }
401         return buf.toString();
402     }
403 
404     /**
405      <p>Determines whether a given string is <code>null</code>, empty,
406      * or only contains whitespace. If it contains anything other than
407      * whitespace then the string is not considered to be blank and the
408      * method returns <code>false</code>.</p>
409      *
410      @param str The string to test.
411      *
412      @return <code>true</code> if the string is <code>null</code>, or
413      * blank.
414      */
415     public static boolean isBlank(String str) {
416         if (str == null || str.length() == 0) {
417             return true;
418         }
419         for (char c : str.toCharArray()) {
420             if (!Character.isWhitespace(c)) {
421                 return false;
422             }
423         }
424 
425         return true;
426     }
427 
428     /**
429      <p>Determines whether a given string is not <code>null</code>, empty,
430      * or only contains whitespace. If it contains anything other than
431      * whitespace then the string is not considered to be blank and the
432      * method returns <code>true</code>.</p>
433      *
434      @param str The string to test.
435      *
436      @return <code>true</code> if the string is not <code>null</code>, nor
437      * blank.
438      */
439     public static boolean isNotBlank(String str) {
440         return !isBlank(str);
441     }
442 
443     /**
444      * Checks that the specified String is not {@code blank}. This
445      * method is designed primarily for doing parameter validation in methods
446      * and constructors, as demonstrated below:
447      <blockquote><pre>
448      * public Foo(String str) {
449      *     this.str = GriffonNameUtils.requireNonBlank(str);
450      * }
451      </pre></blockquote>
452      *
453      @param str the String to check for blank
454      *
455      @return {@code str} if not {@code blank}
456      *
457      @throws IllegalArgumentException if {@code str} is {@code blank}
458      */
459     public static String requireNonBlank(String str) {
460         if (isBlank(str)) {
461             throw new IllegalArgumentException();
462         }
463         return str;
464     }
465 
466     /**
467      * Checks that the specified String is not {@code blank} and
468      * throws a customized {@link IllegalArgumentException} if it is. This method
469      * is designed primarily for doing parameter validation in methods and
470      * constructors with multiple parameters, as demonstrated below:
471      <blockquote><pre>
472      * public Foo(String str) {
473      *     this.str = GriffonNameUtils.requireNonBlank(str, "str must not be null");
474      * }
475      </pre></blockquote>
476      *
477      @param str     the String to check for blank
478      @param message detail message to be used in the event that a {@code
479      *                IllegalArgumentException} is thrown
480      *
481      @return {@code str} if not {@code blank}
482      *
483      @throws IllegalArgumentException if {@code str} is {@code blank}
484      */
485     public static String requireNonBlank(String str, String message) {
486         if (isBlank(str)) {
487             throw new IllegalArgumentException(message);
488         }
489         return str;
490     }
491 
492     /**
493      * Retrieves the hyphenated name representation of the supplied class. For example
494      * MyFunkyGriffonThingy would be my-funky-griffon-thingy.
495      *
496      @param clazz The class to convert
497      *
498      @return The hyphenated name representation
499      */
500     public static String getHyphenatedName(Class<?> clazz) {
501         if (clazz == null) {
502             return null;
503         }
504         return getHyphenatedName(clazz.getName());
505     }
506 
507     /**
508      * Retrieves the hyphenated name representation of the given class name.
509      * For example MyFunkyGriffonThingy would be my-funky-griffon-thingy.
510      *
511      @param name The class name to convert.
512      *
513      @return The hyphenated name representation.
514      */
515     public static String getHyphenatedName(String name) {
516         if (isBlank(name)) { return name; }
517         if (name.endsWith(".groovy")) {
518             name = name.substring(0, name.length() 7);
519         }
520         String naturalName = getNaturalName(getShortName(name));
521         return naturalName.replaceAll("\\s""-").toLowerCase();
522     }
523 
524     /**
525      * Concatenates the <code>toString()</code> representation of each
526      * item in this Iterable, with the given String as a separator between each item.
527      *
528      @param self      an Iterable of objects
529      @param separator a String separator
530      *
531      @return the joined String
532      */
533     @Nonnull
534     public static String join(@Nonnull Iterable<?> self, @Nullable String separator) {
535         StringBuilder buffer = new StringBuilder();
536         boolean first = true;
537 
538         if (separator == null) { separator = ""}
539 
540         for (Object value : self) {
541             if (first) {
542                 first = false;
543             else {
544                 buffer.append(separator);
545             }
546             buffer.append(String.valueOf(value));
547         }
548         return buffer.toString();
549     }
550 
551     /**
552      * Applies single or double quotes to a string if it contains whitespace characters
553      *
554      @param str the String to be surrounded by quotes
555      *
556      @return a copy of the original String, surrounded by quotes
557      */
558     public static String quote(String str) {
559         if (isBlank(str)) { return str; }
560         for (int i = 0; i < str.length(); i++) {
561             if (Character.isWhitespace(str.charAt(i))) {
562                 str = applyQuotes(str);
563                 break;
564             }
565         }
566         return str;
567     }
568 
569     /**
570      * Removes single or double quotes from a String
571      *
572      @param str the String from which quotes will be removed
573      *
574      @return the unquoted String
575      */
576     public static String unquote(String str) {
577         if (isBlank(str)) { return str; }
578         if ((str.startsWith("'"&& str.endsWith("'")) ||
579             (str.startsWith("\""&& str.endsWith("\""))) {
580             return str.substring(1, str.length() 1);
581         }
582         return str;
583     }
584 
585     private static String applyQuotes(String string) {
586         if (string == null || string.length() == 0) {
587             return "\"\"";
588         }
589 
590         char b;
591         char c = 0;
592         int i;
593         int len = string.length();
594         StringBuilder sb = new StringBuilder(len * 2);
595         String t;
596         char[] chars = string.toCharArray();
597         char[] buffer = new char[1030];
598         int bufferIndex = 0;
599         sb.append('"');
600         for (i = 0; i < len; i += 1) {
601             if (bufferIndex > 1024) {
602                 sb.append(buffer, 0, bufferIndex);
603                 bufferIndex = 0;
604             }
605             b = c;
606             c = chars[i];
607             switch (c) {
608                 case '\\':
609                 case '"':
610                     buffer[bufferIndex++'\\';
611                     buffer[bufferIndex++= c;
612                     break;
613                 case '/':
614                     if (b == '<') {
615                         buffer[bufferIndex++'\\';
616                     }
617                     buffer[bufferIndex++= c;
618                     break;
619                 default:
620                     if (c < ' ') {
621                         switch (c) {
622                             case '\b':
623                                 buffer[bufferIndex++'\\';
624                                 buffer[bufferIndex++'b';
625                                 break;
626                             case '\t':
627                                 buffer[bufferIndex++'\\';
628                                 buffer[bufferIndex++'t';
629                                 break;
630                             case '\n':
631                                 buffer[bufferIndex++'\\';
632                                 buffer[bufferIndex++'n';
633                                 break;
634                             case '\f':
635                                 buffer[bufferIndex++'\\';
636                                 buffer[bufferIndex++'f';
637                                 break;
638                             case '\r':
639                                 buffer[bufferIndex++'\\';
640                                 buffer[bufferIndex++'r';
641                                 break;
642                             default:
643                                 t = "000" + Integer.toHexString(c);
644                                 int tLength = t.length();
645                                 buffer[bufferIndex++'\\';
646                                 buffer[bufferIndex++'u';
647                                 buffer[bufferIndex++= t.charAt(tLength - 4);
648                                 buffer[bufferIndex++= t.charAt(tLength - 3);
649                                 buffer[bufferIndex++= t.charAt(tLength - 2);
650                                 buffer[bufferIndex++= t.charAt(tLength - 1);
651                         }
652                     else {
653                         buffer[bufferIndex++= c;
654                     }
655             }
656         }
657         sb.append(buffer, 0, bufferIndex);
658         sb.append('"');
659         return sb.toString();
660     }
661 }