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.regex.Matcher;
24 import java.util.regex.Pattern;
25 import org.checkerframework.checker.regex.qual.Regex;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.yangtools.yang.binding.Action;
28 import org.opendaylight.yangtools.yang.binding.Augmentable;
29 import org.opendaylight.yangtools.yang.binding.BindingContract;
30 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
31 import org.opendaylight.yangtools.yang.binding.KeyAware;
32 import org.opendaylight.yangtools.yang.binding.Rpc;
33 import org.opendaylight.yangtools.yang.binding.RpcInput;
34 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
35 import org.opendaylight.yangtools.yang.common.QName;
36 import org.opendaylight.yangtools.yang.common.QNameModule;
37 import org.opendaylight.yangtools.yang.common.YangDataName;
40 public final class Naming {
42 public static final @NonNull String VERSION = "0.6";
44 // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
45 public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
46 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
47 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
48 "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
49 "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
50 "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
51 "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
52 // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
54 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
56 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
58 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
60 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
62 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
65 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
66 public static final @NonNull String BUILDER_SUFFIX = "Builder";
67 public static final @NonNull String KEY_SUFFIX = "Key";
68 // ietf-restconf:yang-data, i.e. YangDataName
69 public static final @NonNull String NAME_STATIC_FIELD_NAME = "NAME";
70 // everything that can have a QName (e.g. identifier bound to a namespace)
71 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
72 // concrete extensible contracts, for example 'feature', 'identity' and similar
73 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
74 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
75 public static final @NonNull String SVC_PACKAGE_PREFIX = "org.opendaylight.yang.svc.v1";
76 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
78 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
79 .omitEmptyStrings().trimResults();
80 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
81 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
82 private static final Splitter DOT_SPLITTER = Splitter.on('.');
84 public static final @NonNull String MODULE_INFO_CLASS_NAME = "YangModuleInfoImpl";
85 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
86 public static final @NonNull String MODULE_INFO_YANGDATANAMEOF_METHOD_NAME = "yangDataNameOf";
87 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "YangModelBindingProviderImpl";
90 * Name of {@link Augmentable#augmentation(Class)}.
92 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
95 * Name of {@link KeyAware#key()}.
97 public static final @NonNull String KEY_AWARE_KEY_NAME = "key";
100 * Name of {@link BindingContract#implementedInterface()}.
102 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
105 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
106 * interface contains this static method.
108 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
111 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
112 * interface contains this static method.
114 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
117 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
118 * interface contains this static method.
120 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
123 * Name of {@link Action#invoke(InstanceIdentifier, RpcInput)}.
125 public static final @NonNull String ACTION_INVOKE_NAME = "invoke";
128 * Name of {@link Rpc#invoke(org.opendaylight.yangtools.yang.binding.RpcInput)}.
130 public static final @NonNull String RPC_INVOKE_NAME = "invoke";
133 * Name of {@link ScalarTypeObject#getValue()}.
135 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
138 * Prefix for normal getter methods.
140 public static final @NonNull String GETTER_PREFIX = "get";
143 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
145 public static final @NonNull String NONNULL_PREFIX = "nonnull";
148 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
151 public static final @NonNull String REQUIRE_PREFIX = "require";
152 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
153 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
155 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
157 private static final String ROOT_PACKAGE_PATTERN_STRING =
158 "(org.opendaylight.yang.(gen|svc).v1.[a-z0-9_\\.]*?\\.(?:rev[0-9][0-9][0-1][0-9][0-3][0-9]|norev))";
159 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
166 * Return the package name for placing generated ServiceLoader entities, like {@link #MODULE_INFO_CLASS_NAME} and
167 * {@link #MODEL_BINDING_PROVIDER_CLASS_NAME}.
169 * @param module module namespace
170 * @return the package name for placing generated ServiceLoader entities
172 public static @NonNull String getServicePackageName(final QNameModule module) {
173 final StringBuilder packageNameBuilder = new StringBuilder().append(SVC_PACKAGE_PREFIX).append('.');
174 return getRootPackageName(packageNameBuilder, module);
177 public static @NonNull String getRootPackageName(final QName module) {
178 return getRootPackageName(module.getModule());
181 public static @NonNull String getRootPackageName(final QNameModule module) {
182 final StringBuilder packageNameBuilder = new StringBuilder().append(PACKAGE_PREFIX).append('.');
183 return getRootPackageName(packageNameBuilder, module);
186 private static @NonNull String getRootPackageName(final StringBuilder builder, final QNameModule module) {
187 String namespace = module.namespace().toString();
188 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
190 final char[] chars = namespace.toCharArray();
191 for (int i = 0; i < chars.length; ++i) {
193 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
200 builder.append(chars);
201 if (chars[chars.length - 1] != '.') {
205 final var revision = module.revision();
206 if (revision != null) {
207 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
209 final String rev = revision.toString();
210 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
211 builder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
213 // No-revision packages are special
214 builder.append("norev");
217 return normalizePackageName(builder.toString());
221 * Convert the result of {@link #getRootPackageName(QNameModule)} to the corresponding result of
222 * {@link #getServicePackageName(QNameModule)}.
224 * @param rootPackageName root package name
225 * @return Service root package name
227 public static @NonNull String rootToServicePackageName(final String rootPackageName) {
228 final var match = ROOT_PACKAGE_PATTERN.matcher(rootPackageName);
229 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", rootPackageName,
230 ROOT_PACKAGE_PATTERN_STRING);
231 return getModelRootPackageName(rootPackageName.replace(Naming.PACKAGE_PREFIX, Naming.SVC_PACKAGE_PREFIX));
234 public static @NonNull String normalizePackageName(final String packageName) {
235 final StringBuilder builder = new StringBuilder();
236 boolean first = true;
238 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
245 if (Character.isDigit(p.charAt(0)) || JAVA_RESERVED_WORDS.contains(p)) {
251 // Prevent duplication of input string
252 return PACKAGE_INTERNER.intern(builder.toString());
255 public static @NonNull String getClassName(final String localName) {
256 return toFirstUpper(toCamelCase(localName));
259 public static @NonNull String getClassName(final QName name) {
260 return toFirstUpper(toCamelCase(name.getLocalName()));
263 public static @NonNull String getMethodName(final String yangIdentifier) {
264 return toFirstLower(toCamelCase(yangIdentifier));
267 public static @NonNull String getMethodName(final QName name) {
268 return getMethodName(name.getLocalName());
271 public static @NonNull String getGetterMethodName(final String localName) {
272 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
275 public static @NonNull String getGetterMethodName(final QName name) {
276 return GETTER_PREFIX + getGetterSuffix(name);
279 public static boolean isGetterMethodName(final String methodName) {
280 return methodName.startsWith(GETTER_PREFIX);
283 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
284 checkArgument(isNonnullMethodName(methodName));
285 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
288 public static @NonNull String getNonnullMethodName(final String localName) {
289 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
292 public static boolean isNonnullMethodName(final String methodName) {
293 return methodName.startsWith(NONNULL_PREFIX);
296 public static @NonNull String getGetterMethodForRequire(final String methodName) {
297 checkArgument(isRequireMethodName(methodName));
298 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
301 public static @NonNull String getRequireMethodName(final String localName) {
302 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
305 public static boolean isRequireMethodName(final String methodName) {
306 return methodName.startsWith(REQUIRE_PREFIX);
309 public static @NonNull String getGetterSuffix(final QName name) {
310 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
311 return "Class".equals(candidate) ? "XmlClass" : candidate;
314 public static @NonNull String getPropertyName(final String yangIdentifier) {
315 final String potential = toFirstLower(toCamelCase(yangIdentifier));
316 if ("class".equals(potential)) {
322 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
324 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
325 final String referencedClassSimpleName) {
326 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
329 private static @NonNull String toCamelCase(final String rawString) {
330 StringBuilder builder = new StringBuilder();
331 for (String comp : CAMEL_SPLITTER.split(rawString)) {
332 builder.append(toFirstUpper(comp));
334 return checkNumericPrefix(builder.toString());
337 private static @NonNull String checkNumericPrefix(final String rawString) {
338 if (rawString.isEmpty()) {
341 final char firstChar = rawString.charAt(0);
342 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
346 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
348 * @param str the string that should get an upper case first character.
349 * @return the {@link String} {@code str} with an upper case first character.
351 public static @NonNull String toFirstUpper(final @NonNull String str) {
355 if (Character.isUpperCase(str.charAt(0))) {
358 if (str.length() == 1) {
359 return str.toUpperCase(Locale.ENGLISH);
361 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
365 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
366 * function is null-safe.
368 * @param str the string that should get an lower case first character. May be <code>null</code>.
369 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
370 * {@link String} {@code str} was empty.
372 private static @NonNull String toFirstLower(final @NonNull String str) {
376 if (Character.isLowerCase(str.charAt(0))) {
379 if (str.length() == 1) {
380 return str.toLowerCase(Locale.ENGLISH);
382 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
386 * Returns root package name for supplied package name.
388 * @param packageName Package for which find model root package.
389 * @return Package of model root.
390 * @throws NullPointerException if {@code packageName} is {@code null}
391 * @throws IllegalArgumentException if {@code packageName} does not start with {@link #PACKAGE_PREFIX} or it does
392 * not match package name formatting rules
394 public static @NonNull String getModelRootPackageName(final String packageName) {
395 checkArgument(packageName.startsWith(PACKAGE_PREFIX) || packageName.startsWith(SVC_PACKAGE_PREFIX),
396 "Package name not starting with %s, is: %s", PACKAGE_PREFIX, packageName);
397 final var match = ROOT_PACKAGE_PATTERN.matcher(packageName);
398 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", packageName,
399 ROOT_PACKAGE_PATTERN_STRING);
400 return match.group(0);
404 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
405 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
406 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
409 * @param assignedNames Collection of assigned names
410 * @return A BiMap keyed by assigned name, with Java identifiers as values
411 * @throws NullPointerException if assignedNames is null or contains null items
412 * @throws IllegalArgumentException if any of the names is empty
414 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
416 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
417 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
418 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
419 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
421 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
423 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
424 boolean valid = true;
425 for (String name : assignedNames) {
426 checkArgument(!name.isEmpty());
427 if (!javaToYang.containsValue(name)) {
428 final String mappedName = getClassName(name);
429 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
437 // Fall back to bijective mapping
439 for (String name : assignedNames) {
440 javaToYang.put(mapEnumAssignedName(name), name);
444 return javaToYang.inverse();
448 * Builds class name representing yang-data template name which is not yang identifier compliant.
450 * @param templateName template name
451 * @return Java class name
452 * @throws NullPointerException if {@code templateName} is {@code null}
453 * @throws IllegalArgumentException if (@code templateName} is empty
455 // TODO: take YangDataName once we have it readily available
456 public static String mapYangDataName(final YangDataName templateName) {
457 return mapEnumAssignedName(templateName.name());
460 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
461 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
462 // which have different exclusions
463 private static boolean isValidJavaIdentifier(final String str) {
464 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
465 && Character.isJavaIdentifierStart(str.codePointAt(0))
466 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
469 private static String mapEnumAssignedName(final String assignedName) {
470 checkArgument(!assignedName.isEmpty());
473 // - if the string is a valid java identifier and does not contain '$', use it as-is
474 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
478 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
479 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
480 final StringBuilder sb = new StringBuilder().append('$');
481 assignedName.codePoints().forEachOrdered(codePoint -> {
482 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
483 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
485 sb.appendCodePoint(codePoint);
488 return sb.toString();