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.checkerframework.checker.regex.qual.Regex;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.yangtools.yang.binding.Action;
29 import org.opendaylight.yangtools.yang.binding.Augmentable;
30 import org.opendaylight.yangtools.yang.binding.BindingContract;
31 import org.opendaylight.yangtools.yang.binding.Identifiable;
32 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
33 import org.opendaylight.yangtools.yang.binding.Rpc;
34 import org.opendaylight.yangtools.yang.binding.RpcInput;
35 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
36 import org.opendaylight.yangtools.yang.common.QName;
37 import org.opendaylight.yangtools.yang.common.QNameModule;
38 import org.opendaylight.yangtools.yang.common.Revision;
41 public final class BindingMapping {
43 public static final @NonNull String VERSION = "0.6";
45 // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
46 public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
47 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
48 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
49 "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
50 "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
51 "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
52 "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
53 // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
55 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
57 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
59 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
61 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
63 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
66 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
67 @Deprecated(since = "11.0.0", forRemoval = true)
68 public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
69 @Deprecated(since = "10.0.3", forRemoval = true)
70 public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
71 public static final @NonNull String BUILDER_SUFFIX = "Builder";
72 public static final @NonNull String KEY_SUFFIX = "Key";
73 // ietf-restconf:yang-data, i.e. YangDataName
74 public static final @NonNull String NAME_STATIC_FIELD_NAME = "NAME";
75 // everything that can have a QName (e.g. identifier bound to a namespace)
76 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
77 // concrete extensible contracts, for example 'feature', 'identity' and similar
78 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
79 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
80 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
82 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
83 .omitEmptyStrings().trimResults();
84 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
85 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
86 private static final Splitter DOT_SPLITTER = Splitter.on('.');
88 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
89 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
90 public static final @NonNull String MODULE_INFO_YANGDATANAMEOF_METHOD_NAME = "yangDataNameOf";
91 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
94 * Name of {@link Augmentable#augmentation(Class)}.
96 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
99 * Name of {@link Identifiable#key()}.
101 public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
104 * Name of {@link BindingContract#implementedInterface()}.
106 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
109 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
110 * interface contains this static method.
112 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
115 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
116 * interface contains this static method.
118 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
121 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
122 * interface contains this static method.
124 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
127 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
129 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
132 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
134 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
137 * Name of {@link ScalarTypeObject#getValue()}.
139 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
142 * Prefix for normal getter methods.
144 public static final @NonNull String GETTER_PREFIX = "get";
147 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
149 public static final @NonNull String NONNULL_PREFIX = "nonnull";
152 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
155 public static final @NonNull String REQUIRE_PREFIX = "require";
156 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
157 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
159 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
161 private static final String ROOT_PACKAGE_PATTERN_STRING =
162 "(org.opendaylight.yang.gen.v1.[a-z0-9_\\.]*\\.(?:rev[0-9][0-9][0-1][0-9][0-3][0-9]|norev))";
163 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
165 private BindingMapping() {
169 public static @NonNull String getRootPackageName(final QName module) {
170 return getRootPackageName(module.getModule());
173 public static @NonNull String getRootPackageName(final QNameModule module) {
174 final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
176 String namespace = module.getNamespace().toString();
177 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
179 final char[] chars = namespace.toCharArray();
180 for (int i = 0; i < chars.length; ++i) {
182 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
189 packageNameBuilder.append(chars);
190 if (chars[chars.length - 1] != '.') {
191 packageNameBuilder.append('.');
194 final Optional<Revision> optRev = module.getRevision();
195 if (optRev.isPresent()) {
196 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
198 final String rev = optRev.get().toString();
199 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
200 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
202 // No-revision packages are special
203 packageNameBuilder.append("norev");
206 return normalizePackageName(packageNameBuilder.toString());
209 public static @NonNull String normalizePackageName(final String packageName) {
210 final StringBuilder builder = new StringBuilder();
211 boolean first = true;
213 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
220 if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
226 // Prevent duplication of input string
227 return PACKAGE_INTERNER.intern(builder.toString());
230 public static @NonNull String getClassName(final String localName) {
231 return toFirstUpper(toCamelCase(localName));
234 public static @NonNull String getClassName(final QName name) {
235 return toFirstUpper(toCamelCase(name.getLocalName()));
238 public static @NonNull String getMethodName(final String yangIdentifier) {
239 return toFirstLower(toCamelCase(yangIdentifier));
242 public static @NonNull String getMethodName(final QName name) {
243 return getMethodName(name.getLocalName());
246 public static @NonNull String getGetterMethodName(final String localName) {
247 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
250 public static @NonNull String getGetterMethodName(final QName name) {
251 return GETTER_PREFIX + getGetterSuffix(name);
254 public static boolean isGetterMethodName(final String methodName) {
255 return methodName.startsWith(GETTER_PREFIX);
258 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
259 checkArgument(isNonnullMethodName(methodName));
260 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
263 public static @NonNull String getNonnullMethodName(final String localName) {
264 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
267 public static boolean isNonnullMethodName(final String methodName) {
268 return methodName.startsWith(NONNULL_PREFIX);
271 public static @NonNull String getGetterMethodForRequire(final String methodName) {
272 checkArgument(isRequireMethodName(methodName));
273 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
276 public static @NonNull String getRequireMethodName(final String localName) {
277 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
280 public static boolean isRequireMethodName(final String methodName) {
281 return methodName.startsWith(REQUIRE_PREFIX);
284 public static @NonNull String getGetterSuffix(final QName name) {
285 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
286 return "Class".equals(candidate) ? "XmlClass" : candidate;
289 public static @NonNull String getPropertyName(final String yangIdentifier) {
290 final String potential = toFirstLower(toCamelCase(yangIdentifier));
291 if ("class".equals(potential)) {
297 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
299 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
300 final String referencedClassSimpleName) {
301 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
304 private static @NonNull String toCamelCase(final String rawString) {
305 StringBuilder builder = new StringBuilder();
306 for (String comp : CAMEL_SPLITTER.split(rawString)) {
307 builder.append(toFirstUpper(comp));
309 return checkNumericPrefix(builder.toString());
312 private static @NonNull String checkNumericPrefix(final String rawString) {
313 if (rawString.isEmpty()) {
316 final char firstChar = rawString.charAt(0);
317 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
321 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
323 * @param str the string that should get an upper case first character.
324 * @return the {@link String} {@code str} with an upper case first character.
326 public static @NonNull String toFirstUpper(final @NonNull String str) {
330 if (Character.isUpperCase(str.charAt(0))) {
333 if (str.length() == 1) {
334 return str.toUpperCase(Locale.ENGLISH);
336 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
340 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
341 * function is null-safe.
343 * @param str the string that should get an lower case first character. May be <code>null</code>.
344 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
345 * {@link String} {@code str} was empty.
347 private static @NonNull String toFirstLower(final @NonNull String str) {
351 if (Character.isLowerCase(str.charAt(0))) {
354 if (str.length() == 1) {
355 return str.toLowerCase(Locale.ENGLISH);
357 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
361 * Returns the {@link String} {@code s} with a '$' character as suffix.
363 * @param qname RPC QName
364 * @return The RPC method name as determined by considering the localname against the JLS.
365 * @throws NullPointerException if {@code qname} is null
367 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
368 final String methodName = getMethodName(qname);
369 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
373 * Returns root package name for supplied package name.
375 * @param packageName Package for which find model root package.
376 * @return Package of model root.
377 * @throws NullPointerException if {@code packageName} is {@code null}
378 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
379 * not match package name formatting rules
381 public static @NonNull String getModelRootPackageName(final String packageName) {
382 checkArgument(packageName.startsWith(PACKAGE_PREFIX), "Package name not starting with %s, is: %s",
383 PACKAGE_PREFIX, packageName);
384 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
385 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
386 ROOT_PACKAGE_PATTERN_STRING);
387 return match.group(0);
391 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
392 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
393 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
396 * @param assignedNames Collection of assigned names
397 * @return A BiMap keyed by assigned name, with Java identifiers as values
398 * @throws NullPointerException if assignedNames is null or contains null items
399 * @throws IllegalArgumentException if any of the names is empty
401 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
403 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
404 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
405 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
406 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
408 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
410 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
411 boolean valid = true;
412 for (String name : assignedNames) {
413 checkArgument(!name.isEmpty());
414 if (!javaToYang.containsValue(name)) {
415 final String mappedName = getClassName(name);
416 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
424 // Fall back to bijective mapping
426 for (String name : assignedNames) {
427 javaToYang.put(mapEnumAssignedName(name), name);
431 return javaToYang.inverse();
435 * Builds class name representing yang-data template name which is not yang identifier compliant.
437 * @param templateName template name
438 * @return Java class name
439 * @throws NullPointerException if {@code templateName} is {@code null}
440 * @throws IllegalArgumentException if (@code templateName} is empty
442 // TODO: take YangDataName once we have it readily available
443 public static String mapYangDataName(final String templateName) {
444 return mapEnumAssignedName(templateName);
447 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
448 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
449 // which have different exclusions
450 private static boolean isValidJavaIdentifier(final String str) {
451 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
452 && Character.isJavaIdentifierStart(str.codePointAt(0))
453 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
456 private static String mapEnumAssignedName(final String assignedName) {
457 checkArgument(!assignedName.isEmpty());
460 // - if the string is a valid java identifier and does not contain '$', use it as-is
461 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
465 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
466 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
467 final StringBuilder sb = new StringBuilder().append('$');
468 assignedName.codePoints().forEachOrdered(codePoint -> {
469 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
470 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
472 sb.appendCodePoint(codePoint);
475 return sb.toString();