MDSAL-301: Binding v2 - Rework javadoc generation
[mdsal.git] / binding2 / mdsal-binding2-generator-util / src / main / java / org / opendaylight / mdsal / binding / javav2 / generator / util / BindingGeneratorUtil.java
1 /*
2  * Copyright (c) 2017 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
9 package org.opendaylight.mdsal.binding.javav2.generator.util;
10
11 import com.google.common.annotations.Beta;
12 import com.google.common.base.CharMatcher;
13 import com.google.common.base.Preconditions;
14 import com.google.common.collect.ImmutableList;
15 import com.google.common.collect.ImmutableList.Builder;
16 import com.google.common.collect.Interner;
17 import com.google.common.collect.Interners;
18 import com.google.common.collect.Iterables;
19 import java.io.ByteArrayOutputStream;
20 import java.io.DataOutputStream;
21 import java.io.IOException;
22 import java.math.BigDecimal;
23 import java.security.MessageDigest;
24 import java.security.NoSuchAlgorithmException;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Optional;
32 import java.util.regex.Pattern;
33 import org.opendaylight.mdsal.binding.javav2.model.api.AccessModifier;
34 import org.opendaylight.mdsal.binding.javav2.model.api.Restrictions;
35 import org.opendaylight.mdsal.binding.javav2.model.api.Type;
36 import org.opendaylight.mdsal.binding.javav2.model.api.type.builder.GeneratedPropertyBuilder;
37 import org.opendaylight.mdsal.binding.javav2.model.api.type.builder.GeneratedTypeBuilderBase;
38 import org.opendaylight.mdsal.binding.javav2.model.api.type.builder.MethodSignatureBuilder;
39 import org.opendaylight.mdsal.binding.javav2.model.api.type.builder.TypeMemberBuilder;
40 import org.opendaylight.mdsal.binding.javav2.spec.runtime.BindingNamespaceType;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
45 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
46 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
47 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
49 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
50 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
51 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
52 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
53 import org.opendaylight.yangtools.yang.model.util.type.BaseTypes;
54 import org.opendaylight.yangtools.yang.model.util.type.DecimalTypeBuilder;
55
56 /**
57  * Standard Util class that contains various method for converting
58  * input strings to valid JAVA language strings e.g. package names,
59  * class names, attribute names and/or valid JavaDoc comments.
60  */
61 @Beta
62 public final class BindingGeneratorUtil {
63
64     private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
65     private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
66     private static final Pattern UNICODE_CHAR_PATTERN = Pattern.compile("\\\\+u");
67
68     private static final Interner<String> PACKAGE_INTERNER = Interners.newWeakInterner();
69     private static final Comparator<TypeMemberBuilder<?>> SUID_MEMBER_COMPARATOR =
70             Comparator.comparing(TypeMemberBuilder::getName);
71
72     private static final Comparator<Type> SUID_NAME_COMPARATOR =
73             Comparator.comparing(Type::getFullyQualifiedName);
74
75     private static final Restrictions EMPTY_RESTRICTIONS = new Restrictions() {
76         @Override
77         public Optional<LengthConstraint> getLengthConstraint() {
78             return Optional.empty();
79         }
80
81         @Override
82         public List<PatternConstraint> getPatternConstraints() {
83             return Collections.emptyList();
84         }
85
86         @Override
87         public Optional<RangeConstraint<?>> getRangeConstraint() {
88             return Optional.empty();
89         }
90
91         @Override
92         public boolean isEmpty() {
93             return true;
94         }
95     };
96
97     private BindingGeneratorUtil() {
98         throw new UnsupportedOperationException("Utility class");
99     }
100
101     /**
102      * Encodes angle brackets in yang statement description
103      * @param description description of a yang statement which is used to generate javadoc comments
104      * @return string with encoded angle brackets
105      */
106     public static String encodeAngleBrackets(final String description) {
107         String newDesc = description;
108         if (newDesc != null) {
109             newDesc = LT_MATCHER.replaceFrom(newDesc, "&lt;");
110             newDesc = GT_MATCHER.replaceFrom(newDesc, "&gt;");
111         }
112         return newDesc;
113     }
114
115     public static long computeDefaultSUID(final GeneratedTypeBuilderBase<?> to) {
116         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
117         try (final DataOutputStream dout = new DataOutputStream(bout)) {
118             dout.writeUTF(to.getName());
119             dout.writeInt(to.isAbstract() ? 3 : 7);
120
121             for (final Type ifc : sortedCollection(SUID_NAME_COMPARATOR, to.getImplementsTypes())) {
122                 dout.writeUTF(ifc.getFullyQualifiedName());
123             }
124
125             for (final GeneratedPropertyBuilder gp : sortedCollection(SUID_MEMBER_COMPARATOR, to.getProperties())) {
126                 dout.writeUTF(gp.getName());
127             }
128
129             for (final MethodSignatureBuilder m : sortedCollection(SUID_MEMBER_COMPARATOR, to.getMethodDefinitions())) {
130                 if (!m.getAccessModifier().equals(AccessModifier.PRIVATE)) {
131                     dout.writeUTF(m.getName());
132                     dout.write(m.getAccessModifier().ordinal());
133                 }
134             }
135
136             dout.flush();
137         } catch (final IOException e) {
138             throw new IllegalStateException("Failed to hash object " + to, e);
139         }
140
141         final byte[] hashBytes = SHA1_MD.get().digest(bout.toByteArray());
142         long hash = 0;
143         for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
144             hash = hash << 8 | hashBytes[i] & 0xFF;
145         }
146         return hash;
147     }
148
149     /**
150      * Creates package name from specified <code>basePackageName</code> (package
151      * name for module) and <code>schemaPath</code>.
152      *
153      * Resulting package name is concatenation of <code>basePackageName</code>
154      * and all local names of YANG nodes which are parents of some node for
155      * which <code>schemaPath</code> is specified.
156      *
157      * Based on type of node, there is also possible suffix added in order
158      * to prevent package name conflicts.
159      *
160      * @param basePackageName
161      *            string with package name of the module, MUST be normalized,
162      *            otherwise this method may return an invalid string.
163      * @param schemaPath
164      *            list of names of YANG nodes which are parents of some node +
165      *            name of this node
166      * @return string with valid JAVA package name
167      * @throws NullPointerException if any of the arguments are null
168      */
169     public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath, final
170         BindingNamespaceType namespaceType) {
171
172         final Iterable<QName> pathTowardsRoot = schemaPath.getPathTowardsRoot();
173         final Iterable<QName> pathFromRoot = schemaPath.getPathFromRoot();
174         final int size = Iterables.size(pathTowardsRoot) - 1;
175         if (size <= 0) {
176             if (namespaceType != null) {
177                 final StringBuilder sb = new StringBuilder();
178                 sb.append(basePackageName)
179                   .append('.')
180                   .append(namespaceType.getPackagePrefix());
181                 return JavaIdentifierNormalizer.normalizeFullPackageName(sb.toString());
182             }
183             return JavaIdentifierNormalizer.normalizeFullPackageName(basePackageName);
184         }
185
186         return generateNormalizedPackageName(basePackageName, pathFromRoot, size, namespaceType);
187     }
188
189     /**
190      * Creates package name from specified <code>basePackageName</code> (package
191      * name for module) and <code>namespaceType</code>.
192      *
193      * Resulting package name is concatenation of <code>basePackageName</code>
194      * and prefix of <code>namespaceType</code>.
195      *
196      * @param basePackageName
197      *            string with package name of the module, MUST be normalized,
198      *            otherwise this method may return an invalid string.
199      * @param namespaceType
200      *            the namespace to which the module belongs
201      * @return string with valid JAVA package name
202      * @throws NullPointerException if any of the arguments are null
203      */
204     public static String packageNameWithNamespacePrefix(final String basePackageName,
205             final BindingNamespaceType namespaceType) {
206         final StringBuilder sb = new StringBuilder();
207         sb.append(basePackageName)
208                 .append('.')
209                 .append(namespaceType.getPackagePrefix());
210         return JavaIdentifierNormalizer.normalizeFullPackageName(sb.toString());
211     }
212
213     public static Restrictions getRestrictions(final TypeDefinition<?> type) {
214         if (type == null || type.getBaseType() == null) {
215             if (type instanceof DecimalTypeDefinition) {
216                 final DecimalTypeDefinition decimal = (DecimalTypeDefinition) type;
217                 final DecimalTypeBuilder tmpBuilder = BaseTypes.decimalTypeBuilder(decimal.getPath());
218                 tmpBuilder.setFractionDigits(decimal.getFractionDigits());
219                 final DecimalTypeDefinition tmp = tmpBuilder.build();
220
221                 if (!tmp.getRangeConstraint().equals(decimal.getRangeConstraint())) {
222                     return new Restrictions() {
223                         @Override
224                         public boolean isEmpty() {
225                             return false;
226                         }
227
228                         @Override
229                         public Optional<RangeConstraint<BigDecimal>> getRangeConstraint() {
230                             return decimal.getRangeConstraint();
231                         }
232
233                         @Override
234                         public List<PatternConstraint> getPatternConstraints() {
235                             return ImmutableList.of();
236                         }
237
238                         @Override
239                         public Optional<LengthConstraint> getLengthConstraint() {
240                             return Optional.empty();
241                         }
242                     };
243                 }
244             }
245
246             return EMPTY_RESTRICTIONS;
247         }
248
249         final Optional<LengthConstraint> length;
250         final List<PatternConstraint> pattern;
251         final Optional<? extends RangeConstraint<?>> range;
252
253         /*
254          * Take care of extended types.
255          *
256          * Other types which support constraints are check afterwards. There is a slight twist with them, as returned
257          * constraints are the effective view, e.g. they are inherited from base type. Since the constraint is already
258          * enforced by the base type, we want to skip them and not perform duplicate checks.
259          *
260          * We end up emitting ConcreteType instances for YANG base types, which leads to their constraints not being
261          * enforced (most notably decimal64). Therefore we need to make sure we do not strip the next-to-last
262          * restrictions.
263          */
264         if (type instanceof BinaryTypeDefinition) {
265             final BinaryTypeDefinition binary = (BinaryTypeDefinition)type;
266             final BinaryTypeDefinition base = binary.getBaseType();
267             if (base != null && base.getBaseType() != null) {
268                 length = currentOrEmpty(binary.getLengthConstraint(), base.getLengthConstraint());
269             } else {
270                 length = binary.getLengthConstraint();
271             }
272
273             pattern = ImmutableList.of();
274             range = Optional.empty();
275         } else if (type instanceof DecimalTypeDefinition) {
276             length = Optional.empty();
277             pattern = ImmutableList.of();
278
279             final DecimalTypeDefinition decimal = (DecimalTypeDefinition)type;
280             final DecimalTypeDefinition base = decimal.getBaseType();
281             if (base != null && base.getBaseType() != null) {
282                 range = currentOrEmpty(decimal.getRangeConstraint(), base.getRangeConstraint());
283             } else {
284                 range = decimal.getRangeConstraint();
285             }
286         } else if (type instanceof RangeRestrictedTypeDefinition) {
287             // Integer-like types
288             length = Optional.empty();
289             pattern = ImmutableList.of();
290             range = extractRangeConstraint((RangeRestrictedTypeDefinition<?, ?>)type);
291         } else if (type instanceof StringTypeDefinition) {
292             final StringTypeDefinition string = (StringTypeDefinition)type;
293             final StringTypeDefinition base = string.getBaseType();
294             if (base != null && base.getBaseType() != null) {
295                 length = currentOrEmpty(string.getLengthConstraint(), base.getLengthConstraint());
296             } else {
297                 length = string.getLengthConstraint();
298             }
299
300             pattern = uniquePatterns(string);
301             range = Optional.empty();
302         } else {
303             length = Optional.empty();
304             pattern = ImmutableList.of();
305             range = Optional.empty();
306         }
307
308         // Now, this may have ended up being empty, too...
309         if (!length.isPresent() && pattern.isEmpty() && !range.isPresent()) {
310             return EMPTY_RESTRICTIONS;
311         }
312
313         // Nope, not empty allocate a holder
314         return new Restrictions() {
315             @Override
316             public Optional<? extends RangeConstraint<?>> getRangeConstraint() {
317                 return range;
318             }
319             @Override
320             public List<PatternConstraint> getPatternConstraints() {
321                 return pattern;
322             }
323             @Override
324             public Optional<LengthConstraint> getLengthConstraint() {
325                 return length;
326             }
327             @Override
328             public boolean isEmpty() {
329                 return false;
330             }
331         };
332     }
333
334     /**
335      * Creates package name from specified <code>basePackageName</code> (package
336      * name for module) and <code>schemaPath</code> which crosses an augmentation.
337      *
338      * Resulting package name is concatenation of <code>basePackageName</code>
339      * and all local names of YANG nodes which are parents of some node for
340      * which <code>schemaPath</code> is specified.
341      *
342      * Based on type of node, there is also possible suffix added in order
343      * to prevent package name conflicts.
344      *
345      * @param basePackageName
346      *            string with package name of the module, MUST be normalized,
347      *            otherwise this method may return an invalid string.
348      * @param schemaPath
349      *            list of names of YANG nodes which are parents of some node +
350      *            name of this node
351      * @return string with valid JAVA package name
352      * @throws NullPointerException if any of the arguments are null
353      */
354     public static String packageNameForAugmentedGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
355         final Iterable<QName> pathTowardsRoot = schemaPath.getPathTowardsRoot();
356         final Iterable<QName> pathFromRoot = schemaPath.getPathFromRoot();
357         final int size = Iterables.size(pathTowardsRoot);
358         if (size == 0) {
359             return basePackageName;
360         }
361
362         return generateNormalizedPackageName(basePackageName, pathFromRoot, size, BindingNamespaceType.Data);
363     }
364
365     /**
366      * Creates package name from <code>parentAugmentPackageName</code> (package
367      * name for direct parent augmentation) and <code>augmentationSchema</code> .
368      *
369      * Resulting package name is concatenation of <code>parentAugmentPackageName</code>
370      * and the local name of <code>schemaPath</code>.
371      *
372      * Based on type of node, there is also possible suffix added in order
373      * to prevent package name conflicts.
374      *
375      * @param parentAugmentPackageName
376      *            string with package name of direct parent augmentation, MUST be normalized,
377      *            otherwise this method may return an invalid string.
378      * @param augmentationSchema
379      *            augmentation schema which is direct son of parent augmentation.
380      * @return string with valid JAVA package name
381      * @throws NullPointerException if any of the arguments are null
382      */
383     public static String packageNameForAugmentedGeneratedType(final String parentAugmentPackageName,
384                                                               final AugmentationSchemaNode augmentationSchema) {
385         final QName last = augmentationSchema.getTargetPath().getLastComponent();
386
387         return generateNormalizedPackageName(parentAugmentPackageName, last);
388     }
389
390     public static String packageNameForSubGeneratedType(final String basePackageName, final SchemaNode node,
391                                                         final BindingNamespaceType namespaceType) {
392         final String parent = packageNameForGeneratedType(basePackageName, node.getPath(), namespaceType);
393         final QName last = node.getPath().getLastComponent();
394
395         return generateNormalizedPackageName(parent, last);
396     }
397
398     public static String replacePackageTopNamespace(final String basePackageName,
399             final String toReplacePackageName,
400             final BindingNamespaceType toReplaceNameSpace,
401             final BindingNamespaceType replacedNameSpace) {
402         Preconditions.checkArgument(basePackageName != null);
403         String normalizeBasePackageName = JavaIdentifierNormalizer.normalizeFullPackageName(basePackageName);
404
405         if (!normalizeBasePackageName.equals(toReplacePackageName)) {
406             final String topPackageName = new StringBuilder(normalizeBasePackageName)
407                     .append('.').append(toReplaceNameSpace.getPackagePrefix()).toString();
408
409             Preconditions.checkState(toReplacePackageName.equals(topPackageName)
410                             || toReplacePackageName.contains(topPackageName),
411                     "Package name to replace does not belong to the given namespace to replace!");
412
413             return new StringBuilder(normalizeBasePackageName)
414                     .append('.')
415                     .append(replacedNameSpace.getPackagePrefix())
416                     .append(toReplacePackageName.substring(topPackageName.length()))
417                     .toString();
418         } else {
419             return new StringBuilder(normalizeBasePackageName)
420                     .append('.').append(replacedNameSpace.getPackagePrefix()).toString();
421         }
422     }
423
424     private static final ThreadLocal<MessageDigest> SHA1_MD = new ThreadLocal<MessageDigest>() {
425         @Override
426         protected MessageDigest initialValue() {
427             try {
428                 return MessageDigest.getInstance("SHA");
429             } catch (final NoSuchAlgorithmException e) {
430                 throw new IllegalStateException("Failed to get a SHA digest provider", e);
431             }
432         }
433     };
434
435     private static String generateNormalizedPackageName(final String base, final Iterable<QName> path, final int
436             size, final BindingNamespaceType namespaceType) {
437         final StringBuilder builder = new StringBuilder(base);
438         if (namespaceType != null) {
439             builder.append('.').append(namespaceType.getPackagePrefix());
440         }
441         final Iterator<QName> iterator = path.iterator();
442         for (int i = 0; i < size; ++i) {
443             builder.append('.');
444             final String nodeLocalName = iterator.next().getLocalName();
445             builder.append(nodeLocalName);
446         }
447         final String normalizedPackageName = JavaIdentifierNormalizer.normalizeFullPackageName(builder.toString());
448         // Prevent duplication of input
449         PACKAGE_INTERNER.intern(normalizedPackageName);
450         return normalizedPackageName;
451     }
452
453     private static String generateNormalizedPackageName(final String parent, final QName path) {
454         final StringBuilder sb = new StringBuilder(parent)
455                 .append('.')
456                 .append(path.getLocalName());
457
458         final String normalizedPackageName = JavaIdentifierNormalizer.normalizeFullPackageName(sb.toString());
459         // Prevent duplication of input
460         PACKAGE_INTERNER.intern(normalizedPackageName);
461         return normalizedPackageName;
462     }
463
464     private static <T> Iterable<T> sortedCollection(final Comparator<? super T> comparator, final Collection<T> input) {
465         if (input.size() > 1) {
466             final List<T> ret = new ArrayList<>(input);
467             ret.sort(comparator);
468             return ret;
469         } else {
470             return input;
471         }
472     }
473
474     private static <T extends Optional<?>> T currentOrEmpty(final T current, final T base) {
475         return current.equals(base) ? (T)Optional.empty() : current;
476     }
477
478     private static List<PatternConstraint> uniquePatterns(final StringTypeDefinition type) {
479         final List<PatternConstraint> constraints = type.getPatternConstraints();
480         if (constraints.isEmpty()) {
481             return constraints;
482         }
483
484         final Builder<PatternConstraint> builder = ImmutableList.builder();
485         boolean filtered = false;
486         for (final PatternConstraint c : constraints) {
487             if (containsConstraint(type.getBaseType(), c)) {
488                 filtered = true;
489             } else {
490                 builder.add(c);
491             }
492         }
493
494         return filtered ? builder.build() : constraints;
495     }
496
497     private static boolean containsConstraint(final StringTypeDefinition type, final PatternConstraint constraint) {
498         for (StringTypeDefinition wlk = type; wlk != null; wlk = wlk.getBaseType()) {
499             if (wlk.getPatternConstraints().contains(constraint)) {
500                 return true;
501             }
502         }
503
504         return false;
505     }
506
507     private static <T extends RangeRestrictedTypeDefinition<?, ?>> Optional<? extends RangeConstraint<?>>
508             extractRangeConstraint(final T def) {
509         final T base = (T) def.getBaseType();
510         if (base != null && base.getBaseType() != null) {
511             return currentOrEmpty(def.getRangeConstraint(), base.getRangeConstraint());
512         }
513
514         return def.getRangeConstraint();
515     }
516
517     public static String replaceAllIllegalChars(final CharSequence stringBuilder){
518         final String ret = UNICODE_CHAR_PATTERN.matcher(stringBuilder).replaceAll("\\\\\\\\u");
519         return ret.isEmpty() ? "" : ret;
520     }
521
522 }