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 com.google.common.base.Verify.verify;
12 import static java.util.Objects.requireNonNull;
14 import com.google.common.annotations.Beta;
15 import com.google.common.base.CharMatcher;
16 import com.google.common.base.Splitter;
17 import com.google.common.collect.BiMap;
18 import com.google.common.collect.HashBiMap;
19 import com.google.common.collect.ImmutableSet;
20 import com.google.common.collect.Interner;
21 import com.google.common.collect.Interners;
22 import java.util.Collection;
23 import java.util.Locale;
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.YangDataName;
41 public final class Naming {
43 public static final @NonNull String VERSION = "0.6";
45 // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
46 public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
47 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
48 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
49 "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
50 "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
51 "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
52 "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
53 // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
55 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
57 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
59 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
61 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
63 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
66 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
67 public static final @NonNull String BUILDER_SUFFIX = "Builder";
68 public static final @NonNull String KEY_SUFFIX = "Key";
69 // ietf-restconf:yang-data, i.e. YangDataName
70 public static final @NonNull String NAME_STATIC_FIELD_NAME = "NAME";
71 // everything that can have a QName (e.g. identifier bound to a namespace)
72 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
73 // concrete extensible contracts, for example 'feature', 'identity' and similar
74 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
75 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
76 public static final @NonNull String SVC_PACKAGE_PREFIX = "org.opendaylight.yang.svc.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 = "YangModelBindingProviderImpl";
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|svc).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);
167 * Return the package name for placing generated ServiceLoader entities, like {@link #MODULE_INFO_CLASS_NAME} and
168 * {@link #MODEL_BINDING_PROVIDER_CLASS_NAME}.
170 * @param module module namespace
171 * @return the package name for placing generated ServiceLoader entities
173 public static @NonNull String getServicePackageName(final QNameModule module) {
174 final StringBuilder packageNameBuilder = new StringBuilder().append(SVC_PACKAGE_PREFIX).append('.');
175 return getRootPackageName(packageNameBuilder, module);
178 public static @NonNull String getRootPackageName(final QName module) {
179 return getRootPackageName(module.getModule());
182 public static @NonNull String getRootPackageName(final QNameModule module) {
183 final StringBuilder packageNameBuilder = new StringBuilder().append(PACKAGE_PREFIX).append('.');
184 return getRootPackageName(packageNameBuilder, module);
187 private static @NonNull String getRootPackageName(final StringBuilder builder, final QNameModule module) {
188 String namespace = module.namespace().toString();
189 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
191 final char[] chars = namespace.toCharArray();
192 for (int i = 0; i < chars.length; ++i) {
194 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
201 builder.append(chars);
202 if (chars[chars.length - 1] != '.') {
206 final var revision = module.revision();
207 if (revision != null) {
208 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
210 final String rev = revision.toString();
211 verify(rev.length() == 10, "Revision.toString() resulted in unexpected '%s'", rev);
212 builder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
214 // No-revision packages are special
215 builder.append("norev");
218 return normalizePackageName(builder.toString());
222 * Convert the result of {@link #getRootPackageName(QNameModule)} to the corresponding result of
223 * {@link #getServicePackageName(QNameModule)}.
225 * @param rootPackageName root package name
226 * @return Service root package name
228 public static @NonNull String rootToServicePackageName(final String rootPackageName) {
229 final var match = ROOT_PACKAGE_PATTERN.matcher(rootPackageName);
230 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", rootPackageName,
231 ROOT_PACKAGE_PATTERN_STRING);
232 return getModelRootPackageName(rootPackageName.replace(Naming.PACKAGE_PREFIX, Naming.SVC_PACKAGE_PREFIX));
235 public static @NonNull String normalizePackageName(final String packageName) {
236 final StringBuilder builder = new StringBuilder();
237 boolean first = true;
239 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
246 if (Character.isDigit(p.charAt(0)) || JAVA_RESERVED_WORDS.contains(p)) {
252 // Prevent duplication of input string
253 return PACKAGE_INTERNER.intern(builder.toString());
256 public static @NonNull String getClassName(final String localName) {
257 return toFirstUpper(toCamelCase(localName));
260 public static @NonNull String getClassName(final QName name) {
261 return toFirstUpper(toCamelCase(name.getLocalName()));
264 public static @NonNull String getMethodName(final String yangIdentifier) {
265 return toFirstLower(toCamelCase(yangIdentifier));
268 public static @NonNull String getMethodName(final QName name) {
269 return getMethodName(name.getLocalName());
272 public static @NonNull String getGetterMethodName(final String localName) {
273 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
276 public static @NonNull String getGetterMethodName(final QName name) {
277 return GETTER_PREFIX + getGetterSuffix(name);
280 public static boolean isGetterMethodName(final String methodName) {
281 return methodName.startsWith(GETTER_PREFIX);
284 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
285 checkArgument(isNonnullMethodName(methodName));
286 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
289 public static @NonNull String getNonnullMethodName(final String localName) {
290 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
293 public static boolean isNonnullMethodName(final String methodName) {
294 return methodName.startsWith(NONNULL_PREFIX);
297 public static @NonNull String getGetterMethodForRequire(final String methodName) {
298 checkArgument(isRequireMethodName(methodName));
299 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
302 public static @NonNull String getRequireMethodName(final String localName) {
303 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
306 public static boolean isRequireMethodName(final String methodName) {
307 return methodName.startsWith(REQUIRE_PREFIX);
310 public static @NonNull String getGetterSuffix(final QName name) {
311 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
312 return "Class".equals(candidate) ? "XmlClass" : candidate;
315 public static @NonNull String getPropertyName(final String yangIdentifier) {
316 final String potential = toFirstLower(toCamelCase(yangIdentifier));
317 if ("class".equals(potential)) {
323 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
325 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
326 final String referencedClassSimpleName) {
327 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
330 private static @NonNull String toCamelCase(final String rawString) {
331 StringBuilder builder = new StringBuilder();
332 for (String comp : CAMEL_SPLITTER.split(rawString)) {
333 builder.append(toFirstUpper(comp));
335 return checkNumericPrefix(builder.toString());
338 private static @NonNull String checkNumericPrefix(final String rawString) {
339 if (rawString.isEmpty()) {
342 final char firstChar = rawString.charAt(0);
343 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
347 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
349 * @param str the string that should get an upper case first character.
350 * @return the {@link String} {@code str} with an upper case first character.
352 public static @NonNull String toFirstUpper(final @NonNull String str) {
356 if (Character.isUpperCase(str.charAt(0))) {
359 if (str.length() == 1) {
360 return str.toUpperCase(Locale.ENGLISH);
362 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
366 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
367 * function is null-safe.
369 * @param str the string that should get an lower case first character. May be <code>null</code>.
370 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
371 * {@link String} {@code str} was empty.
373 private static @NonNull String toFirstLower(final @NonNull String str) {
377 if (Character.isLowerCase(str.charAt(0))) {
380 if (str.length() == 1) {
381 return str.toLowerCase(Locale.ENGLISH);
383 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
387 * Returns root package name for supplied package name.
389 * @param packageName Package for which find model root package.
390 * @return Package of model root.
391 * @throws NullPointerException if {@code packageName} is {@code null}
392 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
393 * not match package name formatting rules
395 public static @NonNull String getModelRootPackageName(final String packageName) {
396 checkArgument(packageName.startsWith(PACKAGE_PREFIX) || packageName.startsWith(SVC_PACKAGE_PREFIX),
397 "Package name not starting with %s, is: %s", PACKAGE_PREFIX, packageName);
398 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
399 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
400 ROOT_PACKAGE_PATTERN_STRING);
401 return match.group(0);
405 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
406 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
407 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
410 * @param assignedNames Collection of assigned names
411 * @return A BiMap keyed by assigned name, with Java identifiers as values
412 * @throws NullPointerException if assignedNames is null or contains null items
413 * @throws IllegalArgumentException if any of the names is empty
415 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
417 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
418 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
419 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
420 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
422 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
424 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
425 boolean valid = true;
426 for (String name : assignedNames) {
427 checkArgument(!name.isEmpty());
428 if (!javaToYang.containsValue(name)) {
429 final String mappedName = getClassName(name);
430 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
438 // Fall back to bijective mapping
440 for (String name : assignedNames) {
441 javaToYang.put(mapEnumAssignedName(name), name);
445 return javaToYang.inverse();
449 * Builds class name representing yang-data template name which is not yang identifier compliant.
451 * @param templateName template name
452 * @return Java class name
453 * @throws NullPointerException if {@code templateName} is {@code null}
454 * @throws IllegalArgumentException if (@code templateName} is empty
456 // TODO: take YangDataName once we have it readily available
457 public static String mapYangDataName(final YangDataName templateName) {
458 return mapEnumAssignedName(templateName.name());
461 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
462 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
463 // which have different exclusions
464 private static boolean isValidJavaIdentifier(final String str) {
465 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
466 && Character.isJavaIdentifierStart(str.codePointAt(0))
467 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
470 private static String mapEnumAssignedName(final String assignedName) {
471 checkArgument(!assignedName.isEmpty());
474 // - if the string is a valid java identifier and does not contain '$', use it as-is
475 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
479 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
480 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
481 final StringBuilder sb = new StringBuilder().append('$');
482 assignedName.codePoints().forEachOrdered(codePoint -> {
483 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
484 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
486 sb.appendCodePoint(codePoint);
489 return sb.toString();