RFC8040 'rc:yang-data' support for mdsal binding generator
[mdsal.git] / binding / mdsal-binding-spec-util / src / main / java / org / opendaylight / mdsal / binding / spec / naming / BindingMapping.java
index 74908b796af6669b747a4b19f01cdd3c53a4285e..81cf8aab123d1dfe8ec191d3bbbd8d9cf3d151d2 100644 (file)
@@ -8,6 +8,7 @@
 package org.opendaylight.mdsal.binding.spec.naming;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.CharMatcher;
@@ -22,10 +23,15 @@ import java.util.Locale;
 import java.util.Optional;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.checkerframework.checker.regex.qual.Regex;
 import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.Action;
 import org.opendaylight.yangtools.yang.binding.Augmentable;
-import org.opendaylight.yangtools.yang.binding.DataContainer;
+import org.opendaylight.yangtools.yang.binding.BindingContract;
 import org.opendaylight.yangtools.yang.binding.Identifiable;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.binding.Rpc;
+import org.opendaylight.yangtools.yang.binding.RpcInput;
 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.QNameModule;
@@ -36,22 +42,40 @@ public final class BindingMapping {
 
     public static final @NonNull String VERSION = "0.6";
 
+    // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
     public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
-        // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9
+        // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
         "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
         "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
         "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
         "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
         "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
+        // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
+
         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
         "false", "true",
         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
-        "null");
+        "null",
+        // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
+        "var",
+        // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
+        "yield",
+        // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
+        "record");
 
     public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
+    @Deprecated(since = "11.0.0", forRemoval = true)
     public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
+    @Deprecated(since = "10.0.3", forRemoval = true)
     public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
+    public static final @NonNull String BUILDER_SUFFIX = "Builder";
+    public static final @NonNull String KEY_SUFFIX = "Key";
+    // ietf-restconf:yang-data, i.e. YangDataName
+    public static final @NonNull String NAME_STATIC_FIELD_NAME = "NAME";
+    // everything that can have a QName (e.g. identifier bound to a namespace)
     public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
+    // concrete extensible contracts, for example 'feature', 'identity' and similar
+    public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
     public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
     public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
 
