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