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