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 @Deprecated(since = "10.0.3", forRemoval = true)
64 public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
65 public static final @NonNull String BUILDER_SUFFIX = "Builder";
66 public static final @NonNull String KEY_SUFFIX = "Key";
67 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
68 public static final @NonNull String VALUE_STATIC_FIELD_NAME = "VALUE";
69 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
70 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
72 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
73 .omitEmptyStrings().trimResults();
74 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
75 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
76 private static final Splitter DOT_SPLITTER = Splitter.on('.');
78 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
79 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
80 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
83 * Name of {@link Augmentable#augmentation(Class)}.
85 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
88 * Name of {@link Identifiable#key()}.
90 public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
93 * Name of {@link BindingContract#implementedInterface()}.
95 public static final @NonNull String BINDING_CONTRACT_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
98 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
99 * interface contains this static method.
101 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
104 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
105 * interface contains this static method.
107 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
110 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
111 * interface contains this static method.
113 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
116 * Name of {@link ScalarTypeObject#getValue()}.
118 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
121 * Prefix for normal getter methods.
123 public static final @NonNull String GETTER_PREFIX = "get";
126 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
128 public static final @NonNull String NONNULL_PREFIX = "nonnull";
131 * Prefix for require default wrapper methods. These methods always wrap a corresponding normal getter
134 public static final @NonNull String REQUIRE_PREFIX = "require";
135 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
136 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
138 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
140 private BindingMapping() {
144 public static @NonNull String getRootPackageName(final QName module) {
145 return getRootPackageName(module.getModule());
148 public static @NonNull String getRootPackageName(final QNameModule module) {
149 final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
151 String namespace = module.getNamespace().toString();
152 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
154 final char[] chars = namespace.toCharArray();
155 for (int i = 0; i < chars.length; ++i) {
157 case '/', ':', '-', '@', '$', '#', '\'', '*', '+', ',', ';', '=' -> chars[i] = '.';
164 packageNameBuilder.append(chars);
165 if (chars[chars.length - 1] != '.') {
166 packageNameBuilder.append('.');
169 final Optional<Revision> optRev = module.getRevision();
170 if (optRev.isPresent()) {
171 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
173 final String rev = optRev.get().toString();
174 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
175 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
177 // No-revision packages are special
178 packageNameBuilder.append("norev");
181 return normalizePackageName(packageNameBuilder.toString());
184 public static @NonNull String normalizePackageName(final String packageName) {
185 final StringBuilder builder = new StringBuilder();
186 boolean first = true;
188 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
195 if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
201 // Prevent duplication of input string
202 return PACKAGE_INTERNER.intern(builder.toString());
205 public static @NonNull String getClassName(final String localName) {
206 return toFirstUpper(toCamelCase(localName));
209 public static @NonNull String getClassName(final QName name) {
210 return toFirstUpper(toCamelCase(name.getLocalName()));
213 public static @NonNull String getMethodName(final String yangIdentifier) {
214 return toFirstLower(toCamelCase(yangIdentifier));
217 public static @NonNull String getMethodName(final QName name) {
218 return getMethodName(name.getLocalName());
221 public static @NonNull String getGetterMethodName(final String localName) {
222 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
225 public static @NonNull String getGetterMethodName(final QName name) {
226 return GETTER_PREFIX + getGetterSuffix(name);
229 public static boolean isGetterMethodName(final String methodName) {
230 return methodName.startsWith(GETTER_PREFIX);
233 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
234 checkArgument(isNonnullMethodName(methodName));
235 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
238 public static @NonNull String getNonnullMethodName(final String localName) {
239 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
242 public static boolean isNonnullMethodName(final String methodName) {
243 return methodName.startsWith(NONNULL_PREFIX);
246 public static @NonNull String getGetterMethodForRequire(final String methodName) {
247 checkArgument(isRequireMethodName(methodName));
248 return GETTER_PREFIX + methodName.substring(REQUIRE_PREFIX.length());
251 public static @NonNull String getRequireMethodName(final String localName) {
252 return REQUIRE_PREFIX + toFirstUpper(getPropertyName(localName));
255 public static boolean isRequireMethodName(final String methodName) {
256 return methodName.startsWith(REQUIRE_PREFIX);
259 public static @NonNull String getGetterSuffix(final QName name) {
260 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
261 return "Class".equals(candidate) ? "XmlClass" : candidate;
264 public static @NonNull String getPropertyName(final String yangIdentifier) {
265 final String potential = toFirstLower(toCamelCase(yangIdentifier));
266 if ("class".equals(potential)) {
272 // FIXME: this is legacy union/leafref property handling. The resulting value is *not* normalized for use as a
274 public static @NonNull String getUnionLeafrefMemberName(final String unionClassSimpleName,
275 final String referencedClassSimpleName) {
276 return requireNonNull(referencedClassSimpleName) + requireNonNull(unionClassSimpleName) + "Value";
279 private static @NonNull String toCamelCase(final String rawString) {
280 StringBuilder builder = new StringBuilder();
281 for (String comp : CAMEL_SPLITTER.split(rawString)) {
282 builder.append(toFirstUpper(comp));
284 return checkNumericPrefix(builder.toString());
287 private static @NonNull String checkNumericPrefix(final String rawString) {
288 if (rawString.isEmpty()) {
291 final char firstChar = rawString.charAt(0);
292 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
296 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
298 * @param str the string that should get an upper case first character.
299 * @return the {@link String} {@code str} with an upper case first character.
301 public static @NonNull String toFirstUpper(final @NonNull String str) {
305 if (Character.isUpperCase(str.charAt(0))) {
308 if (str.length() == 1) {
309 return str.toUpperCase(Locale.ENGLISH);
311 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
315 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
316 * function is null-safe.
318 * @param str the string that should get an lower case first character. May be <code>null</code>.
319 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
320 * {@link String} {@code str} was empty.
322 private static @NonNull String toFirstLower(final @NonNull String str) {
326 if (Character.isLowerCase(str.charAt(0))) {
329 if (str.length() == 1) {
330 return str.toLowerCase(Locale.ENGLISH);
332 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
336 * Returns the {@link String} {@code s} with a '$' character as suffix.
338 * @param qname RPC QName
339 * @return The RPC method name as determined by considering the localname against the JLS.
340 * @throws NullPointerException if {@code qname} is null
342 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
343 final String methodName = getMethodName(qname);
344 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
348 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
349 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
350 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
353 * @param assignedNames Collection of assigned names
354 * @return A BiMap keyed by assigned name, with Java identifiers as values
355 * @throws NullPointerException if assignedNames is null or contains null items
356 * @throws IllegalArgumentException if any of the names is empty
358 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
360 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
361 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
362 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
363 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
365 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
367 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
368 boolean valid = true;
369 for (String name : assignedNames) {
370 checkArgument(!name.isEmpty());
371 if (!javaToYang.containsValue(name)) {
372 final String mappedName = getClassName(name);
373 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
381 // Fall back to bijective mapping
383 for (String name : assignedNames) {
384 javaToYang.put(mapEnumAssignedName(name), name);
388 return javaToYang.inverse();
391 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
392 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
393 // which have different exclusions
394 private static boolean isValidJavaIdentifier(final String str) {
395 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
396 && Character.isJavaIdentifierStart(str.codePointAt(0))
397 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
400 private static String mapEnumAssignedName(final String assignedName) {
401 checkArgument(!assignedName.isEmpty());
404 // - if the string is a valid java identifier and does not contain '$', use it as-is
405 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
409 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
410 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
411 final StringBuilder sb = new StringBuilder().append('$');
412 assignedName.codePoints().forEachOrdered(codePoint -> {
413 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
414 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
416 sb.appendCodePoint(codePoint);
419 return sb.toString();