6de5d251d02338bfbec3c37afc488666a1e29755
[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     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";
77
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('.');
83
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";
87
88     /**
89      * Name of {@link Augmentable#augmentation(Class)}.
90      */
91     public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
92
93     /**
94      * Name of {@link Identifiable#key()}.
95      */
96     public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
97
98     /**
99      * Name of {@link BindingContract#implementedInterface()}.
100      */
101     public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
102
103     /**
104      * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
105      * interface contains this static method.
106      */
107     public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
108
109     /**
110      * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
111      * interface contains this static method.
112      */
113     public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
114
115     /**
116      * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
117      * interface contains this static method.
118      */
119     public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
120
121     /**
122      * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
123      */
124     public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
125
126     /**
127      * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
128      */
129     public static final @NonNull String RPC_INVOKE_NAME = "invoke";
130
131     /**
132      * Name of {@link ScalarTypeObject#getValue()}.
133      */
134     public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
135
136     /**
137      * Prefix for normal getter methods.
138      */
139     public static final @NonNull String GETTER_PREFIX = "get";
140
141     /**
142      * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
143      */
144     public static final @NonNull String NONNULL_PREFIX = "nonnull";
145
146     /**
147      * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
148      * of leaf objects.
149      */
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";
153
154     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
155     @Regex
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);
159
160     private BindingMapping() {
161         // Hidden on purpose
162     }
163
164     public static @NonNull String getRootPackageName(final QName module) {
165         return getRootPackageName(module.getModule());
166     }
167
168     public static @NonNull String getRootPackageName(final QNameModule module) {
169         final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
170
171         String namespace = module.getNamespace().toString();
172         namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
173
174         final char[] chars = namespace.toCharArray();
175         for (int i = 0; i < chars.length; ++i) {
176             switch (chars[i]) {
177                 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
178                 default -> {
179                     // no-op
180                 }
181             }
182         }
183
184         packageNameBuilder.append(chars);
185         if (chars[chars.length - 1] != '.') {
186             packageNameBuilder.append('.');
187         }
188
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
192             // right characters.
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));
196         } else {
197             // No-revision packages are special
198             packageNameBuilder.append("norev");
199         }
200
201         return normalizePackageName(packageNameBuilder.toString());
202     }
203
204     public static @NonNull String normalizePackageName(final String packageName) {
205         final StringBuilder builder = new StringBuilder();
206         boolean first = true;
207
208         for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
209             if (first) {
210                 first = false;
211             } else {
212                 builder.append('.');
213             }
214
215             if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
216                 builder.append('_');
217             }
218             builder.append(p);
219         }
220
221         // Prevent duplication of input string
222         return PACKAGE_INTERNER.intern(builder.toString());
223     }
224
225     public static @NonNull String getClassName(final String localName) {
226         return toFirstUpper(toCamelCase(localName));
227     }
228
229     public static @NonNull String getClassName(final QName name) {
230         return toFirstUpper(toCamelCase(name.getLocalName()));
231     }
232
233     public static @NonNull String getMethodName(final String yangIdentifier) {
234         return toFirstLower(toCamelCase(yangIdentifier));
235     }
236
237     public static @NonNull String getMethodName(final QName name) {
238         return getMethodName(name.getLocalName());
239     }
240
241     public static @NonNull String getGetterMethodName(final String localName) {
242         return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
243     }
244
245     public static @NonNull String getGetterMethodName(final QName name) {
246         return GETTER_PREFIX + getGetterSuffix(name);
247     }
248
249     public static boolean isGetterMethodName(final String methodName) {
250         return methodName.startsWith(GETTER_PREFIX);
251     }
252
253     public static @NonNull String getGetterMethodForNonnull(final String methodName) {
254         checkArgument(isNonnullMethodName(methodName));
255         return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
256     }
257
258     public static @NonNull String getNonnullMethodName(final String localName) {
259         return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
260     }
261
262     public static boolean isNonnullMethodName(final String methodName) {
263         return methodName.startsWith(NONNULL_PREFIX);
264     }
265
266     public static @NonNull String getGetterMethodForRequire(final String methodName) {
267         checkArgument(isRequireMethodName(methodName));
268         return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
269     }
270
271     public static @NonNull String getRequireMethodName(final String localName) {
272         return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
273     }
274
275     public static boolean isRequireMethodName(final String methodName) {
276         return methodName.startsWith(REQUIRE_PREFIX);
277     }
278
279     public static @NonNull String getGetterSuffix(final QName name) {
280         final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
281         return "Class".equals(candidate) ? "XmlClass" : candidate;
282     }
283
284     public static @NonNull String getPropertyName(final String yangIdentifier) {
285         final String potential = toFirstLower(toCamelCase(yangIdentifier));
286         if ("class".equals(potential)) {
287             return "xmlClass";
288         }
289         return potential;
290     }
291
292     // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
293     //        property.
294     public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
295             final String referencedClassSimpleName) {
296         return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
297     }
298
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));
303         }
304         return checkNumericPrefix(builder.toString());
305     }
306
307     private static @NonNull String checkNumericPrefix(final String rawString) {
308         if (rawString.isEmpty()) {
309             return rawString;
310         }
311         final char firstChar = rawString.charAt(0);
312         return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
313     }
314
315     /**
316      * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
317      *
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.
320      */
321     public static @NonNull String toFirstUpper(final @NonNull String str) {
322         if (str.isEmpty()) {
323             return str;
324         }
325         if (Character.isUpperCase(str.charAt(0))) {
326             return str;
327         }
328         if (str.length() == 1) {
329             return str.toUpperCase(Locale.ENGLISH);
330         }
331         return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
332     }
333
334     /**
335      * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
336      * function is null-safe.
337      *
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.
341      */
342     private static @NonNull String toFirstLower(final @NonNull String str) {
343         if (str.isEmpty()) {
344             return str;
345         }
346         if (Character.isLowerCase(str.charAt(0))) {
347             return str;
348         }
349         if (str.length() == 1) {
350             return str.toLowerCase(Locale.ENGLISH);
351         }
352         return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
353     }
354
355     /**
356      * Returns the {@link String} {@code s} with a '$' character as suffix.
357      *
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
361      */
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;
365     }
366
367     /**
368      * Returns root package name for supplied package name.
369      *
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
375      */
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);
383     }
384
385     /**
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
389      * Unicode strings.
390      *
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
395      */
396     public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
397         /*
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.
402          *
403          * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
404          */
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) {
412                     valid = false;
413                     break;
414                 }
415             }
416         }
417
418         if (!valid) {
419             // Fall back to bijective mapping
420             javaToYang.clear();
421             for (String name : assignedNames) {
422                 javaToYang.put(mapEnumAssignedName(name), name);
423             }
424         }
425
426         return javaToYang.inverse();
427     }
428
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);
436     }
437
438     private static String mapEnumAssignedName(final String assignedName) {
439         checkArgument(!assignedName.isEmpty());
440
441         // Mapping rules:
442         // - if the string is a valid java identifier and does not contain '$', use it as-is
443         if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
444             return assignedName;
445         }
446
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('$');
453             } else {
454                 sb.appendCodePoint(codePoint);
455             }
456         });
457         return sb.toString();
458     }
459 }