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