Define getImplementedInterface name in BindingMapping
[mdsal.git] / binding / mdsal-binding-spec-util / src / main / java / org / opendaylight / mdsal / binding / spec / naming / BindingMapping.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.mdsal.binding.spec.naming;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
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.Set;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
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.common.QName;
30 import org.opendaylight.yangtools.yang.common.QNameModule;
31 import org.opendaylight.yangtools.yang.common.Revision;
32
33 @Beta
34 public final class BindingMapping {
35
36     public static final String VERSION = "0.6";
37
38     public static final Set<String> JAVA_RESERVED_WORDS = ImmutableSet.of(
39         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.9
40         "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
41         "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
42         "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
43         "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
44         "throw", "throws", "transient", "try", "void", "volatile", "while", "_",
45         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.3
46         "false", "true",
47         // https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.10.7
48         "null");
49
50     public static final String DATA_ROOT_SUFFIX = "Data";
51     public static final String RPC_SERVICE_SUFFIX = "Service";
52     public static final String NOTIFICATION_LISTENER_SUFFIX = "Listener";
53     public static final String QNAME_STATIC_FIELD_NAME = "QNAME";
54     public static final String PACKAGE_PREFIX = "org.opendaylight.yang.gen.v1";
55     public static final String AUGMENTATION_FIELD = "augmentation";
56
57     private static final Splitter CAMEL_SPLITTER = Splitter.on(CharMatcher.anyOf(" _.-/").precomputed())
58             .omitEmptyStrings().trimResults();
59     private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
60     private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
61     private static final Splitter DOT_SPLITTER = Splitter.on('.');
62
63     public static final String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
64     public static final String MODULE_INFO_QNAMEOF_METHOD_NAME = "qnameOf";
65     public static final String MODEL_BINDING_PROVIDER_CLASS_NAME = "$YangModelBindingProvider";
66
67     /**
68      * Name of {@link Augmentable#augmentation(Class)}.
69      */
70     public static final String AUGMENTABLE_AUGMENTATION_NAME = "augmentation";
71
72     /**
73      * Name of {@link Identifiable#key()}.
74      */
75     public static final String IDENTIFIABLE_KEY_NAME = "key";
76
77     /**
78      * Name of {@link DataContainer#getImplementedInterface()}.
79      */
80     public static final String DATA_CONTAINER_GET_IMPLEMENTED_INTERFACE_NAME = "getImplementedInterface";
81
82     public static final String RPC_INPUT_SUFFIX = "Input";
83     public static final String RPC_OUTPUT_SUFFIX = "Output";
84
85     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
86
87     private BindingMapping() {
88         throw new UnsupportedOperationException("Utility class should not be instantiated");
89     }
90
91     public static String getRootPackageName(final QName module) {
92         return getRootPackageName(module.getModule());
93     }
94
95     public static String getRootPackageName(final QNameModule module) {
96         checkArgument(module != null, "Module must not be null");
97         checkArgument(module.getRevision() != null, "Revision must not be null");
98         checkArgument(module.getNamespace() != null, "Namespace must not be null");
99         final StringBuilder packageNameBuilder = new StringBuilder();
100
101         packageNameBuilder.append(BindingMapping.PACKAGE_PREFIX);
102         packageNameBuilder.append('.');
103
104         String namespace = module.getNamespace().toString();
105         namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
106
107         final char[] chars = namespace.toCharArray();
108         for (int i = 0; i < chars.length; ++i) {
109             switch (chars[i]) {
110                 case '/':
111                 case ':':
112                 case '-':
113                 case '@':
114                 case '$':
115                 case '#':
116                 case '\'':
117                 case '*':
118                 case '+':
119                 case ',':
120                 case ';':
121                 case '=':
122                     chars[i] = '.';
123                     break;
124                 default:
125                     // no-op
126             }
127         }
128
129         packageNameBuilder.append(chars);
130         if (chars[chars.length - 1] != '.') {
131             packageNameBuilder.append('.');
132         }
133
134         final Optional<Revision> optRev = module.getRevision();
135         if (optRev.isPresent()) {
136             // Revision is in format 2017-10-26, we want the output to be 171026, which is a matter of picking the
137             // right characters.
138             final String rev = optRev.get().toString();
139             checkArgument(rev.length() == 10, "Unsupported revision %s", rev);
140             packageNameBuilder.append("rev");
141             packageNameBuilder.append(rev.substring(2, 4)).append(rev.substring(5, 7)).append(rev.substring(8));
142         } else {
143             // No-revision packages are special
144             packageNameBuilder.append("norev");
145         }
146
147         return normalizePackageName(packageNameBuilder.toString());
148     }
149
150     public static String normalizePackageName(final String packageName) {
151         if (packageName == null) {
152             return null;
153         }
154
155         final StringBuilder builder = new StringBuilder();
156         boolean first = true;
157
158         for (String p : DOT_SPLITTER.split(packageName.toLowerCase())) {
159             if (first) {
160                 first = false;
161             } else {
162                 builder.append('.');
163             }
164
165             if (Character.isDigit(p.charAt(0)) || BindingMapping.JAVA_RESERVED_WORDS.contains(p)) {
166                 builder.append('_');
167             }
168             builder.append(p);
169         }
170
171         // Prevent duplication of input string
172         return PACKAGE_INTERNER.intern(builder.toString());
173     }
174
175     public static String getClassName(final String localName) {
176         checkArgument(localName != null, "Name should not be null.");
177         return toFirstUpper(toCamelCase(localName));
178     }
179
180     public static String getClassName(final QName name) {
181         checkArgument(name != null, "Name should not be null.");
182         return toFirstUpper(toCamelCase(name.getLocalName()));
183     }
184
185     public static String getMethodName(final String yangIdentifier) {
186         checkArgument(yangIdentifier != null,"Identifier should not be null");
187         return toFirstLower(toCamelCase(yangIdentifier));
188     }
189
190     public static String getMethodName(final QName name) {
191         checkArgument(name != null, "Name should not be null.");
192         return getMethodName(name.getLocalName());
193     }
194
195     public static String getGetterSuffix(final QName name) {
196         checkArgument(name != null, "Name should not be null.");
197         final String candidate = toFirstUpper(toCamelCase(name.getLocalName()));
198         return "Class".equals(candidate) ? "XmlClass" : candidate;
199     }
200
201     public static String getPropertyName(final String yangIdentifier) {
202         final String potential = toFirstLower(toCamelCase(yangIdentifier));
203         if ("class".equals(potential)) {
204             return "xmlClass";
205         }
206         return potential;
207     }
208
209     private static String toCamelCase(final String rawString) {
210         checkArgument(rawString != null, "String should not be null");
211         Iterable<String> components = CAMEL_SPLITTER.split(rawString);
212         StringBuilder builder = new StringBuilder();
213         for (String comp : components) {
214             builder.append(toFirstUpper(comp));
215         }
216         return checkNumericPrefix(builder.toString());
217     }
218
219     private static String checkNumericPrefix(final String rawString) {
220         if (rawString == null || rawString.isEmpty()) {
221             return rawString;
222         }
223         char firstChar = rawString.charAt(0);
224         if (firstChar >= '0' && firstChar <= '9') {
225             return "_" + rawString;
226         } else {
227             return rawString;
228         }
229     }
230
231     /**
232      * Returns the {@link String} {@code s} with an {@link Character#isUpperCase(char) upper case} first character. This
233      * function is null-safe.
234      *
235      * @param str the string that should get an upper case first character. May be <code>null</code>.
236      * @return the {@link String} {@code str} with an upper case first character or <code>null</code> if the input
237      *         {@link String} {@code str} was <code>null</code>.
238      */
239     public static String toFirstUpper(final String str) {
240         if (str == null || str.length() == 0) {
241             return str;
242         }
243         if (Character.isUpperCase(str.charAt(0))) {
244             return str;
245         }
246         if (str.length() == 1) {
247             return str.toUpperCase();
248         }
249         return str.substring(0, 1).toUpperCase() + str.substring(1);
250     }
251
252     /**
253      * Returns the {@link String} {@code s} with a {@link Character#isLowerCase(char) lower case} first character. This
254      * function is null-safe.
255      *
256      * @param str the string that should get an lower case first character. May be <code>null</code>.
257      * @return the {@link String} {@code str} with an lower case first character or <code>null</code> if the input
258      *         {@link String} {@code str} was <code>null</code>.
259      */
260     private static String toFirstLower(final String str) {
261         if (str == null || str.length() == 0) {
262             return str;
263         }
264         if (Character.isLowerCase(str.charAt(0))) {
265             return str;
266         }
267         if (str.length() == 1) {
268             return str.toLowerCase();
269         }
270         return str.substring(0, 1).toLowerCase() + str.substring(1);
271     }
272
273     /**
274      * Returns Java identifiers, conforming to JLS9 Section 3.8 to use for specified YANG assigned names
275      * (RFC7950 Section 9.6.4). This method considers two distinct encodings: one the pre-Fluorine mapping, which is
276      * okay and convenient for sane strings, and an escaping-based bijective mapping which works for all possible
277      * Unicode strings.
278      *
279      * @param assignedNames Collection of assigned names
280      * @return A BiMap keyed by assigned name, with Java identifiers as values
281      * @throws NullPointerException if assignedNames is null or contains null items
282      * @throws IllegalArgumentException if any of the names is empty
283      */
284     public static BiMap<String, String> mapEnumAssignedNames(final Collection<String> assignedNames) {
285         /*
286          * Original mapping assumed strings encountered are identifiers, hence it used getClassName to map the names
287          * and that function is not an injection -- this is evidenced in MDSAL-208 and results in a failure to compile
288          * generated code. If we encounter such a conflict or if the result is not a valid identifier (like '*'), we
289          * abort and switch the mapping schema to mapEnumAssignedName(), which is a bijection.
290          *
291          * Note that assignedNames can contain duplicates, which must not trigger a duplication fallback.
292          */
293         final BiMap<String, String> javaToYang = HashBiMap.create(assignedNames.size());
294         boolean valid = true;
295         for (String name : assignedNames) {
296             checkArgument(!name.isEmpty());
297             if (!javaToYang.containsValue(name)) {
298                 final String mappedName = getClassName(name);
299                 if (!isValidJavaIdentifier(mappedName) || javaToYang.forcePut(mappedName, name) != null) {
300                     valid = false;
301                     break;
302                 }
303             }
304         }
305
306         if (!valid) {
307             // Fall back to bijective mapping
308             javaToYang.clear();
309             for (String name : assignedNames) {
310                 javaToYang.put(mapEnumAssignedName(name), name);
311             }
312         }
313
314         return javaToYang.inverse();
315     }
316
317     // See https://docs.oracle.com/javase/specs/jls/se9/html/jls-3.html#jls-3.8
318     private static boolean isValidJavaIdentifier(final String str) {
319         return !str.isEmpty() && !JAVA_RESERVED_WORDS.contains(str)
320                 && Character.isJavaIdentifierStart(str.codePointAt(0))
321                 && str.codePoints().skip(1).allMatch(Character::isJavaIdentifierPart);
322     }
323
324     private static String mapEnumAssignedName(final String assignedName) {
325         checkArgument(!assignedName.isEmpty());
326
327         // Mapping rules:
328         // - if the string is a valid java identifier and does not contain '$', use it as-is
329         if (assignedName.indexOf('$') == -1 && isValidJavaIdentifier(assignedName)) {
330             return assignedName;
331         }
332
333         // - otherwise prefix it with '$' and replace any invalid character (including '$') with '$XX$', where XX is
334         //   hex-encoded unicode codepoint (including plane, stripping leading zeroes)
335         final StringBuilder sb = new StringBuilder().append('$');
336         assignedName.codePoints().forEachOrdered(codePoint -> {
337             if (codePoint == '$' || !Character.isJavaIdentifierPart(codePoint)) {
338                 sb.append('$').append(Integer.toHexString(codePoint).toUpperCase(Locale.ROOT)).append('$');
339             } else {
340                 sb.appendCodePoint(codePoint);
341             }
342         });
343         return sb.toString();
344     }
345 }