Bug 6859: Cleanup package names for mdsal-binding-generator-util module
[mdsal.git] / binding / mdsal-binding-generator-util / src / main / java / org / opendaylight / mdsal / binding / generator / 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.generator.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 org.opendaylight.mdsal.binding.model.api.AccessModifier;
26 import org.opendaylight.mdsal.binding.model.api.Restrictions;
27 import org.opendaylight.mdsal.binding.model.api.Type;
28 import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedPropertyBuilder;
29 import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedTypeBuilderBase;
30 import org.opendaylight.mdsal.binding.model.api.type.builder.MethodSignatureBuilder;
31 import org.opendaylight.mdsal.binding.model.api.type.builder.TypeMemberBuilder;
32 import org.opendaylight.yangtools.yang.binding.BindingMapping;
33 import org.opendaylight.yangtools.yang.common.QName;
34 import org.opendaylight.yangtools.yang.common.QNameModule;
35 import org.opendaylight.yangtools.yang.model.api.Module;
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.IntegerTypeDefinition;
41 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
42 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
43 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
44 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
45 import org.opendaylight.yangtools.yang.model.api.type.UnsignedIntegerTypeDefinition;
46 import org.opendaylight.yangtools.yang.model.util.type.BaseTypes;
47 import org.opendaylight.yangtools.yang.model.util.type.DecimalTypeBuilder;
48
49 /**
50  * Contains the methods for converting strings to valid JAVA language strings
51  * (package names, class names, attribute names) and to valid javadoc comments.
52  *
53  *
54  */
55 public final class BindingGeneratorUtil {
56
57     /**
58      * Impossible to instantiate this class. All of the methods or attributes
59      * are static.
60      */
61     private BindingGeneratorUtil() {
62     }
63
64     /**
65      * Pre-compiled replacement pattern.
66      */
67     private static final CharMatcher DOT_MATCHER = CharMatcher.is('.');
68     private static final CharMatcher DASH_COLON_MATCHER = CharMatcher.anyOf("-:");
69     private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
70     private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
71
72     private static final Restrictions EMPTY_RESTRICTIONS = new Restrictions() {
73         @Override
74         public List<LengthConstraint> getLengthConstraints() {
75             return Collections.emptyList();
76         }
77
78         @Override
79         public List<PatternConstraint> getPatternConstraints() {
80             return Collections.emptyList();
81         }
82
83         @Override
84         public List<RangeConstraint> getRangeConstraints() {
85             return Collections.emptyList();
86         }
87
88         @Override
89         public boolean isEmpty() {
90             return true;
91         }
92     };
93
94     private static final Comparator<TypeMemberBuilder<?>> SUID_MEMBER_COMPARATOR =
95         (o1, o2) -> o1.getName().compareTo(o2.getName());
96
97     private static final Comparator<Type> SUID_NAME_COMPARATOR =
98         (o1, o2) -> o1.getFullyQualifiedName().compareTo(o2.getFullyQualifiedName());
99
100     /**
101      * Converts <code>parameterName</code> to valid JAVA parameter name.
102      *
103      * If the <code>parameterName</code> is one of the JAVA reserved words then
104      * it is prefixed with underscore character.
105      *
106      * @param parameterName
107      *            string with the parameter name
108      * @return string with the admissible parameter name
109      */
110     public static String resolveJavaReservedWordEquivalency(final String parameterName) {
111         if (parameterName != null && BindingMapping.JAVA_RESERVED_WORDS.contains(parameterName)) {
112             return "_" + parameterName;
113         }
114         return parameterName;
115     }
116
117     /**
118      * Converts module name to valid JAVA package name.
119      *
120      * The package name consists of:
121      * <ul>
122      * <li>prefix - <i>org.opendaylight.yang.gen.v</i></li>
123      * <li>module YANG version - <i>org.opendaylight.yang.gen.v</i></li>
124      * <li>module namespace - invalid characters are replaced with dots</li>
125      * <li>revision prefix - <i>.rev</i></li>
126      * <li>revision - YYYYMMDD (MM and DD aren't spread to the whole length)</li>
127      * </ul>
128      *
129      * @param module
130      *            module which contains data about namespace and revision date
131      * @return string with the valid JAVA package name
132      * @throws IllegalArgumentException
133      *             if the revision date of the <code>module</code> equals
134      *             <code>null</code>
135      * @deprecated USe {@link BindingMapping#getRootPackageName(QNameModule)} with {@link Module#getQNameModule()}.
136      */
137     @Deprecated
138     public static String moduleNamespaceToPackageName(final Module module) {
139         return BindingMapping.getRootPackageName(module.getQNameModule());
140     }
141
142     /**
143      * Creates package name from specified <code>basePackageName</code> (package
144      * name for module) and <code>schemaPath</code>.
145      *
146      * Resulting package name is concatenation of <code>basePackageName</code>
147      * and all local names of YANG nodes which are parents of some node for
148      * which <code>schemaPath</code> is specified.
149      *
150      * @param basePackageName
151      *            string with package name of the module, MUST be normalized,
152      *            otherwise this method may return an invalid string.
153      * @param schemaPath
154      *            list of names of YANG nodes which are parents of some node +
155      *            name of this node
156      * @return string with valid JAVA package name
157      * @throws NullPointerException if any of the arguments are null
158      */
159     public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
160         final int size = Iterables.size(schemaPath.getPathTowardsRoot()) - 1;
161         if (size <= 0) {
162             return basePackageName;
163         }
164
165         return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
166     }
167
168     /**
169      * Creates package name from specified <code>basePackageName</code> (package
170      * name for module) and <code>schemaPath</code> which crosses an augmentation.
171      *
172      * Resulting package name is concatenation of <code>basePackageName</code>
173      * and all local names of YANG nodes which are parents of some node for
174      * which <code>schemaPath</code> is specified.
175      *
176      * @param basePackageName
177      *            string with package name of the module, MUST be normalized,
178      *            otherwise this method may return an invalid string.
179      * @param schemaPath
180      *            list of names of YANG nodes which are parents of some node +
181      *            name of this node
182      * @return string with valid JAVA package name
183      * @throws NullPointerException if any of the arguments are null
184      */
185     public static String packageNameForAugmentedGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
186         final int size = Iterables.size(schemaPath.getPathTowardsRoot());
187         if (size == 0) {
188             return basePackageName;
189         }
190
191         return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
192     }
193
194     private static String generateNormalizedPackageName(final String base, final Iterable<QName> path, final int size) {
195         final StringBuilder builder = new StringBuilder(base);
196         final Iterator<QName> iterator = path.iterator();
197         for (int i = 0; i < size; ++i) {
198             builder.append('.');
199             String nodeLocalName = iterator.next().getLocalName();
200             // FIXME: Collon ":" is invalid in node local name as per RFC6020, identifier statement.
201             builder.append(DASH_COLON_MATCHER.replaceFrom(nodeLocalName, '.'));
202         }
203         return BindingMapping.normalizePackageName(builder.toString());
204     }
205
206     /**
207      * Creates package name from specified <code>basePackageName</code> (package
208      * name for module) and <code>schemaPath</code>.
209      *
210      * Resulting package name is concatenation of <code>basePackageName</code>
211      * and all local names of YANG nodes which are parents of some node for
212      * which <code>schemaPath</code> is specified.
213      *
214      * @param basePackageName
215      *            string with package name of the module
216      * @param schemaPath
217      *            list of names of YANG nodes which are parents of some node +
218      *            name of this node
219      * @param isUsesAugment
220      *            boolean true if using augment
221      * @return string with valid JAVA package name
222      *
223      * @deprecated Use {@link #packageNameForGeneratedType(String, SchemaPath)} or
224      *             {@link #packageNameForAugmentedGeneratedType(String, SchemaPath)} instead.
225      */
226     @Deprecated
227     public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath,
228             final boolean isUsesAugment) {
229         if (basePackageName == null) {
230             throw new IllegalArgumentException("Base Package Name cannot be NULL!");
231         }
232         if (schemaPath == null) {
233             throw new IllegalArgumentException("Schema Path cannot be NULL!");
234         }
235
236         final Iterable<QName> iterable = schemaPath.getPathFromRoot();
237         final int size = Iterables.size(iterable);
238         final int traversalSteps;
239         if (isUsesAugment) {
240             traversalSteps = size;
241         } else {
242             traversalSteps = size - 1;
243         }
244
245         if (traversalSteps == 0) {
246             return BindingMapping.normalizePackageName(basePackageName);
247         }
248
249         return generateNormalizedPackageName(basePackageName, iterable, traversalSteps);
250     }
251
252     /**
253      * Generates the package name for type definition from
254      * <code>typeDefinition</code> and <code>basePackageName</code>.
255      *
256      * @param basePackageName
257      *            string with the package name of the module
258      * @param typeDefinition
259      *            type definition for which the package name will be generated *
260      * @return string with valid JAVA package name
261      * @throws IllegalArgumentException
262      *             <ul>
263      *             <li>if <code>basePackageName</code> equals <code>null</code></li>
264      *             <li>if <code>typeDefinition</code> equals <code>null</code></li>
265      *             </ul>
266      * @deprecated This method ignores typeDefinition argument and its result is only
267      *             <code>BindingMapping.normalizePackageName(basePackageName)</code>.
268      *             Aside from tests, there is not a single user in OpenDaylight codebase,
269      *             hence it can be considered buggy and defunct. It is scheduled for removal
270      *             in Boron release.
271      */
272     @Deprecated
273     public static String packageNameForTypeDefinition(final String basePackageName,
274             final TypeDefinition<?> typeDefinition) {
275         if (basePackageName == null) {
276             throw new IllegalArgumentException("Base Package Name cannot be NULL!");
277         }
278         if (typeDefinition == null) {
279             throw new IllegalArgumentException("Type Definition reference cannot be NULL!");
280         }
281
282         return BindingMapping.normalizePackageName(basePackageName);
283     }
284
285     /**
286      * Converts <code>token</code> to string which is in accordance with best
287      * practices for JAVA class names.
288      *
289      * @param token
290      *            string which contains characters which should be converted to
291      *            JAVA class name
292      * @return string which is in accordance with best practices for JAVA class
293      *         name.
294      *
295      * @deprecated Use {@link BindingMapping#getClassName(QName)} instead.
296      */
297     @Deprecated
298     public static String parseToClassName(final String token) {
299         return parseToCamelCase(token, true);
300     }
301
302     /**
303      * Converts <code>token</code> to string which is in accordance with best
304      * practices for JAVA parameter names.
305      *
306      * @param token
307      *            string which contains characters which should be converted to
308      *            JAVA parameter name
309      * @return string which is in accordance with best practices for JAVA
310      *         parameter name.
311      *
312      * @deprecated Use {@link BindingMapping#getPropertyName(String)} instead.
313      */
314     @Deprecated public static String parseToValidParamName(final String token) {
315         return resolveJavaReservedWordEquivalency(parseToCamelCase(token, false));
316     }
317
318     /**
319      *
320      * Converts string <code>token</code> to the cammel case format.
321      *
322      * @param token
323      *            string which should be converted to the cammel case format
324      * @param uppercase
325      *            boolean value which says whether the first character of the
326      *            <code>token</code> should|shuldn't be uppercased
327      * @return string in the cammel case format
328      * @throws IllegalArgumentException
329      *             <ul>
330      *             <li>if <code>token</code> without white spaces is empty</li>
331      *             <li>if <code>token</code> equals null</li>
332      *             </ul>
333      */
334     private static String parseToCamelCase(final String token, final boolean uppercase) {
335         if (token == null) {
336             throw new IllegalArgumentException("Name can not be null");
337         }
338
339         String correctStr = DOT_MATCHER.removeFrom(token.trim());
340         if (correctStr.isEmpty()) {
341             throw new IllegalArgumentException("Name can not be empty");
342         }
343
344         correctStr = replaceWithCamelCase(correctStr, ' ');
345         correctStr = replaceWithCamelCase(correctStr, '-');
346         correctStr = replaceWithCamelCase(correctStr, '_');
347
348         char firstChar = correctStr.charAt(0);
349         firstChar = uppercase ? Character.toUpperCase(firstChar) : Character.toLowerCase(firstChar);
350
351         if (firstChar >= '0' && firstChar <= '9') {
352             return '_' + correctStr;
353         } else {
354             return firstChar + correctStr.substring(1);
355         }
356     }
357
358     /**
359      * Replaces all the occurrences of the <code>removalChar</code> in the
360      * <code>text</code> with empty string and converts following character to
361      * upper case.
362      *
363      * @param text
364      *            string with source text which should be converted
365      * @param removalChar
366      *            character which is sought in the <code>text</code>
367      * @return string which doesn't contain <code>removalChar</code> and has
368      *         following characters converted to upper case
369      * @throws IllegalArgumentException
370      *             if the length of the returning string has length 0
371      */
372     private static String replaceWithCamelCase(final String text, final char removalChar) {
373         int toBeRemovedPos = text.indexOf(removalChar);
374         if (toBeRemovedPos == -1) {
375             return text;
376         }
377
378         StringBuilder sb = new StringBuilder(text);
379         String toBeRemoved = String.valueOf(removalChar);
380         do {
381             sb.replace(toBeRemovedPos, toBeRemovedPos + 1, "");
382             // check if 'toBeRemoved' character is not the only character in
383             // 'text'
384             if (sb.length() == 0) {
385                 throw new IllegalArgumentException("The resulting string can not be empty");
386             }
387             char replacement = Character.toUpperCase(sb.charAt(toBeRemovedPos));
388             sb.setCharAt(toBeRemovedPos, replacement);
389             toBeRemovedPos = sb.indexOf(toBeRemoved);
390         } while (toBeRemovedPos != -1);
391
392         return sb.toString();
393     }
394
395     private static <T> Iterable<T> sortedCollection(final Comparator<? super T> comparator, final Collection<T> input) {
396         if (input.size() > 1) {
397             final List<T> ret = new ArrayList<>(input);
398             Collections.sort(ret, comparator);
399             return ret;
400         } else {
401             return input;
402         }
403     }
404
405     private static final ThreadLocal<MessageDigest> SHA1_MD = new ThreadLocal<MessageDigest>() {
406         @Override
407         protected MessageDigest initialValue() {
408             try {
409                 return MessageDigest.getInstance("SHA");
410             } catch (NoSuchAlgorithmException e) {
411                 throw new IllegalStateException("Failed to get a SHA digest provider", e);
412             }
413         }
414     };
415
416     public static long computeDefaultSUID(final GeneratedTypeBuilderBase<?> to) {
417         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
418         try (final DataOutputStream dout = new DataOutputStream(bout)) {
419             dout.writeUTF(to.getName());
420             dout.writeInt(to.isAbstract() ? 3 : 7);
421
422             for (Type ifc : sortedCollection(SUID_NAME_COMPARATOR, to.getImplementsTypes())) {
423                 dout.writeUTF(ifc.getFullyQualifiedName());
424             }
425
426             for (GeneratedPropertyBuilder gp : sortedCollection(SUID_MEMBER_COMPARATOR, to.getProperties())) {
427                 dout.writeUTF(gp.getName());
428             }
429
430             for (MethodSignatureBuilder m : sortedCollection(SUID_MEMBER_COMPARATOR, to.getMethodDefinitions())) {
431                 if (!(m.getAccessModifier().equals(AccessModifier.PRIVATE))) {
432                     dout.writeUTF(m.getName());
433                     dout.write(m.getAccessModifier().ordinal());
434                 }
435             }
436
437             dout.flush();
438         } catch (IOException e) {
439             throw new IllegalStateException("Failed to hash object " + to, e);
440         }
441
442         final byte[] hashBytes = SHA1_MD.get().digest(bout.toByteArray());
443         long hash = 0;
444         for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
445             hash = (hash << 8) | (hashBytes[i] & 0xFF);
446         }
447         return hash;
448     }
449
450     private static <T> List<T> currentOrEmpty(final List<T> current, final List<T> base) {
451         return current.equals(base) ? ImmutableList.<T>of() : current;
452     }
453
454     private static boolean containsConstraint(final StringTypeDefinition type, final PatternConstraint constraint) {
455         for (StringTypeDefinition wlk = type; wlk != null; wlk = wlk.getBaseType()) {
456             if (wlk.getPatternConstraints().contains(constraint)) {
457                 return true;
458             }
459         }
460
461         return false;
462     }
463
464     private static List<PatternConstraint> uniquePatterns(final StringTypeDefinition type) {
465         final List<PatternConstraint> constraints = type.getPatternConstraints();
466         if (constraints.isEmpty()) {
467             return constraints;
468         }
469
470         final Builder<PatternConstraint> builder = ImmutableList.builder();
471         boolean filtered = false;
472         for (PatternConstraint c : constraints) {
473             if (containsConstraint(type.getBaseType(), c)) {
474                 filtered = true;
475             } else {
476                 builder.add(c);
477             }
478         }
479
480         return filtered ? builder.build() : constraints;
481     }
482
483     public static Restrictions getRestrictions(final TypeDefinition<?> type) {
484         // Old parser generated types which actually contained based restrictions, but our code deals with that when
485         // binding to core Java types. Hence we'll emit empty restrictions for base types.
486         if (type == null || type.getBaseType() == null) {
487             // Handling of decimal64 has changed in the new parser. It contains range restrictions applied to the type
488             // directly, without an extended type. We need to capture such constraints. In order to retain behavior we
489             // need to analyze the new semantics and see if the constraints have been overridden. To do that we
490             // instantiate a temporary unconstrained type and compare them.
491             //
492             // FIXME: looking at the generated code it looks as though we need to pass the restrictions without
493             //        comparison
494             if (type instanceof DecimalTypeDefinition) {
495                 final DecimalTypeDefinition decimal = (DecimalTypeDefinition) type;
496                 final DecimalTypeBuilder tmpBuilder = BaseTypes.decimalTypeBuilder(decimal.getPath());
497                 tmpBuilder.setFractionDigits(decimal.getFractionDigits());
498                 final DecimalTypeDefinition tmp = tmpBuilder.build();
499
500                 if (!tmp.getRangeConstraints().equals(decimal.getRangeConstraints())) {
501                     return new Restrictions() {
502                         @Override
503                         public boolean isEmpty() {
504                             return false;
505                         }
506
507                         @Override
508                         public List<RangeConstraint> getRangeConstraints() {
509                             return decimal.getRangeConstraints();
510                         }
511
512                         @Override
513                         public List<PatternConstraint> getPatternConstraints() {
514                             return ImmutableList.of();
515                         }
516
517                         @Override
518                         public List<LengthConstraint> getLengthConstraints() {
519                             return ImmutableList.of();
520                         }
521                     };
522                 }
523             }
524
525             return EMPTY_RESTRICTIONS;
526         }
527
528         final List<LengthConstraint> length;
529         final List<PatternConstraint> pattern;
530         final List<RangeConstraint> range;
531
532         /*
533          * Take care of extended types.
534          *
535          * Other types which support constraints are check afterwards. There is a slight twist with them, as returned
536          * constraints are the effective view, e.g. they are inherited from base type. Since the constraint is already
537          * enforced by the base type, we want to skip them and not perform duplicate checks.
538          *
539          * We end up emitting ConcreteType instances for YANG base types, which leads to their constraints not being
540          * enforced (most notably decimal64). Therefore we need to make sure we do not strip the next-to-last
541          * restrictions.
542          *
543          * FIXME: this probably not the best solution and needs further analysis.
544          */
545         if (type instanceof BinaryTypeDefinition) {
546             final BinaryTypeDefinition binary = (BinaryTypeDefinition)type;
547             final BinaryTypeDefinition base = binary.getBaseType();
548             if (base != null && base.getBaseType() != null) {
549                 length = currentOrEmpty(binary.getLengthConstraints(), base.getLengthConstraints());
550             } else {
551                 length = binary.getLengthConstraints();
552             }
553
554             pattern = ImmutableList.of();
555             range = ImmutableList.of();
556         } else if (type instanceof DecimalTypeDefinition) {
557             length = ImmutableList.of();
558             pattern = ImmutableList.of();
559
560             final DecimalTypeDefinition decimal = (DecimalTypeDefinition)type;
561             final DecimalTypeDefinition base = decimal.getBaseType();
562             if (base != null && base.getBaseType() != null) {
563                 range = currentOrEmpty(decimal.getRangeConstraints(), base.getRangeConstraints());
564             } else {
565                 range = decimal.getRangeConstraints();
566             }
567         } else if (type instanceof IntegerTypeDefinition) {
568             length = ImmutableList.of();
569             pattern = ImmutableList.of();
570
571             final IntegerTypeDefinition integer = (IntegerTypeDefinition)type;
572             final IntegerTypeDefinition base = integer.getBaseType();
573             if (base != null && base.getBaseType() != null) {
574                 range = currentOrEmpty(integer.getRangeConstraints(), base.getRangeConstraints());
575             } else {
576                 range = integer.getRangeConstraints();
577             }
578         } else if (type instanceof StringTypeDefinition) {
579             final StringTypeDefinition string = (StringTypeDefinition)type;
580             final StringTypeDefinition base = string.getBaseType();
581             if (base != null && base.getBaseType() != null) {
582                 length = currentOrEmpty(string.getLengthConstraints(), base.getLengthConstraints());
583             } else {
584                 length = string.getLengthConstraints();
585             }
586
587             pattern = uniquePatterns(string);
588             range = ImmutableList.of();
589         } else if (type instanceof UnsignedIntegerTypeDefinition) {
590             length = ImmutableList.of();
591             pattern = ImmutableList.of();
592
593             final UnsignedIntegerTypeDefinition unsigned = (UnsignedIntegerTypeDefinition)type;
594             final UnsignedIntegerTypeDefinition base = unsigned.getBaseType();
595             if (base != null && base.getBaseType() != null) {
596                 range = currentOrEmpty(unsigned.getRangeConstraints(), base.getRangeConstraints());
597             } else {
598                 range = unsigned.getRangeConstraints();
599             }
600         } else {
601             length = ImmutableList.of();
602             pattern = ImmutableList.of();
603             range = ImmutableList.of();
604         }
605
606         // Now, this may have ended up being empty, too...
607         if (length.isEmpty() && pattern.isEmpty() && range.isEmpty()) {
608             return EMPTY_RESTRICTIONS;
609         }
610
611         // Nope, not empty allocate a holder
612         return new Restrictions() {
613             @Override
614             public List<RangeConstraint> getRangeConstraints() {
615                 return range;
616             }
617             @Override
618             public List<PatternConstraint> getPatternConstraints() {
619                 return pattern;
620             }
621             @Override
622             public List<LengthConstraint> getLengthConstraints() {
623                 return length;
624             }
625             @Override
626             public boolean isEmpty() {
627                 return false;
628             }
629         };
630     }
631
632     /**
633      * Encodes angle brackets in yang statement description
634      * @param description description of a yang statement which is used to generate javadoc comments
635      * @return string with encoded angle brackets
636      */
637     public static String encodeAngleBrackets(String description) {
638         if (description != null) {
639             description = LT_MATCHER.replaceFrom(description, "&lt;");
640             description = GT_MATCHER.replaceFrom(description, "&gt;");
641         }
642         return description;
643     }
644 }