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