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;
12 import com.google.common.annotations.Beta;
13 import com.google.common.base.CharMatcher;
14 import com.google.common.base.Splitter;
15 import com.google.common.collect.BiMap;
16 import com.google.common.collect.HashBiMap;
17 import com.google.common.collect.ImmutableSet;
18 import com.google.common.collect.Interner;
19 import com.google.common.collect.Interners;
20 import java.util.Collection;
21 import java.util.Locale;
22 import java.util.Optional;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.opendaylight.yangtools.yang.binding.Augmentable;
27 import org.opendaylight.yangtools.yang.binding.DataContainer;
28 import org.opendaylight.yangtools.yang.binding.Identifiable;
29 import org.opendaylight.yangtools.yang.binding.ScalarTypeObject;
30 import org.opendaylight.yangtools.yang.common.QName;
31 import org.opendaylight.yangtools.yang.common.QNameModule;
32 import org.opendaylight.yangtools.yang.common.Revision;
35 public final class BindingMapping {
37 public static final @NonNull String VERSION = "0.6";
39 // Note: these are not just JLS keywords, but rather character sequences which are reserved in codegen contexts
40 public static final ImmutableSet<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
41 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9 except module-info.java constructs
42 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
43 "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
44 "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
45 "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
46 "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
47 // "open", "module", "requires", "transitive", "exports, "opens", "to", "uses", "provides", "with",
49 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
51 // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
53 // https://docs.oracle.com/javase/specs/jls/se10/html/jls-3.html#jls-3.9
55 // https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.9
57 // https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.9
60 public static final @NonNull String DATA_ROOT_SUFFIX = "Data";
61 public static final @NonNull String RPC_SERVICE_SUFFIX = "Service";
62 public static final @NonNull String NOTIFICATION_LISTENER_SUFFIX = "Listener";
63 public static final @NonNull String QNAME_STATIC_FIELD_NAME = "QNAME";
64 public static final @NonNull String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
65 public static final @NonNull String AUGMENTATION_FIELD = "augmentation";
67 private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
68 .omitEmptyStrings().trimResults();
69 private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
70 private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
71 private static final Splitter DOT_SPLITTER = Splitter.on('.');
73 public static final @NonNull String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
74 public static final @NonNull String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
75 public static final @NonNull String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
78 * Name of {@link Augmentable#augmentation(Class)}.
80 public static final @NonNull String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
83 * Name of {@link Identifiable#key()}.
85 public static final @NonNull String IDENTIFIABLE_KEY_NAME = "key";
88 * Name of {@link DataContainer#implementedInterface()}.
90 public static final @NonNull String DATA_CONTAINER_IMPLEMENTED_INTERFACE_NAME = "implementedInterface";
93 * Name of default {@link Object#hashCode()} implementation for instantiated DataObjects. Each such generated
94 * interface contains this static method.
96 public static final @NonNull String BINDING_HASHCODE_NAME = "bindingHashCode";
99 * Name of default {@link Object#equals(Object)} implementation for instantiated DataObjects. Each such generated
100 * interface contains this static method.
102 public static final @NonNull String BINDING_EQUALS_NAME = "bindingEquals";
105 * Name of default {@link Object#toString()} implementation for instantiated DataObjects. Each such generated
106 * interface contains this static method.
108 public static final @NonNull String BINDING_TO_STRING_NAME = "bindingToString";
111 * Name of {@link ScalarTypeObject#getValue()}.
113 public static final @NonNull String SCALAR_TYPE_OBJECT_GET_VALUE_NAME = "getValue";
116 * Prefix for normal getter methods.
118 public static final @NonNull String GETTER_PREFIX = "get";
121 * Prefix for non-null default wrapper methods. These methods always wrap a corresponding normal getter.
123 public static final @NonNull String NONNULL_PREFIX = "nonnull";
125 public static final @NonNull String RPC_INPUT_SUFFIX = "Input";
126 public static final @NonNull String RPC_OUTPUT_SUFFIX = "Output";
128 private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
130 private BindingMapping() {
134 public static @NonNull String getRootPackageName(final QName module) {
135 return getRootPackageName(module.getModule());
138 public static @NonNull String getRootPackageName(final QNameModule module) {
139 final StringBuilder packageNameBuilder = new StringBuilder().append(BindingMapping.PACKAGE_PREFIX).append('.');
141 String namespace = module.getNamespace().toString();
142 namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
144 final char[] chars = namespace.toCharArray();
145 for (int i = 0; i < chars.length; ++i) {
166 packageNameBuilder.append(chars);
167 if (chars[chars.length - 1] != '.') {
168 packageNameBuilder.append('.');
171 final Optional<Revision> optRev = module.getRevision();
172 if (optRev.isPresent()) {
173 // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
175 final String rev = optRev.get().toString();
176 checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
177 packageNameBuilder.append("rev").append(rev, 2, 4).append(rev, 5, 7).append(rev.substring(8));
179 // No-revision packages are special
180 packageNameBuilder.append("norev");
183 return normalizePackageName(packageNameBuilder.toString());
186 public static @NonNull String normalizePackageName(final String packageName) {
187 final StringBuilder builder = new StringBuilder();
188 boolean first = true;
190 for (String p : DOT_SPLITTER.split(packageName.toLowerCase(Locale.ENGLISH))) {
197 if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
203 // Prevent duplication of input string
204 return PACKAGE_INTERNER.intern(builder.toString());
207 public static @NonNull String getClassName(final String localName) {
208 return toFirstUpper(toCamelCase(localName));
211 public static @NonNull String getClassName(final QName name) {
212 return toFirstUpper(toCamelCase(name.getLocalName()));
215 public static @NonNull String getMethodName(final String yangIdentifier) {
216 return toFirstLower(toCamelCase(yangIdentifier));
219 public static @NonNull String getMethodName(final QName name) {
220 return getMethodName(name.getLocalName());
223 public static @NonNull String getGetterMethodName(final String localName) {
224 return GETTER_PREFIX + toFirstUpper(getPropertyName(localName));
227 public static @NonNull String getGetterMethodName(final QName name) {
228 return GETTER_PREFIX + getGetterSuffix(name);
231 public static boolean isGetterMethodName(final String methodName) {
232 return methodName.startsWith(GETTER_PREFIX);
235 public static @NonNull String getGetterMethodForNonnull(final String methodName) {
236 checkArgument(isNonnullMethodName(methodName));
237 return GETTER_PREFIX + methodName.substring(NONNULL_PREFIX.length());
240 public static @NonNull String getNonnullMethodName(final String localName) {
241 return NONNULL_PREFIX + toFirstUpper(getPropertyName(localName));
244 public static boolean isNonnullMethodName(final String methodName) {
245 return methodName.startsWith(NONNULL_PREFIX);
248 public static @NonNull String getGetterSuffix(final QName name) {
249 final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
250 return "Class".equals(candidate) ? "XmlClass" : candidate;
253 public static @NonNull String getPropertyName(final String yangIdentifier) {
254 final String potential = toFirstLower(toCamelCase(yangIdentifier));
255 if ("class".equals(potential)) {
261 private static @NonNull String toCamelCase(final String rawString) {
262 StringBuilder builder = new StringBuilder();
263 for (String comp : CAMEL_SPLITTER.split(rawString)) {
264 builder.append(toFirstUpper(comp));
266 return checkNumericPrefix(builder.toString());
269 private static @NonNull String checkNumericPrefix(final String rawString) {
270 if (rawString.isEmpty()) {
273 final char firstChar = rawString.charAt(0);
274 return firstChar >= '0' && firstChar <= '9' ? "_" + rawString : rawString;
278 * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character.
280 * @param str the string that should get an upper case first character.
281 * @return the {@link String} {@code str} with an upper case first character.
283 private static @NonNull String toFirstUpper(final @NonNull String str) {
287 if (Character.isUpperCase(str.charAt(0))) {
290 if (str.length() == 1) {
291 return str.toUpperCase(Locale.ENGLISH);
293 return str.substring(0, 1).toUpperCase(Locale.ENGLISH) + str.substring(1);
297 * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
298 * function is null-safe.
300 * @param str the string that should get an lower case first character. May be <code>null</code>.
301 * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
302 * {@link String} {@code str} was empty.
304 private static @NonNull String toFirstLower(final @NonNull String str) {
308 if (Character.isLowerCase(str.charAt(0))) {
311 if (str.length() == 1) {
312 return str.toLowerCase(Locale.ENGLISH);
314 return str.substring(0, 1).toLowerCase(Locale.ENGLISH) + str.substring(1);
318 * Returns the {@link String} {@code s} with a '$' character as suffix.
320 * @param qname RPC QName
321 * @return The RPC method name as determined by considering the localname against the JLS.
322 * @throws NullPointerException if {@code qname} is null
324 public static @NonNull String getRpcMethodName(final @NonNull QName qname) {
325 final String methodName = getMethodName(qname);
326 return JAVA_RESERVED_WORDS.contains(methodName) ? methodName + "$" : methodName;
330 * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
331 * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
332 * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
335 * @param assignedNames Collection of assigned names
336 * @return A BiMap keyed by assigned name, with Java identifiers as values
337 * @throws NullPointerException if assignedNames is null or contains null items
338 * @throws IllegalArgumentException if any of the names is empty
340 public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
342 * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
343 * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
344 * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
345 * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
347 * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
349 final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
350 boolean valid = true;
351 for (String name : assignedNames) {
352 checkArgument(!name.isEmpty());
353 if (!javaToYang.containsValue(name)) {
354 final String mappedName = getClassName(name);
355 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
363 // Fall back to bijective mapping
365 for (String name : assignedNames) {
366 javaToYang.put(mapEnumAssignedName(name), name);
370 return javaToYang.inverse();
373 // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-3.8
374 // TODO: we are being conservative here, but should differentiate TypeIdentifier and UnqualifiedMethodIdentifier,
375 // which have different exclusions
376 private static boolean isValidJavaIdentifier(final String str) {
377 return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
378 && Character.isJavaIdentifierStart(str.codePointAt(0))
379 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
382 private static String mapEnumAssignedName(final String assignedName) {
383 checkArgument(!assignedName.isEmpty());
386 // - if the string is a valid java identifier and does not contain '$', use it as-is
387 if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
391 // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
392 // hex-encoded unicode codepoint (including plane, stripping leading zeroes)
393 final StringBuilder sb = new StringBuilder().append('$');
394 assignedName.codePoints().forEachOrdered(codePoint -> {
395 if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
396 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
398 sb.appendCodePoint(codePoint);
401 return sb.toString();