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