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 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
74 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
75 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
76 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
78 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
79 .omitEmptyStrings().trimResults();
80 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
81 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
82 private static final Splitter DOT_SPLITTER = Splitter.on('.');
84 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
85 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
86 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
89 * Name of {@link Augmentable#augmentation(Class)}.
91 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
94 * Name of {@link Identifiable#key()}.
96 public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
99 * Name of {@link BindingContract#implementedInterface()}.
101 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
104 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
105 * interface contains this static method.
107 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
110 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
111 * interface contains this static method.
113 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
116 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
117 * interface contains this static method.
119 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
122 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
124 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
127 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
129 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
132 * Name of {@link ScalarTypeObject#getValue()}.
134 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
137 * Prefix for normal getter methods.
139 public static final @NonNull String GETTER_PREFIX = "get";
142 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
144 public static final @NonNull String NONNULL_PREFIX = "nonnull";
147 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
150 public static final @NonNull String REQUIRE_PREFIX = "require";
151 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
152 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
154 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
156 private static final String ROOT_PACKAGE_PATTERN_STRING =
157 "(org.opendaylight.yang.gen.v1.[a-z0-9_\\.]*\\.(?:rev[0-9][0-9][0-1][0-9][0-3][0-9]|norev))";
158 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
160 private BindingMapping() {
164 public static @NonNull String getRootPackageName(final QName module) {
165 return getRootPackageName(module.getModule());
168 public static @NonNull String getRootPackageName(final QNameModule module) {
169 final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
171 String namespace = module.getNamespace().toString();
172 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
174 final char[] chars = namespace.toCharArray();
175 for (int i = 0; i < chars.length; ++i) {
177 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
184 packageNameBuilder.append(chars);
185 if (chars[chars.length - 1] != '.') {
186 packageNameBuilder.append('.');
189 final Optional<Revision> optRev = module.getRevision();
190 if (optRev.isPresent()) {
191 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
193 final String rev = optRev.get().toString();
194 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
195 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
197 // No-revision packages are special
198 packageNameBuilder.append("norev");
201 return normalizePackageName(packageNameBuilder.toString());
204 public static @NonNull String normalizePackageName(final String packageName) {
205 final StringBuilder builder = new StringBuilder();
206 boolean first = true;
208 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
215 if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
221 // Prevent duplication of input string
222 return PACKAGE_INTERNER.intern(builder.toString());
225 public static @NonNull String getClassName(final String localName) {
226 return toFirstUpper(toCamelCase(localName));
229 public static @NonNull String getClassName(final QName name) {
230 return toFirstUpper(toCamelCase(name.getLocalName()));
233 public static @NonNull String getMethodName(final String yangIdentifier) {
234 return toFirstLower(toCamelCase(yangIdentifier));
237 public static @NonNull String getMethodName(final QName name) {
238 return getMethodName(name.getLocalName());
241 public static @NonNull String getGetterMethodName(final String localName) {
242 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
245 public static @NonNull String getGetterMethodName(final QName name) {
246 return GETTER_PREFIX + getGetterSuffix(name);
249 public static boolean isGetterMethodName(final String methodName) {
250 return methodName.startsWith(GETTER_PREFIX);
253 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
254 checkArgument(isNonnullMethodName(methodName));
255 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
258 public static @NonNull String getNonnullMethodName(final String localName) {
259 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
262 public static boolean isNonnullMethodName(final String methodName) {
263 return methodName.startsWith(NONNULL_PREFIX);
266 public static @NonNull String getGetterMethodForRequire(final String methodName) {
267 checkArgument(isRequireMethodName(methodName));
268 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
271 public static @NonNull String getRequireMethodName(final String localName) {
272 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
275 public static boolean isRequireMethodName(final String methodName) {
276 return methodName.startsWith(REQUIRE_PREFIX);
279 public static @NonNull String getGetterSuffix(final QName name) {
280 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
281 return "Class".equals(candidate) ? "XmlClass" : candidate;
284 public static @NonNull String getPropertyName(final String yangIdentifier) {
285 final String potential = toFirstLower(toCamelCase(yangIdentifier));
286 if ("class".equals(potential)) {
292 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
294 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
295 final String referencedClassSimpleName) {
296 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
299 private static @NonNull String toCamelCase(final String rawString) {
300 StringBuilder builder = new StringBuilder();
301 for (String comp : CAMEL_SPLITTER.split(rawString)) {
302 builder.append(toFirstUpper(comp));
304 return checkNumericPrefix(builder.toString());
307 private static @NonNull String checkNumericPrefix(final String rawString) {
308 if (rawString.isEmpty()) {
311 final char firstChar = rawString.charAt(0);
312 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
316 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
318 * @param str the string that should get an upper case first character.
319 * @return the {@link String} {@code str} with an upper case first character.
321 public static @NonNull String toFirstUpper(final @NonNull String str) {
325 if (Character.isUpperCase(str.charAt(0))) {
328 if (str.length() == 1) {
329 return str.toUpperCase(Locale.ENGLISH);
331 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
335 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
336 * function is null-safe.
338 * @param str the string that should get an lower case first character. May be <code>null</code>.
339 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
340 * {@link String} {@code str} was empty.
342 private static @NonNull String toFirstLower(final @NonNull String str) {
346 if (Character.isLowerCase(str.charAt(0))) {
349 if (str.length() == 1) {
350 return str.toLowerCase(Locale.ENGLISH);
352 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
356 * Returns the {@link String} {@code s} with a '$' character as suffix.
358 * @param qname RPC QName
359 * @return The RPC method name as determined by considering the localname against the JLS.
360 * @throws NullPointerException if {@code qname} is null
362 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
363 final String methodName = getMethodName(qname);
364 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
368 * Returns root package name for supplied package name.
370 * @param packageName Package for which find model root package.
371 * @return Package of model root.
372 * @throws NullPointerException if {@code packageName} is {@code null}
373 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
374 * not match package name formatting rules
376 public static @NonNull String getModelRootPackageName(final String packageName) {
377 checkArgument(packageName.startsWith(PACKAGE_PREFIX), "Package name not starting with %s, is: %s",
378 PACKAGE_PREFIX, packageName);
379 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
380 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
381 ROOT_PACKAGE_PATTERN_STRING);
382 return match.group(0);
386 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
387 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
388 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
391 * @param assignedNames Collection of assigned names
392 * @return A BiMap keyed by assigned name, with Java identifiers as values
393 * @throws NullPointerException if assignedNames is null or contains null items
394 * @throws IllegalArgumentException if any of the names is empty
396 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
398 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
399 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
400 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
401 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
403 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
405 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
406 boolean valid = true;
407 for (String name : assignedNames) {
408 checkArgument(!name.isEmpty());
409 if (!javaToYang.containsValue(name)) {
410 final String mappedName = getClassName(name);
411 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
419 // Fall back to bijective mapping
421 for (String name : assignedNames) {
422 javaToYang.put(mapEnumAssignedName(name), name);
426 return javaToYang.inverse();
429 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
430 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
431 // which have different exclusions
432 private static boolean isValidJavaIdentifier(final String str) {
433 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
434 && Character.isJavaIdentifierStart(str.codePointAt(0))
435 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
438 private static String mapEnumAssignedName(final String assignedName) {
439 checkArgument(!assignedName.isEmpty());
442 // - if the string is a valid java identifier and does not contain '$', use it as-is
443 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
447 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
448 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
449 final StringBuilder sb = new StringBuilder().append('$');
450 assignedName.codePoints().forEachOrdered(codePoint -> {
451 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
452 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
454 sb.appendCodePoint(codePoint);
457 return sb.toString();