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