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.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;
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;
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}:
30 * <li>class, enum, interface</li>
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>
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>
51 * <li>enum value, constant</li>
54 * <li>used underscore as special separator</li>
55 * <li>converted identifier to upper case</li>
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>
70 * <li>method, variable</li>
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
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>
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>
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>
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>
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.
130 * <li>class, enum, interface</li>
133 * <li>foo-cont - FooCont</li>
134 * <li>foo--cont - FooHyphenMinusHyphenMinusCont</li>
135 * <li>-foo - HyphenMinusFoo</li>
136 * <li>foo- - FooHyphenMinus</li>
139 * <li>enum value, constant
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>
148 * <li>method, variable</li>
151 * <li>foo-cont - fooCont</li>
152 * <li>foo--cont - fooHyphenMinusHyphenMinusCont</li>
153 * <li>-foo - hyphenMinusFoo</li>
154 * <li>foo- - fooHyphenMinus</li>
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:
168 * <li>class, enum, interface</li>
171 * <li>package name org.example, class (or interface or enum) Foo - normalized
173 * <li>package name org.example, class (or interface or enum) fOo - normalized
177 * <li>enum value</li>
190 * <li>YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum
197 public final class JavaIdentifierNormalizer {
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",
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[]");
216 private static final Splitter DOT_SPLITTER = Splitter.on('.');
218 private JavaIdentifierNormalizer() {
219 throw new UnsupportedOperationException("Util class");
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}
240 * YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum values.
243 * - name of new enum value
245 * - list of all actual enum values
246 * @return converted and fixed name of new enum value
248 public static String normalizeEnumValueIdentifier(final String name, final List<Pair> values) {
249 return convertIdentifierEnumValue(name, name, values, FIRST_INDEX);
253 * Normalizing full package name by non java chars and reserved keywords.
255 * @param fullPackageName
256 * - full package name
257 * @return normalized name
259 public static String normalizeFullPackageName(final String fullPackageName) {
260 final Iterator<String> it = DOT_SPLITTER.split(fullPackageName).iterator();
262 return fullPackageName;
265 final StringBuilder sb = new StringBuilder(fullPackageName.length());
267 sb.append(normalizePartialPackageName(it.next()));
269 return sb.toString();
276 * Normalizing part of package name by non java chars.
278 * @param packageNamePart
279 * - part of package name
280 * @return normalized name
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();
289 String normalizedPackageNamePart = packageNamePart;
290 if (packageNamePart.contains(String.valueOf(DASH))) {
291 normalizedPackageNamePart = packageNamePart.replaceAll(String.valueOf(DASH), String.valueOf(UNDERSCORE));
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();
301 sb.append(UNDERSCORE);
303 innserSb.append(normalizedPackageNamePart.charAt(i));
306 if (!innserSb.toString().isEmpty()) {
307 sb.append(normalizeSpecificIdentifier(innserSb.toString(), JavaIdentifier.PACKAGE));
309 // returned normalized part of package name
310 return sb.toString();
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.
321 * - package of identifier
323 * - name of identifier
324 * @return - java acceptable identifier
326 public static String normalizeClassIdentifier(final String packageName, final String className) {
327 if (packageName.isEmpty() && PRIMITIVE_TYPES.contains(className)) {
330 for (final String reservedPath : SPECIAL_RESERVED_PATHS) {
331 if (packageName.startsWith(reservedPath)) {
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
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))) {
357 suppInnerClassPackageName = sb.toString();
361 return normalizeClassIdentifier(suppInnerClassPackageName, convertedClassName, convertedClassName, FIRST_INDEX);
365 * Find and convert non Java chars in identifiers of generated transfer objects, initially
366 * derived from corresponding YANG.
369 * - name of identifier
370 * @param javaIdentifier
371 * - java type of identifier
372 * @return - java acceptable identifier
374 public static String normalizeSpecificIdentifier(final String identifier, final JavaIdentifier javaIdentifier) {
375 final StringBuilder sb = new StringBuilder();
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(),
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)));
393 sb.append(firstChar);
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);
406 if (!Character.isJavaIdentifierPart(actualChar)) {
407 // prepare actual string of sb for checking if underscore exist on position of the
409 final String partialConvertedIdentifier = sb.toString();
410 sb.append(convert(actualChar, existNext(identifier, i),
411 partialConvertedIdentifier.charAt(partialConvertedIdentifier.length() - 1)));
413 sb.append(actualChar);
416 // apply camel case in appropriate way
417 return fixCasesByJavaType(sb.toString().replace("__", "_").toLowerCase(), javaIdentifier);
421 * Checking while there doesn't exist any class name with the same name
422 * (regardless of camel cases) in package.
425 * - package of class name
426 * @param origClassName
427 * - original class name
428 * @param actualClassName
429 * - actual class name with rank (serial number)
431 * - actual rank (serial number)
432 * @return converted identifier
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);
445 PACKAGES_MAP.put(packageName, actualClassName);
446 return actualClassName;
450 * Fix cases of converted identifiers by Java type
453 * - converted identifier
454 * @param javaIdentifier
455 * - java type of identifier
456 * @return converted identifier with right cases according to java type
458 private static String fixCasesByJavaType(final String convertedIdentifier, final JavaIdentifier javaIdentifier) {
459 switch (javaIdentifier) {
463 return capitalize(fixCases(convertedIdentifier));
466 return convertedIdentifier.toUpperCase();
469 return fixCases(convertedIdentifier);
471 return convertedIdentifier.replaceAll(String.valueOf(UNDERSCORE), EMPTY_STRING);
473 throw new IllegalArgumentException("Unknown java type of identifier : " + javaIdentifier.toString());
478 * Delete unnecessary chars in converted identifier and apply camel case in appropriate way.
480 * @param convertedIdentifier
481 * - original converted identifier
482 * @return resolved identifier
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))) {
493 sb.append(capitalize(part));
497 sb.append(convertedIdentifier);
499 return sb.toString();
503 * Check if there exist next char in identifier behind actual char position
506 * - original identifier
508 * - actual char position
509 * @return true if there is another char, false otherwise
511 private static boolean existNext(final String identifier, final int actual) {
512 return identifier.length() > actual + 1;
516 * Converting first char of identifier. This happen only if this char is
522 * - existing of next char behind actual char
523 * @return converted char
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));
530 name = existNext ? name + "_" : name;
531 return name.contains(" ") ? name.replaceAll(" ", "_") : name;
535 * Converting any char in java identifier, This happen only if this char is
541 * - existing of next char behind actual char
542 * @param partialLastChar
543 * - last char of partial converted identifier
544 * @return converted char
546 private static String convert(final char c, final boolean existNext, final char partialLastChar) {
547 return partialLastChar == '_' ? convertFirst(c, existNext) : "_" + convertFirst(c, existNext);
551 * Capitalize input string
554 * - string to be capitalized
556 private static String capitalize(final String identifier) {
557 return identifier.substring(FIRST_CHAR, FIRST_CHAR + 1).toUpperCase() + identifier.substring(1);
560 private static String convertIdentifierEnumValue(final String name, final String origName, final List<Pair> values,
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);
572 return normalizeSpecificIdentifier(newName, JavaIdentifier.ENUM_VALUE);