GriffonNameUtils.java
001 /*
002  * Copyright 2008-2017 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.util;
017 
018 import javax.annotation.Nonnull;
019 import javax.annotation.Nullable;
020 import java.util.ArrayList;
021 import java.util.Arrays;
022 import java.util.Iterator;
023 import java.util.List;
024 import java.util.Locale;
025 
026 /**
027  * Contains utility methods for converting between different name types,
028  * for example from class names -> property names and vice-versa. The
029  * key aspect of this class is that it has no dependencies outside the
030  * JDK!
031  */
032 public class GriffonNameUtils {
033     private static final String PROPERTY_SET_PREFIX = "set";
034     private static final String PROPERTY_GET_PREFIX = "get";
035 
036     private static final String[] KEYWORDS = new String[]{
037         "abstract",
038         "assert",
039         "as",
040         "break",
041         "case",
042         "catch",
043         "class",
044         "const",
045         "continue",
046         "default",
047         "do",
048         "else",
049         "enum",
050         "extends",
051         "final",
052         "finally",
053         "for",
054         "goto",
055         "if",
056         "implements",
057         "import",
058         "in",
059         "instanceof",
060         "interface",
061         "native",
062         "new",
063         "package",
064         "private",
065         "protected",
066         "public",
067         "return",
068         "static",
069         "strictfp",
070         "super",
071         "switch",
072         "synchronized",
073         "this",
074         "throw",
075         "throws",
076         "transient",
077         "try",
078         "void",
079         "volatile",
080         "while"
081     };
082 
083     private GriffonNameUtils() {
084         // prevent instantiation
085     }
086 
087     /**
088      * Finds out if the given String is a Java/Groovy keyword.
089      *
090      @param str The String to test
091      *
092      @return <tt>true</tt> if the given String is a keyword, false otherwise
093      */
094     public static boolean isKeyword(String str) {
095         return !isBlank(str&& Arrays.binarySearch(KEYWORDS, str.toLowerCase(Locale.ENGLISH)) > -1;
096     }
097 
098     /**
099      * Capitalizes a String (makes the first char uppercase) taking care
100      * of blank strings and single character strings.
101      *
102      @param str The String to be capitalized
103      *
104      @return Capitalized version of the target string if it is not blank
105      */
106     public static String capitalize(String str) {
107         if (isBlank(str)) { return str; }
108         if (str.length() == 1) { return str.toUpperCase()}
109         return str.substring(01).toUpperCase(Locale.ENGLISH+ str.substring(1);
110     }
111 
112     /**
113      * Uncapitalizes a String (makes the first char lowercase) taking care
114      * of blank strings and single character strings.
115      *
116      @param str The String to be uncapitalized
117      *
118      @return Uncapitalized version of the target string if it is not blank
119      */
120     public static String uncapitalize(String str) {
121         if (isBlank(str)) { return str; }
122         if (str.length() == 1) { return String.valueOf(Character.toLowerCase(str.charAt(0)))}
123         return Character.toLowerCase(str.charAt(0)) + str.substring(1);
124     }
125 
126     /**
127      * Retrieves the name of a setter for the specified property name
128      *
129      @param propertyName The property name
130      *
131      @return The setter equivalent
132      */
133     public static String getSetterName(String propertyName) {
134         return PROPERTY_SET_PREFIX + capitalize(propertyName);
135     }
136 
137     /**
138      * Calculate the name for a getter method to retrieve the specified property
139      *
140      @param propertyName The property name
141      *
142      @return The name for the getter method for this property, if it were to exist, i.e. getConstraints
143      */
144     public static String getGetterName(String propertyName) {
145         return PROPERTY_GET_PREFIX + capitalize(propertyName);
146     }
147 
148     /**
149      * Returns the class name for the given logical name and trailing name. For example "person" and "Controller" would evaluate to "PersonController"
150      *
151      @param logicalName  The logical name
152      @param trailingName The trailing name
153      *
154      @return The class name
155      */
156     public static String getClassName(String logicalName, String trailingName) {
157         if (isBlank(logicalName)) {
158             throw new IllegalArgumentException("Argument [logicalName] must not be null or blank");
159         }
160 
161         String className = capitalize(logicalName);
162         if (trailingName != null) {
163             className = className + trailingName;
164         }
165         return className;
166     }
167 
168     /**
169      * Returns the class name representation of the given name
170      *
171      @param name The name to convert
172      *
173      @return The property name representation
174      */
175     public static String getClassNameRepresentation(String name) {
176         StringBuilder buf = new StringBuilder();
177         if (name != null && name.length() 0) {
178             String[] tokens = name.split("[^\\w\\d]");
179             for (String token1 : tokens) {
180                 String token = token1.trim();
181                 buf.append(capitalize(token));
182             }
183         }
184 
185         return buf.toString();
186     }
187 
188     /**
189      * Converts foo-bar into FooBar. Empty and null strings are returned
190      * as-is.
191      *
192      @param name The lower case hyphen separated name
193      *
194      @return The class name equivalent.
195      */
196     public static String getClassNameForLowerCaseHyphenSeparatedName(String name) {
197         // Handle null and empty strings.
198         if (isBlank(name)) { return name; }
199 
200         if (name.indexOf('-'> -1) {
201             StringBuilder buf = new StringBuilder();
202             String[] tokens = name.split("-");
203             for (String token : tokens) {
204                 if (token == null || token.length() == 0) { continue}
205                 buf.append(capitalize(token));
206             }
207             return buf.toString();
208         }
209 
210         return capitalize(name);
211     }
212 
213     /**
214      * Retrieves the logical class name of a Griffon artifact given the Griffon class
215      * and a specified trailing name
216      *
217      @param clazz        The class
218      @param trailingName The trailing name such as "Controller" or "TagLib"
219      *
220      @return The logical class name
221      */
222     public static String getLogicalName(Class<?> clazz, String trailingName) {
223         return getLogicalName(clazz.getName(), trailingName);
224     }
225 
226     /**
227      * Retrieves the logical name of the class without the trailing name
228      *
229      @param name         The name of the class
230      @param trailingName The trailing name
231      *
232      @return The logical name
233      */
234     public static String getLogicalName(String name, String trailingName) {
235         if (!isBlank(name&& !isBlank(trailingName)) {
236             String shortName = getShortName(name);
237             if (shortName.endsWith(trailingName)) {
238                 return shortName.substring(0, shortName.length() - trailingName.length());
239             }
240         }
241         return name;
242     }
243 
244     public static String getLogicalPropertyName(String className, String trailingName) {
245         if (!isBlank(className&& !isBlank(trailingName)) {
246             if (className.length() == trailingName.length() && className.endsWith(trailingName)) {
247                 return className.substring(01).toLowerCase();
248             }
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      <p>We could use Commons Lang for this, but we don't want GriffonNameUtils
410      * to have a dependency on any external library to minimise the number of
411      * dependencies required to bootstrap Griffon.</p>
412      *
413      @param str The string to test.
414      *
415      @return <code>true</code> if the string is <code>null</code>, or
416      * blank.
417      */
418     public static boolean isBlank(String str) {
419         if (str == null || str.length() == 0) {
420             return true;
421         }
422         for (char c : str.toCharArray()) {
423             if (!Character.isWhitespace(c)) {
424                 return false;
425             }
426         }
427 
428         return true;
429     }
430 
431     /**
432      * Checks that the specified String is not {@code blank}. This
433      * method is designed primarily for doing parameter validation in methods
434      * and constructors, as demonstrated below:
435      <blockquote><pre>
436      * public Foo(String str) {
437      *     this.str = GriffonNameUtils.requireNonBlank(str);
438      * }
439      </pre></blockquote>
440      *
441      @param str the String to check for blank
442      *
443      @return {@code str} if not {@code blank}
444      *
445      @throws IllegalArgumentException if {@code str} is {@code blank}
446      */
447     public static String requireNonBlank(String str) {
448         if (isBlank(str)) {
449             throw new IllegalArgumentException();
450         }
451         return str;
452     }
453 
454     /**
455      * Checks that the specified String is not {@code blank} and
456      * throws a customized {@link IllegalArgumentException} if it is. This method
457      * is designed primarily for doing parameter validation in methods and
458      * constructors with multiple parameters, as demonstrated below:
459      <blockquote><pre>
460      * public Foo(String str) {
461      *     this.str = GriffonNameUtils.requireNonBlank(str, "str must not be null");
462      * }
463      </pre></blockquote>
464      *
465      @param str     the String to check for blank
466      @param message detail message to be used in the event that a {@code
467      *                IllegalArgumentException} is thrown
468      *
469      @return {@code str} if not {@code blank}
470      *
471      @throws IllegalArgumentException if {@code str} is {@code blank}
472      */
473     public static String requireNonBlank(String str, String message) {
474         if (isBlank(str)) {
475             throw new IllegalArgumentException(message);
476         }
477         return str;
478     }
479 
480     /**
481      * Retrieves the hyphenated name representation of the supplied class. For example
482      * MyFunkyGriffonThingy would be my-funky-griffon-thingy.
483      *
484      @param clazz The class to convert
485      *
486      @return The hyphenated name representation
487      */
488     public static String getHyphenatedName(Class<?> clazz) {
489         if (clazz == null) {
490             return null;
491         }
492         return getHyphenatedName(clazz.getName());
493     }
494 
495     /**
496      * Retrieves the hyphenated name representation of the given class name.
497      * For example MyFunkyGriffonThingy would be my-funky-griffon-thingy.
498      *
499      @param name The class name to convert.
500      *
501      @return The hyphenated name representation.
502      */
503     public static String getHyphenatedName(String name) {
504         if (isBlank(name)) { return name; }
505         if (name.endsWith(".groovy")) {
506             name = name.substring(0, name.length() 7);
507         }
508         String naturalName = getNaturalName(getShortName(name));
509         return naturalName.replaceAll("\\s""-").toLowerCase();
510     }
511 
512     /**
513      * Concatenates the <code>toString()</code> representation of each
514      * item in this Iterable, with the given String as a separator between each item.
515      *
516      @param self      an Iterable of objects
517      @param separator a String separator
518      *
519      @return the joined String
520      */
521     @Nonnull
522     public static String join(@Nonnull Iterable<?> self, @Nullable String separator) {
523         StringBuilder buffer = new StringBuilder();
524         boolean first = true;
525 
526         if (separator == null) { separator = ""}
527 
528         for (Object value : self) {
529             if (first) {
530                 first = false;
531             else {
532                 buffer.append(separator);
533             }
534             buffer.append(String.valueOf(value));
535         }
536         return buffer.toString();
537     }
538 
539     /**
540      * Applies single or double quotes to a string if it contains whitespace characters
541      *
542      @param str the String to be surrounded by quotes
543      *
544      @return a copy of the original String, surrounded by quotes
545      */
546     public static String quote(String str) {
547         if (isBlank(str)) { return str; }
548         for (int i = 0; i < str.length(); i++) {
549             if (Character.isWhitespace(str.charAt(i))) {
550                 str = applyQuotes(str);
551                 break;
552             }
553         }
554         return str;
555     }
556 
557     /**
558      * Removes single or double quotes from a String
559      *
560      @param str the String from which quotes will be removed
561      *
562      @return the unquoted String
563      */
564     public static String unquote(String str) {
565         if (isBlank(str)) { return str; }
566         if ((str.startsWith("'"&& str.endsWith("'")) ||
567             (str.startsWith("\""&& str.endsWith("\""))) {
568             return str.substring(1, str.length() 1);
569         }
570         return str;
571     }
572 
573     private static String applyQuotes(String string) {
574         if (string == null || string.length() == 0) {
575             return "\"\"";
576         }
577 
578         char b;
579         char c = 0;
580         int i;
581         int len = string.length();
582         StringBuilder sb = new StringBuilder(len * 2);
583         String t;
584         char[] chars = string.toCharArray();
585         char[] buffer = new char[1030];
586         int bufferIndex = 0;
587         sb.append('"');
588         for (i = 0; i < len; i += 1) {
589             if (bufferIndex > 1024) {
590                 sb.append(buffer, 0, bufferIndex);
591                 bufferIndex = 0;
592             }
593             b = c;
594             c = chars[i];
595             switch (c) {
596                 case '\\':
597                 case '"':
598                     buffer[bufferIndex++'\\';
599                     buffer[bufferIndex++= c;
600                     break;
601                 case '/':
602                     if (b == '<') {
603                         buffer[bufferIndex++'\\';
604                     }
605                     buffer[bufferIndex++= c;
606                     break;
607                 default:
608                     if (c < ' ') {
609                         switch (c) {
610                             case '\b':
611                                 buffer[bufferIndex++'\\';
612                                 buffer[bufferIndex++'b';
613                                 break;
614                             case '\t':
615                                 buffer[bufferIndex++'\\';
616                                 buffer[bufferIndex++'t';
617                                 break;
618                             case '\n':
619                                 buffer[bufferIndex++'\\';
620                                 buffer[bufferIndex++'n';
621                                 break;
622                             case '\f':
623                                 buffer[bufferIndex++'\\';
624                                 buffer[bufferIndex++'f';
625                                 break;
626                             case '\r':
627                                 buffer[bufferIndex++'\\';
628                                 buffer[bufferIndex++'r';
629                                 break;
630                             default:
631                                 t = "000" + Integer.toHexString(c);
632                                 int tLength = t.length();
633                                 buffer[bufferIndex++'\\';
634                                 buffer[bufferIndex++'u';
635                                 buffer[bufferIndex++= t.charAt(tLength - 4);
636                                 buffer[bufferIndex++= t.charAt(tLength - 3);
637                                 buffer[bufferIndex++= t.charAt(tLength - 2);
638                                 buffer[bufferIndex++= t.charAt(tLength - 1);
639                         }
640                     else {
641                         buffer[bufferIndex++= c;
642                     }
643             }
644         }
645         sb.append(buffer, 0, bufferIndex);
646         sb.append('"');
647         return sb.toString();
648     }
649 }