9c2c90536382dfcfd2cb4b4fbf1481c6ea86c3ba
[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.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.yangtools.yang.binding.Augmentable;
28 import org.opendaylight.yangtools.yang.binding.BindingContract;
29 import org.opendaylight.yangtools.yang.binding.Identifiable;
30 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
31 import org.opendaylight.yangtools.yang.common.QName;
32 import org.opendaylight.yangtools.yang.common.QNameModule;
33 import org.opendaylight.yangtools.yang.common.Revision;
34
35 @Beta
36 public final class BindingMapping {
37
38     public static final @NonNull String VERSION = "0.6";
39
40     // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
41     public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
42         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
43         "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
44         "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
45         "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
46         "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
47         "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
48         // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
49
50         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
51         "false", "true",
52         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
53         "null",
54         // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
55         "var",
56         // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
57         "yield",
58         // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
59         "record");
60
61     public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
62     public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
63     @Deprecated(since = "10.0.3", forRemoval = true)
64     public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
65     public static final @NonNull String BUILDER_SUFFIX = "Builder";
66     public static final @NonNull String KEY_SUFFIX = "Key";
67     public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
68     public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
69     public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
70     public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
71
72     private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
73             .omitEmptyStrings().trimResults();
74     private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
75     private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
76     private static final Splitter DOT_SPLITTER = Splitter.on('.');
77
78     public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
79     public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
80     public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
81
82     /**
83      * Name of {@link Augmentable#augmentation(Class)}.
84      */
85     public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
86
87     /**
88      * Name of {@link Identifiable#key()}.
89      */
90     public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
91
92     /**
93      * Name of {@link BindingContract#implementedInterface()}.
94      */
95     public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
96
97     /**
98      * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
99      * interface contains this static method.
100      */
101     public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
102
103     /**
104      * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
105      * interface contains this static method.
106      */
107     public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
108
109     /**
110      * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
111      * interface contains this static method.
112      */
113     public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
114
115     /**
116      * Name of {@link ScalarTypeObject#getValue()}.
117      */
118     public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
119
120     /**
121      * Prefix for normal getter methods.
122      */
123     public static final @NonNull String GETTER_PREFIX = "get";
124
125     /**
126      * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
127      */
128     public static final @NonNull String NONNULL_PREFIX = "nonnull";
129
130     /**
131      * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
132      * of leaf objects.
133      */
134     public static final @NonNull String REQUIRE_PREFIX = "require";
135     public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
136     public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
137
138     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
139
140     private BindingMapping() {
141         // Hidden on purpose
142     }
143
144     public static @NonNull String getRootPackageName(final QName module) {
145         return getRootPackageName(module.getModule());
146     }
147
148     public static @NonNull String getRootPackageName(final QNameModule module) {
149         final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
150
151         String namespace = module.getNamespace().toString();
152         namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
153
154         final char[] chars = namespace.toCharArray();
155         for (int i = 0; i < chars.length; ++i) {
156             switch (chars[i]) {
157                 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
158                 default -> {
159                     // no-op
160                 }
161             }
162         }
163
164         packageNameBuilder.append(chars);
165         if (chars[chars.length - 1] != '.') {
166             packageNameBuilder.append('.');
167         }
168
169         final Optional<Revision> optRev = module.getRevision();
170         if (optRev.isPresent()) {
171             // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
172             // right characters.
173             final String rev = optRev.get().toString();
174             checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
175             packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
176         } else {
177             // No-revision packages are special
178             packageNameBuilder.append("norev");
179         }
180
181         return normalizePackageName(packageNameBuilder.toString());
182     }
183
184     public static @NonNull String normalizePackageName(final String packageName) {
185         final StringBuilder builder = new StringBuilder();
186         boolean first = true;
187
188         for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
189             if (first) {
190                 first = false;
191             } else {
192                 builder.append('.');
193             }
194
195             if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
196                 builder.append('_');
197             }
198             builder.append(p);
199         }
200
201         // Prevent duplication of input string
202         return PACKAGE_INTERNER.intern(builder.toString());
203     }
204
205     public static @NonNull String getClassName(final String localName) {
206         return toFirstUpper(toCamelCase(localName));
207     }
208
209     public static @NonNull String getClassName(final QName name) {
210         return toFirstUpper(toCamelCase(name.getLocalName()));
211     }
212
213     public static @NonNull String getMethodName(final String yangIdentifier) {
214         return toFirstLower(toCamelCase(yangIdentifier));
215     }
216
217     public static @NonNull String getMethodName(final QName name) {
218         return getMethodName(name.getLocalName());
219     }
220
221     public static @NonNull String getGetterMethodName(final String localName) {
222         return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
223     }
224
225     public static @NonNull String getGetterMethodName(final QName name) {
226         return GETTER_PREFIX + getGetterSuffix(name);
227     }
228
229     public static boolean isGetterMethodName(final String methodName) {
230         return methodName.startsWith(GETTER_PREFIX);
231     }
232
233     public static @NonNull String getGetterMethodForNonnull(final String methodName) {
234         checkArgument(isNonnullMethodName(methodName));
235         return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
236     }
237
238     public static @NonNull String getNonnullMethodName(final String localName) {
239         return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
240     }
241
242     public static boolean isNonnullMethodName(final String methodName) {
243         return methodName.startsWith(NONNULL_PREFIX);
244     }
245
246     public static @NonNull String getGetterMethodForRequire(final String methodName) {
247         checkArgument(isRequireMethodName(methodName));
248         return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
249     }
250
251     public static @NonNull String getRequireMethodName(final String localName) {
252         return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
253     }
254
255     public static boolean isRequireMethodName(final String methodName) {
256         return methodName.startsWith(REQUIRE_PREFIX);
257     }
258
259     public static @NonNull String getGetterSuffix(final QName name) {
260         final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
261         return "Class".equals(candidate) ? "XmlClass" : candidate;
262     }
263
264     public static @NonNull String getPropertyName(final String yangIdentifier) {
265         final String potential = toFirstLower(toCamelCase(yangIdentifier));
266         if ("class".equals(potential)) {
267             return "xmlClass";
268         }
269         return potential;
270     }
271
272     // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
273     //        property.
274     public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
275             final String referencedClassSimpleName) {
276         return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
277     }
278
279     private static @NonNull String toCamelCase(final String rawString) {
280         StringBuilder builder = new StringBuilder();
281         for (String comp : CAMEL_SPLITTER.split(rawString)) {
282             builder.append(toFirstUpper(comp));
283         }
284         return checkNumericPrefix(builder.toString());
285     }
286
287     private static @NonNull String checkNumericPrefix(final String rawString) {
288         if (rawString.isEmpty()) {
289             return rawString;
290         }
291         final char firstChar = rawString.charAt(0);
292         return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
293     }
294
295     /**
296      * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
297      *
298      * @param str the string that should get an upper case first character.
299      * @return the {@link String} {@code str} with an upper case first character.
300      */
301     public static @NonNull String toFirstUpper(final @NonNull String str) {
302         if (str.isEmpty()) {
303             return str;
304         }
305         if (Character.isUpperCase(str.charAt(0))) {
306             return str;
307         }
308         if (str.length() == 1) {
309             return str.toUpperCase(Locale.ENGLISH);
310         }
311         return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
312     }
313
314     /**
315      * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
316      * function is null-safe.
317      *
318      * @param str the string that should get an lower case first character. May be <code>null</code>.
319      * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
320      *         {@link String} {@code str} was empty.
321      */
322     private static @NonNull String toFirstLower(final @NonNull String str) {
323         if (str.isEmpty()) {
324             return str;
325         }
326         if (Character.isLowerCase(str.charAt(0))) {
327             return str;
328         }
329         if (str.length() == 1) {
330             return str.toLowerCase(Locale.ENGLISH);
331         }
332         return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
333     }
334
335     /**
336      * Returns the {@link String} {@code s} with a '$' character as suffix.
337      *
338      * @param qname RPC QName
339      * @return The RPC method name as determined by considering the localname against the JLS.
340      * @throws NullPointerException if {@code qname} is null
341      */
342     public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
343         final String methodName = getMethodName(qname);
344         return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
345     }
346
347     /**
348      * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
349      * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
350      * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
351      * Unicode strings.
352      *
353      * @param assignedNames Collection of assigned names
354      * @return A BiMap keyed by assigned name, with Java identifiers as values
355      * @throws NullPointerException if assignedNames is null or contains null items
356      * @throws IllegalArgumentException if any of the names is empty
357      */
358     public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
359         /*
360          * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
361          * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
362          * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
363          * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
364          *
365          * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
366          */
367         final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
368         boolean valid = true;
369         for (String name : assignedNames) {
370             checkArgument(!name.isEmpty());
371             if (!javaToYang.containsValue(name)) {
372                 final String mappedName = getClassName(name);
373                 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
374                     valid = false;
375                     break;
376                 }
377             }
378         }
379
380         if (!valid) {
381             // Fall back to bijective mapping
382             javaToYang.clear();
383             for (String name : assignedNames) {
384                 javaToYang.put(mapEnumAssignedName(name), name);
385             }
386         }
387
388         return javaToYang.inverse();
389     }
390
391     // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
392     // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
393     //       which have different exclusions
394     private static boolean isValidJavaIdentifier(final String str) {
395         return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
396                 && Character.isJavaIdentifierStart(str.codePointAt(0))
397                 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
398     }
399
400     private static String mapEnumAssignedName(final String assignedName) {
401         checkArgument(!assignedName.isEmpty());
402
403         // Mapping rules:
404         // - if the string is a valid java identifier and does not contain '$', use it as-is
405         if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
406             return assignedName;
407         }
408
409         // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
410         //   hex-encoded unicode codepoint (including plane, stripping leading zeroes)
411         final StringBuilder sb = new StringBuilder().append('$');
412         assignedName.codePoints().forEachOrdered(codePoint -> {
413             if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
414                 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
415             } else {
416                 sb.appendCodePoint(codePoint);
417             }
418         });
419         return sb.toString();
420     }
421 }