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