2 * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.mdsal.binding.generator.util;
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;
50 * Contains the methods for converting strings to valid JAVA language strings
51 * (package names, class names, attribute names) and to valid javadoc comments.
53 public final class BindingGeneratorUtil {
56 * Impossible to instantiate this class. All of the methods or attributes
59 private BindingGeneratorUtil() {
63 * Pre-compiled replacement pattern.
65 private static final CharMatcher DOT_MATCHER = CharMatcher.is('.');
66 private static final CharMatcher DASH_COLON_MATCHER = CharMatcher.anyOf("-:");
67 private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
68 private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
70 private static final Restrictions EMPTY_RESTRICTIONS = new Restrictions() {
72 public List<LengthConstraint> getLengthConstraints() {
73 return Collections.emptyList();
77 public List<PatternConstraint> getPatternConstraints() {
78 return Collections.emptyList();
82 public List<RangeConstraint> getRangeConstraints() {
83 return Collections.emptyList();
87 public boolean isEmpty() {
92 private static final Comparator<TypeMemberBuilder<?>> SUID_MEMBER_COMPARATOR =
93 (o1, o2) -> o1.getName().compareTo(o2.getName());
95 private static final Comparator<Type> SUID_NAME_COMPARATOR =
96 (o1, o2) -> o1.getFullyQualifiedName().compareTo(o2.getFullyQualifiedName());
99 * Converts <code>parameterName</code> to valid JAVA parameter name.
101 * If the <code>parameterName</code> is one of the JAVA reserved words then
102 * it is prefixed with underscore character.
104 * @param parameterName
105 * string with the parameter name
106 * @return string with the admissible parameter name
108 public static String resolveJavaReservedWordEquivalency(final String parameterName) {
109 if ((parameterName != null) && BindingMapping.JAVA_RESERVED_WORDS.contains(parameterName)) {
110 return "_" + parameterName;
112 return parameterName;
116 * Converts module name to valid JAVA package name.
118 * The package name consists of:
120 * <li>prefix - <i>org.opendaylight.yang.gen.v</i></li>
121 * <li>module YANG version - <i>org.opendaylight.yang.gen.v</i></li>
122 * <li>module namespace - invalid characters are replaced with dots</li>
123 * <li>revision prefix - <i>.rev</i></li>
124 * <li>revision - YYYYMMDD (MM and DD aren't spread to the whole length)</li>
128 * module which contains data about namespace and revision date
129 * @return string with the valid JAVA package name
130 * @throws IllegalArgumentException
131 * if the revision date of the <code>module</code> equals
133 * @deprecated USe {@link BindingMapping#getRootPackageName(QNameModule)} with {@link Module#getQNameModule()}.
136 public static String moduleNamespaceToPackageName(final Module module) {
137 return BindingMapping.getRootPackageName(module.getQNameModule());
141 * Creates package name from specified <code>basePackageName</code> (package
142 * name for module) and <code>schemaPath</code>.
144 * Resulting package name is concatenation of <code>basePackageName</code>
145 * and all local names of YANG nodes which are parents of some node for
146 * which <code>schemaPath</code> is specified.
148 * @param basePackageName
149 * string with package name of the module, MUST be normalized,
150 * otherwise this method may return an invalid string.
152 * list of names of YANG nodes which are parents of some node +
154 * @return string with valid JAVA package name
155 * @throws NullPointerException if any of the arguments are null
157 public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
158 final int size = Iterables.size(schemaPath.getPathTowardsRoot()) - 1;
160 return basePackageName;
163 return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
167 * Creates package name from specified <code>basePackageName</code> (package
168 * name for module) and <code>schemaPath</code> which crosses an augmentation.
170 * Resulting package name is concatenation of <code>basePackageName</code>
171 * and all local names of YANG nodes which are parents of some node for
172 * which <code>schemaPath</code> is specified.
174 * @param basePackageName
175 * string with package name of the module, MUST be normalized,
176 * otherwise this method may return an invalid string.
178 * list of names of YANG nodes which are parents of some node +
180 * @return string with valid JAVA package name
181 * @throws NullPointerException if any of the arguments are null
183 public static String packageNameForAugmentedGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
184 final int size = Iterables.size(schemaPath.getPathTowardsRoot());
186 return basePackageName;
189 return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
192 private static String generateNormalizedPackageName(final String base, final Iterable<QName> path, final int size) {
193 final StringBuilder builder = new StringBuilder(base);
194 final Iterator<QName> iterator = path.iterator();
195 for (int i = 0; i < size; ++i) {
197 final String nodeLocalName = iterator.next().getLocalName();
198 // FIXME: Collon ":" is invalid in node local name as per RFC6020, identifier statement.
199 builder.append(DASH_COLON_MATCHER.replaceFrom(nodeLocalName, '.'));
201 return BindingMapping.normalizePackageName(builder.toString());
205 * Creates package name from specified <code>basePackageName</code> (package
206 * name for module) and <code>schemaPath</code>.
208 * Resulting package name is concatenation of <code>basePackageName</code>
209 * and all local names of YANG nodes which are parents of some node for
210 * which <code>schemaPath</code> is specified.
212 * @param basePackageName
213 * string with package name of the module
215 * list of names of YANG nodes which are parents of some node +
217 * @param isUsesAugment
218 * boolean true if using augment
219 * @return string with valid JAVA package name
221 * @deprecated Use {@link #packageNameForGeneratedType(String, SchemaPath)} or
222 * {@link #packageNameForAugmentedGeneratedType(String, SchemaPath)} instead.
225 public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath,
226 final boolean isUsesAugment) {
227 if (basePackageName == null) {
228 throw new IllegalArgumentException("Base Package Name cannot be NULL!");
230 if (schemaPath == null) {
231 throw new IllegalArgumentException("Schema Path cannot be NULL!");
234 final Iterable<QName> iterable = schemaPath.getPathFromRoot();
235 final int size = Iterables.size(iterable);
236 final int traversalSteps;
238 traversalSteps = size;
240 traversalSteps = size - 1;
243 if (traversalSteps == 0) {
244 return BindingMapping.normalizePackageName(basePackageName);
247 return generateNormalizedPackageName(basePackageName, iterable, traversalSteps);
251 * Generates the package name for type definition from
252 * <code>typeDefinition</code> and <code>basePackageName</code>.
254 * @param basePackageName
255 * string with the package name of the module
256 * @param typeDefinition
257 * type definition for which the package name will be generated *
258 * @return string with valid JAVA package name
259 * @throws IllegalArgumentException
261 * <li>if <code>basePackageName</code> equals <code>null</code></li>
262 * <li>if <code>typeDefinition</code> equals <code>null</code></li>
264 * @deprecated This method ignores typeDefinition argument and its result is only
265 * <code>BindingMapping.normalizePackageName(basePackageName)</code>.
266 * Aside from tests, there is not a single user in OpenDaylight codebase,
267 * hence it can be considered buggy and defunct. It is scheduled for removal
271 public static String packageNameForTypeDefinition(final String basePackageName,
272 final TypeDefinition<?> typeDefinition) {
273 if (basePackageName == null) {
274 throw new IllegalArgumentException("Base Package Name cannot be NULL!");
276 if (typeDefinition == null) {
277 throw new IllegalArgumentException("Type Definition reference cannot be NULL!");
280 return BindingMapping.normalizePackageName(basePackageName);
284 * Converts <code>token</code> to string which is in accordance with best
285 * practices for JAVA class names.
288 * string which contains characters which should be converted to
290 * @return string which is in accordance with best practices for JAVA class
293 * @deprecated Use {@link BindingMapping#getClassName(QName)} instead.
296 public static String parseToClassName(final String token) {
297 return parseToCamelCase(token, true);
301 * Converts <code>token</code> to string which is in accordance with best
302 * practices for JAVA parameter names.
305 * string which contains characters which should be converted to
306 * JAVA parameter name
307 * @return string which is in accordance with best practices for JAVA
310 * @deprecated Use {@link BindingMapping#getPropertyName(String)} instead.
312 @Deprecated public static String parseToValidParamName(final String token) {
313 return resolveJavaReservedWordEquivalency(parseToCamelCase(token, false));
318 * Converts string <code>token</code> to the cammel case format.
321 * string which should be converted to the cammel case format
323 * boolean value which says whether the first character of the
324 * <code>token</code> should|shuldn't be uppercased
325 * @return string in the cammel case format
326 * @throws IllegalArgumentException
328 * <li>if <code>token</code> without white spaces is empty</li>
329 * <li>if <code>token</code> equals null</li>
332 private static String parseToCamelCase(final String token, final boolean uppercase) {
334 throw new IllegalArgumentException("Name can not be null");
337 String correctStr = DOT_MATCHER.removeFrom(token.trim());
338 if (correctStr.isEmpty()) {
339 throw new IllegalArgumentException("Name can not be empty");
342 correctStr = replaceWithCamelCase(correctStr, ' ');
343 correctStr = replaceWithCamelCase(correctStr, '-');
344 correctStr = replaceWithCamelCase(correctStr, '_');
346 char firstChar = correctStr.charAt(0);
347 firstChar = uppercase ? Character.toUpperCase(firstChar) : Character.toLowerCase(firstChar);
349 if ((firstChar >= '0') && (firstChar <= '9')) {
350 return '_' + correctStr;
352 return firstChar + correctStr.substring(1);
357 * Replaces all the occurrences of the <code>removalChar</code> in the
358 * <code>text</code> with empty string and converts following character to
362 * string with source text which should be converted
364 * character which is sought in the <code>text</code>
365 * @return string which doesn't contain <code>removalChar</code> and has
366 * following characters converted to upper case
367 * @throws IllegalArgumentException
368 * if the length of the returning string has length 0
370 private static String replaceWithCamelCase(final String text, final char removalChar) {
371 int toBeRemovedPos = text.indexOf(removalChar);
372 if (toBeRemovedPos == -1) {
376 final StringBuilder sb = new StringBuilder(text);
377 final String toBeRemoved = String.valueOf(removalChar);
379 sb.replace(toBeRemovedPos, toBeRemovedPos + 1, "");
380 // check if 'toBeRemoved' character is not the only character in
382 if (sb.length() == 0) {
383 throw new IllegalArgumentException("The resulting string can not be empty");
385 final char replacement = Character.toUpperCase(sb.charAt(toBeRemovedPos));
386 sb.setCharAt(toBeRemovedPos, replacement);
387 toBeRemovedPos = sb.indexOf(toBeRemoved);
388 } while (toBeRemovedPos != -1);
390 return sb.toString();
393 private static <T> Iterable<T> sortedCollection(final Comparator<? super T> comparator, final Collection<T> input) {
394 if (input.size() > 1) {
395 final List<T> ret = new ArrayList<>(input);
396 Collections.sort(ret, comparator);
403 private static final ThreadLocal<MessageDigest> SHA1_MD = new ThreadLocal<MessageDigest>() {
405 protected MessageDigest initialValue() {
407 return MessageDigest.getInstance("SHA");
408 } catch (final NoSuchAlgorithmException e) {
409 throw new IllegalStateException("Failed to get a SHA digest provider", e);
414 public static long computeDefaultSUID(final GeneratedTypeBuilderBase<?> to) {
415 final ByteArrayOutputStream bout = new ByteArrayOutputStream();
416 try (final DataOutputStream dout = new DataOutputStream(bout)) {
417 dout.writeUTF(to.getName());
418 dout.writeInt(to.isAbstract() ? 3 : 7);
420 for (final Type ifc : sortedCollection(SUID_NAME_COMPARATOR, to.getImplementsTypes())) {
421 dout.writeUTF(ifc.getFullyQualifiedName());
424 for (final GeneratedPropertyBuilder gp : sortedCollection(SUID_MEMBER_COMPARATOR, to.getProperties())) {
425 dout.writeUTF(gp.getName());
428 for (final MethodSignatureBuilder m : sortedCollection(SUID_MEMBER_COMPARATOR, to.getMethodDefinitions())) {
429 if (!(m.getAccessModifier().equals(AccessModifier.PRIVATE))) {
430 dout.writeUTF(m.getName());
431 dout.write(m.getAccessModifier().ordinal());
436 } catch (final IOException e) {
437 throw new IllegalStateException("Failed to hash object " + to, e);
440 final byte[] hashBytes = SHA1_MD.get().digest(bout.toByteArray());
442 for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
443 hash = (hash << 8) | (hashBytes[i] & 0xFF);
448 private static <T> List<T> currentOrEmpty(final List<T> current, final List<T> base) {
449 return current.equals(base) ? ImmutableList.<T>of() : current;
452 private static boolean containsConstraint(final StringTypeDefinition type, final PatternConstraint constraint) {
453 for (StringTypeDefinition wlk = type; wlk != null; wlk = wlk.getBaseType()) {
454 if (wlk.getPatternConstraints().contains(constraint)) {
462 private static List<PatternConstraint> uniquePatterns(final StringTypeDefinition type) {
463 final List<PatternConstraint> constraints = type.getPatternConstraints();
464 if (constraints.isEmpty()) {
468 final Builder<PatternConstraint> builder = ImmutableList.builder();
469 boolean filtered = false;
470 for (final PatternConstraint c : constraints) {
471 if (containsConstraint(type.getBaseType(), c)) {
478 return filtered ? builder.build() : constraints;
481 public static Restrictions getRestrictions(final TypeDefinition<?> type) {
482 // Old parser generated types which actually contained based restrictions, but our code deals with that when
483 // binding to core Java types. Hence we'll emit empty restrictions for base types.
484 if ((type == null) || (type.getBaseType() == null)) {
485 // Handling of decimal64 has changed in the new parser. It contains range restrictions applied to the type
486 // directly, without an extended type. We need to capture such constraints. In order to retain behavior we
487 // need to analyze the new semantics and see if the constraints have been overridden. To do that we
488 // instantiate a temporary unconstrained type and compare them.
490 // FIXME: looking at the generated code it looks as though we need to pass the restrictions without
492 if (type instanceof DecimalTypeDefinition) {
493 final DecimalTypeDefinition decimal = (DecimalTypeDefinition) type;
494 final DecimalTypeBuilder tmpBuilder = BaseTypes.decimalTypeBuilder(decimal.getPath());
495 tmpBuilder.setFractionDigits(decimal.getFractionDigits());
496 final DecimalTypeDefinition tmp = tmpBuilder.build();
498 if (!tmp.getRangeConstraints().equals(decimal.getRangeConstraints())) {
499 return new Restrictions() {
501 public boolean isEmpty() {
506 public List<RangeConstraint> getRangeConstraints() {
507 return decimal.getRangeConstraints();
511 public List<PatternConstraint> getPatternConstraints() {
512 return ImmutableList.of();
516 public List<LengthConstraint> getLengthConstraints() {
517 return ImmutableList.of();
523 return EMPTY_RESTRICTIONS;
526 final List<LengthConstraint> length;
527 final List<PatternConstraint> pattern;
528 final List<RangeConstraint> range;
531 * Take care of extended types.
533 * Other types which support constraints are check afterwards. There is a slight twist with them, as returned
534 * constraints are the effective view, e.g. they are inherited from base type. Since the constraint is already
535 * enforced by the base type, we want to skip them and not perform duplicate checks.
537 * We end up emitting ConcreteType instances for YANG base types, which leads to their constraints not being
538 * enforced (most notably decimal64). Therefore we need to make sure we do not strip the next-to-last
541 * FIXME: this probably not the best solution and needs further analysis.
543 if (type instanceof BinaryTypeDefinition) {
544 final BinaryTypeDefinition binary = (BinaryTypeDefinition)type;
545 final BinaryTypeDefinition base = binary.getBaseType();
546 if ((base != null) && (base.getBaseType() != null)) {
547 length = currentOrEmpty(binary.getLengthConstraints(), base.getLengthConstraints());
549 length = binary.getLengthConstraints();
552 pattern = ImmutableList.of();
553 range = ImmutableList.of();
554 } else if (type instanceof DecimalTypeDefinition) {
555 length = ImmutableList.of();
556 pattern = ImmutableList.of();
558 final DecimalTypeDefinition decimal = (DecimalTypeDefinition)type;
559 final DecimalTypeDefinition base = decimal.getBaseType();
560 if ((base != null) && (base.getBaseType() != null)) {
561 range = currentOrEmpty(decimal.getRangeConstraints(), base.getRangeConstraints());
563 range = decimal.getRangeConstraints();
565 } else if (type instanceof IntegerTypeDefinition) {
566 length = ImmutableList.of();
567 pattern = ImmutableList.of();
569 final IntegerTypeDefinition integer = (IntegerTypeDefinition)type;
570 final IntegerTypeDefinition base = integer.getBaseType();
571 if ((base != null) && (base.getBaseType() != null)) {
572 range = currentOrEmpty(integer.getRangeConstraints(), base.getRangeConstraints());
574 range = integer.getRangeConstraints();
576 } else if (type instanceof StringTypeDefinition) {
577 final StringTypeDefinition string = (StringTypeDefinition)type;
578 final StringTypeDefinition base = string.getBaseType();
579 if ((base != null) && (base.getBaseType() != null)) {
580 length = currentOrEmpty(string.getLengthConstraints(), base.getLengthConstraints());
582 length = string.getLengthConstraints();
585 pattern = uniquePatterns(string);
586 range = ImmutableList.of();
587 } else if (type instanceof UnsignedIntegerTypeDefinition) {
588 length = ImmutableList.of();
589 pattern = ImmutableList.of();
591 final UnsignedIntegerTypeDefinition unsigned = (UnsignedIntegerTypeDefinition)type;
592 final UnsignedIntegerTypeDefinition base = unsigned.getBaseType();
593 if ((base != null) && (base.getBaseType() != null)) {
594 range = currentOrEmpty(unsigned.getRangeConstraints(), base.getRangeConstraints());
596 range = unsigned.getRangeConstraints();
599 length = ImmutableList.of();
600 pattern = ImmutableList.of();
601 range = ImmutableList.of();
604 // Now, this may have ended up being empty, too...
605 if (length.isEmpty() && pattern.isEmpty() && range.isEmpty()) {
606 return EMPTY_RESTRICTIONS;
609 // Nope, not empty allocate a holder
610 return new Restrictions() {
612 public List<RangeConstraint> getRangeConstraints() {
616 public List<PatternConstraint> getPatternConstraints() {
620 public List<LengthConstraint> getLengthConstraints() {
624 public boolean isEmpty() {
631 * Encodes angle brackets in yang statement description
632 * @param description description of a yang statement which is used to generate javadoc comments
633 * @return string with encoded angle brackets
635 public static String encodeAngleBrackets(String description) {
636 if (description != null) {
637 description = LT_MATCHER.replaceFrom(description, "<");
638 description = GT_MATCHER.replaceFrom(description, ">");