2 * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.yangtools.yang.binding.contract;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
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;
42 public final class Naming {
44 public static final @NonNull String VERSION = "0.6";
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",
56 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
58 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
60 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
62 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
64 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
67 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
68 @Deprecated(since = "11.0.0", forRemoval = true)
69 public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
70 @Deprecated(since = "10.0.3", forRemoval = true)
71 public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
72 public static final @NonNull String BUILDER_SUFFIX = "Builder";
73 public static final @NonNull String KEY_SUFFIX = "Key";
74 // ietf-restconf:yang-data, i.e. YangDataName
75 public static final @NonNull String NAME_STATIC_FIELD_NAME = "NAME";
76 // everything that can have a QName (e.g. identifier bound to a namespace)
77 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
78 // concrete extensible contracts, for example 'feature', 'identity' and similar
79 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
80 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
81 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
83 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
84 .omitEmptyStrings().trimResults();
85 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
86 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
87 private static final Splitter DOT_SPLITTER = Splitter.on('.');
89 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
90 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
91 public static final @NonNull String MODULE_INFO_YANGDATANAMEOF_METHOD_NAME = "yangDataNameOf";
92 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
95 * Name of {@link Augmentable#augmentation(Class)}.
97 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
100 * Name of {@link KeyAware#key()}.
102 public static final @NonNull String KEY_AWARE_KEY_NAME = "key";
105 * Name of {@link BindingContract#implementedInterface()}.
107 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
110 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
111 * interface contains this static method.
113 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
116 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
117 * interface contains this static method.
119 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
122 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
123 * interface contains this static method.
125 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
128 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
130 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
133 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
135 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
138 * Name of {@link ScalarTypeObject#getValue()}.
140 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
143 * Prefix for normal getter methods.
145 public static final @NonNull String GETTER_PREFIX = "get";
148 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
150 public static final @NonNull String NONNULL_PREFIX = "nonnull";
153 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
156 public static final @NonNull String REQUIRE_PREFIX = "require";
157 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
158 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
160 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
162 private static final String ROOT_PACKAGE_PATTERN_STRING =
163 "(org.opendaylight.yang.gen.v1.[a-z0-9_\\.]*?\\.(?:rev[0-9][0-9][0-1][0-9][0-3][0-9]|norev))";
164 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
170 public static @NonNull String getRootPackageName(final QName module) {
171 return getRootPackageName(module.getModule());
174 public static @NonNull String getRootPackageName(final QNameModule module) {
175 final StringBuilder packageNameBuilder = new StringBuilder().append(PACKAGE_PREFIX).append('.');
177 String namespace = module.getNamespace().toString();
178 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
180 final char[] chars = namespace.toCharArray();
181 for (int i = 0; i < chars.length; ++i) {
183 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
190 packageNameBuilder.append(chars);
191 if (chars[chars.length - 1] != '.') {
192 packageNameBuilder.append('.');
195 final Optional<Revision> optRev = module.getRevision();
196 if (optRev.isPresent()) {
197 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
199 final String rev = optRev.orElseThrow().toString();
200 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
201 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
203 // No-revision packages are special
204 packageNameBuilder.append("norev");
207 return normalizePackageName(packageNameBuilder.toString());
210 public static @NonNull String normalizePackageName(final String packageName) {
211 final StringBuilder builder = new StringBuilder();
212 boolean first = true;
214 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
221 if (Character.isDigit(p.charAt(0)) || JAVA_RESERVED_WORDS.contains(p)) {
227 // Prevent duplication of input string
228 return PACKAGE_INTERNER.intern(builder.toString());
231 public static @NonNull String getClassName(final String localName) {
232 return toFirstUpper(toCamelCase(localName));
235 public static @NonNull String getClassName(final QName name) {
236 return toFirstUpper(toCamelCase(name.getLocalName()));
239 public static @NonNull String getMethodName(final String yangIdentifier) {
240 return toFirstLower(toCamelCase(yangIdentifier));
243 public static @NonNull String getMethodName(final QName name) {
244 return getMethodName(name.getLocalName());
247 public static @NonNull String getGetterMethodName(final String localName) {
248 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
251 public static @NonNull String getGetterMethodName(final QName name) {
252 return GETTER_PREFIX + getGetterSuffix(name);
255 public static boolean isGetterMethodName(final String methodName) {
256 return methodName.startsWith(GETTER_PREFIX);
259 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
260 checkArgument(isNonnullMethodName(methodName));
261 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
264 public static @NonNull String getNonnullMethodName(final String localName) {
265 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
268 public static boolean isNonnullMethodName(final String methodName) {
269 return methodName.startsWith(NONNULL_PREFIX);
272 public static @NonNull String getGetterMethodForRequire(final String methodName) {
273 checkArgument(isRequireMethodName(methodName));
274 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
277 public static @NonNull String getRequireMethodName(final String localName) {
278 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
281 public static boolean isRequireMethodName(final String methodName) {
282 return methodName.startsWith(REQUIRE_PREFIX);
285 public static @NonNull String getGetterSuffix(final QName name) {
286 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
287 return "Class".equals(candidate) ? "XmlClass" : candidate;
290 public static @NonNull String getPropertyName(final String yangIdentifier) {
291 final String potential = toFirstLower(toCamelCase(yangIdentifier));
292 if ("class".equals(potential)) {
298 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
300 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
301 final String referencedClassSimpleName) {
302 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
305 private static @NonNull String toCamelCase(final String rawString) {
306 StringBuilder builder = new StringBuilder();
307 for (String comp : CAMEL_SPLITTER.split(rawString)) {
308 builder.append(toFirstUpper(comp));
310 return checkNumericPrefix(builder.toString());
313 private static @NonNull String checkNumericPrefix(final String rawString) {
314 if (rawString.isEmpty()) {
317 final char firstChar = rawString.charAt(0);
318 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
322 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
324 * @param str the string that should get an upper case first character.
325 * @return the {@link String} {@code str} with an upper case first character.
327 public static @NonNull String toFirstUpper(final @NonNull String str) {
331 if (Character.isUpperCase(str.charAt(0))) {
334 if (str.length() == 1) {
335 return str.toUpperCase(Locale.ENGLISH);
337 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
341 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
342 * function is null-safe.
344 * @param str the string that should get an lower case first character. May be <code>null</code>.
345 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
346 * {@link String} {@code str} was empty.
348 private static @NonNull String toFirstLower(final @NonNull String str) {
352 if (Character.isLowerCase(str.charAt(0))) {
355 if (str.length() == 1) {
356 return str.toLowerCase(Locale.ENGLISH);
358 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
362 * Returns the {@link String} {@code s} with a '$' character as suffix.
364 * @param qname RPC QName
365 * @return The RPC method name as determined by considering the localname against the JLS.
366 * @throws NullPointerException if {@code qname} is null
368 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
369 final String methodName = getMethodName(qname);
370 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
374 * Returns root package name for supplied package name.
376 * @param packageName Package for which find model root package.
377 * @return Package of model root.
378 * @throws NullPointerException if {@code packageName} is {@code null}
379 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
380 * not match package name formatting rules
382 public static @NonNull String getModelRootPackageName(final String packageName) {
383 checkArgument(packageName.startsWith(PACKAGE_PREFIX), "Package name not starting with %s, is: %s",
384 PACKAGE_PREFIX, packageName);
385 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
386 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
387 ROOT_PACKAGE_PATTERN_STRING);
388 return match.group(0);
392 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
393 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
394 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
397 * @param assignedNames Collection of assigned names
398 * @return A BiMap keyed by assigned name, with Java identifiers as values
399 * @throws NullPointerException if assignedNames is null or contains null items
400 * @throws IllegalArgumentException if any of the names is empty
402 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
404 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
405 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
406 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
407 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
409 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
411 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
412 boolean valid = true;
413 for (String name : assignedNames) {
414 checkArgument(!name.isEmpty());
415 if (!javaToYang.containsValue(name)) {
416 final String mappedName = getClassName(name);
417 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
425 // Fall back to bijective mapping
427 for (String name : assignedNames) {
428 javaToYang.put(mapEnumAssignedName(name), name);
432 return javaToYang.inverse();
436 * Builds class name representing yang-data template name which is not yang identifier compliant.
438 * @param templateName template name
439 * @return Java class name
440 * @throws NullPointerException if {@code templateName} is {@code null}
441 * @throws IllegalArgumentException if (@code templateName} is empty
443 // TODO: take YangDataName once we have it readily available
444 public static String mapYangDataName(final YangDataName templateName) {
445 return mapEnumAssignedName(templateName.name());
448 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
449 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
450 // which have different exclusions
451 private static boolean isValidJavaIdentifier(final String str) {
452 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
453 && Character.isJavaIdentifierStart(str.codePointAt(0))
454 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
457 private static String mapEnumAssignedName(final String assignedName) {
458 checkArgument(!assignedName.isEmpty());
461 // - if the string is a valid java identifier and does not contain '$', use it as-is
462 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
466 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
467 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
468 final StringBuilder sb = new StringBuilder().append('$');
469 assignedName.codePoints().forEachOrdered(codePoint -> {
470 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
471 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
473 sb.appendCodePoint(codePoint);
476 return sb.toString();