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