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 root package name for supplied package name.
364 * @param packageName Package for which find model root package.
365 * @return Package of model root.
366 * @throws NullPointerException if {@code packageName} is {@code null}
367 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
368 * not match package name formatting rules
370 public static @NonNull String getModelRootPackageName(final String packageName) {
371 checkArgument(packageName.startsWith(PACKAGE_PREFIX), "Package name not starting with %s, is: %s",
372 PACKAGE_PREFIX, packageName);
373 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
374 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
375 ROOT_PACKAGE_PATTERN_STRING);
376 return match.group(0);
380 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
381 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
382 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
385 * @param assignedNames Collection of assigned names
386 * @return A BiMap keyed by assigned name, with Java identifiers as values
387 * @throws NullPointerException if assignedNames is null or contains null items
388 * @throws IllegalArgumentException if any of the names is empty
390 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
392 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
393 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
394 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
395 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
397 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
399 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
400 boolean valid = true;
401 for (String name : assignedNames) {
402 checkArgument(!name.isEmpty());
403 if (!javaToYang.containsValue(name)) {
404 final String mappedName = getClassName(name);
405 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
413 // Fall back to bijective mapping
415 for (String name : assignedNames) {
416 javaToYang.put(mapEnumAssignedName(name), name);
420 return javaToYang.inverse();
424 * Builds class name representing yang-data template name which is not yang identifier compliant.
426 * @param templateName template name
427 * @return Java class name
428 * @throws NullPointerException if {@code templateName} is {@code null}
429 * @throws IllegalArgumentException if (@code templateName} is empty
431 // TODO: take YangDataName once we have it readily available
432 public static String mapYangDataName(final YangDataName templateName) {
433 return mapEnumAssignedName(templateName.name());
436 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
437 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
438 // which have different exclusions
439 private static boolean isValidJavaIdentifier(final String str) {
440 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
441 && Character.isJavaIdentifierStart(str.codePointAt(0))
442 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
445 private static String mapEnumAssignedName(final String assignedName) {
446 checkArgument(!assignedName.isEmpty());
449 // - if the string is a valid java identifier and does not contain '$', use it as-is
450 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
454 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
455 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
456 final StringBuilder sb = new StringBuilder().append('$');
457 assignedName.codePoints().forEachOrdered(codePoint -> {
458 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
459 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
461 sb.appendCodePoint(codePoint);
464 return sb.toString();