BUG-1485: Move restriction generation
[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     /**
332      * Print length constraint.
333      * This should always be a BigInteger (only string and binary can have length restriction)
334      */
335     def printLengthConstraint(Type returnType, Class<? extends Number> clazz, String paramName, boolean isNestedType, boolean isArray) '''
336         «clazz.importedNumber» _constraint = «clazz.importedNumber».valueOf(«paramName»«IF isNestedType».getValue()«ENDIF».length«IF !isArray»()«ENDIF»);
337     '''
338
339     def printRangeConstraint(Type returnType, String paramName, boolean isNestedType) '''
340         «IF BigDecimal.canonicalName.equals(returnType.fullyQualifiedName)»
341             «BigDecimal.importedName» _constraint = new «BigDecimal.importedName»(«paramName»«IF isNestedType».getValue()«ENDIF».toString());
342         «ELSE»
343             «IF isNestedType»
344                 «val propReturnType = findProperty(returnType as GeneratedTransferObject, "value").returnType»
345                 «IF propReturnType.fullyQualifiedName.equals(BigInteger.canonicalName)»
346                     «BigInteger.importedName» _constraint = «paramName».getValue();
347                 «ELSE»
348                     «BigInteger.importedName» _constraint = «BigInteger.importedName».valueOf(«paramName».getValue());
349                 «ENDIF»
350             «ELSE»
351                 «IF returnType.fullyQualifiedName.equals(BigInteger.canonicalName)»
352                     «BigInteger.importedName» _constraint = «paramName»;
353                 «ELSE»
354                     «BigInteger.importedName» _constraint = «BigInteger.importedName».valueOf(«paramName»);
355                 «ENDIF»
356             «ENDIF»
357         «ENDIF»
358     '''
359
360     def protected generateToString(Collection<GeneratedProperty> properties) '''
361         «IF !properties.empty»
362             @Override
363             public «String.importedName» toString() {
364                 «StringBuilder.importedName» builder = new «StringBuilder.importedName»(«type.importedName».class.getSimpleName()).append(" [");
365                 boolean first = true;
366
367                 «FOR property : properties»
368                     if («property.fieldName» != null) {
369                         if (first) {
370                             first = false;
371                         } else {
372                             builder.append(", ");
373                         }
374                         builder.append("«property.fieldName»=");
375                         «IF property.returnType.name.contains("[")»
376                             builder.append(«Arrays.importedName».toString(«property.fieldName»));
377                         «ELSE»
378                             builder.append(«property.fieldName»);
379                         «ENDIF»
380                      }
381                 «ENDFOR»
382                 return builder.append(']').toString();
383             }
384         «ENDIF»
385     '''
386
387     def getRestrictions(Type type) {
388         var Restrictions restrictions = null
389         if (type instanceof ConcreteType) {
390             restrictions = type.restrictions
391         } else if (type instanceof GeneratedTransferObject) {
392             restrictions = type.restrictions
393         }
394         return restrictions
395     }
396
397     def boolean isArrayType(GeneratedTransferObject type) {
398         var isArray = false
399         val GeneratedProperty value = findProperty(type, "value")
400         if (value != null && value.returnType.name.contains("[")) {
401             isArray = true
402         }
403         return isArray
404     }
405
406     def String toQuote(Object obj) {
407         return "\"" + obj.toString + "\"";
408     }
409
410     /**
411      * Template method which generates method parameters with their types from <code>parameters</code>.
412      *
413      * @param parameters
414      * list of parameter instances which are transformed to the method parameters
415      * @return string with the list of the method parameters with their types in JAVA format
416      */
417     def protected generateParameters(List<MethodSignature.Parameter> parameters) '''«
418         IF !parameters.empty»«
419             FOR parameter : parameters SEPARATOR ", "»«
420                 parameter.type.importedName» «parameter.name»«
421             ENDFOR»«
422         ENDIF
423     »'''
424
425     def protected generateLengthMethod(String methodName, Type type, String className, String varName) '''
426         «val Restrictions restrictions = type.restrictions»
427         «IF restrictions != null && !(restrictions.lengthConstraints.empty)»
428             «val numberClass = restrictions.lengthConstraints.iterator.next.min.class»
429             public static «List.importedName»<«Range.importedName»<«numberClass.importedNumber»>> «methodName»() {
430                 «IF numberClass.equals(typeof(BigDecimal))»
431                     «lengthBody(restrictions, numberClass, className, varName)»
432                 «ELSE»
433                     «lengthBody(restrictions, typeof(BigInteger), className, varName)»
434                 «ENDIF»
435             }
436         «ENDIF»
437     '''
438
439     def private lengthBody(Restrictions restrictions, Class<? extends Number> numberClass, String className, String varName) '''
440         if («varName» == null) {
441             synchronized («className».class) {
442                 if («varName» == null) {
443                     «ImmutableList.importedName».Builder<«Range.importedName»<«numberClass.importedName»>> builder = «ImmutableList.importedName».builder();
444                     «FOR r : restrictions.lengthConstraints»
445                         builder.add(«Range.importedName».closed(«numericValue(numberClass, r.min)», «numericValue(numberClass, r.max)»));
446                     «ENDFOR»
447                     «varName» = builder.build();
448                 }
449             }
450         }
451         return «varName»;
452     '''
453
454     def protected generateRangeMethod(String methodName, Restrictions restrictions, Type returnType, String className, String varName) '''
455         «IF restrictions != null && !(restrictions.rangeConstraints.empty)»
456             «val number = returnType.importedNumber»
457             public static «List.importedName»<«Range.importedName»<«number»>> «methodName»() {
458                 «IF returnType.fullyQualifiedName.equals(BigDecimal.canonicalName)»
459                     «rangeBody(restrictions, BigDecimal, className, varName)»
460                 «ELSE»
461                     «rangeBody(restrictions, BigInteger, className, varName)»
462                 «ENDIF»
463             }
464         «ENDIF»
465     '''
466
467     def protected generateRangeMethod(String methodName, Restrictions restrictions, String className, String varName, Iterable<GeneratedProperty> properties) '''
468         «IF restrictions != null && !(restrictions.rangeConstraints.empty)»
469             «val returnType = properties.iterator.next.returnType»
470             public static «List.importedName»<«Range.importedName»<«returnType.importedNumber»>> «methodName»() {
471                 «IF returnType.fullyQualifiedName.equals(BigDecimal.canonicalName)»
472                     «rangeBody(restrictions, BigDecimal, className, varName)»
473                 «ELSE»
474                     «rangeBody(restrictions, BigInteger, className, varName)»
475                 «ENDIF»
476             }
477         «ENDIF»
478     '''
479
480     def private rangeBody(Restrictions restrictions, Class<? extends Number> numberClass, String className, String varName) '''
481         if («varName» == null) {
482             synchronized («className».class) {
483                 if («varName» == null) {
484                     «ImmutableList.importedName».Builder<«Range.importedName»<«numberClass.importedName»>> builder = «ImmutableList.importedName».builder();
485                     «FOR r : restrictions.rangeConstraints»
486                         builder.add(«Range.importedName».closed(«numericValue(numberClass, r.min)», «numericValue(numberClass, r.max)»));
487                     «ENDFOR»
488                     «varName» = builder.build();
489                 }
490             }
491         }
492         return «varName»;
493     '''
494
495     def protected String importedNumber(Class<? extends Number> clazz) {
496         if (clazz.equals(typeof(BigDecimal))) {
497             return BigDecimal.importedName
498         }
499         return BigInteger.importedName
500     }
501
502     def protected String importedNumber(Type clazz) {
503         if (clazz.fullyQualifiedName.equals(BigDecimal.canonicalName)) {
504             return BigDecimal.importedName
505         }
506         return BigInteger.importedName
507     }
508
509     def protected String numericValue(Class<? extends Number> clazz, Object numberValue) {
510         val number = clazz.importedName;
511         val value = numberValue.toString
512         if (clazz.equals(typeof(BigInteger)) || clazz.equals(typeof(BigDecimal))) {
513             if (value.equals("0")) {
514                 return number + ".ZERO"
515             } else if (value.equals("1")) {
516                 return number + ".ONE"
517             } else if (value.equals("10")) {
518                 return number + ".TEN"
519             } else {
520                 try {
521                     val Long longVal = Long.valueOf(value)
522                     return number + ".valueOf(" + longVal + "L)"
523                 } catch (NumberFormatException e) {
524                     if (clazz.equals(typeof(BigDecimal))) {
525                         try {
526                             val Double doubleVal = Double.valueOf(value);
527                             return number + ".valueOf(" + doubleVal + ")"
528                         } catch (NumberFormatException e2) {
529                         }
530                     }
531                 }
532             }
533         }
534         return "new " + number + "(\"" + value + "\")"
535     }
536
537     def private GeneratedProperty findProperty(GeneratedTransferObject gto, String name) {
538         val props = gto.properties
539         for (prop : props) {
540             if (prop.name.equals(name)) {
541                 return prop
542             }
543         }
544         val GeneratedTransferObject parent = gto.superType
545         if (parent != null) {
546             return findProperty(parent, name)
547         }
548         return null
549     }
550
551 }