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.model.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 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.common.QNameModule;
37 import org.opendaylight.yangtools.yang.model.api.Module;
38 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
39 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
40 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
41 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
42 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
43 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
44 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
45 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
46 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
47 import org.opendaylight.yangtools.yang.model.util.type.BaseTypes;
48 import org.opendaylight.yangtools.yang.model.util.type.DecimalTypeBuilder;
51 * Contains the methods for converting strings to valid JAVA language strings
52 * (package names, class names, attribute names) and to valid javadoc comments.
54 public final class BindingGeneratorUtil {
57 * Impossible to instantiate this class. All of the methods or attributes
60 private BindingGeneratorUtil() {
64 * Pre-compiled replacement pattern.
66 private static final CharMatcher DOT_MATCHER = CharMatcher.is('.');
67 private static final CharMatcher DASH_COLON_MATCHER = CharMatcher.anyOf("-:");
68 private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
69 private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
70 private static final Pattern UNICODE_CHAR_PATTERN = Pattern.compile("\\\\+u");
72 private static final Restrictions EMPTY_RESTRICTIONS = new Restrictions() {
74 public Optional<LengthConstraint> getLengthConstraint() {
75 return Optional.empty();
79 public List<PatternConstraint> getPatternConstraints() {
80 return Collections.emptyList();
84 public Optional<RangeConstraint<?>> getRangeConstraint() {
85 return Optional.empty();
89 public boolean isEmpty() {
94 private static final Comparator<TypeMemberBuilder<?>> SUID_MEMBER_COMPARATOR =
95 (o1, o2) -> o1.getName().compareTo(o2.getName());
97 private static final Comparator<Type> SUID_NAME_COMPARATOR =
98 (o1, o2) -> o1.getFullyQualifiedName().compareTo(o2.getFullyQualifiedName());
101 * Converts <code>parameterName</code> to valid JAVA parameter name.
103 * If the <code>parameterName</code> is one of the JAVA reserved words then
104 * it is prefixed with underscore character.
106 * @param parameterName
107 * string with the parameter name
108 * @return string with the admissible parameter name
110 public static String resolveJavaReservedWordEquivalency(final String parameterName) {
111 if (parameterName != null && BindingMapping.JAVA_RESERVED_WORDS.contains(parameterName)) {
112 return "_" + parameterName;
114 return parameterName;
118 * Converts module name to valid JAVA package name.
120 * The package name consists of:
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>
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
135 * @deprecated USe {@link BindingMapping#getRootPackageName(QNameModule)} with {@link Module#getQNameModule()}.
138 public static String moduleNamespaceToPackageName(final Module module) {
139 return BindingMapping.getRootPackageName(module.getQNameModule());
143 * Creates package name from specified <code>basePackageName</code> (package
144 * name for module) and <code>schemaPath</code>.
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.
150 * @param basePackageName
151 * string with package name of the module, MUST be normalized,
152 * otherwise this method may return an invalid string.
154 * list of names of YANG nodes which are parents of some node +
156 * @return string with valid JAVA package name
157 * @throws NullPointerException if any of the arguments are null
159 public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
160 final int size = Iterables.size(schemaPath.getPathTowardsRoot()) - 1;
162 return basePackageName;
165 return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
169 * Creates package name from specified <code>basePackageName</code> (package
170 * name for module) and <code>schemaPath</code> which crosses an augmentation.
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.
176 * @param basePackageName
177 * string with package name of the module, MUST be normalized,
178 * otherwise this method may return an invalid string.
180 * list of names of YANG nodes which are parents of some node +
182 * @return string with valid JAVA package name
183 * @throws NullPointerException if any of the arguments are null
185 public static String packageNameForAugmentedGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
186 final int size = Iterables.size(schemaPath.getPathTowardsRoot());
188 return basePackageName;
191 return generateNormalizedPackageName(basePackageName, schemaPath.getPathFromRoot(), size);
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) {
199 final 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, '.'));
203 return BindingMapping.normalizePackageName(builder.toString());
207 * Creates package name from specified <code>basePackageName</code> (package
208 * name for module) and <code>schemaPath</code>.
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.
214 * @param basePackageName
215 * string with package name of the module
217 * list of names of YANG nodes which are parents of some node +
219 * @param isUsesAugment
220 * boolean true if using augment
221 * @return string with valid JAVA package name
223 * @deprecated Use {@link #packageNameForGeneratedType(String, SchemaPath)} or
224 * {@link #packageNameForAugmentedGeneratedType(String, SchemaPath)} instead.
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!");
232 if (schemaPath == null) {
233 throw new IllegalArgumentException("Schema Path cannot be NULL!");
236 final Iterable<QName> iterable = schemaPath.getPathFromRoot();
237 final int size = Iterables.size(iterable);
238 final int traversalSteps;
240 traversalSteps = size;
242 traversalSteps = size - 1;
245 if (traversalSteps == 0) {
246 return BindingMapping.normalizePackageName(basePackageName);
249 return generateNormalizedPackageName(basePackageName, iterable, traversalSteps);
253 * Generates the package name for type definition from
254 * <code>typeDefinition</code> and <code>basePackageName</code>.
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
263 * <li>if <code>basePackageName</code> equals <code>null</code></li>
264 * <li>if <code>typeDefinition</code> equals <code>null</code></li>
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
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!");
278 if (typeDefinition == null) {
279 throw new IllegalArgumentException("Type Definition reference cannot be NULL!");
282 return BindingMapping.normalizePackageName(basePackageName);
286 * Converts <code>token</code> to string which is in accordance with best
287 * practices for JAVA class names.
290 * string which contains characters which should be converted to
292 * @return string which is in accordance with best practices for JAVA class
295 * @deprecated Use {@link BindingMapping#getClassName(QName)} instead.
298 public static String parseToClassName(final String token) {
299 return parseToCamelCase(token, true);
303 * Converts <code>token</code> to string which is in accordance with best
304 * practices for JAVA parameter names.
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
312 * @deprecated Use {@link BindingMapping#getPropertyName(String)} instead.
314 @Deprecated public static String parseToValidParamName(final String token) {
315 return resolveJavaReservedWordEquivalency(parseToCamelCase(token, false));
320 * Converts string <code>token</code> to the cammel case format.
323 * string which should be converted to the cammel case format
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
330 * <li>if <code>token</code> without white spaces is empty</li>
331 * <li>if <code>token</code> equals null</li>
334 private static String parseToCamelCase(final String token, final boolean uppercase) {
336 throw new IllegalArgumentException("Name can not be null");
339 String correctStr = DOT_MATCHER.removeFrom(token.trim());
340 if (correctStr.isEmpty()) {
341 throw new IllegalArgumentException("Name can not be empty");
344 correctStr = replaceWithCamelCase(correctStr, ' ');
345 correctStr = replaceWithCamelCase(correctStr, '-');
346 correctStr = replaceWithCamelCase(correctStr, '_');
348 char firstChar = correctStr.charAt(0);
349 firstChar = uppercase ? Character.toUpperCase(firstChar) : Character.toLowerCase(firstChar);
351 if (firstChar >= '0' && firstChar <= '9') {
352 return '_' + correctStr;
354 return firstChar + correctStr.substring(1);
359 * Replaces all the occurrences of the <code>removalChar</code> in the
360 * <code>text</code> with empty string and converts following character to
364 * string with source text which should be converted
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
372 private static String replaceWithCamelCase(final String text, final char removalChar) {
373 int toBeRemovedPos = text.indexOf(removalChar);
374 if (toBeRemovedPos == -1) {
378 final StringBuilder sb = new StringBuilder(text);
379 final String toBeRemoved = String.valueOf(removalChar);
381 sb.replace(toBeRemovedPos, toBeRemovedPos + 1, "");
382 // check if 'toBeRemoved' character is not the only character in
384 if (sb.length() == 0) {
385 throw new IllegalArgumentException("The resulting string can not be empty");
387 final char replacement = Character.toUpperCase(sb.charAt(toBeRemovedPos));
388 sb.setCharAt(toBeRemovedPos, replacement);
389 toBeRemovedPos = sb.indexOf(toBeRemoved);
390 } while (toBeRemovedPos != -1);
392 return sb.toString();
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);
405 private static final ThreadLocal<MessageDigest> SHA1_MD = new ThreadLocal<MessageDigest>() {
407 protected MessageDigest initialValue() {
409 return MessageDigest.getInstance("SHA");
410 } catch (final NoSuchAlgorithmException e) {
411 throw new IllegalStateException("Failed to get a SHA digest provider", e);
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);
422 for (final Type ifc : sortedCollection(SUID_NAME_COMPARATOR, to.getImplementsTypes())) {
423 dout.writeUTF(ifc.getFullyQualifiedName());
426 for (final GeneratedPropertyBuilder gp : sortedCollection(SUID_MEMBER_COMPARATOR, to.getProperties())) {
427 dout.writeUTF(gp.getName());
430 for (final 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());
438 } catch (final IOException e) {
439 throw new IllegalStateException("Failed to hash object " + to, e);
442 final byte[] hashBytes = SHA1_MD.get().digest(bout.toByteArray());
444 for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
445 hash = hash << 8 | hashBytes[i] & 0xFF;
450 private static <T extends Optional<?>> T currentOrEmpty(final T current, final T base) {
451 return current.equals(base) ? (T)Optional.empty() : current;
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)) {
464 private static List<PatternConstraint> uniquePatterns(final StringTypeDefinition type) {
465 final List<PatternConstraint> constraints = type.getPatternConstraints();
466 if (constraints.isEmpty()) {
470 final Builder<PatternConstraint> builder = ImmutableList.builder();
471 boolean filtered = false;
472 for (final PatternConstraint c : constraints) {
473 if (containsConstraint(type.getBaseType(), c)) {
480 return filtered ? builder.build() : constraints;
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.
492 // FIXME: looking at the generated code it looks as though we need to pass the restrictions without
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();
500 if (!tmp.getRangeConstraint().equals(decimal.getRangeConstraint())) {
501 return new Restrictions() {
503 public boolean isEmpty() {
508 public Optional<? extends RangeConstraint<?>> getRangeConstraint() {
509 return decimal.getRangeConstraint();
513 public List<PatternConstraint> getPatternConstraints() {
514 return ImmutableList.of();
518 public Optional<LengthConstraint> getLengthConstraint() {
519 return Optional.empty();
525 return EMPTY_RESTRICTIONS;
528 final Optional<LengthConstraint> length;
529 final List<PatternConstraint> pattern;
530 final Optional<? extends RangeConstraint<?>> range;
533 * Take care of extended types.
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.
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
543 * FIXME: this probably not the best solution and needs further analysis.
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.getLengthConstraint(), base.getLengthConstraint());
551 length = binary.getLengthConstraint();
554 pattern = ImmutableList.of();
555 range = Optional.empty();
556 } else if (type instanceof DecimalTypeDefinition) {
557 length = Optional.empty();
558 pattern = ImmutableList.of();
560 final DecimalTypeDefinition decimal = (DecimalTypeDefinition)type;
561 final DecimalTypeDefinition base = decimal.getBaseType();
562 if (base != null && base.getBaseType() != null) {
563 range = currentOrEmpty(decimal.getRangeConstraint(), base.getRangeConstraint());
565 range = decimal.getRangeConstraint();
567 } else if (type instanceof RangeRestrictedTypeDefinition) {
568 // Integer-like types
569 length = Optional.empty();
570 pattern = ImmutableList.of();
571 range = extractRangeConstraint((RangeRestrictedTypeDefinition<?, ?>)type);
572 } else if (type instanceof StringTypeDefinition) {
573 final StringTypeDefinition string = (StringTypeDefinition)type;
574 final StringTypeDefinition base = string.getBaseType();
575 if (base != null && base.getBaseType() != null) {
576 length = currentOrEmpty(string.getLengthConstraint(), base.getLengthConstraint());
578 length = string.getLengthConstraint();
581 pattern = uniquePatterns(string);
582 range = Optional.empty();
584 length = Optional.empty();
585 pattern = ImmutableList.of();
586 range = Optional.empty();
589 // Now, this may have ended up being empty, too...
590 if (!length.isPresent() && pattern.isEmpty() && !range.isPresent()) {
591 return EMPTY_RESTRICTIONS;
594 // Nope, not empty allocate a holder
595 return new Restrictions() {
597 public Optional<? extends RangeConstraint<?>> getRangeConstraint() {
601 public List<PatternConstraint> getPatternConstraints() {
605 public Optional<LengthConstraint> getLengthConstraint() {
609 public boolean isEmpty() {
615 private static <T extends RangeRestrictedTypeDefinition<?, ?>> Optional<? extends RangeConstraint<?>>
616 extractRangeConstraint(final T def) {
617 final T base = (T) def.getBaseType();
618 if (base != null && base.getBaseType() != null) {
619 return currentOrEmpty(def.getRangeConstraint(), base.getRangeConstraint());
622 return def.getRangeConstraint();
626 * Encodes angle brackets in yang statement description
627 * @param description description of a yang statement which is used to generate javadoc comments
628 * @return string with encoded angle brackets
630 public static String encodeAngleBrackets(String description) {
631 if (description != null) {
632 description = LT_MATCHER.replaceFrom(description, "<");
633 description = GT_MATCHER.replaceFrom(description, ">");
638 public static String replaceAllIllegalChars(final CharSequence stringBuilder){
639 final String ret = UNICODE_CHAR_PATTERN.matcher(stringBuilder).replaceAll("\\\\\\\\u");
640 return ret.isEmpty() ? "" : ret;