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