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