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