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 SVC_PACKAGE_PREFIX = "org.opendaylight.yang.svc.v1";
78 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
80 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
81 .omitEmptyStrings().trimResults();
82 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
83 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
84 private static final Splitter DOT_SPLITTER = Splitter.on('.');
86 public static final @NonNull String MODULE_INFO_CLASS_NAME = "YangModuleInfoImpl";
87 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
88 public static final @NonNull String MODULE_INFO_YANGDATANAMEOF_METHOD_NAME = "yangDataNameOf";
89 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "YangModelBindingProviderImpl";
92 * Name of {@link Augmentable#augmentation(Class)}.
94 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
97 * Name of {@link KeyAware#key()}.
99 public static final @NonNull String KEY_AWARE_KEY_NAME = "key";
102 * Name of {@link BindingContract#implementedInterface()}.
104 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
107 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
108 * interface contains this static method.
110 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
113 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
114 * interface contains this static method.
116 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
119 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
120 * interface contains this static method.
122 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
125 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
127 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
130 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
132 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
135 * Name of {@link ScalarTypeObject#getValue()}.
137 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
140 * Prefix for normal getter methods.
142 public static final @NonNull String GETTER_PREFIX = "get";
145 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
147 public static final @NonNull String NONNULL_PREFIX = "nonnull";
150 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
153 public static final @NonNull String REQUIRE_PREFIX = "require";
154 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
155 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
157 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
159 private static final String ROOT_PACKAGE_PATTERN_STRING =
160 "(org.opendaylight.yang.(gen|svc).v1.[a-z0-9_\\.]*?\\.(?:rev[0-9][0-9][0-1][0-9][0-3][0-9]|norev))";
161 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
168 * Return the package name for placing generated ServiceLoader entities, like {@link #MODULE_INFO_CLASS_NAME} and
169 * {@link #MODEL_BINDING_PROVIDER_CLASS_NAME}.
171 * @param module module namespace
172 * @return the package name for placing generated ServiceLoader entities
174 public static @NonNull String getServicePackageName(final QNameModule module) {
175 final StringBuilder packageNameBuilder = new StringBuilder().append(SVC_PACKAGE_PREFIX).append('.');
176 return getRootPackageName(packageNameBuilder, module);
179 public static @NonNull String getRootPackageName(final QName module) {
180 return getRootPackageName(module.getModule());
183 public static @NonNull String getRootPackageName(final QNameModule module) {
184 final StringBuilder packageNameBuilder = new StringBuilder().append(PACKAGE_PREFIX).append('.');
185 return getRootPackageName(packageNameBuilder, module);
188 private static @NonNull String getRootPackageName(final StringBuilder builder, final QNameModule module) {
189 String namespace = module.getNamespace().toString();
190 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
192 final char[] chars = namespace.toCharArray();
193 for (int i = 0; i < chars.length; ++i) {
195 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
202 builder.append(chars);
203 if (chars[chars.length - 1] != '.') {
207 final Optional<Revision> optRev = module.getRevision();
208 if (optRev.isPresent()) {
209 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
211 final String rev = optRev.orElseThrow().toString();
212 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
213 builder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
215 // No-revision packages are special
216 builder.append("norev");
219 return normalizePackageName(builder.toString());
223 * Convert the result of {@link #getRootPackageName(QNameModule)} to the corresponding result of
224 * {@link #getServicePackageName(QNameModule)}.
226 * @param rootPackageName root package name
227 * @return Service root package name
229 public static @NonNull String rootToServicePackageName(final String rootPackageName) {
230 final var match = ROOT_PACKAGE_PATTERN.matcher(rootPackageName);
231 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", rootPackageName,
232 ROOT_PACKAGE_PATTERN_STRING);
233 return getModelRootPackageName(rootPackageName.replace(Naming.PACKAGE_PREFIX, Naming.SVC_PACKAGE_PREFIX));
236 public static @NonNull String normalizePackageName(final String packageName) {
237 final StringBuilder builder = new StringBuilder();
238 boolean first = true;
240 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
247 if (Character.isDigit(p.charAt(0)) || JAVA_RESERVED_WORDS.contains(p)) {
253 // Prevent duplication of input string
254 return PACKAGE_INTERNER.intern(builder.toString());
257 public static @NonNull String getClassName(final String localName) {
258 return toFirstUpper(toCamelCase(localName));
261 public static @NonNull String getClassName(final QName name) {
262 return toFirstUpper(toCamelCase(name.getLocalName()));
265 public static @NonNull String getMethodName(final String yangIdentifier) {
266 return toFirstLower(toCamelCase(yangIdentifier));
269 public static @NonNull String getMethodName(final QName name) {
270 return getMethodName(name.getLocalName());
273 public static @NonNull String getGetterMethodName(final String localName) {
274 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
277 public static @NonNull String getGetterMethodName(final QName name) {
278 return GETTER_PREFIX + getGetterSuffix(name);
281 public static boolean isGetterMethodName(final String methodName) {
282 return methodName.startsWith(GETTER_PREFIX);
285 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
286 checkArgument(isNonnullMethodName(methodName));
287 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
290 public static @NonNull String getNonnullMethodName(final String localName) {
291 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
294 public static boolean isNonnullMethodName(final String methodName) {
295 return methodName.startsWith(NONNULL_PREFIX);
298 public static @NonNull String getGetterMethodForRequire(final String methodName) {
299 checkArgument(isRequireMethodName(methodName));
300 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
303 public static @NonNull String getRequireMethodName(final String localName) {
304 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
307 public static boolean isRequireMethodName(final String methodName) {
308 return methodName.startsWith(REQUIRE_PREFIX);
311 public static @NonNull String getGetterSuffix(final QName name) {
312 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
313 return "Class".equals(candidate) ? "XmlClass" : candidate;
316 public static @NonNull String getPropertyName(final String yangIdentifier) {
317 final String potential = toFirstLower(toCamelCase(yangIdentifier));
318 if ("class".equals(potential)) {
324 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
326 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
327 final String referencedClassSimpleName) {
328 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
331 private static @NonNull String toCamelCase(final String rawString) {
332 StringBuilder builder = new StringBuilder();
333 for (String comp : CAMEL_SPLITTER.split(rawString)) {
334 builder.append(toFirstUpper(comp));
336 return checkNumericPrefix(builder.toString());
339 private static @NonNull String checkNumericPrefix(final String rawString) {
340 if (rawString.isEmpty()) {
343 final char firstChar = rawString.charAt(0);
344 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
348 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
350 * @param str the string that should get an upper case first character.
351 * @return the {@link String} {@code str} with an upper case first character.
353 public static @NonNull String toFirstUpper(final @NonNull String str) {
357 if (Character.isUpperCase(str.charAt(0))) {
360 if (str.length() == 1) {
361 return str.toUpperCase(Locale.ENGLISH);
363 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
367 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
368 * function is null-safe.
370 * @param str the string that should get an lower case first character. May be <code>null</code>.
371 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
372 * {@link String} {@code str} was empty.
374 private static @NonNull String toFirstLower(final @NonNull String str) {
378 if (Character.isLowerCase(str.charAt(0))) {
381 if (str.length() == 1) {
382 return str.toLowerCase(Locale.ENGLISH);
384 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
388 * Returns root package name for supplied package name.
390 * @param packageName Package for which find model root package.
391 * @return Package of model root.
392 * @throws NullPointerException if {@code packageName} is {@code null}
393 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
394 * not match package name formatting rules
396 public static @NonNull String getModelRootPackageName(final String packageName) {
397 checkArgument(packageName.startsWith(PACKAGE_PREFIX) || packageName.startsWith(SVC_PACKAGE_PREFIX),
398 "Package name not starting with %s, is: %s", PACKAGE_PREFIX, packageName);
399 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
400 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
401 ROOT_PACKAGE_PATTERN_STRING);
402 return match.group(0);
406 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
407 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
408 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
411 * @param assignedNames Collection of assigned names
412 * @return A BiMap keyed by assigned name, with Java identifiers as values
413 * @throws NullPointerException if assignedNames is null or contains null items
414 * @throws IllegalArgumentException if any of the names is empty
416 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
418 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
419 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
420 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
421 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
423 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
425 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
426 boolean valid = true;
427 for (String name : assignedNames) {
428 checkArgument(!name.isEmpty());
429 if (!javaToYang.containsValue(name)) {
430 final String mappedName = getClassName(name);
431 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
439 // Fall back to bijective mapping
441 for (String name : assignedNames) {
442 javaToYang.put(mapEnumAssignedName(name), name);
446 return javaToYang.inverse();
450 * Builds class name representing yang-data template name which is not yang identifier compliant.
452 * @param templateName template name
453 * @return Java class name
454 * @throws NullPointerException if {@code templateName} is {@code null}
455 * @throws IllegalArgumentException if (@code templateName} is empty
457 // TODO: take YangDataName once we have it readily available
458 public static String mapYangDataName(final YangDataName templateName) {
459 return mapEnumAssignedName(templateName.name());
462 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
463 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
464 // which have different exclusions
465 private static boolean isValidJavaIdentifier(final String str) {
466 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
467 && Character.isJavaIdentifierStart(str.codePointAt(0))
468 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
471 private static String mapEnumAssignedName(final String assignedName) {
472 checkArgument(!assignedName.isEmpty());
475 // - if the string is a valid java identifier and does not contain '$', use it as-is
476 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
480 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
481 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
482 final StringBuilder sb = new StringBuilder().append('$');
483 assignedName.codePoints().forEachOrdered(codePoint -> {
484 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
485 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
487 sb.appendCodePoint(codePoint);
490 return sb.toString();