@@ -63,6 +87,7 @@ public final class BindingMapping {
 
     public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
     public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
+    public static final @NonNull String MODULE_INFO_YANGDATANAMEOF_METHOD_NAME = "yangDataNameOf";
     public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
 
     /**
@@ -76,9 +101,21 @@ public final class BindingMapping {
     public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
 
     /**
-     * Name of {@link DataContainer#implementedInterface()}.
+     * Name of {@link BindingContract#implementedInterface()}.
+     */
+    public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
+
+    /**
+     * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
+     * interface contains this static method.
+     */
+    public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
+
+    /**
+     * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
+     * interface contains this static method.
      */
-    public static final @NonNull String DATA_CONTAINER_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
+    public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
 
     /**
      * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
@@ -87,14 +124,19 @@ public final class BindingMapping {
     public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
 
     /**
-     * Name of {@link ScalarTypeObject#getValue()}.
+     * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
      */
-    public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
+    public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
 
     /**
-     * Prefix for getter methods working on top of boolean.
+     * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
+     */
+    public static final @NonNull String RPC_INVOKE_NAME = "invoke";
+
+    /**
+     * Name of {@link ScalarTypeObject#getValue()}.
      */
-    public static final @NonNull String BOOLEAN_GETTER_PREFIX = "is";
+    public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
 
     /**
      * Prefix for normal getter methods.
@@ -106,10 +148,19 @@ public final class BindingMapping {
      */
     public static final @NonNull String NONNULL_PREFIX = "nonnull";
 
+    /**
+     * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
+     * of leaf objects.
+     */
+    public static final @NonNull String REQUIRE_PREFIX = "require";
     public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
     public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
 
     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
+    @Regex
+    private static final String ROOT_PACKAGE_PATTERN_STRING =
+            "(org.opendaylight.yang.gen.v1.[a-z0-9_\\.]*\\.(?:rev[0-9][0-9][0-1][0-9][0-3][0-9]|norev))";
+    private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
 
     private BindingMapping() {
         // Hidden on purpose
@@ -128,22 +179,10 @@ public final class BindingMapping {
         final char[] chars = namespace.toCharArray();
         for (int i = 0; i < chars.length; ++i) {
             switch (chars[i]) {
-                case '/':
-                case ':':
-                case '-':
-                case '@':
-                case '$':
-                case '#':
-                case '\'':
-                case '*':
-                case '+':
-                case ',':
-                case ';':
-                case '=':
-                    chars[i] = '.';
-                    break;
-                default:
+                case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
+                default -> {
                     // no-op
+                }
             }
         }
 
@@ -204,20 +243,16 @@ public final class BindingMapping {
         return getMethodName(name.getLocalName());
     }
 
-    public static @NonNull String getGetterPrefix(final boolean isBoolean) {
-        return isBoolean ? BOOLEAN_GETTER_PREFIX : GETTER_PREFIX;
+    public static @NonNull String getGetterMethodName(final String localName) {
+        return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
     }
 
-    public static @NonNull String getGetterMethodName(final String localName, final boolean isBoolean) {
-        return getGetterPrefix(isBoolean) + toFirstUpper(getPropertyName(localName));
-    }
-
-    public static @NonNull String getGetterMethodName(final QName name, final boolean isBoolean) {
-        return getGetterPrefix(isBoolean) + getGetterSuffix(name);
+    public static @NonNull String getGetterMethodName(final QName name) {
+        return GETTER_PREFIX + getGetterSuffix(name);
     }
 
     public static boolean isGetterMethodName(final String methodName) {
-        return methodName.startsWith(GETTER_PREFIX) || methodName.startsWith(BOOLEAN_GETTER_PREFIX);
+        return methodName.startsWith(GETTER_PREFIX);
     }
 
     public static @NonNull String getGetterMethodForNonnull(final String methodName) {
@@ -233,6 +268,19 @@ public final class BindingMapping {
         return methodName.startsWith(NONNULL_PREFIX);
     }
 
+    public static @NonNull String getGetterMethodForRequire(final String methodName) {
+        checkArgument(isRequireMethodName(methodName));
+        return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
+    }
+
+    public static @NonNull String getRequireMethodName(final String localName) {
+        return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
+    }
+
+    public static boolean isRequireMethodName(final String methodName) {
+        return methodName.startsWith(REQUIRE_PREFIX);
+    }
+
     public static @NonNull String getGetterSuffix(final QName name) {
         final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
         return "Class".equals(candidate) ? "XmlClass" : candidate;
@@ -246,6 +294,13 @@ public final class BindingMapping {
         return potential;
     }
 
+    // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
+    //        property.
+    public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
+            final String referencedClassSimpleName) {
+        return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
+    }
+
     private static @NonNull String toCamelCase(final String rawString) {
         StringBuilder builder = new StringBuilder();
         for (String comp : CAMEL_SPLITTER.split(rawString)) {
@@ -268,7 +323,7 @@ public final class BindingMapping {
      * @param str the string that should get an upper case first character.
      * @return the {@link String} {@code str} with an upper case first character.
      */
-    private static @NonNull String toFirstUpper(final @NonNull String str) {
+    public static @NonNull String toFirstUpper(final @NonNull String str) {
         if (str.isEmpty()) {
             return str;
         }
@@ -314,6 +369,24 @@ public final class BindingMapping {
         return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
     }
 
+    /**
+     * Returns root package name for supplied package name.
+     *
+     * @param packageName Package for which find model root package.
+     * @return Package of model root.
+     * @throws NullPointerException if {@code packageName} is {@code null}
+     * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
+     *                                  not match package name formatting rules
+     */
+    public static @NonNull String getModelRootPackageName(final String packageName) {
+        checkArgument(packageName.startsWith(PACKAGE_PREFIX), "Package name not starting with %s, is: %s",
+            PACKAGE_PREFIX, packageName);
+        final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
+        checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
+                ROOT_PACKAGE_PATTERN_STRING);
+        return match.group(0);
+    }
+
     /**
      * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
      * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
@@ -358,7 +431,22 @@ public final class BindingMapping {
         return javaToYang.inverse();
     }
 
-    // See https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.8
+    /**
+     * Builds class name representing yang-data template name which is not yang identifier compliant.
+     *
+     * @param templateName template name
+     * @return Java class name
+     * @throws NullPointerException if {@code templateName} is {@code null}
+     * @throws IllegalArgumentException if (@code templateName} is empty
+     */
+    // TODO: take YangDataName once we have it readily available
+    public static String mapYangDataName(final String templateName) {
+        return mapEnumAssignedName(templateName);
+    }
+
+    // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
+    // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
+    //       which have different exclusions
     private static boolean isValidJavaIdentifier(final String str) {
         return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
                 && Character.isJavaIdentifierStart(str.codePointAt(0))