2 * Copyright (c) 2013 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.spec.naming;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.annotations.Beta;
14 import com.google.common.base.CharMatcher;
15 import com.google.common.base.Splitter;
16 import com.google.common.collect.BiMap;
17 import com.google.common.collect.HashBiMap;
18 import com.google.common.collect.ImmutableSet;
19 import com.google.common.collect.Interner;
20 import com.google.common.collect.Interners;
21 import java.util.Collection;
22 import java.util.Locale;
23 import java.util.Optional;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.yangtools.yang.binding.Augmentable;
28 import org.opendaylight.yangtools.yang.binding.DataContainer;
29 import org.opendaylight.yangtools.yang.binding.Identifiable;
30 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
31 import org.opendaylight.yangtools.yang.common.QName;
32 import org.opendaylight.yangtools.yang.common.QNameModule;
33 import org.opendaylight.yangtools.yang.common.Revision;
36 public final class BindingMapping {
38 public static final @NonNull String VERSION = "0.6";
40 // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
41 public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
42 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
43 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
44 "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
45 "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
46 "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
47 "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
48 // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
50 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
52 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
54 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
56 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
58 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
61 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
62 public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
63 public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
64 public static final @NonNull String BUILDER_SUFFIX = "Builder";
65 public static final @NonNull String KEY_SUFFIX = "Key";
66 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
67 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
68 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
70 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
71 .omitEmptyStrings().trimResults();
72 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
73 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
74 private static final Splitter DOT_SPLITTER = Splitter.on('.');
76 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
77 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
78 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
81 * Name of {@link Augmentable#augmentation(Class)}.
83 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
86 * Name of {@link Identifiable#key()}.
88 public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
91 * Name of {@link DataContainer#implementedInterface()}.
93 public static final @NonNull String DATA_CONTAINER_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
96 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
97 * interface contains this static method.
99 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
102 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
103 * interface contains this static method.
105 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
108 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
109 * interface contains this static method.
111 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
114 * Name of {@link ScalarTypeObject#getValue()}.
116 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
119 * Prefix for normal getter methods.
121 public static final @NonNull String GETTER_PREFIX = "get";
124 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
126 public static final @NonNull String NONNULL_PREFIX = "nonnull";
129 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
132 public static final @NonNull String REQUIRE_PREFIX = "require";
133 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
134 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
136 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
138 private BindingMapping() {
142 public static @NonNull String getRootPackageName(final QName module) {
143 return getRootPackageName(module.getModule());
146 public static @NonNull String getRootPackageName(final QNameModule module) {
147 final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
149 String namespace = module.getNamespace().toString();
150 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
152 final char[] chars = namespace.toCharArray();
153 for (int i = 0; i < chars.length; ++i) {
174 packageNameBuilder.append(chars);
175 if (chars[chars.length - 1] != '.') {
176 packageNameBuilder.append('.');
179 final Optional<Revision> optRev = module.getRevision();
180 if (optRev.isPresent()) {
181 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
183 final String rev = optRev.get().toString();
184 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
185 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
187 // No-revision packages are special
188 packageNameBuilder.append("norev");
191 return normalizePackageName(packageNameBuilder.toString());
194 public static @NonNull String normalizePackageName(final String packageName) {
195 final StringBuilder builder = new StringBuilder();
196 boolean first = true;
198 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
205 if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
211 // Prevent duplication of input string
212 return PACKAGE_INTERNER.intern(builder.toString());
215 public static @NonNull String getClassName(final String localName) {
216 return toFirstUpper(toCamelCase(localName));
219 public static @NonNull String getClassName(final QName name) {
220 return toFirstUpper(toCamelCase(name.getLocalName()));
223 public static @NonNull String getMethodName(final String yangIdentifier) {
224 return toFirstLower(toCamelCase(yangIdentifier));
227 public static @NonNull String getMethodName(final QName name) {
228 return getMethodName(name.getLocalName());
231 public static @NonNull String getGetterMethodName(final String localName) {
232 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
235 public static @NonNull String getGetterMethodName(final QName name) {
236 return GETTER_PREFIX + getGetterSuffix(name);
239 public static boolean isGetterMethodName(final String methodName) {
240 return methodName.startsWith(GETTER_PREFIX);
243 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
244 checkArgument(isNonnullMethodName(methodName));
245 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
248 public static @NonNull String getNonnullMethodName(final String localName) {
249 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
252 public static boolean isNonnullMethodName(final String methodName) {
253 return methodName.startsWith(NONNULL_PREFIX);
256 public static @NonNull String getGetterMethodForRequire(final String methodName) {
257 checkArgument(isRequireMethodName(methodName));
258 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
261 public static @NonNull String getRequireMethodName(final String localName) {
262 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
265 public static boolean isRequireMethodName(final String methodName) {
266 return methodName.startsWith(REQUIRE_PREFIX);
269 public static @NonNull String getGetterSuffix(final QName name) {
270 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
271 return "Class".equals(candidate) ? "XmlClass" : candidate;
274 public static @NonNull String getPropertyName(final String yangIdentifier) {
275 final String potential = toFirstLower(toCamelCase(yangIdentifier));
276 if ("class".equals(potential)) {
282 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
284 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
285 final String referencedClassSimpleName) {
286 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
289 private static @NonNull String toCamelCase(final String rawString) {
290 StringBuilder builder = new StringBuilder();
291 for (String comp : CAMEL_SPLITTER.split(rawString)) {
292 builder.append(toFirstUpper(comp));
294 return checkNumericPrefix(builder.toString());
297 private static @NonNull String checkNumericPrefix(final String rawString) {
298 if (rawString.isEmpty()) {
301 final char firstChar = rawString.charAt(0);
302 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
306 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
308 * @param str the string that should get an upper case first character.
309 * @return the {@link String} {@code str} with an upper case first character.
311 public static @NonNull String toFirstUpper(final @NonNull String str) {
315 if (Character.isUpperCase(str.charAt(0))) {
318 if (str.length() == 1) {
319 return str.toUpperCase(Locale.ENGLISH);
321 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
325 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
326 * function is null-safe.
328 * @param str the string that should get an lower case first character. May be <code>null</code>.
329 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
330 * {@link String} {@code str} was empty.
332 private static @NonNull String toFirstLower(final @NonNull String str) {
336 if (Character.isLowerCase(str.charAt(0))) {
339 if (str.length() == 1) {
340 return str.toLowerCase(Locale.ENGLISH);
342 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
346 * Returns the {@link String} {@code s} with a '$' character as suffix.
348 * @param qname RPC QName
349 * @return The RPC method name as determined by considering the localname against the JLS.
350 * @throws NullPointerException if {@code qname} is null
352 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
353 final String methodName = getMethodName(qname);
354 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
358 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
359 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
360 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
363 * @param assignedNames Collection of assigned names
364 * @return A BiMap keyed by assigned name, with Java identifiers as values
365 * @throws NullPointerException if assignedNames is null or contains null items
366 * @throws IllegalArgumentException if any of the names is empty
368 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
370 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
371 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
372 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
373 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
375 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
377 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
378 boolean valid = true;
379 for (String name : assignedNames) {
380 checkArgument(!name.isEmpty());
381 if (!javaToYang.containsValue(name)) {
382 final String mappedName = getClassName(name);
383 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
391 // Fall back to bijective mapping
393 for (String name : assignedNames) {
394 javaToYang.put(mapEnumAssignedName(name), name);
398 return javaToYang.inverse();
401 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
402 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
403 // which have different exclusions
404 private static boolean isValidJavaIdentifier(final String str) {
405 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
406 && Character.isJavaIdentifierStart(str.codePointAt(0))
407 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
410 private static String mapEnumAssignedName(final String assignedName) {
411 checkArgument(!assignedName.isEmpty());
414 // - if the string is a valid java identifier and does not contain '$', use it as-is
415 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
419 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
420 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
421 final StringBuilder sb = new StringBuilder().append('$');
422 assignedName.codePoints().forEachOrdered(codePoint -> {
423 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
424 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
426 sb.appendCodePoint(codePoint);
429 return sb.toString();