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.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;
19 import java.util.stream.Collectors;
20 import org.opendaylight.mdsal.binding.javav2.model.api.Enumeration;
21 import org.opendaylight.mdsal.binding.javav2.model.api.Enumeration.Pair;
22 import org.opendaylight.mdsal.binding.javav2.util.BindingMapping;
25 * This util class converts every non-java char in identifier to java char by
26 * its unicode name (<a href=
27 * "http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8">JAVA SE
28 * SPECIFICATIONS - Identifiers</a>). There are special types of mapping
29 * non-java chars to original identifiers according to specific
30 * {@linkplain JavaIdentifier java type}:
32 * <li>class, enum, interface</li>
35 * <li>without special separator</li>
36 * <li>the first character of identifier, any other first character of
37 * identifier part mapped by non-Java char name from unicode and char in
38 * identifier behind non-java char name are converting to upper case</li>
42 * <li>example* - ExampleAsterisk</li>
43 * <li>example*example - ExampleAserisksExample</li>
44 * <li>\example - ReverseSolidusExample</li>
45 * <li>1example - DigitOneExample</li>
46 * <li>example1 - Example1</li>
47 * <li>int - IntReservedKeyword</li>
48 * <li>con - ConReservedKeyword</li>
53 * <li>enum value, constant</li>
56 * <li>used underscore as special separator</li>
57 * <li>converted identifier to upper case</li>
61 * <li>example* - EXAMPLE_ASTERISK</li>
62 * <li>example*example - EXAMPLE_ASTERISK_EXAMPLE</li>
63 * <li>\example - REVERSE_SOLIDUS_EXAMPLE</li>
64 * <li>1example - DIGIT_ONE_EXAMPLE</li>
65 * <li>example1 - EXAMPLE1</li>
66 * <li>int - INT_RESERVED_KEYWORD</li>
67 * <li>con - CON_RESERVED_KEYWORD</li>
72 * <li>method, variable</li>
76 * <li>without special separator</li>
77 * <li>the first character of identifier is converting to lower case</li>
78 * <li>any other first character of identifier part mapped by non-Java char name
79 * from unicode and char in identifier behind non-java char name are converting
84 * <li>example* - exampleAsterisk</li>
85 * <li>example*example - exampleAserisksExample</li>
86 * <li>\example - reverseSolidusExample</li>
87 * <li>1example - digitOneExample</li>
88 * <li>example1 - example1</li>
89 * <li>int - intReservedKeyword</li>
90 * <li>con - conReservedKeyword</li>
95 * <li>package - full package name (<a href=
96 * "https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html">
97 * Naming a package</a>)</li>
101 * <li>parts of package name are separated by dots</li>
102 * <li>parts of package name are converting to lower case</li>
103 * <li>if parts of package name are reserved Java or Windows keywords, such as
104 * 'int' the suggested convention is to add an underscore to keyword</li>
105 * <li>dash is parsed as underscore according to <a href=
106 * "https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html">
107 * Naming a package</a></li>
111 * <li>org.example* - org.exampleasterisk</li>
112 * <li>org.example*example - org.exampleasteriskexample</li>
113 * <li>org.\example - org.reversesolidusexample</li>
114 * <li>org.1example - org.digitoneexample</li>
115 * <li>org.example1 - org.example1</li>
116 * <li>org.int - org.int_</li>
117 * <li>org.con - org.con_</li>
118 * <li>org.foo-cont - org.foo_cont</li>
125 * There is special case in CLASS, INTERFACE, ENUM, ENUM VALUE, CONSTANT, METHOD
126 * and VARIABLE if identifier contains single dash - then the converter ignores
127 * the single dash in the way of the non-java chars. In other way, if dash is
128 * the first or the last char in the identifier or there is more dashes in a row
129 * in the identifier, then these dashes are converted as non-java chars.
132 * <li>class, enum, interface</li>
135 * <li>foo-cont - FooCont</li>
136 * <li>foo--cont - FooHyphenMinusHyphenMinusCont</li>
137 * <li>-foo - HyphenMinusFoo</li>
138 * <li>foo- - FooHyphenMinus</li>
141 * <li>enum value, constant
144 * <li>foo-cont - FOO_CONT</li>
145 * <li>foo--cont - FOO_HYPHEN_MINUS_HYPHEN_MINUS_CONT</li>
146 * <li>-foo - HYPHEN_MINUS_FOO</li>
147 * <li>foo- - FOO_HYPHEN_MINUS</li>
150 * <li>method, variable</li>
153 * <li>foo-cont - fooCont</li>
154 * <li>foo--cont - fooHyphenMinusHyphenMinusCont</li>
155 * <li>-foo - hyphenMinusFoo</li>
156 * <li>foo- - fooHyphenMinus</li>
161 * Next special case talks about normalizing class name which already exists in
162 * package - but with different camel cases (foo, Foo, fOo, ...). To every next
163 * classes with same names will by added their actual rank (serial number),
164 * except the first one. This working for CLASS, ENUM and INTEFACE java
165 * identifiers. If there exist the same ENUM VALUES in ENUM (with different
166 * camel cases), then it's parsed with same logic like CLASSES, ENUMS and
167 * INTERFACES but according to list of pairs of their ENUM parent. Example:
170 * <li>class, enum, interface</li>
173 * <li>package name org.example, class (or interface or enum) Foo - normalized
175 * <li>package name org.example, class (or interface or enum) fOo - normalized
179 * <li>enum value</li>
192 * <li>YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum
199 public final class JavaIdentifierNormalizer {
201 public static final Set<String> SPECIAL_RESERVED_PATHS = ImmutableSet.of(
202 "org.opendaylight.yangtools.concepts",
203 "org.opendaylight.yangtools.yang.common",
204 "org.opendaylight.yangtools.yang.model",
205 "org.opendaylight.mdsal.binding.javav2.spec",
209 private static final char UNDERSCORE = '_';
210 private static final char DASH = '-';
211 private static final String RESERVED_KEYWORD = "reserved_keyword";
212 private static final Set<String> PRIMITIVE_TYPES = ImmutableSet.of("char[]", "byte[]");
214 private static final CharMatcher DASH_MATCHER = CharMatcher.is(DASH);
215 private static final CharMatcher DASH_OR_SPACE_MATCHER = CharMatcher.anyOf(" -");
216 private static final CharMatcher UNDERSCORE_MATCHER = CharMatcher.is(UNDERSCORE);
217 private static final Splitter DOT_SPLITTER = Splitter.on('.');
218 private static final Splitter UNDERSCORE_SPLITTER = Splitter.on(UNDERSCORE);
220 // Converted to lower case
221 private static final Set<String> WINDOWS_RESERVED_WORDS = BindingMapping.WINDOWS_RESERVED_WORDS.stream()
222 .map(String::toLowerCase).collect(Collectors.collectingAndThen(Collectors.toSet(), ImmutableSet::copyOf));
224 // FIXME: this thing makes this class non-threadsafe and leak memory
225 private static final ListMultimap<String, String> PACKAGES_MAP = ArrayListMultimap.create();
227 private JavaIdentifierNormalizer() {
228 throw new UnsupportedOperationException("Util class");
233 * According to <a href="https://tools.ietf.org/html/rfc7950#section-9.6.4">YANG RFC 7950</a>,
234 * all assigned names in an enumeration MUST be unique. Created names are contained in the list
235 * of {@link Enumeration.Pair}. This method adds actual index with underscore behind name of new
236 * enum value only if this name already exists in one of the list of {@link Enumeration.Pair}.
237 * Then, the name will be converted to java chars according to {@link JavaIdentifier#ENUM_VALUE}
249 * YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum values.
252 * - name of new enum value
254 * - list of all actual enum values
255 * @return converted and fixed name of new enum value
257 public static String normalizeEnumValueIdentifier(final String name, final List<Pair> values) {
258 return convertIdentifierEnumValue(name, name, values, 1);
262 * Normalizing full package name by non java chars and reserved keywords.
264 * @param fullPackageName
265 * - full package name
266 * @return normalized name
268 public static String normalizeFullPackageName(final String fullPackageName) {
269 final Iterator<String> it = DOT_SPLITTER.split(fullPackageName).iterator();
271 return fullPackageName;
274 final StringBuilder sb = new StringBuilder(fullPackageName.length());
276 sb.append(normalizePartialPackageName(it.next()));
278 return sb.toString();
285 * Normalizing part of package name by non java chars.
287 * @param packageNamePart
288 * - part of package name
289 * @return normalized name
291 public static String normalizePartialPackageName(final String packageNamePart) {
292 // if part of package name consist from java or windows reserved word, return it with
293 // underscore at the end and in lower case
294 final String lowerPart = packageNamePart.toLowerCase();
295 if (BindingMapping.JAVA_RESERVED_WORDS.contains(lowerPart) || WINDOWS_RESERVED_WORDS.contains(lowerPart)) {
296 return lowerPart + UNDERSCORE;
299 final String normalizedPart = DASH_MATCHER.replaceFrom(packageNamePart, UNDERSCORE);
301 final StringBuilder sb = new StringBuilder();
302 final StringBuilder innerSb = new StringBuilder();
303 for (int i = 0; i < normalizedPart.length(); i++) {
304 final char c = normalizedPart.charAt(i);
305 if (c == UNDERSCORE) {
306 if (innerSb.length() != 0) {
307 sb.append(normalizeSpecificIdentifier(innerSb.toString(), JavaIdentifier.PACKAGE));
308 innerSb.setLength(0);
310 sb.append(UNDERSCORE);
315 if (innerSb.length() != 0) {
316 sb.append(normalizeSpecificIdentifier(innerSb.toString(), JavaIdentifier.PACKAGE));
318 // returned normalized part of package name
319 return sb.toString();
323 * Find and convert non Java chars in identifiers of generated transfer objects, initially
324 * derived from corresponding YANG according to
325 * <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8"> Java
326 * Specifications - Identifiers</a>. If there is more same class names at the same package, then
327 * append rank (serial number) to the end of them. Works for class, enum, interface.
330 * - package of identifier
332 * - name of identifier
333 * @return - java acceptable identifier
335 public static String normalizeClassIdentifier(final String packageName, final String className) {
336 if (packageName.isEmpty() && PRIMITIVE_TYPES.contains(className)) {
339 for (final String reservedPath : SPECIAL_RESERVED_PATHS) {
340 if (packageName.startsWith(reservedPath)) {
344 final String convertedClassName = normalizeSpecificIdentifier(className, JavaIdentifier.CLASS);
346 // if packageName contains class name at the end, then the className is name of inner class
347 final String basePackageName;
348 final int lastDot = packageName.lastIndexOf('.');
349 if (lastDot != -1 && Character.isUpperCase(packageName.charAt(lastDot + 1))) {
350 // ignore class name in package name - inner class name has to be normalized according to original package
352 basePackageName = packageName.substring(0, lastDot);
354 basePackageName = packageName;
357 return normalizeClassIdentifier(basePackageName, convertedClassName, convertedClassName, 1);
361 * Find and convert non Java chars in identifiers of generated transfer objects, initially
362 * derived from corresponding YANG.
365 * - name of identifier
366 * @param javaIdentifier
367 * - java type of identifier
368 * @return - java acceptable identifier
370 public static String normalizeSpecificIdentifier(final String identifier, final JavaIdentifier javaIdentifier) {
371 // if identifier isn't PACKAGE type then check it by reserved keywords
372 if (javaIdentifier != JavaIdentifier.PACKAGE) {
373 final String lower = identifier.toLowerCase();
374 if (BindingMapping.JAVA_RESERVED_WORDS.contains(lower) || WINDOWS_RESERVED_WORDS.contains(lower)) {
375 return fixCasesByJavaType(lower + UNDERSCORE + RESERVED_KEYWORD, javaIdentifier);
379 // check and convert first char in identifier if there is non-java char
380 final StringBuilder sb = new StringBuilder();
381 final char firstChar = identifier.charAt(0);
382 if (!Character.isJavaIdentifierStart(firstChar)) {
383 // converting first char of identifier
384 sb.append(convertFirst(firstChar, existNext(identifier, 0)));
386 sb.append(firstChar);
388 // check and convert other chars in identifier, if there is non-java char
389 for (int i = 1; i < identifier.length(); i++) {
390 final char actualChar = identifier.charAt(i);
391 // ignore single dash as non java char - if there is more dashes in a row or dash is as
392 // the last char in identifier then parse these dashes as non java chars
393 if (actualChar == DASH && existNext(identifier, i)) {
394 if (identifier.charAt(i - 1) != DASH && identifier.charAt(i + 1) != DASH) {
395 sb.append(UNDERSCORE);
399 if (!Character.isJavaIdentifierPart(actualChar)) {
400 // prepare actual string of sb for checking if underscore exist on position of the last char
401 sb.append(convert(actualChar, existNext(identifier, i), sb.charAt(sb.length() - 1)));
403 sb.append(actualChar);
406 // apply camel case in appropriate way
407 return fixCasesByJavaType(sb.toString().replace("__", "_").toLowerCase(), javaIdentifier);
411 * Checking while there doesn't exist any class name with the same name
412 * (regardless of camel cases) in package.
415 * - package of class name
416 * @param origClassName
417 * - original class name
418 * @param actualClassName
419 * - actual class name with rank (serial number)
421 * - actual rank (serial number)
422 * @return converted identifier
424 private static String normalizeClassIdentifier(final String packageName, final String origClassName,
425 final String actualClassName, final int rank) {
427 // FIXME: this does not look thread-safe and seems to leak memory
428 if (PACKAGES_MAP.containsKey(packageName)) {
429 for (final String existingName : PACKAGES_MAP.get(packageName)) {
430 if (actualClassName.equalsIgnoreCase(existingName)) {
431 return normalizeClassIdentifier(packageName, origClassName, origClassName + rank, rank + 1);
435 PACKAGES_MAP.put(packageName, actualClassName);
436 return actualClassName;
440 * Fix cases of converted identifiers by Java type
442 * @param convertedIdentifier
443 * - converted identifier
444 * @param javaIdentifier
445 * - java type of identifier
446 * @return converted identifier with right cases according to java type
448 private static String fixCasesByJavaType(final String convertedIdentifier, final JavaIdentifier javaIdentifier) {
449 switch (javaIdentifier) {
453 return capitalize(fixCases(convertedIdentifier));
456 return convertedIdentifier.toUpperCase();
459 return fixCases(convertedIdentifier);
461 return UNDERSCORE_MATCHER.removeFrom(convertedIdentifier);
463 throw new IllegalArgumentException("Unknown java type of identifier : " + javaIdentifier.toString());
468 * Delete unnecessary chars in converted identifier and apply camel case in appropriate way.
470 * @param convertedIdentifier
471 * - original converted identifier
472 * @return resolved identifier
474 private static String fixCases(final String convertedIdentifier) {
475 if (convertedIdentifier.indexOf(UNDERSCORE) == -1) {
476 return convertedIdentifier;
479 final StringBuilder sb = new StringBuilder(convertedIdentifier.length());
480 final Iterator<String> it = UNDERSCORE_SPLITTER.split(convertedIdentifier).iterator();
481 sb.append(it.next());
482 while (it.hasNext()) {
483 sb.append(capitalize(it.next()));
485 return sb.toString();
489 * Check if there exist next char in identifier behind actual char position
492 * - original identifier
494 * - actual char position
495 * @return true if there is another char, false otherwise
497 private static boolean existNext(final String identifier, final int actual) {
498 return identifier.length() > actual + 1;
502 * Converting first char of identifier. This happen only if this char is
508 * - existing of next char behind actual char
509 * @return converted char
511 private static String convertFirst(final char c, final boolean existNext) {
512 final String name = DASH_OR_SPACE_MATCHER.replaceFrom(Character.getName(c), UNDERSCORE);
513 return existNext ? name + '_' : name;
517 * Converting any char in java identifier, This happen only if this char is
523 * - existing of next char behind actual char
524 * @param partialLastChar
525 * - last char of partial converted identifier
526 * @return converted char
528 private static String convert(final char c, final boolean existNext, final char partialLastChar) {
529 return partialLastChar == '_' ? convertFirst(c, existNext) : "_" + convertFirst(c, existNext);
533 * Capitalize input string
536 * - string to be capitalized
538 private static String capitalize(final String identifier) {
539 return identifier.substring(0, 1).toUpperCase() + identifier.substring(1);
542 private static String convertIdentifierEnumValue(final String name, final String origName, final List<Pair> values,
544 String newName = name;
545 for (final Pair pair : values) {
546 if (name.equalsIgnoreCase(pair.getName()) || name.equalsIgnoreCase(pair.getMappedName())) {
547 int actualRank = rank;
548 final String actualName = origName + UNDERSCORE + actualRank;
549 newName = convertIdentifierEnumValue(actualName, origName, values, ++actualRank);
552 return normalizeSpecificIdentifier(newName, JavaIdentifier.ENUM_VALUE);