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 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";
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('.');
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";
91 * Name of {@link Augmentable#augmentation(Class)}.
93 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
96 * Name of {@link KeyAware#key()}.
98 public static final @NonNull String KEY_AWARE_KEY_NAME = "key";
101 * Name of {@link BindingContract#implementedInterface()}.
103 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
106 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
107 * interface contains this static method.
109 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
112 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
113 * interface contains this static method.
115 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
118 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
119 * interface contains this static method.
121 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
124 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
126 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
129 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
131 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
134 * Name of {@link ScalarTypeObject#getValue()}.
136 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
139 * Prefix for normal getter methods.
141 public static final @NonNull String GETTER_PREFIX = "get";
144 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
146 public static final @NonNull String NONNULL_PREFIX = "nonnull";
149 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
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";
156 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
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);
166 public static @NonNull String getRootPackageName(final QName module) {
167 return getRootPackageName(module.getModule());
170 public static @NonNull String getRootPackageName(final QNameModule module) {
171 final StringBuilder packageNameBuilder = new StringBuilder().append(PACKAGE_PREFIX).append('.');
173 String namespace = module.getNamespace().toString();
174 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
176 final char[] chars = namespace.toCharArray();
177 for (int i = 0; i < chars.length; ++i) {
179 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
186 packageNameBuilder.append(chars);
187 if (chars[chars.length - 1] != '.') {
188 packageNameBuilder.append('.');
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
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));
199 // No-revision packages are special
200 packageNameBuilder.append("norev");
203 return normalizePackageName(packageNameBuilder.toString());
206 public static @NonNull String normalizePackageName(final String packageName) {
207 final StringBuilder builder = new StringBuilder();
208 boolean first = true;
210 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
217 if (Character.isDigit(p.charAt(0)) || JAVA_RESERVED_WORDS.contains(p)) {
223 // Prevent duplication of input string
224 return PACKAGE_INTERNER.intern(builder.toString());
227 public static @NonNull String getClassName(final String localName) {
228 return toFirstUpper(toCamelCase(localName));
231 public static @NonNull String getClassName(final QName name) {
232 return toFirstUpper(toCamelCase(name.getLocalName()));
235 public static @NonNull String getMethodName(final String yangIdentifier) {
236 return toFirstLower(toCamelCase(yangIdentifier));
239 public static @NonNull String getMethodName(final QName name) {
240 return getMethodName(name.getLocalName());
243 public static @NonNull String getGetterMethodName(final String localName) {
244 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
247 public static @NonNull String getGetterMethodName(final QName name) {
248 return GETTER_PREFIX + getGetterSuffix(name);
251 public static boolean isGetterMethodName(final String methodName) {
252 return methodName.startsWith(GETTER_PREFIX);
255 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
256 checkArgument(isNonnullMethodName(methodName));
257 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
260 public static @NonNull String getNonnullMethodName(final String localName) {
261 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
264 public static boolean isNonnullMethodName(final String methodName) {
265 return methodName.startsWith(NONNULL_PREFIX);
268 public static @NonNull String getGetterMethodForRequire(final String methodName) {
269 checkArgument(isRequireMethodName(methodName));
270 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
273 public static @NonNull String getRequireMethodName(final String localName) {
274 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
277 public static boolean isRequireMethodName(final String methodName) {
278 return methodName.startsWith(REQUIRE_PREFIX);
281 public static @NonNull String getGetterSuffix(final QName name) {
282 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
283 return "Class".equals(candidate) ? "XmlClass" : candidate;
286 public static @NonNull String getPropertyName(final String yangIdentifier) {
287 final String potential = toFirstLower(toCamelCase(yangIdentifier));
288 if ("class".equals(potential)) {
294 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
296 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
297 final String referencedClassSimpleName) {
298 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
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));
306 return checkNumericPrefix(builder.toString());
309 private static @NonNull String checkNumericPrefix(final String rawString) {
310 if (rawString.isEmpty()) {
313 final char firstChar = rawString.charAt(0);
314 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
318 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
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.
323 public static @NonNull String toFirstUpper(final @NonNull String str) {
327 if (Character.isUpperCase(str.charAt(0))) {
330 if (str.length() == 1) {
331 return str.toUpperCase(Locale.ENGLISH);
333 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
337 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
338 * function is null-safe.
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.
344 private static @NonNull String toFirstLower(final @NonNull String str) {
348 if (Character.isLowerCase(str.charAt(0))) {
351 if (str.length() == 1) {
352 return str.toLowerCase(Locale.ENGLISH);
354 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
358 * Returns root package name for supplied package name.
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
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);
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
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
386 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
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.
393 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
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) {
409 // Fall back to bijective mapping
411 for (String name : assignedNames) {
412 javaToYang.put(mapEnumAssignedName(name), name);
416 return javaToYang.inverse();
420 * Builds class name representing yang-data template name which is not yang identifier compliant.
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
427 // TODO: take YangDataName once we have it readily available
428 public static String mapYangDataName(final YangDataName templateName) {
429 return mapEnumAssignedName(templateName.name());
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);
441 private static String mapEnumAssignedName(final String assignedName) {
442 checkArgument(!assignedName.isEmpty());
445 // - if the string is a valid java identifier and does not contain '$', use it as-is
446 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
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('$');
457 sb.appendCodePoint(codePoint);
460 return sb.toString();