BUG-582: Operate on characters when checking for leading digit
[yangtools.git] / code-generator / binding-generator-util / src / main / java / org / opendaylight / yangtools / 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.yangtools.binding.generator.util;
9
10 import java.io.ByteArrayOutputStream;
11 import java.io.DataOutputStream;
12 import java.io.IOException;
13 import java.security.MessageDigest;
14 import java.security.NoSuchAlgorithmException;
15 import java.text.SimpleDateFormat;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collections;
19 import java.util.Comparator;
20 import java.util.HashSet;
21 import java.util.List;
22 import java.util.Set;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
26 import org.opendaylight.yangtools.binding.generator.util.generated.type.builder.GeneratedTOBuilderImpl;
27 import org.opendaylight.yangtools.sal.binding.model.api.AccessModifier;
28 import org.opendaylight.yangtools.sal.binding.model.api.Restrictions;
29 import org.opendaylight.yangtools.sal.binding.model.api.Type;
30 import org.opendaylight.yangtools.sal.binding.model.api.type.builder.GeneratedPropertyBuilder;
31 import org.opendaylight.yangtools.sal.binding.model.api.type.builder.MethodSignatureBuilder;
32 import org.opendaylight.yangtools.sal.binding.model.api.type.builder.TypeMemberBuilder;
33 import org.opendaylight.yangtools.yang.binding.BindingMapping;
34 import org.opendaylight.yangtools.yang.common.QName;
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.DecimalTypeDefinition;
39 import org.opendaylight.yangtools.yang.model.api.type.IntegerTypeDefinition;
40 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
41 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
42 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
43 import org.opendaylight.yangtools.yang.model.api.type.UnsignedIntegerTypeDefinition;
44 import org.opendaylight.yangtools.yang.model.util.ExtendedType;
45
46 /**
47  * Contains the methods for converting strings to valid JAVA language strings
48  * (package names, class names, attribute names).
49  *
50  *
51  */
52 public final class BindingGeneratorUtil {
53
54     private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
55
56         @Override
57         protected SimpleDateFormat initialValue() {
58             return new SimpleDateFormat("yyMMdd");
59         }
60
61         @Override
62         public void set(final SimpleDateFormat value) {
63             throw new UnsupportedOperationException();
64         }
65     };
66
67     /**
68      * Array of strings values which represents JAVA reserved words.
69      */
70     @Deprecated
71     private static final String[] SET_VALUES = new String[] { "abstract", "assert", "boolean", "break", "byte", "case",
72             "catch", "char", "class", "const", "continue", "default", "double", "do", "else", "enum", "extends",
73             "false", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int",
74             "interface", "long", "native", "new", "null", "package", "private", "protected", "public", "return",
75             "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient",
76             "true", "try", "void", "volatile", "while" };
77
78     /**
79      * Impossible to instantiate this class. All of the methods or attributes
80      * are static.
81      */
82     private BindingGeneratorUtil() {
83     }
84
85     /**
86      * Hash set of words which are reserved in JAVA language.
87      */
88     @Deprecated
89     private static final Set<String> JAVA_RESERVED_WORDS = new HashSet<String>(Arrays.asList(SET_VALUES));
90
91     /**
92      * Pre-compiled replacement pattern.
93      */
94     private static final Pattern COLON_SLASH_SLASH = Pattern.compile("://", Pattern.LITERAL);
95     private static final String QUOTED_DOT = Matcher.quoteReplacement(".");
96
97     /**
98      * Converts string <code>packageName</code> to valid JAVA package name.
99      *
100      * If some words of package name are digits of JAVA reserved words they are
101      * prefixed with underscore character.
102      *
103      * @param packageName
104      *            string which contains words separated by point.
105      * @return package name which contains words separated by point.
106      */
107     private static String validateJavaPackage(final String packageName) {
108         if (packageName != null) {
109             final String[] packNameParts = packageName.toLowerCase().split("\\.");
110             if (packNameParts != null) {
111                 final StringBuilder builder = new StringBuilder();
112                 for (int i = 0; i < packNameParts.length; ++i) {
113                     final String packNamePart = packNameParts[i];
114                     if (Character.isDigit(packNamePart.charAt(0))) {
115                         packNameParts[i] = "_" + packNamePart;
116                     } else if (JAVA_RESERVED_WORDS.contains(packNamePart)) {
117                         packNameParts[i] = "_" + packNamePart;
118                     }
119                     if (i > 0) {
120                         builder.append(".");
121                     }
122                     builder.append(packNameParts[i]);
123                 }
124                 return builder.toString();
125             }
126         }
127         return packageName;
128     }
129
130     /**
131      * Converts <code>parameterName</code> to valid JAVA parameter name.
132      *
133      * If the <code>parameterName</code> is one of the JAVA reserved words then
134      * it is prefixed with underscore character.
135      *
136      * @param parameterName
137      *            string with the parameter name
138      * @return string with the admissible parameter name
139      */
140     public static String resolveJavaReservedWordEquivalency(final String parameterName) {
141         if (parameterName != null && JAVA_RESERVED_WORDS.contains(parameterName)) {
142             return "_" + parameterName;
143         }
144         return parameterName;
145     }
146
147     /**
148      * Converts module name to valid JAVA package name.
149      *
150      * The package name consists of:
151      * <ul>
152      * <li>prefix - <i>org.opendaylight.yang.gen.v</i></li>
153      * <li>module YANG version - <i>org.opendaylight.yang.gen.v</i></li>
154      * <li>module namespace - invalid characters are replaced with dots</li>
155      * <li>revision prefix - <i>.rev</i></li>
156      * <li>revision - YYYYMMDD (MM and DD aren't spread to the whole length)</li>
157      * </ul>
158      *
159      * @param module
160      *            module which contains data about namespace and revision date
161      * @return string with the valid JAVA package name
162      * @throws IllegalArgumentException
163      *             if the revision date of the <code>module</code> equals
164      *             <code>null</code>
165      */
166     public static String moduleNamespaceToPackageName(final Module module) {
167         final StringBuilder packageNameBuilder = new StringBuilder();
168
169         if (module.getRevision() == null) {
170             throw new IllegalArgumentException("Module " + module.getName() + " does not specify revision date!");
171         }
172         packageNameBuilder.append("org.opendaylight.yang.gen.v");
173         packageNameBuilder.append(module.getYangVersion());
174         packageNameBuilder.append('.');
175
176         String namespace = module.getNamespace().toString();
177         namespace = COLON_SLASH_SLASH.matcher(namespace).replaceAll(QUOTED_DOT);
178
179         // FIXME: improve these
180         namespace = namespace.replace("/", ".");
181         namespace = namespace.replace(":", ".");
182         namespace = namespace.replace("-", ".");
183         namespace = namespace.replace("@", ".");
184         namespace = namespace.replace("$", ".");
185         namespace = namespace.replace("#", ".");
186         namespace = namespace.replace("'", ".");
187         namespace = namespace.replace("*", ".");
188         namespace = namespace.replace("+", ".");
189         namespace = namespace.replace(",", ".");
190         namespace = namespace.replace(";", ".");
191         namespace = namespace.replace("=", ".");
192
193         packageNameBuilder.append(namespace);
194         packageNameBuilder.append(".rev");
195         packageNameBuilder.append(DATE_FORMAT.get().format(module.getRevision()));
196
197         return validateJavaPackage(packageNameBuilder.toString());
198     }
199
200     public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath) {
201         return packageNameForGeneratedType(basePackageName, schemaPath, false);
202     }
203
204     /**
205      * Creates package name from specified <code>basePackageName</code> (package
206      * name for module) and <code>schemaPath</code>.
207      *
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.
211      *
212      * @param basePackageName
213      *            string with package name of the module
214      * @param schemaPath
215      *            list of names of YANG nodes which are parents of some node +
216      *            name of this node
217      * @return string with valid JAVA package name
218      */
219     public static String packageNameForGeneratedType(final String basePackageName, final SchemaPath schemaPath,
220             final boolean isUsesAugment) {
221         if (basePackageName == null) {
222             throw new IllegalArgumentException("Base Package Name cannot be NULL!");
223         }
224         if (schemaPath == null) {
225             throw new IllegalArgumentException("Schema Path cannot be NULL!");
226         }
227
228         final StringBuilder builder = new StringBuilder();
229         builder.append(basePackageName);
230         final List<QName> pathToNode = schemaPath.getPath();
231         final int traversalSteps;
232         if (isUsesAugment) {
233             traversalSteps = (pathToNode.size());
234         } else {
235             traversalSteps = (pathToNode.size() - 1);
236         }
237         for (int i = 0; i < traversalSteps; ++i) {
238             builder.append(".");
239             String nodeLocalName = pathToNode.get(i).getLocalName();
240
241             nodeLocalName = nodeLocalName.replace(":", ".");
242             nodeLocalName = nodeLocalName.replace("-", ".");
243             builder.append(nodeLocalName);
244         }
245         return validateJavaPackage(builder.toString());
246     }
247
248     /**
249      * Generates the package name for type definition from
250      * <code>typeDefinition</code> and <code>basePackageName</code>.
251      *
252      * @param basePackageName
253      *            string with the package name of the module
254      * @param typeDefinition
255      *            type definition for which the package name will be generated *
256      * @return string with valid JAVA package name
257      * @throws IllegalArgumentException
258      *             <ul>
259      *             <li>if <code>basePackageName</code> equals <code>null</code></li>
260      *             <li>if <code>typeDefinition</code> equals <code>null</code></li>
261      *             </ul>
262      */
263     public static String packageNameForTypeDefinition(final String basePackageName,
264             final TypeDefinition<?> typeDefinition) {
265         if (basePackageName == null) {
266             throw new IllegalArgumentException("Base Package Name cannot be NULL!");
267         }
268         if (typeDefinition == null) {
269             throw new IllegalArgumentException("Type Definition reference cannot be NULL!");
270         }
271
272         final StringBuilder builder = new StringBuilder();
273         builder.append(basePackageName);
274         return validateJavaPackage(builder.toString());
275     }
276
277     /**
278      * Converts <code>token</code> to string which is in accordance with best
279      * practices for JAVA class names.
280      *
281      * @param token
282      *            string which contains characters which should be converted to
283      *            JAVA class name
284      * @return string which is in accordance with best practices for JAVA class
285      *         name.
286      *
287      * @deprecated Use {@link BindingMapping#getClassName(QName)} instead.
288      */
289     @Deprecated
290     public static String parseToClassName(final String token) {
291         return parseToCamelCase(token, true);
292     }
293
294     /**
295      * Converts <code>token</code> to string which is in accordance with best
296      * practices for JAVA parameter names.
297      *
298      * @param token
299      *            string which contains characters which should be converted to
300      *            JAVA parameter name
301      * @return string which is in accordance with best practices for JAVA
302      *         parameter name.
303      */
304     public static String parseToValidParamName(final String token) {
305         return resolveJavaReservedWordEquivalency(parseToCamelCase(token, false));
306     }
307
308     /**
309      *
310      * Converts string <code>token</code> to the cammel case format.
311      *
312      * @param token
313      *            string which should be converted to the cammel case format
314      * @param uppercase
315      *            boolean value which says whether the first character of the
316      *            <code>token</code> should|shuldn't be uppercased
317      * @return string in the cammel case format
318      * @throws IllegalArgumentException
319      *             <ul>
320      *             <li>if <code>token</code> without white spaces is empty</li>
321      *             <li>if <code>token</code> equals null</li>
322      *             </ul>
323      */
324
325     private static String parseToCamelCase(final String token, final boolean uppercase) {
326         if (token == null) {
327             throw new IllegalArgumentException("Name can not be null");
328         }
329
330         String correctStr = token.trim();
331         correctStr = correctStr.replace(".", "");
332
333         if (correctStr.isEmpty()) {
334             throw new IllegalArgumentException("Name can not be emty");
335         }
336
337         correctStr = replaceWithCamelCase(correctStr, ' ');
338         correctStr = replaceWithCamelCase(correctStr, '-');
339         correctStr = replaceWithCamelCase(correctStr, '_');
340
341         char firstChar = correctStr.charAt(0);
342         firstChar = uppercase ? Character.toUpperCase(firstChar) : Character.toLowerCase(firstChar);
343
344         if (firstChar >= '0' && firstChar <= '9') {
345             return correctStr = '_' + correctStr;
346         } else {
347             return correctStr = firstChar + correctStr.substring(1);
348         }
349     }
350
351     /**
352      * Replaces all the occurances of the <code>removalChar</code> in the
353      * <code>text</code> with empty string and converts following character to
354      * upper case.
355      *
356      * @param text
357      *            string with source text which should be converted
358      * @param removalChar
359      *            character which is sought in the <code>text</code>
360      * @return string which doesn't contain <code>removalChar</code> and has
361      *         following characters converted to upper case
362      * @throws IllegalArgumentException
363      *             if the length of the returning string has length 0
364      */
365     private static String replaceWithCamelCase(final String text, final char removalChar) {
366         StringBuilder sb = new StringBuilder(text);
367         String toBeRemoved = String.valueOf(removalChar);
368
369         int toBeRemovedPos = sb.indexOf(toBeRemoved);
370         while (toBeRemovedPos != -1) {
371             sb.replace(toBeRemovedPos, toBeRemovedPos + 1, "");
372             // check if 'toBeRemoved' character is not the only character in
373             // 'text'
374             if (sb.length() == 0) {
375                 throw new IllegalArgumentException("The resulting string can not be empty");
376             }
377             String replacement = String.valueOf(sb.charAt(toBeRemovedPos)).toUpperCase();
378             sb.setCharAt(toBeRemovedPos, replacement.charAt(0));
379             toBeRemovedPos = sb.indexOf(toBeRemoved);
380         }
381         return sb.toString();
382     }
383
384     public static long computeDefaultSUID(final GeneratedTOBuilderImpl to) {
385         try {
386             ByteArrayOutputStream bout = new ByteArrayOutputStream();
387             DataOutputStream dout = new DataOutputStream(bout);
388
389             dout.writeUTF(to.getName());
390             dout.writeInt(to.isAbstract() ? 3 : 7);
391
392             List<Type> impl = to.getImplementsTypes();
393             Collections.sort(impl, new Comparator<Type>() {
394                 @Override
395                 public int compare(final Type o1, final Type o2) {
396                     return o1.getFullyQualifiedName().compareTo(o2.getFullyQualifiedName());
397                 }
398             });
399             for (Type ifc : impl) {
400                 dout.writeUTF(ifc.getFullyQualifiedName());
401             }
402
403             Comparator<TypeMemberBuilder<?>> comparator = new Comparator<TypeMemberBuilder<?>>() {
404                 @Override
405                 public int compare(final TypeMemberBuilder<?> o1, final TypeMemberBuilder<?> o2) {
406                     return o1.getName().compareTo(o2.getName());
407                 }
408             };
409
410             List<GeneratedPropertyBuilder> props = to.getProperties();
411             Collections.sort(props, comparator);
412             for (GeneratedPropertyBuilder gp : props) {
413                 dout.writeUTF(gp.getName());
414             }
415
416             List<MethodSignatureBuilder> methods = to.getMethodDefinitions();
417             Collections.sort(methods, comparator);
418             for (MethodSignatureBuilder m : methods) {
419                 if (!(m.getAccessModifier().equals(AccessModifier.PRIVATE))) {
420                     dout.writeUTF(m.getName());
421                     dout.write(m.getAccessModifier().ordinal());
422                 }
423             }
424
425             dout.flush();
426
427             MessageDigest md = MessageDigest.getInstance("SHA");
428             byte[] hashBytes = md.digest(bout.toByteArray());
429             long hash = 0;
430             for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
431                 hash = (hash << 8) | (hashBytes[i] & 0xFF);
432             }
433             return hash;
434         } catch (IOException ex) {
435             throw new InternalError();
436         } catch (NoSuchAlgorithmException ex) {
437             throw new SecurityException(ex.getMessage());
438         }
439     }
440
441     public static Restrictions getRestrictions(final TypeDefinition<?> type) {
442         final List<LengthConstraint> length = new ArrayList<>();
443         final List<PatternConstraint> pattern = new ArrayList<>();
444         final List<RangeConstraint> range = new ArrayList<>();
445
446         if (type instanceof ExtendedType) {
447             ExtendedType ext = (ExtendedType)type;
448             TypeDefinition<?> base = ext.getBaseType();
449             length.addAll(ext.getLengthConstraints());
450             pattern.addAll(ext.getPatternConstraints());
451             range.addAll(ext.getRangeConstraints());
452
453             if (base instanceof IntegerTypeDefinition && range.isEmpty()) {
454                 range.addAll(((IntegerTypeDefinition)base).getRangeConstraints());
455             } else if (base instanceof UnsignedIntegerTypeDefinition && range.isEmpty()) {
456                 range.addAll(((UnsignedIntegerTypeDefinition)base).getRangeConstraints());
457             } else if (base instanceof DecimalTypeDefinition && range.isEmpty()) {
458                 range.addAll(((DecimalTypeDefinition)base).getRangeConstraints());
459             }
460
461         }
462
463         return new Restrictions() {
464             @Override
465             public List<RangeConstraint> getRangeConstraints() {
466                 return range;
467             }
468             @Override
469             public List<PatternConstraint> getPatternConstraints() {
470                 return pattern;
471             }
472             @Override
473             public List<LengthConstraint> getLengthConstraints() {
474                 return length;
475             }
476             @Override
477             public boolean isEmpty() {
478                 return range.isEmpty() && pattern.isEmpty() && length.isEmpty();
479             }
480         };
481     }
482
483 }