Fix YANG snippet escaping
[mdsal.git] / binding / mdsal-binding-java-api-generator / src / main / java / org / opendaylight / mdsal / binding / 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.mdsal.binding.java.api.generator
9
10 import static org.opendaylight.mdsal.binding.model.util.BindingGeneratorUtil.encodeAngleBrackets
11
12 import com.google.common.base.CharMatcher
13 import com.google.common.base.MoreObjects
14 import com.google.common.base.Splitter
15 import com.google.common.collect.Iterables
16 import java.util.Collection
17 import java.util.List
18 import java.util.Locale
19 import java.util.Map.Entry
20 import java.util.StringTokenizer
21 import java.util.regex.Pattern
22 import org.opendaylight.mdsal.binding.model.api.AnnotationType
23 import org.opendaylight.mdsal.binding.model.api.ConcreteType
24 import org.opendaylight.mdsal.binding.model.api.Constant
25 import org.opendaylight.mdsal.binding.model.api.GeneratedProperty
26 import org.opendaylight.mdsal.binding.model.api.GeneratedType
27 import org.opendaylight.mdsal.binding.model.api.JavaTypeName
28 import org.opendaylight.mdsal.binding.model.api.MethodSignature
29 import org.opendaylight.mdsal.binding.model.api.Restrictions
30 import org.opendaylight.mdsal.binding.model.api.Type
31 import org.opendaylight.mdsal.binding.model.api.TypeMember
32 import org.opendaylight.mdsal.binding.model.api.YangSourceDefinition.Single
33 import org.opendaylight.mdsal.binding.model.api.YangSourceDefinition.Multiple
34 import org.opendaylight.mdsal.binding.model.util.BindingGeneratorUtil
35 import org.opendaylight.mdsal.binding.model.util.TypeConstants
36 import org.opendaylight.mdsal.binding.model.util.Types
37 import org.opendaylight.mdsal.binding.spec.naming.BindingMapping
38 import org.opendaylight.yangtools.yang.binding.CodeHelpers
39 import org.opendaylight.yangtools.yang.common.QName
40 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode
41 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode
42 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition
43 import org.opendaylight.yangtools.yang.model.api.RpcDefinition
44 import org.opendaylight.yangtools.yang.model.api.SchemaNode
45 import org.opendaylight.yangtools.yang.model.api.YangStmtMapping
46 import org.opendaylight.yangtools.yang.model.api.meta.DeclaredStatement
47 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement
48 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement
49 import org.opendaylight.yangtools.yang.model.export.DeclaredStatementFormatter
50
51 abstract class BaseTemplate extends JavaFileTemplate {
52     static final char NEW_LINE = '\n'
53     static final char SPACE = ' '
54     static val AMP_MATCHER = CharMatcher.is('&')
55     static val WS_MATCHER = CharMatcher.anyOf("\n\t")
56     static val SPACES_PATTERN = Pattern.compile(" +")
57     static val NL_SPLITTER = Splitter.on(NEW_LINE)
58     static val TAIL_COMMENT_PATTERN = Pattern.compile("*/", Pattern.LITERAL);
59     static val YANG_FORMATTER = DeclaredStatementFormatter.builder()
60         .addIgnoredStatement(YangStmtMapping.CONTACT)
61         .addIgnoredStatement(YangStmtMapping.DESCRIPTION)
62         .addIgnoredStatement(YangStmtMapping.REFERENCE)
63         .addIgnoredStatement(YangStmtMapping.ORGANIZATION)
64         .build();
65
66     new(GeneratedType type) {
67         super(type)
68     }
69
70     new(AbstractJavaGeneratedType javaType, GeneratedType type) {
71         super(javaType, type)
72     }
73
74     final def generate() {
75         val _body = body()
76         '''
77             package «type.packageName»;
78             «generateImportBlock»
79
80             «_body»
81         '''.toString
82     }
83
84     protected abstract def CharSequence body();
85
86     // Helper patterns
87     final protected def fieldName(GeneratedProperty property) '''_«property.name»'''
88
89     final protected def propertyNameFromGetter(MethodSignature getter) {
90         var String prefix;
91         if (getter.name.startsWith(BindingMapping.BOOLEAN_GETTER_PREFIX)) {
92             prefix = BindingMapping.BOOLEAN_GETTER_PREFIX
93         } else if (getter.name.startsWith(BindingMapping.GETTER_PREFIX)) {
94             prefix = BindingMapping.GETTER_PREFIX
95         } else if (getter.name.startsWith(BindingMapping.NONNULL_PREFIX)) {
96             prefix = BindingMapping.NONNULL_PREFIX
97         } else {
98             throw new IllegalArgumentException(getter + " is not a getter")
99         }
100         return getter.name.substring(prefix.length).toFirstLower;
101     }
102
103     /**
104      * Template method which generates the getter method for <code>field</code>
105      *
106      * @param field
107      * generated property with data about field which is generated as the getter method
108      * @return string with the getter method source code in JAVA format
109      */
110     protected def getterMethod(GeneratedProperty field) {
111         '''
112             public «field.returnType.importedName» «field.getterMethodName»() {
113                 «val fieldName = field.fieldName»
114                 «IF field.returnType.name.endsWith("[]")»
115                 return «fieldName» == null ? null : «fieldName».clone();
116                 «ELSE»
117                 return «fieldName»;
118                 «ENDIF»
119             }
120         '''
121     }
122
123     final protected def getterMethodName(GeneratedProperty field) {
124         val prefix = if(field.returnType.equals(Types.BOOLEAN)) "is" else "get"
125         return '''«prefix»«field.name.toFirstUpper»'''
126     }
127
128     /**
129      * Template method which generates the setter method for <code>field</code>
130      *
131      * @param field
132      * generated property with data about field which is generated as the setter method
133      * @return string with the setter method source code in JAVA format
134      */
135     final protected def setterMethod(GeneratedProperty field) '''
136         «val returnType = field.returnType.importedName»
137         public «type.name» set«field.name.toFirstUpper»(«returnType» value) {
138             this.«field.fieldName» = value;
139             return this;
140         }
141     '''
142
143     /**
144      * Template method which generates method parameters with their types from <code>parameters</code>.
145      *
146      * @param parameters
147      * group of generated property instances which are transformed to the method parameters
148      * @return string with the list of the method parameters with their types in JAVA format
149      */
150     def final protected asArgumentsDeclaration(Iterable<GeneratedProperty> parameters) '''«IF !parameters.empty»«FOR parameter : parameters SEPARATOR ", "»«parameter.
151         returnType.importedName» «parameter.fieldName»«ENDFOR»«ENDIF»'''
152
153     /**
154      * Template method which generates sequence of the names of the class attributes from <code>parameters</code>.
155      *
156      * @param parameters
157      * group of generated property instances which are transformed to the sequence of parameter names
158      * @return string with the list of the parameter names of the <code>parameters</code>
159      */
160     def final protected asArguments(Collection<GeneratedProperty> parameters) '''«IF !parameters.empty»«FOR parameter : parameters SEPARATOR ", "»«parameter.
161         fieldName»«ENDFOR»«ENDIF»'''
162
163     /**
164      * Template method which generates JAVA comments.
165      *
166      * @param comment string with the comment for whole JAVA class
167      * @return string with comment in JAVA format
168      */
169     def protected CharSequence asJavadoc(String comment) {
170         if (comment === null) {
171             return ''
172         }
173         return '''
174             «wrapToDocumentation(formatToParagraph(comment.trim))»
175         '''
176     }
177
178     def static String wrapToDocumentation(String text) {
179         if (text.empty)
180             return ""
181
182         val StringBuilder sb = new StringBuilder().append("/**\n")
183         for (String t : NL_SPLITTER.split(text)) {
184             sb.append(" *")
185             if (!t.isEmpty()) {
186                 sb.append(SPACE).append(t)
187             }
188             sb.append(NEW_LINE)
189         }
190         sb.append(" */")
191
192         return sb.toString
193     }
194
195     def protected String formatDataForJavaDoc(GeneratedType type) {
196         val sb = new StringBuilder()
197         val comment = type.comment
198         if (comment !== null) {
199             sb.append(comment.javadoc)
200         }
201
202         appendSnippet(sb, type)
203
204         return '''
205             «IF sb.length != 0»
206             «sb.toString»
207             «ENDIF»
208         '''.toString
209     }
210
211     def static encodeJavadocSymbols(String description) {
212         if (description.nullOrEmpty) {
213             return description;
214         }
215
216         return TAIL_COMMENT_PATTERN.matcher(AMP_MATCHER.replaceFrom(description, "&amp;")).replaceAll("&#42;&#47;")
217     }
218
219     def protected String formatDataForJavaDoc(GeneratedType type, String additionalComment) {
220         val comment = type.comment
221         if (comment === null) {
222             return '''
223                 «additionalComment»
224             '''
225         }
226
227         val sb = new StringBuilder().append(comment.javadoc)
228         appendSnippet(sb, type)
229
230         sb.append(NEW_LINE)
231         .append(NEW_LINE)
232         .append(NEW_LINE)
233         .append(additionalComment)
234
235         return '''
236             «sb.toString»
237         '''
238     }
239
240     def private static void appendSnippet(StringBuilder sb, GeneratedType type) {
241         val optDef = type.yangSourceDefinition
242         if (optDef.present) {
243             val def = optDef.get
244             sb.append(NEW_LINE)
245
246             if (def instanceof Single) {
247                 val node = def.node
248                 sb.append("<p>\n")
249                 .append("This class represents the following YANG schema fragment defined in module <b>")
250                 .append(def.module.argument).append("</b>\n")
251                 .append("<pre>\n")
252                 appendYangSnippet(sb, def.module, (node as EffectiveStatement<?, ?>).declared)
253                 sb.append("</pre>")
254
255                 if (node instanceof SchemaNode) {
256                     sb.append("The schema path to identify an instance is\n")
257                     .append("<i>")
258                     .append(formatSchemaPath(def.module.argument, node.path.pathFromRoot))
259                     .append("</i>\n")
260
261                     if (hasBuilderClass(node)) {
262                         val builderName = type.name + "Builder";
263
264                         sb.append("\n<p>To create instances of this class use {@link ").append(builderName)
265                         .append("}.\n")
266                         .append("@see ").append(builderName).append('\n')
267                         if (node instanceof ListSchemaNode) {
268                             val keyDef = node.keyDefinition
269                             if (keyDef !== null && !keyDef.empty) {
270                                 sb.append("@see ").append(type.name).append("Key")
271                             }
272                             sb.append('\n');
273                         }
274                     }
275                 }
276             } else if (def instanceof Multiple) {
277                 sb.append("<pre>\n")
278                 for (SchemaNode node : def.nodes) {
279                     appendYangSnippet(sb, def.module, (node as EffectiveStatement<?, ?>).declared)
280                 }
281                 sb.append("</pre>\n")
282             }
283         }
284     }
285
286     def private static void appendYangSnippet(StringBuilder sb, ModuleEffectiveStatement module,
287             DeclaredStatement<?> stmt) {
288         for (String str : YANG_FORMATTER.toYangTextSnippet(module, stmt)) {
289             sb.append(BindingGeneratorUtil.replaceAllIllegalChars(encodeAngleBrackets(encodeJavadocSymbols(str))))
290         }
291     }
292
293     def private static boolean hasBuilderClass(SchemaNode schemaNode) {
294         return schemaNode instanceof ContainerSchemaNode || schemaNode instanceof ListSchemaNode
295                 || schemaNode instanceof RpcDefinition || schemaNode instanceof NotificationDefinition;
296     }
297
298     def private static String formatSchemaPath(String moduleName, Iterable<QName> schemaPath) {
299         val sb = new StringBuilder().append(moduleName);
300
301         var currentElement = Iterables.getFirst(schemaPath, null);
302         for (QName pathElement : schemaPath) {
303             sb.append('/')
304             if (!currentElement.namespace.equals(pathElement.namespace)) {
305                 currentElement = pathElement
306                 sb.append(pathElement)
307             } else {
308                 sb.append(pathElement.getLocalName())
309             }
310         }
311         return sb.toString();
312     }
313
314     def protected static String formatDataForJavaDoc(TypeMember type, String additionalComment) {
315         val StringBuilder typeDescriptionBuilder = new StringBuilder();
316         if (!type.comment.nullOrEmpty) {
317             typeDescriptionBuilder.append(formatToParagraph(type.comment))
318             typeDescriptionBuilder.append(NEW_LINE)
319             typeDescriptionBuilder.append(NEW_LINE)
320             typeDescriptionBuilder.append(NEW_LINE)
321         }
322         typeDescriptionBuilder.append(additionalComment)
323         var typeDescription = wrapToDocumentation(typeDescriptionBuilder.toString)
324         return '''
325             «typeDescription»
326         '''.toString
327     }
328
329     def asCode(String text) {
330         return "<code>" + text + "</code>"
331     }
332
333     def asLink(String text) {
334         val StringBuilder sb = new StringBuilder()
335         var tempText = text
336         var char lastChar = SPACE
337         var boolean badEnding = false
338
339         if (text.endsWith('.') || text.endsWith(':') || text.endsWith(',')) {
340             tempText = text.substring(0, text.length - 1)
341             lastChar = text.charAt(text.length - 1)
342             badEnding = true
343         }
344         sb.append("<a href = \"")
345         sb.append(tempText)
346         sb.append("\">")
347         sb.append(tempText)
348         sb.append("</a>")
349
350         if(badEnding)
351             sb.append(lastChar)
352
353         return sb.toString
354     }
355
356     protected static def formatToParagraph(String text) {
357         if(text === null || text.isEmpty)
358             return text
359
360         var formattedText = text
361         val StringBuilder sb = new StringBuilder();
362         var StringBuilder lineBuilder = new StringBuilder();
363         var boolean isFirstElementOnNewLineEmptyChar = false;
364
365         formattedText = encodeJavadocSymbols(formattedText)
366         formattedText = WS_MATCHER.replaceFrom(formattedText, SPACE)
367         formattedText = SPACES_PATTERN.matcher(formattedText).replaceAll(" ")
368
369         val StringTokenizer tokenizer = new StringTokenizer(formattedText, " ", true);
370
371         while (tokenizer.hasMoreTokens) {
372             val nextElement = tokenizer.nextToken
373
374             if (lineBuilder.length != 0 && lineBuilder.length + nextElement.length > 80) {
375                 if (lineBuilder.charAt(lineBuilder.length - 1) == SPACE) {
376                     lineBuilder.setLength(lineBuilder.length - 1)
377                 }
378                 if (lineBuilder.length != 0 && lineBuilder.charAt(0) == SPACE) {
379                     lineBuilder.deleteCharAt(0)
380                 }
381
382                 sb.append(lineBuilder).append(NEW_LINE)
383                 lineBuilder.setLength(0)
384
385                 if (nextElement == " ") {
386                     isFirstElementOnNewLineEmptyChar = !isFirstElementOnNewLineEmptyChar;
387                 }
388             }
389
390             if (isFirstElementOnNewLineEmptyChar) {
391                 isFirstElementOnNewLineEmptyChar = !isFirstElementOnNewLineEmptyChar
392             } else {
393                 lineBuilder.append(nextElement)
394             }
395         }
396
397         return sb.append(lineBuilder).append(NEW_LINE).toString
398     }
399
400     def protected generateToString(Collection<GeneratedProperty> properties) '''
401         «IF !properties.empty»
402             @«Override.importedName»
403             public «String.importedName» toString() {
404                 final «MoreObjects.importedName».ToStringHelper helper = «MoreObjects.importedName».toStringHelper(«type.importedName».class);
405                 «FOR property : properties»
406                     «CodeHelpers.importedName».appendValue(helper, "«property.fieldName»", «property.fieldName»);
407                 «ENDFOR»
408                 return helper.toString();
409             }
410         «ENDIF»
411     '''
412
413     /**
414      * Template method which generates method parameters with their types from <code>parameters</code>.
415      *
416      * @param parameters
417      * list of parameter instances which are transformed to the method parameters
418      * @return string with the list of the method parameters with their types in JAVA format
419      */
420     def protected generateParameters(List<MethodSignature.Parameter> parameters) '''«
421         IF !parameters.empty»«
422             FOR parameter : parameters SEPARATOR ", "»«
423                 parameter.type.importedName» «parameter.name»«
424             ENDFOR»«
425         ENDIF
426     »'''
427
428     def protected emitConstant(Constant c) '''
429         «IF BindingMapping.QNAME_STATIC_FIELD_NAME.equals(c.name)»
430             «val entry = c.value as Entry<JavaTypeName, String>»
431             public static final «c.type.importedNonNull» «c.name» = «entry.key.importedName».«BindingMapping.MODULE_INFO_QNAMEOF_METHOD_NAME»("«entry.value»");
432         «ELSE»
433             public static final «c.type.importedName» «c.name» = «c.value»;
434         «ENDIF»
435     '''
436
437     def protected generateCheckers(GeneratedProperty field, Restrictions restrictions, Type actualType) '''
438        «IF restrictions.rangeConstraint.present»
439            «AbstractRangeGenerator.forType(actualType).generateRangeChecker(field.name.toFirstUpper,
440                restrictions.rangeConstraint.get, this)»
441        «ENDIF»
442        «IF restrictions.lengthConstraint.present»
443            «LengthGenerator.generateLengthChecker(field.fieldName.toString, actualType, restrictions.lengthConstraint.get, this)»
444        «ENDIF»
445     '''
446
447     def protected checkArgument(GeneratedProperty property, Restrictions restrictions, Type actualType, String value) '''
448        «IF restrictions.getRangeConstraint.isPresent»
449            «IF actualType instanceof ConcreteType»
450                «AbstractRangeGenerator.forType(actualType).generateRangeCheckerCall(property.getName.toFirstUpper, value)»
451            «ELSE»
452                «AbstractRangeGenerator.forType(actualType).generateRangeCheckerCall(property.getName.toFirstUpper, value + ".getValue()")»
453            «ENDIF»
454        «ENDIF»
455        «val fieldName = property.fieldName»
456        «IF restrictions.getLengthConstraint.isPresent»
457            «IF actualType instanceof ConcreteType»
458                «LengthGenerator.generateLengthCheckerCall(fieldName.toString, value)»
459            «ELSE»
460                «LengthGenerator.generateLengthCheckerCall(fieldName.toString, value + ".getValue()")»
461            «ENDIF»
462        «ENDIF»
463
464        «val fieldUpperCase = fieldName.toString.toUpperCase(Locale.ENGLISH)»
465        «FOR currentConstant : type.getConstantDefinitions»
466            «IF currentConstant.getName.startsWith(TypeConstants.PATTERN_CONSTANT_NAME)
467                && fieldUpperCase.equals(currentConstant.getName.substring(TypeConstants.PATTERN_CONSTANT_NAME.length))»
468            «CodeHelpers.importedName».checkPattern(value, «Constants.MEMBER_PATTERN_LIST»«fieldName», «Constants.MEMBER_REGEX_LIST»«fieldName»);
469            «ENDIF»
470        «ENDFOR»
471     '''
472
473     def protected hashCodeResult(Collection<GeneratedProperty> properties) '''
474         final int prime = 31;
475         int result = 1;
476         «FOR property : properties»
477             result = prime * result + «property.importedUtilClass».hashCode(«property.fieldName»);
478         «ENDFOR»
479     '''
480
481     def protected final generateAnnotation(AnnotationType annotation) '''
482         @«annotation.importedName»
483         «IF annotation.parameters !== null && !annotation.parameters.empty»
484         (
485         «FOR param : annotation.parameters SEPARATOR ","»
486             «param.name»=«param.value»
487         «ENDFOR»
488         )
489         «ENDIF»
490     '''
491 }