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