/*
* Copyright (c) 2017 Cisco Systems, Inc. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.mdsal.binding.javav2.generator.util;
import com.google.common.annotations.Beta;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.opendaylight.mdsal.binding.javav2.model.api.Enumeration;
import org.opendaylight.mdsal.binding.javav2.model.api.Enumeration.Pair;
import org.opendaylight.mdsal.binding.javav2.util.BindingMapping;
/**
* This util class converts every non-java char in identifier to java char by
* its unicode name (JAVA SE
* SPECIFICATIONS - Identifiers). There are special types of mapping
* non-java chars to original identifiers according to specific
* {@linkplain JavaIdentifier java type}:
*
* - class, enum, interface
* -
*
* - without special separator
* - the first character of identifier, any other first character of
* identifier part mapped by non-Java char name from unicode and char in
* identifier behind non-java char name are converting to upper case
* - examples:
* -
*
* - example* - ExampleAsterisk
* - example*example - ExampleAserisksExample
* - \example - ReverseSolidusExample
* - 1example - DigitOneExample
* - example1 - Example1
* - int - IntReservedKeyword
* - con - ConReservedKeyword
*
*
*
*
* - enum value, constant
* -
*
* - used underscore as special separator
* - converted identifier to upper case
* - examples:
* -
*
* - example* - EXAMPLE_ASTERISK
* - example*example - EXAMPLE_ASTERISK_EXAMPLE
* - \example - REVERSE_SOLIDUS_EXAMPLE
* - 1example - DIGIT_ONE_EXAMPLE
* - example1 - EXAMPLE1
* - int - INT_RESERVED_KEYWORD
* - con - CON_RESERVED_KEYWORD
*
*
*
*
* - method, variable
* -
*
-
*
* - without special separator
* - the first character of identifier is converting to lower case
* - any other first character of identifier part mapped by non-Java char name
* from unicode and char in identifier behind non-java char name are converting
* to upper case
* - examples:
* -
*
* - example* - exampleAsterisk
* - example*example - exampleAserisksExample
* - \example - reverseSolidusExample
* - 1example - digitOneExample
* - example1 - example1
* - int - intReservedKeyword
* - con - conReservedKeyword
*
*
*
*
* - package - full package name (
* Naming a package)
* -
*
-
*
* - parts of package name are separated by dots
* - parts of package name are converting to lower case
* - if parts of package name are reserved Java or Windows keywords, such as
* 'int' the suggested convention is to add an underscore to keyword
* - dash is parsed as underscore according to
* Naming a package
* - examples:
* -
*
* - org.example* - org.exampleasterisk
* - org.example*example - org.exampleasteriskexample
* - org.\example - org.reversesolidusexample
* - org.1example - org.digitoneexample
* - org.example1 - org.example1
* - org.int - org.int_
* - org.con - org.con_
* - org.foo-cont - org.foo_cont
*
*
*
*
*
*
* There is special case in CLASS, INTERFACE, ENUM, ENUM VALUE, CONSTANT, METHOD
* and VARIABLE if identifier contains single dash - then the converter ignores
* the single dash in the way of the non-java chars. In other way, if dash is
* the first or the last char in the identifier or there is more dashes in a row
* in the identifier, then these dashes are converted as non-java chars.
* Example:
*
* - class, enum, interface
* -
*
* - foo-cont - FooCont
* - foo--cont - FooHyphenMinusHyphenMinusCont
* - -foo - HyphenMinusFoo
* - foo- - FooHyphenMinus
*
*
* - enum value, constant
*
-
*
* - foo-cont - FOO_CONT
* - foo--cont - FOO_HYPHEN_MINUS_HYPHEN_MINUS_CONT
* - -foo - HYPHEN_MINUS_FOO
* - foo- - FOO_HYPHEN_MINUS
*
*
* - method, variable
* -
*
* - foo-cont - fooCont
* - foo--cont - fooHyphenMinusHyphenMinusCont
* - -foo - hyphenMinusFoo
* - foo- - fooHyphenMinus
*
*
*
*
* Next special case talks about normalizing class name which already exists in
* package - but with different camel cases (foo, Foo, fOo, ...). To every next
* classes with same names will by added their actual rank (serial number),
* except the first one. This working for CLASS, ENUM and INTEFACE java
* identifiers. If there exist the same ENUM VALUES in ENUM (with different
* camel cases), then it's parsed with same logic like CLASSES, ENUMS and
* INTERFACES but according to list of pairs of their ENUM parent. Example:
*
*
* - class, enum, interface
* -
*
* - package name org.example, class (or interface or enum) Foo - normalized
* to Foo
*
- package name org.example, class (or interface or enum) fOo - normalized
* to Foo1
*
*
* - enum value
* -
*
*
*
*/
@Beta
public final class JavaIdentifierNormalizer {
public static final Set SPECIAL_RESERVED_PATHS = ImmutableSet.of(
"org.opendaylight.yangtools.concepts",
"org.opendaylight.yangtools.yang.common",
"org.opendaylight.yangtools.yang.model",
"org.opendaylight.mdsal.binding.javav2.spec",
"java",
"com");
private static final int FIRST_CHAR = 0;
private static final int FIRST_INDEX = 1;
private static final char UNDERSCORE = '_';
private static final char DASH = '-';
private static final String RESERVED_KEYWORD = "reserved_keyword";
private static final ListMultimap PACKAGES_MAP = ArrayListMultimap.create();
private static final Set PRIMITIVE_TYPES = ImmutableSet.of("char[]", "byte[]");
private static final CharMatcher DASH_MATCHER = CharMatcher.is(DASH);
private static final CharMatcher DASH_OR_SPACE_MATCHER = CharMatcher.anyOf(" -");
private static final CharMatcher UNDERSCORE_MATCHER = CharMatcher.is(UNDERSCORE);
private static final Splitter DOT_SPLITTER = Splitter.on('.');
private static final Splitter UNDERSCORE_SPLITTER = Splitter.on(UNDERSCORE);
private JavaIdentifierNormalizer() {
throw new UnsupportedOperationException("Util class");
}
/**
*
* According to YANG RFC 7950,
* all assigned names in an enumeration MUST be unique. Created names are contained in the list
* of {@link Enumeration.Pair}. This method adds actual index with underscore behind name of new
* enum value only if this name already exists in one of the list of {@link Enumeration.Pair}.
* Then, the name will be converted to java chars according to {@link JavaIdentifier#ENUM_VALUE}
* and returned.
*
* Example:
*
*
* type enumeration {
* enum foo;
* enum Foo;
* }
*
*
* YANG enum values will be mapped to 'FOO' and 'FOO_1' Java enum values.
*
* @param name
* - name of new enum value
* @param values
* - list of all actual enum values
* @return converted and fixed name of new enum value
*/
public static String normalizeEnumValueIdentifier(final String name, final List values) {
return convertIdentifierEnumValue(name, name, values, FIRST_INDEX);
}
/**
* Normalizing full package name by non java chars and reserved keywords.
*
* @param fullPackageName
* - full package name
* @return normalized name
*/
public static String normalizeFullPackageName(final String fullPackageName) {
final Iterator it = DOT_SPLITTER.split(fullPackageName).iterator();
if (!it.hasNext()) {
return fullPackageName;
}
final StringBuilder sb = new StringBuilder(fullPackageName.length());
while (true) {
sb.append(normalizePartialPackageName(it.next()));
if (!it.hasNext()) {
return sb.toString();
}
sb.append('.');
}
}
/**
* Normalizing part of package name by non java chars.
*
* @param packageNamePart
* - part of package name
* @return normalized name
*/
public static String normalizePartialPackageName(final String packageNamePart) {
// if part of package name consist from java or windows reserved word, return it with
// underscore at the end and in lower case
final String lowerPart = packageNamePart.toLowerCase();
if (BindingMapping.JAVA_RESERVED_WORDS.contains(lowerPart)
|| BindingMapping.WINDOWS_RESERVED_WORDS.contains(packageNamePart.toUpperCase())) {
return lowerPart + UNDERSCORE;
}
final String normalizedPart = DASH_MATCHER.replaceFrom(packageNamePart, UNDERSCORE);
final StringBuilder sb = new StringBuilder();
final StringBuilder innerSb = new StringBuilder();
for (int i = 0; i < normalizedPart.length(); i++) {
final char c = normalizedPart.charAt(i);
if (c == UNDERSCORE) {
if (innerSb.length() != 0) {
sb.append(normalizeSpecificIdentifier(innerSb.toString(), JavaIdentifier.PACKAGE));
innerSb.setLength(0);
}
sb.append(UNDERSCORE);
} else {
innerSb.append(c);
}
}
if (innerSb.length() != 0) {
sb.append(normalizeSpecificIdentifier(innerSb.toString(), JavaIdentifier.PACKAGE));
}
// returned normalized part of package name
return sb.toString();
}
/**
* Find and convert non Java chars in identifiers of generated transfer objects, initially
* derived from corresponding YANG according to
* Java
* Specifications - Identifiers. If there is more same class names at the same package, then
* append rank (serial number) to the end of them. Works for class, enum, interface.
*
* @param packageName
* - package of identifier
* @param className
* - name of identifier
* @return - java acceptable identifier
*/
public static String normalizeClassIdentifier(final String packageName, final String className) {
if (packageName.isEmpty() && PRIMITIVE_TYPES.contains(className)) {
return className;
}
for (final String reservedPath : SPECIAL_RESERVED_PATHS) {
if (packageName.startsWith(reservedPath)) {
return className;
}
}
final String convertedClassName = normalizeSpecificIdentifier(className, JavaIdentifier.CLASS);
// if packageName contains class name at the end, then the className is
// name of inner class
final String[] packageNameParts = packageName.split("\\.");
String suppInnerClassPackageName = packageName;
if (packageNameParts.length > 1) {
if (Character.isUpperCase(packageNameParts[packageNameParts.length - 1].charAt(FIRST_CHAR))) {
final StringBuilder sb = new StringBuilder();
// ignore class name in package name - inner class name has
// to be normalizing according to original package of parent
// class
for (int i = 0; i < packageNameParts.length - 1; i++) {
if (!Character.isUpperCase(packageNameParts[i].charAt(FIRST_CHAR))) {
sb.append(packageNameParts[i]);
if (i != packageNameParts.length - 2 &&
!Character.isUpperCase(packageNameParts[i+1].charAt(FIRST_CHAR))) {
sb.append('.');
}
} else {
break;
}
}
suppInnerClassPackageName = sb.toString();
}
}
return normalizeClassIdentifier(suppInnerClassPackageName, convertedClassName, convertedClassName, FIRST_INDEX);
}
/**
* Find and convert non Java chars in identifiers of generated transfer objects, initially
* derived from corresponding YANG.
*
* @param identifier
* - name of identifier
* @param javaIdentifier
* - java type of identifier
* @return - java acceptable identifier
*/
public static String normalizeSpecificIdentifier(final String identifier, final JavaIdentifier javaIdentifier) {
final StringBuilder sb = new StringBuilder();
// if identifier isn't PACKAGE type then check it by reserved keywords
if(javaIdentifier != JavaIdentifier.PACKAGE) {
if (BindingMapping.JAVA_RESERVED_WORDS.contains(identifier.toLowerCase())
|| BindingMapping.WINDOWS_RESERVED_WORDS.contains(identifier.toUpperCase())) {
return fixCasesByJavaType(
sb.append(identifier).append(UNDERSCORE).append(RESERVED_KEYWORD).toString().toLowerCase(),
javaIdentifier);
}
}
// check and convert first char in identifier if there is non-java char
final char firstChar = identifier.charAt(FIRST_CHAR);
if (!Character.isJavaIdentifierStart(firstChar)) {
// converting first char of identifier
sb.append(convertFirst(firstChar, existNext(identifier, FIRST_CHAR)));
} else {
sb.append(firstChar);
}
// check and convert other chars in identifier, if there is non-java char
for (int i = 1; i < identifier.length(); i++) {
final char actualChar = identifier.charAt(i);
// ignore single dash as non java char - if there is more dashes in a row or dash is as
// the last char in identifier then parse these dashes as non java chars
if (actualChar == '-' && existNext(identifier, i)) {
if (identifier.charAt(i - 1) != DASH && identifier.charAt(i + 1) != DASH) {
sb.append(UNDERSCORE);
continue;
}
}
if (!Character.isJavaIdentifierPart(actualChar)) {
// prepare actual string of sb for checking if underscore exist on position of the
// last char
final String partialConvertedIdentifier = sb.toString();
sb.append(convert(actualChar, existNext(identifier, i),
partialConvertedIdentifier.charAt(partialConvertedIdentifier.length() - 1)));
} else {
sb.append(actualChar);
}
}
// apply camel case in appropriate way
return fixCasesByJavaType(sb.toString().replace("__", "_").toLowerCase(), javaIdentifier);
}
/**
* Checking while there doesn't exist any class name with the same name
* (regardless of camel cases) in package.
*
* @param packageName
* - package of class name
* @param origClassName
* - original class name
* @param actualClassName
* - actual class name with rank (serial number)
* @param rank
* - actual rank (serial number)
* @return converted identifier
*/
private static String normalizeClassIdentifier(final String packageName, final String origClassName,
final String actualClassName, final int rank) {
// FIXME: this does not look thread-safe and seems to leak memory
if (PACKAGES_MAP.containsKey(packageName)) {
for (final String existingName : PACKAGES_MAP.get(packageName)) {
if (actualClassName.equalsIgnoreCase(existingName)) {
return normalizeClassIdentifier(packageName, origClassName, origClassName + rank, rank + 1);
}
}
}
PACKAGES_MAP.put(packageName, actualClassName);
return actualClassName;
}
/**
* Fix cases of converted identifiers by Java type
*
* @param string
* - converted identifier
* @param javaIdentifier
* - java type of identifier
* @return converted identifier with right cases according to java type
*/
private static String fixCasesByJavaType(final String convertedIdentifier, final JavaIdentifier javaIdentifier) {
switch (javaIdentifier) {
case CLASS:
case ENUM:
case INTERFACE:
return capitalize(fixCases(convertedIdentifier));
case ENUM_VALUE:
case CONSTANT:
return convertedIdentifier.toUpperCase();
case METHOD:
case VARIABLE:
return fixCases(convertedIdentifier);
case PACKAGE:
return UNDERSCORE_MATCHER.removeFrom(convertedIdentifier);
default:
throw new IllegalArgumentException("Unknown java type of identifier : " + javaIdentifier.toString());
}
}
/**
* Delete unnecessary chars in converted identifier and apply camel case in appropriate way.
*
* @param convertedIdentifier
* - original converted identifier
* @return resolved identifier
*/
private static String fixCases(final String convertedIdentifier) {
if (convertedIdentifier.indexOf(UNDERSCORE) == -1) {
return convertedIdentifier;
}
final StringBuilder sb = new StringBuilder(convertedIdentifier.length());
final Iterator it = UNDERSCORE_SPLITTER.split(convertedIdentifier).iterator();
sb.append(it.next());
while (it.hasNext()) {
sb.append(capitalize(it.next()));
}
return sb.toString();
}
/**
* Check if there exist next char in identifier behind actual char position
*
* @param identifier
* - original identifier
* @param actual
* - actual char position
* @return true if there is another char, false otherwise
*/
private static boolean existNext(final String identifier, final int actual) {
return identifier.length() > actual + 1;
}
/**
* Converting first char of identifier. This happen only if this char is
* non-java char
*
* @param c
* - first char
* @param existNext
* - existing of next char behind actual char
* @return converted char
*/
private static String convertFirst(final char c, final boolean existNext) {
final String name = DASH_OR_SPACE_MATCHER.replaceFrom(Character.getName(c), UNDERSCORE);
return existNext ? name + '_' : name;
}
/**
* Converting any char in java identifier, This happen only if this char is
* non-java char
*
* @param c
* - actual char
* @param existNext
* - existing of next char behind actual char
* @param partialLastChar
* - last char of partial converted identifier
* @return converted char
*/
private static String convert(final char c, final boolean existNext, final char partialLastChar) {
return partialLastChar == '_' ? convertFirst(c, existNext) : "_" + convertFirst(c, existNext);
}
/**
* Capitalize input string
*
* @param identifier
* - string to be capitalized
*/
private static String capitalize(final String identifier) {
return identifier.substring(FIRST_CHAR, FIRST_CHAR + 1).toUpperCase() + identifier.substring(1);
}
private static String convertIdentifierEnumValue(final String name, final String origName, final List values,
final int rank) {
String newName = name;
for (final Pair pair : values) {
if (name.equalsIgnoreCase(pair.getName()) || name.equalsIgnoreCase(pair.getMappedName())) {
int actualRank = rank;
final String actualName = origName + UNDERSCORE + actualRank;
newName = convertIdentifierEnumValue(actualName, origName, values, ++actualRank);
}
}
return normalizeSpecificIdentifier(newName, JavaIdentifier.ENUM_VALUE);
}
}