Optimize JavaIdentifierNormalizer.normalizeFullPackageName()
[mdsal.git] / binding2 / mdsal-binding2-generator-util / src / main / java / org / opendaylight / mdsal / binding / javav2 / generator / util / JavaIdentifierNormalizer.java
1 /*
2  * Copyright (c) 2017 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.binding.javav2.generator.util;
9
10 import com.google.common.annotations.Beta;
11 import com.google.common.base.Splitter;
12 import com.google.common.collect.ArrayListMultimap;
13 import com.google.common.collect.ImmutableSet;
14 import com.google.common.collect.ListMultimap;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Set;
18 import org.opendaylight.mdsal.binding.javav2.model.api.Enumeration;
19 import org.opendaylight.mdsal.binding.javav2.model.api.Enumeration.Pair;
20 import org.opendaylight.mdsal.binding.javav2.util.BindingMapping;
21
22 /**
23  * This util class converts every non-java char in identifier to java char by
24  * its unicode name (<a href=
25  * "http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8">JAVA SE
26  * SPECIFICATIONS - Identifiers</a>). There are special types of mapping
27  * non-java chars to original identifiers according to specific
28  * {@linkplain JavaIdentifier java type}:
29  * <ul>
30  * <li>class, enum, interface</li>
31  * <li>
32  * <ul>
33  * <li>without special separator</li>
34  * <li>the first character of identifier, any other first character of
35  * identifier part mapped by non-Java char name from unicode and char in
36  * identifier behind non-java char name are converting to upper case</li>
37  * <li>examples:</li>
38  * <li>
39  * <ul>
40  * <li>example* - ExampleAsterisk</li>
41  * <li>example*example - ExampleAserisksExample</li>
42  * <li>\example - ReverseSolidusExample</li>
43  * <li>1example - DigitOneExample</li>
44  * <li>example1 - Example1</li>
45  * <li>int - IntReservedKeyword</li>
46  * <li>con - ConReservedKeyword</li>
47  * </ul>
48  * </li>
49  * </ul>
50  * </li>
51  * <li>enum value, constant</li>
52  * <li>
53  * <ul>
54  * <li>used underscore as special separator</li>
55  * <li>converted identifier to upper case</li>
56  * <li>examples:</li>
57  * <li>
58  * <ul>
59  * <li>example* - EXAMPLE_ASTERISK</li>
60  * <li>example*example - EXAMPLE_ASTERISK_EXAMPLE</li>
61  * <li>\example - REVERSE_SOLIDUS_EXAMPLE</li>
62  * <li>1example - DIGIT_ONE_EXAMPLE</li>
63  * <li>example1 - EXAMPLE1</li>
64  * <li>int - INT_RESERVED_KEYWORD</li>
65  * <li>con - CON_RESERVED_KEYWORD</li>
66  * </ul>
67  * </li>
68  * </ul>
69  * </li>
70  * <li>method, variable</li>
71  * <li>
72  * <li>
73  * <ul>
74  * <li>without special separator</li>
75  * <li>the first character of identifier is converting to lower case</li>
76  * <li>any other first character of identifier part mapped by non-Java char name
77  * from unicode and char in identifier behind non-java char name are converting
78  * to upper case</li>
79  * <li>examples:</li>
80  * <li>
81  * <ul>
82  * <li>example* - exampleAsterisk</li>
83  * <li>example*example - exampleAserisksExample</li>
84  * <li>\example - reverseSolidusExample</li>
85  * <li>1example - digitOneExample</li>
86  * <li>example1 - example1</li>
87  * <li>int - intReservedKeyword</li>
88  * <li>con - conReservedKeyword</li>
89  * </ul>
90  * </li>
91  * </ul>
92  * </li>
93  * <li>package - full package name (<a href=
94  * "https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html">
95  * Naming a package</a>)</li>
96  * <li>
97  * <li>
98  * <ul>
99  * <li>parts of package name are separated by dots</li>
100  * <li>parts of package name are converting to lower case</li>
101  * <li>if parts of package name are reserved Java or Windows keywords, such as
102  * 'int' the suggested convention is to add an underscore to keyword</li>
103  * <li>dash is parsed as underscore according to <a href=
104  * "https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html">
105  * Naming a package</a></li>
106  * <li>examples:</li>
107  * <li>
108  * <ul>
109  * <li>org.example* - org.exampleasterisk</li>
110  * <li>org.example*example - org.exampleasteriskexample</li>
111  * <li>org.\example - org.reversesolidusexample</li>
112  * <li>org.1example - org.digitoneexample</li>
113  * <li>org.example1 - org.example1</li>
114  * <li>org.int - org.int_</li>
115  * <li>org.con - org.con_</li>
116  * <li>org.foo-cont - org.foo_cont</li>
117  * </ul>
118  * </li>
119  * </ul>
120  * </li>
121  * </ul>
122  *
123  * There is special case in CLASS, INTERFACE, ENUM, ENUM VALUE, CONSTANT, METHOD
124  * and VARIABLE if identifier contains single dash - then the converter ignores
125  * the single dash in the way of the non-java chars. In other way, if dash is
126  * the first or the last char in the identifier or there is more dashes in a row
127  * in the identifier, then these dashes are converted as non-java chars.
128  * Example:
129  * <ul>
130  * <li>class, enum, interface</li>
131  * <li>
132  * <ul>
133  * <li>foo-cont - FooCont</li>
134  * <li>foo--cont - FooHyphenMinusHyphenMinusCont</li>
135  * <li>-foo - HyphenMinusFoo</li>
136  * <li>foo- - FooHyphenMinus</li>
137  * </ul>
138  * </li>
139  * <li>enum value, constant
140  * <li>
141  * <ul>
142  * <li>foo-cont - FOO_CONT</li>
143  * <li>foo--cont - FOO_HYPHEN_MINUS_HYPHEN_MINUS_CONT</li>
144  * <li>-foo - HYPHEN_MINUS_FOO</li>
145  * <li>foo- - FOO_HYPHEN_MINUS</li>
146  * </ul>
147  * </li>
148  * <li>method, variable</li>
149  * <li>
150  * <ul>
151  * <li>foo-cont - fooCont</li>
152  * <li>foo--cont - fooHyphenMinusHyphenMinusCont</li>
153  * <li>-foo - hyphenMinusFoo</li>
154  * <li>foo- - fooHyphenMinus</li>
155  * </ul>
156  * </li>
157  * </ul>
158  *
159  * Next special case talks about normalizing class name which already exists in
160  * package - but with different camel cases (foo, Foo, fOo, ...). To every next
161  * classes with same names will by added their actual rank (serial number),
162  * except the first one. This working for CLASS, ENUM and INTEFACE java
163  * identifiers. If there exist the same ENUM VALUES in ENUM (with different
164  * camel cases), then it's parsed with same logic like CLASSES, ENUMS and
165  * INTERFACES but according to list of pairs of their ENUM parent. Example:
166  *
167  * <ul>
168  * <li>class, enum, interface</li>
169  * <li>
170  * <ul>
171  * <li>package name org.example, class (or interface or enum) Foo - normalized
172  * to Foo
173  * <li>package name org.example, class (or interface or enum) fOo - normalized
174  * to Foo1
175  * </ul>
176  * </li>
177  * <li>enum value</li>
178  * <li>
179  * <ul>
180  * <li>
181  *
182  * <pre>
183  * type enumeration {
184  *     enum foo;
185  *     enum Foo;
186  * }
187  * </pre>
188  *
189  * </li>
190  * <li>YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum
191  * values.</li>
192  * </ul>
193  * </li>
194  * </ul>
195  */
196 @Beta
197 public final class JavaIdentifierNormalizer {
198
199     public static final Set<String> SPECIAL_RESERVED_PATHS = ImmutableSet.of(
200         "org.opendaylight.yangtools.concepts",
201         "org.opendaylight.yangtools.yang.common",
202         "org.opendaylight.yangtools.yang.model",
203         "org.opendaylight.mdsal.binding.javav2.spec",
204         "java",
205         "com");
206
207     private static final int FIRST_CHAR = 0;
208     private static final int FIRST_INDEX = 1;
209     private static final char UNDERSCORE = '_';
210     private static final char DASH = '-';
211     private static final String EMPTY_STRING = "";
212     private static final String RESERVED_KEYWORD = "reserved_keyword";
213     private static final ListMultimap<String, String> PACKAGES_MAP = ArrayListMultimap.create();
214     private static final Set<String> PRIMITIVE_TYPES = ImmutableSet.of("char[]", "byte[]");
215
216     private static final Splitter DOT_SPLITTER = Splitter.on('.');
217
218     private JavaIdentifierNormalizer() {
219         throw new UnsupportedOperationException("Util class");
220     }
221
222     /**
223      * <p>
224      * According to <a href="https://tools.ietf.org/html/rfc7950#section-9.6.4">YANG RFC 7950</a>,
225      * all assigned names in an enumeration MUST be unique. Created names are contained in the list
226      * of {@link Enumeration.Pair}. This method adds actual index with underscore behind name of new
227      * enum value only if this name already exists in one of the list of {@link Enumeration.Pair}.
228      * Then, the name will be converted to java chars according to {@link JavaIdentifier#ENUM_VALUE}
229      * and returned.
230      * </p>
231      * Example:
232      *
233      * <pre>
234      * type enumeration {
235      *     enum foo;
236      *     enum Foo;
237      * }
238      * </pre>
239      *
240      * YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum values.
241      *
242      * @param name
243      *            - name of new enum value
244      * @param values
245      *            - list of all actual enum values
246      * @return converted and fixed name of new enum value
247      */
248     public static String normalizeEnumValueIdentifier(final String name, final List<Pair> values) {
249         return convertIdentifierEnumValue(name, name, values, FIRST_INDEX);
250     }
251
252     /**
253      * Normalizing full package name by non java chars and reserved keywords.
254      *
255      * @param fullPackageName
256      *            - full package name
257      * @return normalized name
258      */
259     public static String normalizeFullPackageName(final String fullPackageName) {
260         final Iterator<String> it = DOT_SPLITTER.split(fullPackageName).iterator();
261         if (!it.hasNext()) {
262             return fullPackageName;
263         }
264
265         final StringBuilder sb = new StringBuilder(fullPackageName.length());
266         while (true) {
267             sb.append(normalizePartialPackageName(it.next()));
268             if (!it.hasNext()) {
269                 return sb.toString();
270             }
271             sb.append('.');
272         }
273     }
274
275     /**
276      * Normalizing part of package name by non java chars.
277      *
278      * @param packageNamePart
279      *            - part of package name
280      * @return normalized name
281      */
282     public static String normalizePartialPackageName(final String packageNamePart) {
283         // if part of package name consist from java or windows reserved word, return it with
284         // underscore at the end and in lower case
285         if (BindingMapping.JAVA_RESERVED_WORDS.contains(packageNamePart.toLowerCase())
286                 || BindingMapping.WINDOWS_RESERVED_WORDS.contains(packageNamePart.toUpperCase())) {
287             return new StringBuilder(packageNamePart).append(UNDERSCORE).toString().toLowerCase();
288         }
289         String normalizedPackageNamePart = packageNamePart;
290         if (packageNamePart.contains(String.valueOf(DASH))) {
291             normalizedPackageNamePart = packageNamePart.replaceAll(String.valueOf(DASH), String.valueOf(UNDERSCORE));
292         }
293         final StringBuilder sb = new StringBuilder();
294         StringBuilder innserSb = new StringBuilder();
295         for (int i = 0; i < normalizedPackageNamePart.length(); i++) {
296             if (normalizedPackageNamePart.charAt(i) == UNDERSCORE) {
297                 if (!innserSb.toString().isEmpty()) {
298                     sb.append(normalizeSpecificIdentifier(innserSb.toString(), JavaIdentifier.PACKAGE));
299                     innserSb = new StringBuilder();
300                 }
301                 sb.append(UNDERSCORE);
302             } else {
303                 innserSb.append(normalizedPackageNamePart.charAt(i));
304             }
305         }
306         if (!innserSb.toString().isEmpty()) {
307             sb.append(normalizeSpecificIdentifier(innserSb.toString(), JavaIdentifier.PACKAGE));
308         }
309         // returned normalized part of package name
310         return sb.toString();
311     }
312
313     /**
314      * Find and convert non Java chars in identifiers of generated transfer objects, initially
315      * derived from corresponding YANG according to
316      * <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8"> Java
317      * Specifications - Identifiers</a>. If there is more same class names at the same package, then
318      * append rank (serial number) to the end of them. Works for class, enum, interface.
319      *
320      * @param packageName
321      *            - package of identifier
322      * @param className
323      *            - name of identifier
324      * @return - java acceptable identifier
325      */
326     public static String normalizeClassIdentifier(final String packageName, final String className) {
327         if (packageName.isEmpty() && PRIMITIVE_TYPES.contains(className)) {
328             return className;
329         }
330         for (final String reservedPath : SPECIAL_RESERVED_PATHS) {
331             if (packageName.startsWith(reservedPath)) {
332                 return className;
333             }
334         }
335         final String convertedClassName = normalizeSpecificIdentifier(className, JavaIdentifier.CLASS);
336         // if packageName contains class name at the end, then the className is
337         // name of inner class
338         final String[] packageNameParts = packageName.split("\\.");
339         String suppInnerClassPackageName = packageName;
340         if (packageNameParts.length > 1) {
341             if (Character.isUpperCase(packageNameParts[packageNameParts.length - 1].charAt(FIRST_CHAR))) {
342                 final StringBuilder sb = new StringBuilder();
343                 // ignore class name in package name - inner class name has
344                 // to be normalizing according to original package of parent
345                 // class
346                 for (int i = 0; i < packageNameParts.length - 1; i++) {
347                     if (!Character.isUpperCase(packageNameParts[i].charAt(FIRST_CHAR))) {
348                         sb.append(packageNameParts[i]);
349                         if (i != packageNameParts.length - 2 &&
350                                 !Character.isUpperCase(packageNameParts[i+1].charAt(FIRST_CHAR))) {
351                             sb.append('.');
352                         }
353                     } else {
354                         break;
355                     }
356                 }
357                 suppInnerClassPackageName = sb.toString();
358             }
359         }
360
361         return normalizeClassIdentifier(suppInnerClassPackageName, convertedClassName, convertedClassName, FIRST_INDEX);
362     }
363
364     /**
365      * Find and convert non Java chars in identifiers of generated transfer objects, initially
366      * derived from corresponding YANG.
367      *
368      * @param identifier
369      *            - name of identifier
370      * @param javaIdentifier
371      *            - java type of identifier
372      * @return - java acceptable identifier
373      */
374     public static String normalizeSpecificIdentifier(final String identifier, final JavaIdentifier javaIdentifier) {
375         final StringBuilder sb = new StringBuilder();
376
377         // if identifier isn't PACKAGE type then check it by reserved keywords
378         if(javaIdentifier != JavaIdentifier.PACKAGE) {
379             if (BindingMapping.JAVA_RESERVED_WORDS.contains(identifier.toLowerCase())
380                     || BindingMapping.WINDOWS_RESERVED_WORDS.contains(identifier.toUpperCase())) {
381                 return fixCasesByJavaType(
382                         sb.append(identifier).append(UNDERSCORE).append(RESERVED_KEYWORD).toString().toLowerCase(),
383                         javaIdentifier);
384             }
385         }
386
387         // check and convert first char in identifier if there is non-java char
388         final char firstChar = identifier.charAt(FIRST_CHAR);
389         if (!Character.isJavaIdentifierStart(firstChar)) {
390             // converting first char of identifier
391             sb.append(convertFirst(firstChar, existNext(identifier, FIRST_CHAR)));
392         } else {
393             sb.append(firstChar);
394         }
395         // check and convert other chars in identifier, if there is non-java char
396         for (int i = 1; i < identifier.length(); i++) {
397             final char actualChar = identifier.charAt(i);
398             // ignore single dash as non java char - if there is more dashes in a row or dash is as
399             // the last char in identifier then parse these dashes as non java chars
400             if (actualChar == '-' && existNext(identifier, i)) {
401                 if (identifier.charAt(i - 1) != DASH && identifier.charAt(i + 1) != DASH) {
402                     sb.append(UNDERSCORE);
403                     continue;
404                 }
405             }
406             if (!Character.isJavaIdentifierPart(actualChar)) {
407                 // prepare actual string of sb for checking if underscore exist on position of the
408                 // last char
409                 final String partialConvertedIdentifier = sb.toString();
410                 sb.append(convert(actualChar, existNext(identifier, i),
411                         partialConvertedIdentifier.charAt(partialConvertedIdentifier.length() - 1)));
412             } else {
413                 sb.append(actualChar);
414             }
415         }
416         // apply camel case in appropriate way
417         return fixCasesByJavaType(sb.toString().replace("__", "_").toLowerCase(), javaIdentifier);
418     }
419
420     /**
421      * Checking while there doesn't exist any class name with the same name
422      * (regardless of camel cases) in package.
423      *
424      * @param packageName
425      *            - package of class name
426      * @param origClassName
427      *            - original class name
428      * @param actualClassName
429      *            - actual class name with rank (serial number)
430      * @param rank
431      *            - actual rank (serial number)
432      * @return converted identifier
433      */
434     private static String normalizeClassIdentifier(final String packageName, final String origClassName,
435             final String actualClassName, final int rank) {
436         if (PACKAGES_MAP.containsKey(packageName)) {
437             for (final String existingName : PACKAGES_MAP.get(packageName)) {
438                 if (existingName.toLowerCase().equals(actualClassName.toLowerCase())) {
439                     final int nextRank = rank + 1;
440                     return normalizeClassIdentifier(packageName, origClassName,
441                             new StringBuilder(origClassName).append(rank).toString(), nextRank);
442                 }
443             }
444         }
445         PACKAGES_MAP.put(packageName, actualClassName);
446         return actualClassName;
447     }
448
449     /**
450      * Fix cases of converted identifiers by Java type
451      *
452      * @param string
453      *            - converted identifier
454      * @param javaIdentifier
455      *            - java type of identifier
456      * @return converted identifier with right cases according to java type
457      */
458     private static String fixCasesByJavaType(final String convertedIdentifier, final JavaIdentifier javaIdentifier) {
459         switch (javaIdentifier) {
460             case CLASS:
461             case ENUM:
462             case INTERFACE:
463                 return capitalize(fixCases(convertedIdentifier));
464             case ENUM_VALUE:
465             case CONSTANT:
466                 return convertedIdentifier.toUpperCase();
467             case METHOD:
468             case VARIABLE:
469                 return fixCases(convertedIdentifier);
470             case PACKAGE:
471                 return convertedIdentifier.replaceAll(String.valueOf(UNDERSCORE), EMPTY_STRING);
472             default:
473                 throw new IllegalArgumentException("Unknown java type of identifier : " + javaIdentifier.toString());
474         }
475     }
476
477     /**
478      * Delete unnecessary chars in converted identifier and apply camel case in appropriate way.
479      *
480      * @param convertedIdentifier
481      *            - original converted identifier
482      * @return resolved identifier
483      */
484     private static String fixCases(final String convertedIdentifier) {
485         final StringBuilder sb = new StringBuilder();
486         if (convertedIdentifier.contains(String.valueOf(UNDERSCORE))) {
487             boolean isFirst = true;
488             for (final String part : convertedIdentifier.split(String.valueOf(UNDERSCORE))) {
489                 if (isFirst) {
490                     isFirst = false;
491                     sb.append(part);
492                 } else {
493                     sb.append(capitalize(part));
494                 }
495             }
496         } else {
497             sb.append(convertedIdentifier);
498         }
499         return sb.toString();
500     }
501
502     /**
503      * Check if there exist next char in identifier behind actual char position
504      *
505      * @param identifier
506      *            - original identifier
507      * @param actual
508      *            - actual char position
509      * @return true if there is another char, false otherwise
510      */
511     private static boolean existNext(final String identifier, final int actual) {
512         return identifier.length() > actual + 1;
513     }
514
515     /**
516      * Converting first char of identifier. This happen only if this char is
517      * non-java char
518      *
519      * @param c
520      *            - first char
521      * @param existNext
522      *            - existing of next char behind actual char
523      * @return converted char
524      */
525     private static String convertFirst(final char c, final boolean existNext) {
526         String name = Character.getName(c);
527         if (name.contains(String.valueOf(DASH))) {
528             name = name.replaceAll(String.valueOf(DASH), String.valueOf(UNDERSCORE));
529         }
530         name = existNext ? name + "_" : name;
531         return name.contains(" ") ? name.replaceAll(" ", "_") : name;
532     }
533
534     /**
535      * Converting any char in java identifier, This happen only if this char is
536      * non-java char
537      *
538      * @param c
539      *            - actual char
540      * @param existNext
541      *            - existing of next char behind actual char
542      * @param partialLastChar
543      *            - last char of partial converted identifier
544      * @return converted char
545      */
546     private static String convert(final char c, final boolean existNext, final char partialLastChar) {
547         return partialLastChar == '_' ? convertFirst(c, existNext) : "_" + convertFirst(c, existNext);
548     }
549
550     /**
551      * Capitalize input string
552      *
553      * @param identifier
554      *            - string to be capitalized
555      */
556     private static String capitalize(final String identifier) {
557         return identifier.substring(FIRST_CHAR, FIRST_CHAR + 1).toUpperCase() + identifier.substring(1);
558     }
559
560     private static String convertIdentifierEnumValue(final String name, final String origName, final List<Pair> values,
561             final int rank) {
562         String newName = name;
563         for (final Pair pair : values) {
564             if (pair.getName().toLowerCase().equals(name.toLowerCase())
565                     || pair.getMappedName().toLowerCase().equals(name.toLowerCase())) {
566                 int actualRank = rank;
567                 final StringBuilder actualNameBuilder =
568                         new StringBuilder(origName).append(UNDERSCORE).append(actualRank);
569                 newName = convertIdentifierEnumValue(actualNameBuilder.toString(), origName, values, ++actualRank);
570             }
571         }
572         return normalizeSpecificIdentifier(newName, JavaIdentifier.ENUM_VALUE);
573     }
574 }