91b4223b81ea9af1c56a5cc16344351c930aa4f9
[mdsal.git] / binding / mdsal-binding-generator-util / src / main / java / org / opendaylight / mdsal / binding / model / util / BindingGeneratorUtil.java
1 /*
2  * Copyright (c) 2014 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.model.util;
9
10 import com.google.common.base.CharMatcher;
11 import com.google.common.collect.ImmutableList;
12 import com.google.common.collect.ImmutableList.Builder;
13 import com.google.common.collect.Iterables;
14 import java.io.ByteArrayOutputStream;
15 import java.io.DataOutputStream;
16 import java.io.IOException;
17 import java.security.MessageDigest;
18 import java.security.NoSuchAlgorithmException;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Comparator;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Optional;
26 import java.util.regex.Pattern;
27 import org.opendaylight.mdsal.binding.model.api.AccessModifier;
28 import org.opendaylight.mdsal.binding.model.api.Restrictions;
29 import org.opendaylight.mdsal.binding.model.api.Type;
30 import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedPropertyBuilder;
31 import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedTypeBuilderBase;
32 import org.opendaylight.mdsal.binding.model.api.type.builder.MethodSignatureBuilder;
33 import org.opendaylight.mdsal.binding.model.api.type.builder.TypeMemberBuilder;
34 import org.opendaylight.mdsal.binding.spec.naming.BindingMapping;
35 import org.opendaylight.yangtools.yang.common.QName;
36 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
37 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
38 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
39 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
40 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
41 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
42 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
43 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
44 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
45 import org.opendaylight.yangtools.yang.model.util.type.BaseTypes;
46 import org.opendaylight.yangtools.yang.model.util.type.DecimalTypeBuilder;
47
48 /**
49  * Contains the methods for converting strings to valid JAVA language strings
50  * (package names, class names, attribute names) and to valid javadoc comments.
51  */
52 public final class BindingGeneratorUtil {
53
54     /**
55      * Impossible to instantiate this class. All of the methods or attributes are static.
56      */
57     private BindingGeneratorUtil() {
58
59     }
60
61     /**
62      * Pre-compiled replacement pattern.
63      */
64     private static final CharMatcher DOT_MATCHER = CharMatcher.is('.');
65     private static final CharMatcher DASH_COLON_MATCHER = CharMatcher.anyOf("-:");
66     private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
67     private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
68     private static final Pattern UNICODE_CHAR_PATTERN = Pattern.compile("\\\\+u");
69
70     private static final Restrictions EMPTY_RESTRICTIONS = new Restrictions() {
71         @Override
72         public Optional<LengthConstraint> getLengthConstraint() {
73             return Optional.empty();
74         }
75
76         @Override
77         public List<PatternConstraint> getPatternConstraints() {
78             return Collections.emptyList();
79         }
80
81         @Override
82         public Optional<RangeConstraint<?>> getRangeConstraint() {
83             return Optional.empty();
84         }
85
86         @Override
87         public boolean isEmpty() {
88             return true;
89         }
90     };
91
92     private static final Comparator<TypeMemberBuilder<?>> SUID_MEMBER_COMPARATOR =
93         Comparator.comparing(TypeMemberBuilder::getName);
94
95     private static final Comparator<Type> SUID_NAME_COMPARATOR = Comparator.comparing(Type::getFullyQualifiedName);
96
97     /**
98      * Converts <code>parameterName</code> to valid JAVA parameter name. If the <code>parameterName</code> is one
99      * of the JAVA reserved words then it is prefixed with underscore character.
100      *
101      * @param parameterName string with the parameter name
102      * @return string with the admissible parameter name
103      */
104     public static String resolveJavaReservedWordEquivalency(final String parameterName) {
105         if (parameterName != null && BindingMapping.JAVA_RESERVED_WORDS.contains(parameterName)) {
106             return "_" + parameterName;
107         }
108         return parameterName;
109     }
110
111     /**
112      * Creates package name from specified <code>basePackageName</code> (package name for module)
113      * and <code>schemaPath</code>. Resulting package name is concatenation of <code>basePackageName</code>
114      * and all local names of YANG nodes which are parents of some node for which <code>schemaPath</code> is specified.
115      *
116      * @param basePackageName string with package name of the module, MUST be normalized, otherwise this method may
117      *                        return an invalid string.
118      * @param schemaPath list of names of YANG nodes which are parents of some node + name of this node
119      * @return string with valid JAVA package name
120      * @throws NullPointerException if any of the arguments are null
121      */
122     public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
123         final int size = Iterables.size(schemaPath.getPathTowardsRoot()) - 1;
124         if (size <= 0) {
125             return basePackageName;
126         }
127
128         return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
129     }
130
131     /**
132      * Creates package name from specified <code>basePackageName</code> (package name for module)
133      * and <code>schemaPath</code> which crosses an augmentation. Resulting package name is concatenation
134      * of <code>basePackageName</code> and all local names of YANG nodes which are parents of some node for which
135      * <code>schemaPath</code> is specified.
136      *
137      * @param basePackageName string with package name of the module, MUST be normalized, otherwise this method may
138      *                        return an invalid string.
139      * @param schemaPath list of names of YANG nodes which are parents of some node + name of this node
140      * @return string with valid JAVA package name
141      * @throws NullPointerException if any of the arguments are null
142      */
143     public static String packageNameForAugmentedGeneratedType(final String basePackageName,
144             final SchemaPath schemaPath) {
145         final int size = Iterables.size(schemaPath.getPathTowardsRoot());
146         if (size == 0) {
147             return basePackageName;
148         }
149
150         return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
151     }
152
153     private static String generateNormalizedPackageName(final String base, final Iterable<QName> path, final int size) {
154         final StringBuilder builder = new StringBuilder(base);
155         final Iterator<QName> iterator = path.iterator();
156         for (int i = 0; i < size; ++i) {
157             builder.append('.');
158             final String nodeLocalName = iterator.next().getLocalName();
159             // FIXME: Collon ":" is invalid in node local name as per RFC6020, identifier statement.
160             builder.append(DASH_COLON_MATCHER.replaceFrom(nodeLocalName, '.'));
161         }
162         return BindingMapping.normalizePackageName(builder.toString());
163     }
164
165     /**
166      * Converts string <code>token</code> to the cammel case format.
167      *
168      * @param token string which should be converted to the cammel case format
169      * @param uppercase boolean value which says whether the first character of the <code>token</code> should be
170      *                  upper-cased or not
171      * @return string in the camel case format
172      * @throws IllegalArgumentException
173      *             <ul>
174      *             <li>if <code>token</code> without white spaces is empty</li>
175      *             <li>if <code>token</code> equals null</li>
176      *             </ul>
177      */
178     private static String parseToCamelCase(final String token, final boolean uppercase) {
179         if (token == null) {
180             throw new IllegalArgumentException("Name can not be null");
181         }
182
183         String correctStr = DOT_MATCHER.removeFrom(token.trim());
184         if (correctStr.isEmpty()) {
185             throw new IllegalArgumentException("Name can not be empty");
186         }
187
188         correctStr = replaceWithCamelCase(correctStr, ' ');
189         correctStr = replaceWithCamelCase(correctStr, '-');
190         correctStr = replaceWithCamelCase(correctStr, '_');
191
192         char firstChar = correctStr.charAt(0);
193         firstChar = uppercase ? Character.toUpperCase(firstChar) : Character.toLowerCase(firstChar);
194
195         if (firstChar >= '0' && firstChar <= '9') {
196             return '_' + correctStr;
197         } else {
198             return firstChar + correctStr.substring(1);
199         }
200     }
201
202     /**
203      * Replaces all the occurrences of the <code>removalChar</code> in the
204      * <code>text</code> with empty string and converts following character to
205      * upper case.
206      *
207      * @param text
208      *            string with source text which should be converted
209      * @param removalChar
210      *            character which is sought in the <code>text</code>
211      * @return string which doesn't contain <code>removalChar</code> and has
212      *         following characters converted to upper case
213      * @throws IllegalArgumentException
214      *             if the length of the returning string has length 0
215      */
216     private static String replaceWithCamelCase(final String text, final char removalChar) {
217         int toBeRemovedPos = text.indexOf(removalChar);
218         if (toBeRemovedPos == -1) {
219             return text;
220         }
221
222         final StringBuilder sb = new StringBuilder(text);
223         final String toBeRemoved = String.valueOf(removalChar);
224         do {
225             sb.replace(toBeRemovedPos, toBeRemovedPos + 1, "");
226             // check if 'toBeRemoved' character is not the only character in
227             // 'text'
228             if (sb.length() == 0) {
229                 throw new IllegalArgumentException("The resulting string can not be empty");
230             }
231             final char replacement = Character.toUpperCase(sb.charAt(toBeRemovedPos));
232             sb.setCharAt(toBeRemovedPos, replacement);
233             toBeRemovedPos = sb.indexOf(toBeRemoved);
234         } while (toBeRemovedPos != -1);
235
236         return sb.toString();
237     }
238
239     private static <T> Iterable<T> sortedCollection(final Comparator<? super T> comparator, final Collection<T> input) {
240         if (input.size() > 1) {
241             final List<T> ret = new ArrayList<>(input);
242             ret.sort(comparator);
243             return ret;
244         } else {
245             return input;
246         }
247     }
248
249     private static final ThreadLocal<MessageDigest> SHA1_MD = ThreadLocal.withInitial(() -> {
250         try {
251             return MessageDigest.getInstance("SHA");
252         } catch (final NoSuchAlgorithmException e) {
253             throw new IllegalStateException("Failed to get a SHA digest provider", e);
254         }
255     });
256
257     public static long computeDefaultSUID(final GeneratedTypeBuilderBase<?> to) {
258         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
259         try (DataOutputStream dout = new DataOutputStream(bout)) {
260             dout.writeUTF(to.getName());
261             dout.writeInt(to.isAbstract() ? 3 : 7);
262
263             for (final Type ifc : sortedCollection(SUID_NAME_COMPARATOR, to.getImplementsTypes())) {
264                 dout.writeUTF(ifc.getFullyQualifiedName());
265             }
266
267             for (final GeneratedPropertyBuilder gp : sortedCollection(SUID_MEMBER_COMPARATOR, to.getProperties())) {
268                 dout.writeUTF(gp.getName());
269             }
270
271             for (final MethodSignatureBuilder m : sortedCollection(SUID_MEMBER_COMPARATOR, to.getMethodDefinitions())) {
272                 if (!m.getAccessModifier().equals(AccessModifier.PRIVATE)) {
273                     dout.writeUTF(m.getName());
274                     dout.write(m.getAccessModifier().ordinal());
275                 }
276             }
277
278             dout.flush();
279         } catch (final IOException e) {
280             throw new IllegalStateException("Failed to hash object " + to, e);
281         }
282
283         final byte[] hashBytes = SHA1_MD.get().digest(bout.toByteArray());
284         long hash = 0;
285         for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
286             hash = hash << 8 | hashBytes[i] & 0xFF;
287         }
288         return hash;
289     }
290
291     private static <T extends Optional<?>> T currentOrEmpty(final T current, final T base) {
292         return current.equals(base) ? (T)Optional.empty() : current;
293     }
294
295     private static boolean containsConstraint(final StringTypeDefinition type, final PatternConstraint constraint) {
296         for (StringTypeDefinition wlk = type; wlk != null; wlk = wlk.getBaseType()) {
297             if (wlk.getPatternConstraints().contains(constraint)) {
298                 return true;
299             }
300         }
301
302         return false;
303     }
304
305     private static List<PatternConstraint> uniquePatterns(final StringTypeDefinition type) {
306         final List<PatternConstraint> constraints = type.getPatternConstraints();
307         if (constraints.isEmpty()) {
308             return constraints;
309         }
310
311         final Builder<PatternConstraint> builder = ImmutableList.builder();
312         boolean filtered = false;
313         for (final PatternConstraint c : constraints) {
314             if (containsConstraint(type.getBaseType(), c)) {
315                 filtered = true;
316             } else {
317                 builder.add(c);
318             }
319         }
320
321         return filtered ? builder.build() : constraints;
322     }
323
324     public static Restrictions getRestrictions(final TypeDefinition<?> type) {
325         // Old parser generated types which actually contained based restrictions, but our code deals with that when
326         // binding to core Java types. Hence we'll emit empty restrictions for base types.
327         if (type == null || type.getBaseType() == null) {
328             // Handling of decimal64 has changed in the new parser. It contains range restrictions applied to the type
329             // directly, without an extended type. We need to capture such constraints. In order to retain behavior we
330             // need to analyze the new semantics and see if the constraints have been overridden. To do that we
331             // instantiate a temporary unconstrained type and compare them.
332             //
333             // FIXME: looking at the generated code it looks as though we need to pass the restrictions without
334             //        comparison
335             if (type instanceof DecimalTypeDefinition) {
336                 final DecimalTypeDefinition decimal = (DecimalTypeDefinition) type;
337                 final DecimalTypeBuilder tmpBuilder = BaseTypes.decimalTypeBuilder(decimal.getPath());
338                 tmpBuilder.setFractionDigits(decimal.getFractionDigits());
339                 final DecimalTypeDefinition tmp = tmpBuilder.build();
340
341                 if (!tmp.getRangeConstraint().equals(decimal.getRangeConstraint())) {
342                     return new Restrictions() {
343                         @Override
344                         public boolean isEmpty() {
345                             return false;
346                         }
347
348                         @Override
349                         public Optional<? extends RangeConstraint<?>> getRangeConstraint() {
350                             return decimal.getRangeConstraint();
351                         }
352
353                         @Override
354                         public List<PatternConstraint> getPatternConstraints() {
355                             return ImmutableList.of();
356                         }
357
358                         @Override
359                         public Optional<LengthConstraint> getLengthConstraint() {
360                             return Optional.empty();
361                         }
362                     };
363                 }
364             }
365
366             return EMPTY_RESTRICTIONS;
367         }
368
369         final Optional<LengthConstraint> length;
370         final List<PatternConstraint> pattern;
371         final Optional<? extends RangeConstraint<?>> range;
372
373         /*
374          * Take care of extended types.
375          *
376          * Other types which support constraints are check afterwards. There is a slight twist with them, as returned
377          * constraints are the effective view, e.g. they are inherited from base type. Since the constraint is already
378          * enforced by the base type, we want to skip them and not perform duplicate checks.
379          *
380          * We end up emitting ConcreteType instances for YANG base types, which leads to their constraints not being
381          * enforced (most notably decimal64). Therefore we need to make sure we do not strip the next-to-last
382          * restrictions.
383          *
384          * FIXME: this probably not the best solution and needs further analysis.
385          */
386         if (type instanceof BinaryTypeDefinition) {
387             final BinaryTypeDefinition binary = (BinaryTypeDefinition)type;
388             final BinaryTypeDefinition base = binary.getBaseType();
389             if (base != null && base.getBaseType() != null) {
390                 length = currentOrEmpty(binary.getLengthConstraint(), base.getLengthConstraint());
391             } else {
392                 length = binary.getLengthConstraint();
393             }
394
395             pattern = ImmutableList.of();
396             range = Optional.empty();
397         } else if (type instanceof DecimalTypeDefinition) {
398             length = Optional.empty();
399             pattern = ImmutableList.of();
400
401             final DecimalTypeDefinition decimal = (DecimalTypeDefinition)type;
402             final DecimalTypeDefinition base = decimal.getBaseType();
403             if (base != null && base.getBaseType() != null) {
404                 range = currentOrEmpty(decimal.getRangeConstraint(), base.getRangeConstraint());
405             } else {
406                 range = decimal.getRangeConstraint();
407             }
408         } else if (type instanceof RangeRestrictedTypeDefinition) {
409             // Integer-like types
410             length = Optional.empty();
411             pattern = ImmutableList.of();
412             range = extractRangeConstraint((RangeRestrictedTypeDefinition<?, ?>)type);
413         } else if (type instanceof StringTypeDefinition) {
414             final StringTypeDefinition string = (StringTypeDefinition)type;
415             final StringTypeDefinition base = string.getBaseType();
416             if (base != null && base.getBaseType() != null) {
417                 length = currentOrEmpty(string.getLengthConstraint(), base.getLengthConstraint());
418             } else {
419                 length = string.getLengthConstraint();
420             }
421
422             pattern = uniquePatterns(string);
423             range = Optional.empty();
424         } else {
425             length = Optional.empty();
426             pattern = ImmutableList.of();
427             range = Optional.empty();
428         }
429
430         // Now, this may have ended up being empty, too...
431         if (!length.isPresent() && pattern.isEmpty() && !range.isPresent()) {
432             return EMPTY_RESTRICTIONS;
433         }
434
435         // Nope, not empty allocate a holder
436         return new Restrictions() {
437             @Override
438             public Optional<? extends RangeConstraint<?>> getRangeConstraint() {
439                 return range;
440             }
441
442             @Override
443             public List<PatternConstraint> getPatternConstraints() {
444                 return pattern;
445             }
446
447             @Override
448             public Optional<LengthConstraint> getLengthConstraint() {
449                 return length;
450             }
451
452             @Override
453             public boolean isEmpty() {
454                 return false;
455             }
456         };
457     }
458
459     private static <T extends RangeRestrictedTypeDefinition<?, ?>> Optional<? extends RangeConstraint<?>>
460             extractRangeConstraint(final T def) {
461         final T base = (T) def.getBaseType();
462         if (base != null && base.getBaseType() != null) {
463             return currentOrEmpty(def.getRangeConstraint(), base.getRangeConstraint());
464         }
465
466         return def.getRangeConstraint();
467     }
468
469     /**
470      * Encodes angle brackets in yang statement description.
471      *
472      * @param description description of a yang statement which is used to generate javadoc comments
473      * @return string with encoded angle brackets
474      */
475     public static String encodeAngleBrackets(String description) {
476         if (description != null) {
477             description = LT_MATCHER.replaceFrom(description, "&lt;");
478             description = GT_MATCHER.replaceFrom(description, "&gt;");
479         }
480         return description;
481     }
482
483     public static String replaceAllIllegalChars(final CharSequence stringBuilder) {
484         final String ret = UNICODE_CHAR_PATTERN.matcher(stringBuilder).replaceAll("\\\\\\\\u");
485         return ret.isEmpty() ? "" : ret;
486     }
487 }