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