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