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.mdsal.binding.spec.naming;
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.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.yangtools.yang.binding.Augmentable;
28 import org.opendaylight.yangtools.yang.binding.BindingContract;
29 import org.opendaylight.yangtools.yang.binding.Identifiable;
30 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
31 import org.opendaylight.yangtools.yang.common.QName;
32 import org.opendaylight.yangtools.yang.common.QNameModule;
33 import org.opendaylight.yangtools.yang.common.Revision;
36 public final class BindingMapping {
38 public static final @NonNull String VERSION = "0.6";
40 // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
41 public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
42 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
43 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
44 "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
45 "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
46 "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
47 "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
48 // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
50 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
52 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
54 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
56 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
58 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
61 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
62 public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
63 public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
64 public static final @NonNull String BUILDER_SUFFIX = "Builder";
65 public static final @NonNull String KEY_SUFFIX = "Key";
66 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
67 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
68 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
69 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
71 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
72 .omitEmptyStrings().trimResults();
73 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
74 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
75 private static final Splitter DOT_SPLITTER = Splitter.on('.');
77 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
78 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
79 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
82 * Name of {@link Augmentable#augmentation(Class)}.
84 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
87 * Name of {@link Identifiable#key()}.
89 public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
92 * Name of {@link BindingContract#implementedInterface()}.
94 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
97 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
98 * interface contains this static method.
100 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
103 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
104 * interface contains this static method.
106 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
109 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
110 * interface contains this static method.
112 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
115 * Name of {@link ScalarTypeObject#getValue()}.
117 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
120 * Prefix for normal getter methods.
122 public static final @NonNull String GETTER_PREFIX = "get";
125 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
127 public static final @NonNull String NONNULL_PREFIX = "nonnull";
130 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
133 public static final @NonNull String REQUIRE_PREFIX = "require";
134 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
135 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
137 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
139 private BindingMapping() {
143 public static @NonNull String getRootPackageName(final QName module) {
144 return getRootPackageName(module.getModule());
147 public static @NonNull String getRootPackageName(final QNameModule module) {
148 final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
150 String namespace = module.getNamespace().toString();
151 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
153 final char[] chars = namespace.toCharArray();
154 for (int i = 0; i < chars.length; ++i) {
175 packageNameBuilder.append(chars);
176 if (chars[chars.length - 1] != '.') {
177 packageNameBuilder.append('.');
180 final Optional<Revision> optRev = module.getRevision();
181 if (optRev.isPresent()) {
182 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
184 final String rev = optRev.get().toString();
185 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
186 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
188 // No-revision packages are special
189 packageNameBuilder.append("norev");
192 return normalizePackageName(packageNameBuilder.toString());
195 public static @NonNull String normalizePackageName(final String packageName) {
196 final StringBuilder builder = new StringBuilder();
197 boolean first = true;
199 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
206 if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
212 // Prevent duplication of input string
213 return PACKAGE_INTERNER.intern(builder.toString());
216 public static @NonNull String getClassName(final String localName) {
217 return toFirstUpper(toCamelCase(localName));
220 public static @NonNull String getClassName(final QName name) {
221 return toFirstUpper(toCamelCase(name.getLocalName()));
224 public static @NonNull String getMethodName(final String yangIdentifier) {
225 return toFirstLower(toCamelCase(yangIdentifier));
228 public static @NonNull String getMethodName(final QName name) {
229 return getMethodName(name.getLocalName());
232 public static @NonNull String getGetterMethodName(final String localName) {
233 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
236 public static @NonNull String getGetterMethodName(final QName name) {
237 return GETTER_PREFIX + getGetterSuffix(name);
240 public static boolean isGetterMethodName(final String methodName) {
241 return methodName.startsWith(GETTER_PREFIX);
244 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
245 checkArgument(isNonnullMethodName(methodName));
246 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
249 public static @NonNull String getNonnullMethodName(final String localName) {
250 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
253 public static boolean isNonnullMethodName(final String methodName) {
254 return methodName.startsWith(NONNULL_PREFIX);
257 public static @NonNull String getGetterMethodForRequire(final String methodName) {
258 checkArgument(isRequireMethodName(methodName));
259 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
262 public static @NonNull String getRequireMethodName(final String localName) {
263 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
266 public static boolean isRequireMethodName(final String methodName) {
267 return methodName.startsWith(REQUIRE_PREFIX);
270 public static @NonNull String getGetterSuffix(final QName name) {
271 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
272 return "Class".equals(candidate) ? "XmlClass" : candidate;
275 public static @NonNull String getPropertyName(final String yangIdentifier) {
276 final String potential = toFirstLower(toCamelCase(yangIdentifier));
277 if ("class".equals(potential)) {
283 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
285 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
286 final String referencedClassSimpleName) {
287 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
290 private static @NonNull String toCamelCase(final String rawString) {
291 StringBuilder builder = new StringBuilder();
292 for (String comp : CAMEL_SPLITTER.split(rawString)) {
293 builder.append(toFirstUpper(comp));
295 return checkNumericPrefix(builder.toString());
298 private static @NonNull String checkNumericPrefix(final String rawString) {
299 if (rawString.isEmpty()) {
302 final char firstChar = rawString.charAt(0);
303 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
307 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
309 * @param str the string that should get an upper case first character.
310 * @return the {@link String} {@code str} with an upper case first character.
312 public static @NonNull String toFirstUpper(final @NonNull String str) {
316 if (Character.isUpperCase(str.charAt(0))) {
319 if (str.length() == 1) {
320 return str.toUpperCase(Locale.ENGLISH);
322 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
326 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
327 * function is null-safe.
329 * @param str the string that should get an lower case first character. May be <code>null</code>.
330 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
331 * {@link String} {@code str} was empty.
333 private static @NonNull String toFirstLower(final @NonNull String str) {
337 if (Character.isLowerCase(str.charAt(0))) {
340 if (str.length() == 1) {
341 return str.toLowerCase(Locale.ENGLISH);
343 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
347 * Returns the {@link String} {@code s} with a '$' character as suffix.
349 * @param qname RPC QName
350 * @return The RPC method name as determined by considering the localname against the JLS.
351 * @throws NullPointerException if {@code qname} is null
353 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
354 final String methodName = getMethodName(qname);
355 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
359 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
360 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
361 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
364 * @param assignedNames Collection of assigned names
365 * @return A BiMap keyed by assigned name, with Java identifiers as values
366 * @throws NullPointerException if assignedNames is null or contains null items
367 * @throws IllegalArgumentException if any of the names is empty
369 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
371 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
372 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
373 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
374 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
376 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
378 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
379 boolean valid = true;
380 for (String name : assignedNames) {
381 checkArgument(!name.isEmpty());
382 if (!javaToYang.containsValue(name)) {
383 final String mappedName = getClassName(name);
384 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
392 // Fall back to bijective mapping
394 for (String name : assignedNames) {
395 javaToYang.put(mapEnumAssignedName(name), name);
399 return javaToYang.inverse();
402 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
403 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
404 // which have different exclusions
405 private static boolean isValidJavaIdentifier(final String str) {
406 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
407 && Character.isJavaIdentifierStart(str.codePointAt(0))
408 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
411 private static String mapEnumAssignedName(final String assignedName) {
412 checkArgument(!assignedName.isEmpty());
415 // - if the string is a valid java identifier and does not contain '$', use it as-is
416 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
420 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
421 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
422 final StringBuilder sb = new StringBuilder().append('$');
423 assignedName.codePoints().forEachOrdered(codePoint -> {
424 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
425 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
427 sb.appendCodePoint(codePoint);
430 return sb.toString();