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 = "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";
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('.');
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";
93 * Name of {@link Augmentable#augmentation(Class)}.
95 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
98 * Name of {@link KeyAware#key()}.
100 public static final @NonNull String KEY_AWARE_KEY_NAME = "key";
103 * Name of {@link BindingContract#implementedInterface()}.
105 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
108 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
109 * interface contains this static method.
111 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
114 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
115 * interface contains this static method.
117 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
120 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
121 * interface contains this static method.
123 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
126 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
128 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
131 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
133 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
136 * Name of {@link ScalarTypeObject#getValue()}.
138 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
141 * Prefix for normal getter methods.
143 public static final @NonNull String GETTER_PREFIX = "get";
146 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
148 public static final @NonNull String NONNULL_PREFIX = "nonnull";
151 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
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";
158 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
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);
168 public static @NonNull String getRootPackageName(final QName module) {
169 return getRootPackageName(module.getModule());
172 public static @NonNull String getRootPackageName(final QNameModule module) {
173 final StringBuilder packageNameBuilder = new StringBuilder().append(PACKAGE_PREFIX).append('.');
175 String namespace = module.getNamespace().toString();
176 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
178 final char[] chars = namespace.toCharArray();
179 for (int i = 0; i < chars.length; ++i) {
181 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
188 packageNameBuilder.append(chars);
189 if (chars[chars.length - 1] != '.') {
190 packageNameBuilder.append('.');
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
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));
201 // No-revision packages are special
202 packageNameBuilder.append("norev");
205 return normalizePackageName(packageNameBuilder.toString());
208 public static @NonNull String normalizePackageName(final String packageName) {
209 final StringBuilder builder = new StringBuilder();
210 boolean first = true;
212 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
219 if (Character.isDigit(p.charAt(0)) || JAVA_RESERVED_WORDS.contains(p)) {
225 // Prevent duplication of input string
226 return PACKAGE_INTERNER.intern(builder.toString());
229 public static @NonNull String getClassName(final String localName) {
230 return toFirstUpper(toCamelCase(localName));
233 public static @NonNull String getClassName(final QName name) {
234 return toFirstUpper(toCamelCase(name.getLocalName()));
237 public static @NonNull String getMethodName(final String yangIdentifier) {
238 return toFirstLower(toCamelCase(yangIdentifier));
241 public static @NonNull String getMethodName(final QName name) {
242 return getMethodName(name.getLocalName());
245 public static @NonNull String getGetterMethodName(final String localName) {
246 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
249 public static @NonNull String getGetterMethodName(final QName name) {
250 return GETTER_PREFIX + getGetterSuffix(name);
253 public static boolean isGetterMethodName(final String methodName) {
254 return methodName.startsWith(GETTER_PREFIX);
257 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
258 checkArgument(isNonnullMethodName(methodName));
259 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
262 public static @NonNull String getNonnullMethodName(final String localName) {
263 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
266 public static boolean isNonnullMethodName(final String methodName) {
267 return methodName.startsWith(NONNULL_PREFIX);
270 public static @NonNull String getGetterMethodForRequire(final String methodName) {
271 checkArgument(isRequireMethodName(methodName));
272 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
275 public static @NonNull String getRequireMethodName(final String localName) {
276 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
279 public static boolean isRequireMethodName(final String methodName) {
280 return methodName.startsWith(REQUIRE_PREFIX);
283 public static @NonNull String getGetterSuffix(final QName name) {
284 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
285 return "Class".equals(candidate) ? "XmlClass" : candidate;
288 public static @NonNull String getPropertyName(final String yangIdentifier) {
289 final String potential = toFirstLower(toCamelCase(yangIdentifier));
290 if ("class".equals(potential)) {
296 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
298 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
299 final String referencedClassSimpleName) {
300 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
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));
308 return checkNumericPrefix(builder.toString());
311 private static @NonNull String checkNumericPrefix(final String rawString) {
312 if (rawString.isEmpty()) {
315 final char firstChar = rawString.charAt(0);
316 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
320 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
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.
325 public static @NonNull String toFirstUpper(final @NonNull String str) {
329 if (Character.isUpperCase(str.charAt(0))) {
332 if (str.length() == 1) {
333 return str.toUpperCase(Locale.ENGLISH);
335 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
339 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
340 * function is null-safe.
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.
346 private static @NonNull String toFirstLower(final @NonNull String str) {
350 if (Character.isLowerCase(str.charAt(0))) {
353 if (str.length() == 1) {
354 return str.toLowerCase(Locale.ENGLISH);
356 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
360 * Returns root package name for supplied package name.
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
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);
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
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
388 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
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.
395 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
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) {
411 // Fall back to bijective mapping
413 for (String name : assignedNames) {
414 javaToYang.put(mapEnumAssignedName(name), name);
418 return javaToYang.inverse();
422 * Builds class name representing yang-data template name which is not yang identifier compliant.
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
429 // TODO: take YangDataName once we have it readily available
430 public static String mapYangDataName(final YangDataName templateName) {
431 return mapEnumAssignedName(templateName.name());
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);
443 private static String mapEnumAssignedName(final String assignedName) {
444 checkArgument(!assignedName.isEmpty());
447 // - if the string is a valid java identifier and does not contain '$', use it as-is
448 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
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('$');
459 sb.appendCodePoint(codePoint);
462 return sb.toString();