Binding v2 - Use YangTextSnippet for generation
[mdsal.git] / binding2 / mdsal-binding2-java-api-generator / src / main / java / org / opendaylight / mdsal / binding / javav2 / java / api / generator / util / TextTemplateUtil.java
1 /*
2  * Copyright (c) 2017 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
9 package org.opendaylight.mdsal.binding.javav2.java.api.generator.util;
10
11 import com.google.common.base.CharMatcher;
12 import com.google.common.base.Preconditions;
13 import com.google.common.base.Splitter;
14 import com.google.common.base.Strings;
15 import com.google.common.collect.Iterables;
16 import com.google.common.collect.Lists;
17 import java.util.List;
18 import java.util.Optional;
19 import java.util.StringTokenizer;
20 import java.util.function.Function;
21 import java.util.regex.Pattern;
22 import org.opendaylight.mdsal.binding.javav2.generator.util.BindingGeneratorUtil;
23 import org.opendaylight.mdsal.binding.javav2.generator.util.Types;
24 import org.opendaylight.mdsal.binding.javav2.model.api.AccessModifier;
25 import org.opendaylight.mdsal.binding.javav2.model.api.ConcreteType;
26 import org.opendaylight.mdsal.binding.javav2.model.api.GeneratedProperty;
27 import org.opendaylight.mdsal.binding.javav2.model.api.GeneratedTransferObject;
28 import org.opendaylight.mdsal.binding.javav2.model.api.GeneratedType;
29 import org.opendaylight.mdsal.binding.javav2.model.api.GeneratedTypeForBuilder;
30 import org.opendaylight.mdsal.binding.javav2.model.api.MethodSignature;
31 import org.opendaylight.mdsal.binding.javav2.model.api.Restrictions;
32 import org.opendaylight.mdsal.binding.javav2.model.api.Type;
33 import org.opendaylight.mdsal.binding.javav2.model.api.TypeComment;
34 import org.opendaylight.mdsal.binding.javav2.model.api.TypeMember;
35 import org.opendaylight.mdsal.binding.javav2.model.api.YangSourceDefinition;
36 import org.opendaylight.mdsal.binding.javav2.model.api.YangSourceDefinition.Multiple;
37 import org.opendaylight.mdsal.binding.javav2.model.api.YangSourceDefinition.Single;
38 import org.opendaylight.mdsal.binding.javav2.spec.runtime.BindingNamespaceType;
39 import org.opendaylight.yangtools.concepts.Builder;
40 import org.opendaylight.yangtools.yang.common.QName;
41 import org.opendaylight.yangtools.yang.model.api.DocumentedNode;
42 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.Module;
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 public final class TextTemplateUtil {
52
53     public static final String DOT = ".";
54
55     private static final char NEW_LINE = '\n';
56     private static final String UNDERSCORE = "_";
57     private static final CharMatcher NEWLINE_OR_TAB = CharMatcher.anyOf("\n\t");
58     private static final CharMatcher NL_MATCHER = CharMatcher.is(NEW_LINE);
59     private static final CharMatcher AMP_MATCHER = CharMatcher.is('&');
60     private static final CharMatcher GT_MATCHER = CharMatcher.is('>');
61     private static final CharMatcher LT_MATCHER = CharMatcher.is('<');
62     private static final Splitter NL_SPLITTER = Splitter.on(NL_MATCHER);
63     private static final Splitter BSDOT_SPLITTER = Splitter.on(".");
64
65     private static final Pattern TAIL_COMMENT_PATTERN = Pattern.compile("*/", Pattern.LITERAL);
66     private static final Pattern MULTIPLE_SPACES_PATTERN = Pattern.compile(" +");
67
68     private static DeclaredStatementFormatter YANG_FORMATTER = DeclaredStatementFormatter.builder()
69         .addIgnoredStatement(YangStmtMapping.CONTACT)
70         .addIgnoredStatement(YangStmtMapping.DESCRIPTION)
71         .addIgnoredStatement(YangStmtMapping.REFERENCE)
72         .addIgnoredStatement(YangStmtMapping.ORGANIZATION)
73         .build();
74
75     private TextTemplateUtil() {
76         throw new UnsupportedOperationException("Util class");
77     }
78
79     /**
80      * Makes start of getter name LowerCase
81      *
82      * @param s getter name without prefix
83      * @return getter name starting in LowerCase
84      */
85     public static String toFirstLower(final String s) {
86         return s != null && s.length() != 0 ? Character.isLowerCase(s.charAt(0)) ? s : s.length() == 1 ?
87                 s.toLowerCase() : s.substring(0, 1).toLowerCase() + s.substring(1) : s;
88     }
89
90     /**
91      * Wraps text as documentation, used in enum description
92      *
93      * @param text text for wrapping
94      * @return wrapped text
95      */
96     public static String wrapToDocumentation(final String text) {
97         if (text.isEmpty()) {
98             return "";
99         }
100         final StringBuilder sb = new StringBuilder(NEW_LINE);
101         sb.append("/**");
102         sb.append(NEW_LINE);
103         for (final String t : NL_SPLITTER.split(text)) {
104             if (!t.isEmpty()) {
105                 sb.append(" * ");
106                 sb.append(t);
107                 sb.append(NEW_LINE);
108             }
109         }
110         sb.append(" */");
111         sb.append(NEW_LINE);
112         return sb.toString();
113     }
114
115     /**
116      * Returns formatted Javadoc, based on type
117      * @param typeName given type name
118      * @return formatted Javadoc, based on type
119      */
120     public static String formatDataForJavaDocBuilder(final String typeName) {
121         final StringBuilder stringBuilder = new StringBuilder();
122         stringBuilder.append("Class that builds {@link ")
123                 .append(typeName)
124                 .append("} instances.")
125                 .append(NEW_LINE)
126                 .append("@see ")
127                 .append(typeName);
128         return stringBuilder.toString();
129     }
130
131     /**
132      * Returns formatted Javadoc with possible additional comment, based on type
133      * @param type given type
134      * @param additionalComment additional comment to format
135      * @return formatted Javadoc with possible additional comment, based on type
136      */
137     public static String formatDataForJavaDoc(final GeneratedType type, final String additionalComment) {
138         final StringBuilder javaDoc = new StringBuilder();
139         javaDoc.append(formatDataForJavaDoc(type)).append(additionalComment);
140         return javaDoc.toString();
141     }
142
143     private static void appendSnippet(final StringBuilder sb, final GeneratedType type) {
144         Optional<YangSourceDefinition> optDef = type.getYangSourceDefinition();
145         if (optDef.isPresent()) {
146             YangSourceDefinition def = optDef.get();
147             sb.append(NEW_LINE);
148
149             if (def instanceof Single) {
150                 DocumentedNode node = ((Single) def).getNode();
151                 sb.append("<p>\n")
152                     .append("This class represents the following YANG schema fragment defined in module <b>")
153                     .append(def.getModule().argument()).append("</b>\n")
154                     .append("<pre>\n");
155                 appendYangSnippet(sb, def.getModule(), ((EffectiveStatement<?, ?>) node).getDeclared());
156                 sb.append("</pre>");
157
158                 if (node instanceof SchemaNode) {
159                     sb.append("The schema path to identify an instance is\n")
160                         .append("<i>")
161                         .append(formatSchemaPath(def.getModule().argument(), ((SchemaNode) node).getPath().getPathFromRoot()))
162                         .append("</i>\n");
163
164                     if (hasBuilderClass(type)) {
165                         final String builderName = new StringBuilder()
166                             .append(((GeneratedTypeForBuilder) type).getPackageNameForBuilder())
167                             .append(".").append(type.getName()).append("Builder").toString();
168
169                         sb.append("\n<p>To create instances of this class use {@link ").append(builderName)
170                             .append("}.\n")
171                             .append("@see ").append(builderName).append('\n');
172                         if (node instanceof ListSchemaNode) {
173                             final StringBuilder linkToKeyClass = new StringBuilder();
174
175                             final String[] namespace = Iterables.toArray(
176                                 BSDOT_SPLITTER.split(type.getFullyQualifiedName()), String.class);
177                             final String className = namespace[namespace.length - 1];
178
179                             linkToKeyClass.append(BindingGeneratorUtil.packageNameForSubGeneratedType(
180                                 ((GeneratedTypeForBuilder) type).getBasePackageName(), (SchemaNode) node,
181                                 BindingNamespaceType.Key)).append('.').append(className).append("Key");
182
183                             List<QName> keyDef = ((ListSchemaNode) node).getKeyDefinition();
184                             if (keyDef != null && !keyDef.isEmpty()) {
185                                 sb.append("@see ").append(linkToKeyClass);
186                             }
187                             sb.append('\n');
188                         }
189                     }
190                 }
191             } else if (def instanceof Multiple) {
192                 sb.append("<pre>\n");
193                 for (SchemaNode node : ((Multiple) def).getNodes()) {
194                     appendYangSnippet(sb, def.getModule(), ((EffectiveStatement<?, ?>) node).getDeclared());
195                 }
196                 sb.append("</pre>\n");
197             }
198         }
199     }
200
201     private static void appendYangSnippet(StringBuilder sb, ModuleEffectiveStatement module,
202             DeclaredStatement<?> stmt) {
203         for (String str : YANG_FORMATTER.toYangTextSnippet(module, stmt)) {
204             sb.append(encodeAngleBrackets(encodeJavadocSymbols(str)));
205         }
206     }
207
208     public static boolean hasBuilderClass(final GeneratedType type) {
209         return type instanceof GeneratedTypeForBuilder;
210     }
211
212     public static String formatSchemaPath(final String moduleName, final Iterable<QName> schemaPath) {
213         final StringBuilder sb = new StringBuilder();
214         sb.append(moduleName);
215
216         QName currentElement = Iterables.getFirst(schemaPath, null);
217         for (final QName pathElement : schemaPath) {
218             sb.append('/');
219             if (!currentElement.getNamespace().equals(pathElement.getNamespace())) {
220                 currentElement = pathElement;
221                 sb.append(pathElement);
222             } else {
223                 sb.append(pathElement.getLocalName());
224             }
225         }
226         return sb.toString();
227     }
228
229     /**
230      * Returns properties names in formatted string
231      * @param properties list of given properties
232      * @return properties names in formatted string
233      */
234     //FIXME: this needs further clarification in future patch
235     public static String valueForBits(final List<GeneratedProperty> properties) {
236         return String.join(",", Lists.transform(properties, TextTemplateUtil::fieldName));
237     }
238
239     /**
240      * Returns formatted type description
241      * @param type given type
242      * @return formatted type description
243      */
244     public static String formatDataForJavaDoc(final GeneratedType type) {
245         final StringBuilder javaDoc = new StringBuilder();
246         final TypeComment comment = type.getComment();
247         if (comment != null) {
248             javaDoc.append(comment.getJavadoc())
249                 .append(NEW_LINE)
250                 .append(NEW_LINE)
251                 .append(NEW_LINE);
252         }
253
254         appendSnippet(javaDoc, type);
255
256         return javaDoc.toString();
257     }
258
259     /**
260      * Returns parameter name, based on given Type
261      * @param returnType given type
262      * @param paramName parameter name
263      * @return parameter name, based on given Type
264      */
265     public static String paramValue(final Type returnType, final String paramName) {
266         return returnType instanceof ConcreteType ? paramName : paramName + ".getValue()";
267     }
268
269     /**
270      * Template method which generates JAVA comments. InterfaceTemplate
271      *
272      * @param comment comment string with the comment for whole JAVA class
273      * @return string with comment in JAVA format
274      */
275     public static String asJavadoc(final String comment) {
276         return comment == null ? "" : wrapToDocumentation(formatToParagraph(comment.trim(), 0));
277     }
278
279     private static String formatDataForJavaDoc(final TypeMember type, final String additionalComment) {
280         final StringBuilder javaDoc = new StringBuilder();
281         if (type.getComment() != null && !type.getComment().isEmpty()) {
282             javaDoc.append(formatToParagraph(type.getComment(), 0))
283                     .append(NEW_LINE)
284                     .append(NEW_LINE)
285                     .append(NEW_LINE);
286         }
287         javaDoc.append(additionalComment);
288         return wrapToDocumentation(javaDoc.toString());
289     }
290
291     /**
292      * Returns related Javadoc
293      * @param methodSignature method signature
294      * @return related Javadoc
295      */
296     public static String getJavaDocForInterface(final MethodSignature methodSignature) {
297         if (methodSignature.getReturnType() == Types.VOID) {
298             return "";
299         }
300         final StringBuilder javaDoc = new StringBuilder();
301         javaDoc.append("@return ")
302                 .append(asCode(methodSignature.getReturnType().getFullyQualifiedName()))
303                 .append(" ")
304                 .append(asCode(propertyNameFromGetter(methodSignature)))
305                 .append(", or ")
306                 .append(asCode("null"))
307                 .append(" if not present");
308         return formatDataForJavaDoc(methodSignature, javaDoc.toString());
309     }
310
311     private static String asCode(final String text) {
312         return "<code>" + text + "</code>";
313     }
314
315     /**
316      * Encodes angle brackets in yang statement description
317      * @param description description of a yang statement which is used to generate javadoc comments
318      * @return string with encoded angle brackets
319      */
320     public static String encodeAngleBrackets(String description) {
321         if (description != null) {
322             description = LT_MATCHER.replaceFrom(description, "&lt;");
323             description = GT_MATCHER.replaceFrom(description, "&gt;");
324         }
325         return description;
326     }
327
328     /**
329      * Returns collection of properties as formatted String
330      * @param properties list of given properties
331      * @return generated properties as formatted String
332      */
333     public static String propsAsArgs(final Iterable<GeneratedProperty> properties) {
334         return String.join(",", Iterables.transform(properties, prop -> "\"" + prop.getName() + "\""));
335     }
336
337     /**
338      * Returns properties as formatted String
339      * @param properties list of given properties
340      * @param booleanName Java Boolean type name
341      * @return Properties as formatted String
342      */
343     public static String propsAsList(final Iterable<GeneratedProperty> properties, final String booleanName) {
344         return String.join(",", Iterables.transform(properties,
345             prop -> "properties.get(i++).equals(defaultValue) ? " + booleanName + ".TRUE : null"));
346     }
347
348     /**
349      * Extracts available restrictions from given type
350      * @param currentType given type
351      * @return restrictions from given type
352      */
353     public static Restrictions getRestrictions(final Type currentType) {
354         Restrictions restrictions = null;
355         if (currentType instanceof ConcreteType) {
356             restrictions = ((ConcreteType) currentType).getRestrictions();
357         } else if (currentType instanceof GeneratedTransferObject) {
358             restrictions = ((GeneratedTransferObject) currentType).getRestrictions();
359         }
360         return restrictions;
361     }
362
363     /**
364      * sets fieldname according to property for return type
365      * method(type fieldname)
366      *
367      * @param property type from getter
368      * @return underscored string form
369      */
370     public static String fieldName(final GeneratedProperty property) {
371         final String name = Preconditions.checkNotNull(property.getName());
372         return UNDERSCORE.concat(name);
373     }
374
375     /**
376      * Template method which generates sequence of the names of the class attributes.
377      *
378      * @param parameters group of generated property instances which are transformed to the sequence of parameter names
379      * @return string with the list of the parameter names
380      */
381     public static String asArguments(final List<GeneratedProperty> parameters) {
382         return String.join(", ", Lists.transform(parameters, TextTemplateUtil::fieldName));
383     }
384
385     /**
386      * Helper method for building getter.
387      *
388      * @param field property name
389      * @return getter for property
390      */
391     public static String getterMethodName(final GeneratedProperty field) {
392         final Type type = Preconditions.checkNotNull(field.getReturnType());
393         final String name = Preconditions.checkNotNull(field.getName());
394         final String prefix = Types.BOOLEAN.equals(type) ? "is" : "get";
395         return prefix.concat(toFirstUpper(name));
396     }
397
398     /**
399      * Returns built setter method body from input parameters.
400      * @param field generated property
401      * @param typeName type name
402      * @param returnTypeName return type name
403      * @return built setter method body
404      */
405     public static String setterMethod(final GeneratedProperty field, final String typeName, final String returnTypeName) {
406         final StringBuilder stringBuilder = new StringBuilder();
407         stringBuilder.append("public ")
408                 .append(typeName)
409                 .append(" set")
410                 .append(toFirstUpper(field.getName()))
411                 .append('(')
412                 .append(returnTypeName)
413                 .append(" value) {\n    this.")
414                 .append(fieldName(field))
415                 .append(" = value;\n    return this;\n}\n");
416         return stringBuilder.toString();
417     }
418
419     /**
420      * Returns simple name of underlying class
421      * @return Simple name of underlying class
422      */
423     public static String getSimpleNameForBuilder() {
424         return Builder.class.getSimpleName();
425     }
426
427     /**
428      * Makes start of getter name uppercase
429      *
430      * @param s getter name without prefix
431      * @return getter name starting in uppercase
432      */
433     public static String toFirstUpper(final String s) {
434         return s != null && s.length() != 0 ? Character.isUpperCase(s.charAt(0)) ? s : s.length() == 1 ?
435                 s.toUpperCase() : s.substring(0, 1).toUpperCase() + s.substring(1) : s;
436     }
437
438     /**
439      * Cuts prefix from getter name.
440      *
441      * @param getter getter name
442      * @return getter name without prefix
443      */
444     public static String propertyNameFromGetter(final MethodSignature getter) {
445         final String name = Preconditions.checkNotNull(getter.getName());
446         int prefix;
447         if (name.startsWith("is")) {
448             prefix = 2;
449         } else if (name.startsWith("get")) {
450             prefix = 3;
451         } else {
452             prefix = 0;
453         }
454         return toFirstLower(name.substring(prefix));
455     }
456
457     /**
458      * Returns list of properties as formatted String.
459      * @param properties input list of generated properties
460      * @return formatted property list as String
461      */
462     public static String getPropertyList(final List<GeneratedProperty> properties) {
463         return String.join(", ", Lists.transform(properties, prop -> "base." + getterMethodName(prop) + "()"));
464     }
465
466     /**
467      * util method for unionTemplateBuilderTemplate
468      * @return string with clarification for javadoc
469      */
470     public static String getClarification() {
471         final StringBuilder clarification = new StringBuilder();
472         clarification.append("The purpose of generated class in src/main/java for Union types is to create new instances of unions from a string representation.\n")
473                 .append("In some cases it is very difficult to automate it since there can be unions such as (uint32 - uint16), or (string - uint32).\n")
474                 .append("\n")
475                 .append("The reason behind putting it under src/main/java is:\n")
476                 .append("This class is generated in form of a stub and needs to be finished by the user. This class is generated only once to prevent\n")
477                 .append("loss of user code.\n")
478                 .append("\n");
479         return clarification.toString();
480     }
481
482     /**
483      * Returns source path as String.
484      * @param module module
485      * @param moduleFilePathResolver function module to module file path
486      * @return formatted String source path
487      */
488     public static String getSourcePath(final Module module, final Function<Module, Optional<String>> moduleFilePathResolver) {
489         final java.util.Optional<String> moduleFilePath = moduleFilePathResolver.apply(module);
490         Preconditions.checkArgument(moduleFilePath.isPresent(),"Module file path for %s is not present", module);
491
492         return moduleFilePath.get();
493     }
494
495     /**
496      * Util method for unionTemplateBuilderTemplate.
497      * @param modifier enum representing Java access modifier
498      * @return needed access modifier
499      */
500     public static String getAccessModifier(final AccessModifier modifier) {
501         switch (modifier) {
502             case PUBLIC: return "public ";
503             case PROTECTED: return "protected ";
504             case PRIVATE: return "private ";
505             default: return "";
506         }
507     }
508
509     /**
510      * @param text Content of tag description
511      * @param nextLineIndent Number of spaces from left side default is 12
512      * @return formatted description
513      */
514     private static String formatToParagraph(final String text, final int nextLineIndent) {
515         if (Strings.isNullOrEmpty(text)) {
516             return "";
517         }
518         boolean isFirstElementOnNewLineEmptyChar = false;
519         final StringBuilder sb = new StringBuilder();
520         final StringBuilder lineBuilder = new StringBuilder();
521         final String lineIndent = Strings.repeat(" ", nextLineIndent);
522         final String textToFormat = NEWLINE_OR_TAB.removeFrom(encodeJavadocSymbols(text));
523         final String formattedText = MULTIPLE_SPACES_PATTERN.matcher(textToFormat).replaceAll(" ");
524         final StringTokenizer tokenizer = new StringTokenizer(formattedText, " ", true);
525
526         while (tokenizer.hasMoreElements()) {
527             final String nextElement = tokenizer.nextElement().toString();
528
529             if (lineBuilder.length() + nextElement.length() > 80) {
530                 // Trim trailing whitespace
531                 for (int i = lineBuilder.length() - 1; i >= 0 && lineBuilder.charAt(i) != ' '; --i) {
532                     lineBuilder.setLength(i);
533                 }
534                 // Trim leading whitespace
535                 while (lineBuilder.charAt(0) == ' ') {
536                     lineBuilder.deleteCharAt(0);
537                 }
538                 sb.append(lineBuilder).append('\n');
539                 lineBuilder.setLength(0);
540
541                 if (nextLineIndent > 0) {
542                     sb.append(lineIndent);
543                 }
544
545                 if (" ".equals(nextElement)) {
546                     isFirstElementOnNewLineEmptyChar = true;
547                 }
548             }
549             if (isFirstElementOnNewLineEmptyChar) {
550                 isFirstElementOnNewLineEmptyChar = false;
551             } else {
552                 lineBuilder.append(nextElement);
553             }
554         }
555         return sb.append(lineBuilder).append('\n').toString();
556     }
557
558     private static String encodeJavadocSymbols(final String description) {
559         return Strings.isNullOrEmpty(description) ? description
560                 : AMP_MATCHER.replaceFrom(TAIL_COMMENT_PATTERN.matcher(description).replaceAll("&#42;&#47;"), "&amp;");
561     }
562 }