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