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