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