/* * 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}: * * * 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: * * * 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: * * */ @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); } }