2 * Copyright (c) 2017 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.mdsal.binding.javav2.generator.util;
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;
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}:
25 * <li>class, enum, interface</li>
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
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>
46 * <li>enum value, constant</li>
49 * <li>used underscore as special separator</li>
50 * <li>converted identifier to upper case</li>
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>
65 * <li>method, variable</li>
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>
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>
87 * <li>package - full package name
88 * (<a href="https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html"> Naming a
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
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>
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.
123 * <li>class, enum, interface</li>
126 * <li>foo-cont - FooCont</li>
127 * <li>foo--cont - FooHyphenMinusHyphenMinusCont</li>
128 * <li>-foo - HyphenMinusFoo</li>
129 * <li>foo- - FooHyphenMinus</li>
132 * <li>enum value, constant
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>
141 * <li>method, variable</li>
144 * <li>foo-cont - fooCont</li>
145 * <li>foo--cont - fooHyphenMinusHyphenMinusCont</li>
146 * <li>-foo - hyphenMinusFoo</li>
147 * <li>foo- - fooHyphenMinus</li>
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:
160 * <li>class, enum, interface</li>
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
167 * <li>enum value</li>
180 * <li>YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum values.</li>
186 public final class NonJavaCharsConverter {
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();
196 private NonJavaCharsConverter() {
197 throw new UnsupportedOperationException("Util class");
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}
218 * YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum values.
221 * - name of new enum value
223 * - list of all actual enum values
224 * @return converted and fixed name of new enum value
226 public static String convertIdentifierEnumValue(final String name, final List<Pair> values) {
227 return convertIdentifierEnumValue(name, name, values, FIRST_INDEX);
231 * Normalizing full package name by non java chars and reserved keywords.
233 * @param fullPackageName
234 * - full package name
235 * @return normalized name
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)) {
246 return sb.toString();
250 * Normalizing part of package name by non java chars.
252 * @param packageNamePart
253 * - part of package name
254 * @return normalized name
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();
264 String normalizedPackageNamePart = packageNamePart;
265 if (packageNamePart.contains(String.valueOf(DASH))) {
266 normalizedPackageNamePart = packageNamePart.replaceAll(String.valueOf(DASH), String.valueOf(UNDERSCORE));
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();
276 sb.append(UNDERSCORE);
278 innserSb.append(normalizedPackageNamePart.charAt(i));
281 if (!innserSb.toString().isEmpty()) {
282 sb.append(convertIdentifier(innserSb.toString(), JavaIdentifier.PACKAGE));
284 // returned normalized part of package name
285 return sb.toString();
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.
296 * - package of identifier
298 * - name of identifier
299 * @return - java acceptable identifier
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);
307 * Checking while there doesn't exist any class name with the same name (regardless of camel
311 * - package of class name
312 * @param origClassName
313 * - original class name
314 * @param actualClassName
315 * - actual class name with rank (serial number)
317 * - actual rank (serial number)
318 * @return converted identifier
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);
331 PACKAGES_MAP.put(packageName, actualClassName);
332 return actualClassName;
336 * Find and convert non Java chars in identifiers of generated transfer objects, initially
337 * derived from corresponding YANG.
340 * - name of identifier
341 * @param javaIdentifier
342 * - java type of identifier
343 * @return - java acceptable identifier
345 public static String convertIdentifier(final String identifier, final JavaIdentifier javaIdentifier) {
346 final StringBuilder sb = new StringBuilder();
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(),
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)));
364 sb.append(firstChar);
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);
377 if (!Character.isJavaIdentifierPart(actualChar)) {
378 // prepare actual string of sb for checking if underscore exist on position of the
380 final String partialConvertedIdentifier = sb.toString();
381 sb.append(convert(actualChar, existNext(identifier, i),
382 partialConvertedIdentifier.charAt(partialConvertedIdentifier.length() - 1)));
384 sb.append(actualChar);
387 // apply camel case in appropriate way
388 return fixCasesByJavaType(sb.toString().replace("__", "_").toLowerCase(), javaIdentifier);
392 * Fix cases of converted identifiers by Java type
395 * - converted identifier
396 * @param javaIdentifier
397 * - java type of identifier
398 * @return converted identifier with right cases according to java type
400 private static String fixCasesByJavaType(final String convertedIdentifier, final JavaIdentifier javaIdentifier) {
401 switch (javaIdentifier) {
405 return capitalize(fixCases(convertedIdentifier));
408 return convertedIdentifier.toUpperCase();
411 return fixCases(convertedIdentifier);
413 return convertedIdentifier.replaceAll(String.valueOf(UNDERSCORE), EMPTY_STRING);
415 throw new IllegalArgumentException("Unknown java type of identifier : " + javaIdentifier.toString());
420 * Delete unnecessary chars in converted identifier and apply camel case in appropriate way.
422 * @param convertedIdentifier
423 * - original converted identifier
424 * @return resolved identifier
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))) {
435 sb.append(capitalize(part));
439 sb.append(convertedIdentifier);
441 return sb.toString();
445 * Check if there exist next char in identifier behind actual char position
448 * - original identifier
450 * - actual char position
451 * @return true if there is another char, false otherwise
453 private static boolean existNext(final String identifier, final int actual) {
454 return (identifier.length() - 1) < (actual + 1) ? false : true;
458 * Converting first char of identifier. This happen only if this char is
464 * - existing of next char behind actual char
465 * @return converted char
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));
472 name = existNext ? (name + "_") : name;
473 return name.contains(" ") ? name.replaceAll(" ", "_") : name;
477 * Converting any char in java identifier, This happen only if this char is
483 * - existing of next char behind actual char
484 * @param partialLastChar
485 * - last char of partial converted identifier
486 * @return converted char
488 private static String convert(final char c, final boolean existNext, final char partialLastChar) {
489 return partialLastChar == '_' ? convertFirst(c, existNext) : "_" + convertFirst(c, existNext);
493 * Capitalize input string
496 * - string to be capitalized
498 private static String capitalize(final String identifier) {
499 return identifier.substring(FIRST_CHAR, FIRST_CHAR + 1).toUpperCase() + identifier.substring(1);
502 private static String convertIdentifierEnumValue(final String name, final String origName, final List<Pair> values,
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);
514 return convertIdentifier(newName, JavaIdentifier.ENUM_VALUE);