Bump byte-buddy to 1.14.2
[mdsal.git] / binding / mdsal-binding-spec-util / src / main / java / org / opendaylight / mdsal / binding / spec / naming / BindingMapping.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.mdsal.binding.spec.naming;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
12
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;
39
40 @Beta
41 public final class BindingMapping {
42
43     public static final @NonNull String VERSION = "0.6";
44
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",
54
55         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
56         "false", "true",
57         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
58         "null",
59         // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
60         "var",
61         // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
62         "yield",
63         // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
64         "record");
65
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";
81
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('.');
87
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";
92
93     /**
94      * Name of {@link Augmentable#augmentation(Class)}.
95      */
96     public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
97
98     /**
99      * Name of {@link Identifiable#key()}.
100      */
101     public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
102
103     /**
104      * Name of {@link BindingContract#implementedInterface()}.
105      */
106     public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
107
108     /**
109      * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
110      * interface contains this static method.
111      */
112     public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
113
114     /**
115      * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
116      * interface contains this static method.
117      */
118     public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
119
120     /**
121      * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
122      * interface contains this static method.
123      */
124     public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
125
126     /**
127      * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
128      */
129     public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
130
131     /**
132      * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
133      */
134     public static final @NonNull String RPC_INVOKE_NAME = "invoke";
135
136     /**
137      * Name of {@link ScalarTypeObject#getValue()}.
138      */
139     public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
140
141     /**
142      * Prefix for normal getter methods.
143      */
144     public static final @NonNull String GETTER_PREFIX = "get";
145
146     /**
147      * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
148      */
149     public static final @NonNull String NONNULL_PREFIX = "nonnull";
150
151     /**
152      * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
153      * of leaf objects.
154      */
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";
158
159     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
160     @Regex
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);
164
165     private BindingMapping() {
166         // Hidden on purpose
167     }
168
169     public static @NonNull String getRootPackageName(final QName module) {
170         return getRootPackageName(module.getModule());
171     }
172
173     public static @NonNull String getRootPackageName(final QNameModule module) {
174         final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
175
176         String namespace = module.getNamespace().toString();
177         namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
178
179         final char[] chars = namespace.toCharArray();
180         for (int i = 0; i < chars.length; ++i) {
181             switch (chars[i]) {
182                 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
183                 default -> {
184                     // no-op
185                 }
186             }
187         }
188
189         packageNameBuilder.append(chars);
190         if (chars[chars.length - 1] != '.') {
191             packageNameBuilder.append('.');
192         }
193
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
197             // right characters.
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));
201         } else {
202             // No-revision packages are special
203             packageNameBuilder.append("norev");
204         }
205
206         return normalizePackageName(packageNameBuilder.toString());
207     }
208
209     public static @NonNull String normalizePackageName(final String packageName) {
210         final StringBuilder builder = new StringBuilder();
211         boolean first = true;
212
213         for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
214             if (first) {
215                 first = false;
216             } else {
217                 builder.append('.');
218             }
219
220             if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
221                 builder.append('_');
222             }
223             builder.append(p);
224         }
225
226         // Prevent duplication of input string
227         return PACKAGE_INTERNER.intern(builder.toString());
228     }
229
230     public static @NonNull String getClassName(final String localName) {
231         return toFirstUpper(toCamelCase(localName));
232     }
233
234     public static @NonNull String getClassName(final QName name) {
235         return toFirstUpper(toCamelCase(name.getLocalName()));
236     }
237
238     public static @NonNull String getMethodName(final String yangIdentifier) {
239         return toFirstLower(toCamelCase(yangIdentifier));
240     }
241
242     public static @NonNull String getMethodName(final QName name) {
243         return getMethodName(name.getLocalName());
244     }
245
246     public static @NonNull String getGetterMethodName(final String localName) {
247         return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
248     }
249
250     public static @NonNull String getGetterMethodName(final QName name) {
251         return GETTER_PREFIX + getGetterSuffix(name);
252     }
253
254     public static boolean isGetterMethodName(final String methodName) {
255         return methodName.startsWith(GETTER_PREFIX);
256     }
257
258     public static @NonNull String getGetterMethodForNonnull(final String methodName) {
259         checkArgument(isNonnullMethodName(methodName));
260         return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
261     }
262
263     public static @NonNull String getNonnullMethodName(final String localName) {
264         return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
265     }
266
267     public static boolean isNonnullMethodName(final String methodName) {
268         return methodName.startsWith(NONNULL_PREFIX);
269     }
270
271     public static @NonNull String getGetterMethodForRequire(final String methodName) {
272         checkArgument(isRequireMethodName(methodName));
273         return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
274     }
275
276     public static @NonNull String getRequireMethodName(final String localName) {
277         return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
278     }
279
280     public static boolean isRequireMethodName(final String methodName) {
281         return methodName.startsWith(REQUIRE_PREFIX);
282     }
283
284     public static @NonNull String getGetterSuffix(final QName name) {
285         final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
286         return "Class".equals(candidate) ? "XmlClass" : candidate;
287     }
288
289     public static @NonNull String getPropertyName(final String yangIdentifier) {
290         final String potential = toFirstLower(toCamelCase(yangIdentifier));
291         if ("class".equals(potential)) {
292             return "xmlClass";
293         }
294         return potential;
295     }
296
297     // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
298     //        property.
299     public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
300             final String referencedClassSimpleName) {
301         return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
302     }
303
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));
308         }
309         return checkNumericPrefix(builder.toString());
310     }
311
312     private static @NonNull String checkNumericPrefix(final String rawString) {
313         if (rawString.isEmpty()) {
314             return rawString;
315         }
316         final char firstChar = rawString.charAt(0);
317         return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
318     }
319
320     /**
321      * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
322      *
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.
325      */
326     public static @NonNull String toFirstUpper(final @NonNull String str) {
327         if (str.isEmpty()) {
328             return str;
329         }
330         if (Character.isUpperCase(str.charAt(0))) {
331             return str;
332         }
333         if (str.length() == 1) {
334             return str.toUpperCase(Locale.ENGLISH);
335         }
336         return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
337     }
338
339     /**
340      * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
341      * function is null-safe.
342      *
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.
346      */
347     private static @NonNull String toFirstLower(final @NonNull String str) {
348         if (str.isEmpty()) {
349             return str;
350         }
351         if (Character.isLowerCase(str.charAt(0))) {
352             return str;
353         }
354         if (str.length() == 1) {
355             return str.toLowerCase(Locale.ENGLISH);
356         }
357         return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
358     }
359
360     /**
361      * Returns the {@link String} {@code s} with a '$' character as suffix.
362      *
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
366      */
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;
370     }
371
372     /**
373      * Returns root package name for supplied package name.
374      *
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
380      */
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);
388     }
389
390     /**
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
394      * Unicode strings.
395      *
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
400      */
401     public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
402         /*
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.
407          *
408          * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
409          */
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) {
417                     valid = false;
418                     break;
419                 }
420             }
421         }
422
423         if (!valid) {
424             // Fall back to bijective mapping
425             javaToYang.clear();
426             for (String name : assignedNames) {
427                 javaToYang.put(mapEnumAssignedName(name), name);
428             }
429         }
430
431         return javaToYang.inverse();
432     }
433
434     /**
435      * Builds class name representing yang-data template name which is not yang identifier compliant.
436      *
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
441      */
442     // TODO: take YangDataName once we have it readily available
443     public static String mapYangDataName(final String templateName) {
444         return mapEnumAssignedName(templateName);
445     }
446
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);
454     }
455
456     private static String mapEnumAssignedName(final String assignedName) {
457         checkArgument(!assignedName.isEmpty());
458
459         // Mapping rules:
460         // - if the string is a valid java identifier and does not contain '$', use it as-is
461         if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
462             return assignedName;
463         }
464
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('$');
471             } else {
472                 sb.appendCodePoint(codePoint);
473             }
474         });
475         return sb.toString();
476     }
477 }