Merge "Improve BaseTemplate speed"
[yangtools.git] / code-generator / binding-java-api-generator / src / main / java / org / opendaylight / yangtools / sal / java / api / generator / BaseTemplate.xtend
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.sal.java.api.generator
9
10 import com.google.common.base.CharMatcher
11 import com.google.common.base.Splitter
12 import com.google.common.collect.ImmutableList
13 import com.google.common.collect.Range
14 import java.math.BigDecimal
15 import java.math.BigInteger
16 import java.util.Arrays
17 import java.util.Collection
18 import java.util.HashMap
19 import java.util.List
20 import java.util.Map
21 import java.util.StringTokenizer
22 import java.util.regex.Pattern
23 import org.opendaylight.yangtools.binding.generator.util.Types
24 import org.opendaylight.yangtools.sal.binding.model.api.ConcreteType
25 import org.opendaylight.yangtools.sal.binding.model.api.GeneratedProperty
26 import org.opendaylight.yangtools.sal.binding.model.api.GeneratedTransferObject
27 import org.opendaylight.yangtools.sal.binding.model.api.GeneratedType
28 import org.opendaylight.yangtools.sal.binding.model.api.MethodSignature
29 import org.opendaylight.yangtools.sal.binding.model.api.Restrictions
30 import org.opendaylight.yangtools.sal.binding.model.api.Type
31
32 abstract class BaseTemplate {
33     protected val GeneratedType type;
34     protected val Map<String, String> importMap;
35
36     private static final char NEW_LINE = '\n'
37     private static final CharMatcher NL_MATCHER = CharMatcher.is(NEW_LINE)
38     private static final CharMatcher TAB_MATCHER = CharMatcher.is('\t')
39     private static final Pattern SPACES_PATTERN = Pattern.compile(" +")
40     private static final Splitter NL_SPLITTER = Splitter.on(NL_MATCHER)
41
42     new(GeneratedType _type) {
43         if (_type == null) {
44             throw new IllegalArgumentException("Generated type reference cannot be NULL!")
45         }
46         this.type = _type;
47         this.importMap = new HashMap<String,String>()
48     }
49
50     def packageDefinition() '''package «type.packageName»;'''
51
52     protected def getFullyQualifiedName() {
53         return type.fullyQualifiedName
54     }
55
56     final public def generate() {
57         val _body = body()
58         '''
59             «packageDefinition»
60             «imports»
61
62             «_body»
63         '''.toString
64     }
65
66     protected def imports() ''' 
67         «IF !importMap.empty»
68             «FOR entry : importMap.entrySet»
69                 «IF entry.value != fullyQualifiedName»
70                     import «entry.value».«entry.key»;
71                 «ENDIF»
72             «ENDFOR»
73         «ENDIF»
74
75     '''
76
77     protected abstract def CharSequence body();
78
79     // Helper patterns
80     final protected def fieldName(GeneratedProperty property) '''_«property.name»'''
81
82     final protected def propertyNameFromGetter(MethodSignature getter) {
83         var int prefix;
84         if (getter.name.startsWith("is")) {
85             prefix = 2
86         } else if (getter.name.startsWith("get")) {
87             prefix = 3
88         } else {
89             throw new IllegalArgumentException("Not a getter")
90         }
91         return getter.name.substring(prefix).toFirstLower;
92     }
93
94     /**
95      * Template method which generates the getter method for <code>field</code>
96      * 
97      * @param field 
98      * generated property with data about field which is generated as the getter method
99      * @return string with the getter method source code in JAVA format 
100      */
101     final protected def getterMethod(GeneratedProperty field) {
102         '''
103             public «field.returnType.importedName» «field.getterMethodName»() {
104                 return «field.fieldName»;
105             }
106         '''
107     }
108
109     final protected def getterMethodName(GeneratedProperty field) {
110         val prefix = if(field.returnType.equals(Types.BOOLEAN)) "is" else "get"
111         return '''«prefix»«field.name.toFirstUpper»'''
112     }
113
114     /**
115      * Template method which generates the setter method for <code>field</code>
116      * 
117      * @param field 
118      * generated property with data about field which is generated as the setter method
119      * @return string with the setter method source code in JAVA format 
120      */
121     final protected def setterMethod(GeneratedProperty field) '''
122         «val returnType = field.returnType.importedName»
123         public «type.name» set«field.name.toFirstUpper»(«returnType» value) {
124             this.«field.fieldName» = value;
125             return this;
126         }
127     '''
128
129     final protected def importedName(Type intype) {
130         GeneratorUtil.putTypeIntoImports(type, intype, importMap);
131         GeneratorUtil.getExplicitType(type, intype, importMap)
132     }
133
134     final protected def importedName(Class<?> cls) {
135         importedName(Types.typeForClass(cls))
136     }
137
138     /**
139      * Template method which generates method parameters with their types from <code>parameters</code>.
140      * 
141      * @param parameters
142      * group of generated property instances which are transformed to the method parameters
143      * @return string with the list of the method parameters with their types in JAVA format
144      */
145     def final protected asArgumentsDeclaration(Iterable<GeneratedProperty> parameters) '''«IF !parameters.empty»«FOR parameter : parameters SEPARATOR ", "»«parameter.
146         returnType.importedName» «parameter.fieldName»«ENDFOR»«ENDIF»'''
147
148     /**
149      * Template method which generates sequence of the names of the class attributes from <code>parameters</code>.
150      * 
151      * @param parameters 
152      * group of generated property instances which are transformed to the sequence of parameter names
153      * @return string with the list of the parameter names of the <code>parameters</code> 
154      */
155     def final protected asArguments(Iterable<GeneratedProperty> parameters) '''«IF !parameters.empty»«FOR parameter : parameters SEPARATOR ", "»«parameter.
156         fieldName»«ENDFOR»«ENDIF»'''
157
158     /**
159      * Template method which generates JAVA comments.
160      * 
161      * @param comment string with the comment for whole JAVA class
162      * @return string with comment in JAVA format
163      */
164     def protected CharSequence asJavadoc(String comment) {
165         if(comment == null) return ''
166         var txt = comment
167
168         txt = comment.trim
169         txt = formatToParagraph(txt)
170
171         return '''
172             «wrapToDocumentation(txt)»
173         '''
174     }
175
176     def String wrapToDocumentation(String text) {
177         if (text.empty)
178             return ""
179
180         val StringBuilder sb = new StringBuilder("/**")
181         sb.append(NEW_LINE)
182
183         for (String t : NL_SPLITTER.split(text)) {
184             sb.append(" * ")
185             sb.append(t)
186             sb.append(NEW_LINE)
187         }
188         sb.append(" */")
189
190         return sb.toString
191     }
192
193     def protected String formatDataForJavaDoc(GeneratedType type) {
194         val typeDescription = type.getDescription().encodeJavadocSymbols;
195
196         return '''
197             «IF !typeDescription.nullOrEmpty»
198             «typeDescription»
199             «ENDIF»
200         '''.toString
201     }
202
203     private static final CharMatcher AMP_MATCHER = CharMatcher.is('&');
204     private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
205     private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
206
207     def encodeJavadocSymbols(String description) {
208         if (description.nullOrEmpty) {
209             return description;
210         }
211
212         var ret = description.replace("*/", "&#42;&#47;")
213
214         // FIXME: Use Guava's HtmlEscapers once we have it available
215         ret = AMP_MATCHER.replaceFrom(ret, "&amp;");
216         ret = GT_MATCHER.replaceFrom(ret, "&gt;");
217         ret = LT_MATCHER.replaceFrom(ret, "&lt;");
218         return ret;
219     }
220
221     def protected String formatDataForJavaDoc(GeneratedType type, String additionalComment) {
222         val StringBuilder typeDescription = new StringBuilder();
223         if (!type.description.nullOrEmpty) {
224             typeDescription.append(type.description)
225             typeDescription.append(NEW_LINE)
226             typeDescription.append(NEW_LINE)
227             typeDescription.append(NEW_LINE)
228             typeDescription.append(additionalComment)
229         } else {
230             typeDescription.append(additionalComment)
231         }
232
233         return '''
234             «typeDescription.toString»
235         '''.toString
236     }
237
238     def asLink(String text) {
239         val StringBuilder sb = new StringBuilder()
240         var tempText = text
241         var char lastChar = ' '
242         var boolean badEnding = false
243
244         if (text.endsWith('.') || text.endsWith(':') || text.endsWith(',')) {
245             tempText = text.substring(0, text.length - 1)
246             lastChar = text.charAt(text.length - 1)
247             badEnding = true
248         }
249         sb.append("<a href = \"")
250         sb.append(tempText)
251         sb.append("\">")
252         sb.append(tempText)
253         sb.append("</a>")
254
255         if(badEnding)
256             sb.append(lastChar)
257
258         return sb.toString
259     }
260
261     protected def formatToParagraph(String text) {
262         if(text == null || text.isEmpty)
263             return text
264
265         var formattedText = text
266         val StringBuilder sb = new StringBuilder();
267         var StringBuilder lineBuilder = new StringBuilder();
268         var boolean isFirstElementOnNewLineEmptyChar = false;
269
270         formattedText = formattedText.encodeJavadocSymbols
271         formattedText = NL_MATCHER.removeFrom(formattedText)
272         formattedText = TAB_MATCHER.removeFrom(formattedText)
273         formattedText = SPACES_PATTERN.matcher(formattedText).replaceAll(" ")
274
275         val StringTokenizer tokenizer = new StringTokenizer(formattedText, " ", true);
276
277         while(tokenizer.hasMoreElements) {
278             val nextElement = tokenizer.nextElement.toString
279
280             if(lineBuilder.length + nextElement.length > 80) {
281                 if (lineBuilder.charAt(lineBuilder.length - 1) == ' ') {
282                     lineBuilder.setLength(0)
283                     lineBuilder.append(lineBuilder.substring(0, lineBuilder.length - 1))
284                 }
285                 if (lineBuilder.charAt(0) == ' ') {
286                     lineBuilder.setLength(0)
287                     lineBuilder.append(lineBuilder.substring(1))
288                 }
289
290                 sb.append(lineBuilder);
291                 lineBuilder.setLength(0)
292                 sb.append(NEW_LINE)
293
294                 if(nextElement.toString == ' ') {
295                     isFirstElementOnNewLineEmptyChar = !isFirstElementOnNewLineEmptyChar;
296                 }
297             }
298
299             if(isFirstElementOnNewLineEmptyChar) {
300                 isFirstElementOnNewLineEmptyChar = !isFirstElementOnNewLineEmptyChar
301             }
302
303             else {
304                 lineBuilder.append(nextElement)
305             }
306         }
307         sb.append(lineBuilder)
308         sb.append(NEW_LINE)
309
310         return sb.toString
311     }
312
313     def isDocumentationParametersNullOrEmtpy(GeneratedType type) {
314         val boolean isTypeDescriptionNullOrEmpty = type.description.nullOrEmpty
315         val boolean isTypeReferenceNullOrEmpty = type.reference.nullOrEmpty
316         val boolean isTypeModuleNameNullOrEmpty = type.moduleName.nullOrEmpty
317         val boolean isTypeSchemaPathNullOrEmpty = type.schemaPath.nullOrEmpty
318
319         if (isTypeDescriptionNullOrEmpty && isTypeReferenceNullOrEmpty && isTypeModuleNameNullOrEmpty
320             && isTypeSchemaPathNullOrEmpty) {
321             return true
322         }
323         return false
324     }
325
326     def generateRestrictions(Type type, String paramName, Type returnType) '''
327         «val restrictions = type.getRestrictions»
328         «IF restrictions !== null»
329             «val boolean isNestedType = !(returnType instanceof ConcreteType)»
330             «IF !restrictions.lengthConstraints.empty»
331                 «generateLengthRestriction(returnType, restrictions, paramName, isNestedType)»
332             «ENDIF»
333             «IF !restrictions.rangeConstraints.empty»
334                 «generateRangeRestriction(returnType, paramName, isNestedType)»
335             «ENDIF»
336         «ENDIF»
337     '''
338
339     def private generateLengthRestriction(Type returnType, Restrictions restrictions, String paramName, boolean isNestedType) '''
340         «val clazz = restrictions.lengthConstraints.iterator.next.min.class»
341         if («paramName» != null) {
342             «printLengthConstraint(returnType, clazz, paramName, isNestedType, returnType.name.contains("["))»
343             boolean isValidLength = false;
344             for («Range.importedName»<«clazz.importedNumber»> r : «IF isNestedType»«returnType.importedName».«ENDIF»length()) {
345                 if (r.contains(_constraint)) {
346                     isValidLength = true;
347                 }
348             }
349             if (!isValidLength) {
350                 throw new IllegalArgumentException(String.format("Invalid length: %s, expected: %s.", «paramName», «IF isNestedType»«returnType.importedName».«ENDIF»length()));
351             }
352         }
353     '''
354
355     def private generateRangeRestriction(Type returnType, String paramName, boolean isNestedType) '''
356         if («paramName» != null) {
357             «printRangeConstraint(returnType, paramName, isNestedType)»
358             boolean isValidRange = false;
359             for («Range.importedName»<«returnType.importedNumber»> r : «IF isNestedType»«returnType.importedName».«ENDIF»range()) {
360                 if (r.contains(_constraint)) {
361                     isValidRange = true;
362                 }
363             }
364             if (!isValidRange) {
365                 throw new IllegalArgumentException(String.format("Invalid range: %s, expected: %s.", «paramName», «IF isNestedType»«returnType.importedName».«ENDIF»range()));
366             }
367         }
368     '''
369
370     /**
371      * Print length constraint.
372      * This should always be a BigInteger (only string and binary can have length restriction)
373      */
374     def printLengthConstraint(Type returnType, Class<? extends Number> clazz, String paramName, boolean isNestedType, boolean isArray) '''
375         «clazz.importedNumber» _constraint = «clazz.importedNumber».valueOf(«paramName»«IF isNestedType».getValue()«ENDIF».length«IF !isArray»()«ENDIF»);
376     '''
377
378     def printRangeConstraint(Type returnType, String paramName, boolean isNestedType) '''
379         «IF BigDecimal.canonicalName.equals(returnType.fullyQualifiedName)»
380             «BigDecimal.importedName» _constraint = new «BigDecimal.importedName»(«paramName»«IF isNestedType».getValue()«ENDIF».toString());
381         «ELSE»
382             «IF isNestedType»
383                 «val propReturnType = findProperty(returnType as GeneratedTransferObject, "value").returnType»
384                 «IF propReturnType.fullyQualifiedName.equals(BigInteger.canonicalName)»
385                     «BigInteger.importedName» _constraint = «paramName».getValue();
386                 «ELSE»
387                     «BigInteger.importedName» _constraint = «BigInteger.importedName».valueOf(«paramName».getValue());
388                 «ENDIF»
389             «ELSE»
390                 «IF returnType.fullyQualifiedName.equals(BigInteger.canonicalName)»
391                     «BigInteger.importedName» _constraint = «paramName»;
392                 «ELSE»
393                     «BigInteger.importedName» _constraint = «BigInteger.importedName».valueOf(«paramName»);
394                 «ENDIF»
395             «ENDIF»
396         «ENDIF»
397     '''
398
399     def protected generateToString(Collection<GeneratedProperty> properties) '''
400         «IF !properties.empty»
401             @Override
402             public «String.importedName» toString() {
403                 «StringBuilder.importedName» builder = new «StringBuilder.importedName»(«type.importedName».class.getSimpleName()).append(" [");
404                 boolean first = true;
405
406                 «FOR property : properties»
407                     if («property.fieldName» != null) {
408                         if (first) {
409                             first = false;
410                         } else {
411                             builder.append(", ");
412                         }
413                         builder.append("«property.fieldName»=");
414                         «IF property.returnType.name.contains("[")»
415                             builder.append(«Arrays.importedName».toString(«property.fieldName»));
416                         «ELSE»
417                             builder.append(«property.fieldName»);
418                         «ENDIF»
419                      }
420                 «ENDFOR»
421                 return builder.append(']').toString();
422             }
423         «ENDIF»
424     '''
425
426     def getRestrictions(Type type) {
427         var Restrictions restrictions = null
428         if (type instanceof ConcreteType) {
429             restrictions = (type as ConcreteType).restrictions
430         } else if (type instanceof GeneratedTransferObject) {
431             restrictions = (type as GeneratedTransferObject).restrictions
432         }
433         return restrictions
434     }
435
436     def boolean isArrayType(GeneratedTransferObject type) {
437         var isArray = false
438         val GeneratedProperty value = findProperty(type, "value")
439         if (value != null && value.returnType.name.contains("[")) {
440             isArray = true
441         }
442         return isArray
443     }
444
445     def String toQuote(Object obj) {
446         return "\"" + obj.toString + "\"";
447     }
448
449     /**
450      * Template method which generates method parameters with their types from <code>parameters</code>.
451      * 
452      * @param parameters
453      * list of parameter instances which are transformed to the method parameters
454      * @return string with the list of the method parameters with their types in JAVA format
455      */
456     def protected generateParameters(List<MethodSignature.Parameter> parameters) '''«
457         IF !parameters.empty»«
458             FOR parameter : parameters SEPARATOR ", "»«
459                 parameter.type.importedName» «parameter.name»«
460             ENDFOR»«
461         ENDIF
462     »'''
463
464     def protected generateLengthMethod(String methodName, Type type, String className, String varName) '''
465         «val Restrictions restrictions = type.restrictions»
466         «IF restrictions != null && !(restrictions.lengthConstraints.empty)»
467             «val numberClass = restrictions.lengthConstraints.iterator.next.min.class»
468             public static «List.importedName»<«Range.importedName»<«numberClass.importedNumber»>> «methodName»() {
469                 «IF numberClass.equals(typeof(BigDecimal))»
470                     «lengthBody(restrictions, numberClass, className, varName)»
471                 «ELSE»
472                     «lengthBody(restrictions, typeof(BigInteger), className, varName)»
473                 «ENDIF»
474             }
475         «ENDIF»
476     '''
477
478     def private lengthBody(Restrictions restrictions, Class<? extends Number> numberClass, String className, String varName) '''
479         if («varName» == null) {
480             synchronized («className».class) {
481                 if («varName» == null) {
482                     «ImmutableList.importedName».Builder<«Range.importedName»<«numberClass.importedName»>> builder = «ImmutableList.importedName».builder();
483                     «FOR r : restrictions.lengthConstraints»
484                         builder.add(«Range.importedName».closed(«numericValue(numberClass, r.min)», «numericValue(numberClass, r.max)»));
485                     «ENDFOR»
486                     «varName» = builder.build();
487                 }
488             }
489         }
490         return «varName»;
491     '''
492
493     def protected generateRangeMethod(String methodName, Restrictions restrictions, Type returnType, String className, String varName) '''
494         «IF restrictions != null && !(restrictions.rangeConstraints.empty)»
495             «val number = returnType.importedNumber»
496             public static «List.importedName»<«Range.importedName»<«number»>> «methodName»() {
497                 «IF returnType.fullyQualifiedName.equals(BigDecimal.canonicalName)»
498                     «rangeBody(restrictions, BigDecimal, className, varName)»
499                 «ELSE»
500                     «rangeBody(restrictions, BigInteger, className, varName)»
501                 «ENDIF»
502             }
503         «ENDIF»
504     '''
505
506     def protected generateRangeMethod(String methodName, Restrictions restrictions, String className, String varName, Iterable<GeneratedProperty> properties) '''
507         «IF restrictions != null && !(restrictions.rangeConstraints.empty)»
508             «val returnType = properties.iterator.next.returnType»
509             public static «List.importedName»<«Range.importedName»<«returnType.importedNumber»>> «methodName»() {
510                 «IF returnType.fullyQualifiedName.equals(BigDecimal.canonicalName)»
511                     «rangeBody(restrictions, BigDecimal, className, varName)»
512                 «ELSE»
513                     «rangeBody(restrictions, BigInteger, className, varName)»
514                 «ENDIF»
515             }
516         «ENDIF»
517     '''
518
519     def private rangeBody(Restrictions restrictions, Class<? extends Number> numberClass, String className, String varName) '''
520         if («varName» == null) {
521             synchronized («className».class) {
522                 if («varName» == null) {
523                     «ImmutableList.importedName».Builder<«Range.importedName»<«numberClass.importedName»>> builder = «ImmutableList.importedName».builder();
524                     «FOR r : restrictions.rangeConstraints»
525                         builder.add(«Range.importedName».closed(«numericValue(numberClass, r.min)», «numericValue(numberClass, r.max)»));
526                     «ENDFOR»
527                     «varName» = builder.build();
528                 }
529             }
530         }
531         return «varName»;
532     '''
533
534     def protected String importedNumber(Class<? extends Number> clazz) {
535         if (clazz.equals(typeof(BigDecimal))) {
536             return BigDecimal.importedName
537         }
538         return BigInteger.importedName
539     }
540
541     def protected String importedNumber(Type clazz) {
542         if (clazz.fullyQualifiedName.equals(BigDecimal.canonicalName)) {
543             return BigDecimal.importedName
544         }
545         return BigInteger.importedName
546     }
547
548     def protected String numericValue(Class<? extends Number> clazz, Object numberValue) {
549         val number = clazz.importedName;
550         val value = numberValue.toString
551         if (clazz.equals(typeof(BigInteger)) || clazz.equals(typeof(BigDecimal))) {
552             if (value.equals("0")) {
553                 return number + ".ZERO"
554             } else if (value.equals("1")) {
555                 return number + ".ONE"
556             } else if (value.equals("10")) {
557                 return number + ".TEN"
558             } else {
559                 try {
560                     val Long longVal = Long.valueOf(value)
561                     return number + ".valueOf(" + longVal + "L)"
562                 } catch (NumberFormatException e) {
563                     if (clazz.equals(typeof(BigDecimal))) {
564                         try {
565                             val Double doubleVal = Double.valueOf(value);
566                             return number + ".valueOf(" + doubleVal + ")"
567                         } catch (NumberFormatException e2) {
568                         }
569                     }
570                 }
571             }
572         }
573         return "new " + number + "(\"" + value + "\")"
574     }
575
576     def private GeneratedProperty findProperty(GeneratedTransferObject gto, String name) {
577         val props = gto.properties
578         for (prop : props) {
579             if (prop.name.equals(name)) {
580                 return prop
581             }
582         }
583         val GeneratedTransferObject parent = gto.superType
584         if (parent != null) {
585             return findProperty(parent, name)
586         }
587         return null
588     }
589
590 }