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