View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.rat.tools;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.FileWriter;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.OutputStreamWriter;
28  import java.io.Writer;
29  import java.nio.charset.StandardCharsets;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.function.Supplier;
35  
36  import org.apache.commons.cli.Option;
37  import org.apache.commons.io.IOUtils;
38  import org.apache.commons.io.LineIterator;
39  import org.apache.commons.lang3.StringUtils;
40  import org.apache.commons.text.StringEscapeUtils;
41  import org.apache.commons.text.WordUtils;
42  import org.apache.rat.OptionCollection;
43  import org.apache.rat.documentation.options.AntOption;
44  import org.apache.rat.documentation.options.AntOptionCollection;
45  import org.apache.rat.utils.CasedString;
46  import org.apache.rat.utils.CasedString.StringCase;
47  
48  import static java.lang.String.format;
49  
50  /**
51   * A simple tool to convert CLI options into an Ant report base class.
52   */
53  public final class AntGenerator {
54  
55      /**
56       * Create a GenerateType for the option
57       * @param antOption the Ant option to generate the type for.
58       */
59      private static GenerateType getGenerateType(final AntOption antOption) {
60          String defaultFmt = """
61                          public void add%$1s(String %2$s) {
62                              addArg(%%1$s, %2$s);
63                          }
64                          """;
65  
66          return switch (antOption.getArgType()) {
67              case FILE, DIRORARCHIVE -> new GenerateType("FileSet") {
68                  @Override
69                  public String getMethod(final AntOption antOption) {
70                      return format("""
71                                      public void addConfiguredFileset(FileSet fileSet) {
72                                          for (Resource resource : fileSet) {
73                                              if (resource.isFilesystemOnly()) {
74                                                  addArg("%1$s", ((FileResource) resource).getFile().getAbsolutePath());
75                                              }
76                                          }
77                                      }
78                              """, antOption.keyValue());
79                  }
80              };
81              case NONE -> new GenerateType("") {
82                  @Override
83                  public String getMethod(final AntOption antOption) {
84                      return "";
85                  }
86              };
87              case STANDARDCOLLECTION -> new GenerateType("Std");
88              case EXPRESSION -> new GenerateType("Expr");
89              case COUNTERPATTERN -> new GenerateType("Cntr");
90              case LICENSEID, FAMILYID -> new GenerateType("Lst");
91              default -> new GenerateType(antOption.getArgType().getDisplayName()) {
92                  @Override
93                  public String getMethod(final AntOption antOption) {
94                      return String.format(defaultFmt, innerClass, WordUtils.uncapitalize(antOption.getArgName()));
95                  }
96              };
97          };
98      }
99  
100     private AntGenerator() { }
101 
102     /**
103      * Gets the key for the Args array.
104      * @param option the option to get the key for.
105      * @return the key for the option.
106      */
107     private static String argsKey(final Option option) {
108         return StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt());
109     }
110 
111     /**
112      * Creates a base class for an Ant task.
113      * Requires 3 arguments:
114      * <ol>
115      *     <li>the package name for the class</li>
116      *     <li>the simple class name</li>
117      *     <li>the directory in which to write the class file.</li>
118      * </ol>
119      * @param args the arguments.
120      * @throws IOException on error.
121      */
122     public static void main(final String[] args) throws IOException {
123         if (args == null || args.length < 3) {
124             System.err.println("At least three arguments are required: package, simple class name, target directory.");
125             return;
126         }
127 
128         String packageName = args[0];
129         String className = args[1];
130         String destDir = args[2];
131 
132         List<AntOption> options = AntOptionCollection.INSTANCE.getMappedOptions().toList();
133 
134         String pkgName = String.join(File.separator, new CasedString(StringCase.DOT, packageName).getSegments());
135         File file = new File(new File(new File(destDir), pkgName), className + ".java");
136         file.getParentFile().mkdirs();
137         try (InputStream template = AntGenerator.class.getResourceAsStream("/Ant.tpl");
138              FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8);
139              ByteArrayOutputStream bos = new ByteArrayOutputStream();
140              OutputStreamWriter customClasses = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
141             if (template == null) {
142                 throw new RuntimeException("Template /Ant.tpl not found");
143             }
144             LineIterator iter = IOUtils.lineIterator(new InputStreamReader(template, StandardCharsets.UTF_8));
145             while (iter.hasNext()) {
146                 String line = iter.next();
147                 switch (line.trim()) {
148                     case "${static}":
149                         for (Map.Entry<?, ?> entry : AntOptionCollection.getRenameMap().entrySet()) {
150                             writer.append(format("        xlateName.put(\"%s\", \"%s\");%n", entry.getKey(), entry.getValue()));
151                         }
152 
153                         for (Option option : AntOptionCollection.INSTANCE.getUnsupportedOptions()
154                                 .getOptions()) {
155                             writer.append(format("        unsupportedArgs.add(\"%s\");%n", argsKey(option)));
156                         }
157 
158                         for (AntOption option : AntOptionCollection.INSTANCE.getMappedOptions().filter(AntOption::isDeprecated).toList()) {
159                             writer.append(format("        deprecatedArgs.put(\"%s\", \"%s\");%n", argsKey(option.getOption()),
160                                     format("Use of deprecated option '%s'. %s", option.getName(), option.getDeprecated())));
161                         }
162                         break;
163                     case "${methods}":
164                         writeMethods(writer, options, customClasses);
165                         break;
166                     case "${package}":
167                         writer.append(format("package %s;%n", packageName));
168                         break;
169                     case "${constructor}":
170                         writer.append(format("""
171                                     protected %s() {
172                                         setDeprecationReporter();
173                                     }%n""", className));
174                         break;
175                     case "${class}":
176                         writer.append(format("public abstract class %s extends Task {%n", className));
177                         break;
178                     case "${classes}":
179                         customClasses.flush();
180                         customClasses.close();
181                         writer.write(bos.toString(StandardCharsets.UTF_8));
182                         break;
183                     case "${commonArgs}":
184                         try (InputStream argsTpl = MavenGenerator.class.getResourceAsStream("/Args.tpl")) {
185                             if (argsTpl == null) {
186                                 throw new RuntimeException("Args.tpl not found");
187                             }
188                             IOUtils.copy(argsTpl, writer, StandardCharsets.UTF_8);
189                         }
190                         break;
191                     default:
192                         writer.append(line).append(System.lineSeparator());
193                         break;
194                 }
195             }
196         }
197     }
198 
199     private static void writeMethods(final FileWriter writer, final List<AntOption> options, final Writer customClasses) throws IOException {
200         for (AntOption antOption : options) {
201             if (antOption.isAttribute()) {
202                 writer.append(getComment(antOption, true));
203                 if (antOption.isDeprecated()) {
204                     writer.append("    @Deprecated\n");
205                 }
206                 writer.append(format("    public void %s {%n%s%n    }%n%n", getAttributeFunctionName(antOption), getAttributeBody(antOption)));
207             } else {
208                 customClasses.append(getComment(antOption, false));
209                 customClasses.append(format("    public %1$s create%1$s() {%n        return new %1$s();%n    }%n%n",
210                         antOption.getCasedName().toCase(StringCase.CAMEL)));
211                 customClasses.append(getElementClass(antOption));
212             }
213         }
214     }
215 
216     private static String getAttributeBody(final AntOption option) {
217         return option.hasArg() ? format("        setArg(\"%s\", %s);%n", option.keyValue(), option.getName())
218             : format("        if (%1$s) { setArg(\"%2$s\", null); } else { removeArg(\"%2$s\"); }", option.getName(), option.keyValue());
219     }
220 
221     private static String getElementClass(final AntOption option) {
222         String elementConstructor =
223                 """
224                             public class %1$s {
225                                 %1$s() { }%n""";
226 
227         String funcName = WordUtils.capitalize(option.getName());
228         StringBuilder result = new StringBuilder(format(elementConstructor, funcName));
229         Set<AntOption> implementedOptions = new HashSet<>();
230         implementedOptions.add(option);
231         implementedOptions.addAll(option.convertedFrom());
232         implementedOptions.forEach(antOption -> result.append(getGenerateType(antOption).getMethod(antOption)));
233         result.append(format("    }%n"));
234 
235         return result.toString();
236     }
237 
238     public static class GenerateType {
239         /** The inner class name text. */
240         protected final String innerClass;
241 
242         GenerateType(final String innerClass) {
243             this.innerClass = innerClass;
244         }
245 
246         public String getMethod(final AntOption antOption) {
247             String variableName = WordUtils.uncapitalize(antOption.getArgName());
248             return String.format("""
249                             public void addConfigured%1$s(%1$s %2$s) {
250                                addArg("%3$s", %2$s.value);
251                             }%n""", innerClass, variableName, antOption.keyValue());
252         }
253 
254         public String getPattern(final AntOption delegateOption, final AntOption antOption) {
255             if (delegateOption.isAttribute()) {
256                 String fmt = "<rat:report %s='%s' />";
257                 return format(fmt, delegateOption.getName(), antOption.hasArg() ? antOption.getArgName() : "true");
258             } else {
259                 String fmt = """
260                     <rat:report>
261                       <%1$s>
262                         <%2$s>%3$s</%2$s>
263                       </%1$s>
264                     </rat:report>
265                     """;
266                 return format(fmt, delegateOption.getName(), innerClass, antOption.getArgName());
267             }
268         }
269     }
270 
271     private static String maybeAddParamDescription(final AntOption antOption, final String desc) {
272         if (antOption.getArgName() != null) {
273             Supplier<String> sup = OptionCollection.ArgumentType.forDisplayName(antOption.getArgName()).map(OptionCollection.ArgumentType::description)
274                     .orElse(null);
275             if (sup == null) {
276                 throw new IllegalStateException(format("Argument type %s must be in OptionCollection.ARGUMENT_TYPES", antOption.getArgName()));
277             }
278             return format("%s Argument%s should be %s%s. (See Argument Types for clarification)", desc, antOption.hasArgs() ? "s" : "",
279                     antOption.hasArgs() ? "" : "a ", antOption.getArgName());
280         }
281         return desc;
282     }
283 
284     /**
285      * Get the method comment for this option.
286      *
287      * @param addParam if {@code true} the parameter annotation is added.
288      * @return the Comment block for the function.
289      */
290     private static String getComment(final AntOption antOption, final boolean addParam) {
291         StringBuilder sb = new StringBuilder();
292         String desc = antOption.getDescription();
293         if (desc == null) {
294             throw new IllegalStateException(format("Description for %s may not be null", antOption.getName()));
295         }
296         if (!desc.contains(".")) {
297             throw new IllegalStateException(format("First sentence of description for %s must end with a '.'", antOption.getName()));
298         }
299         if (addParam) {
300             String arg;
301             if (antOption.hasArg()) {
302                 arg = desc.substring(desc.indexOf(" ") + 1, desc.indexOf(".") + 1);
303                 arg = WordUtils.capitalize(arg.substring(0, 1)) + arg.substring(1);
304             } else {
305                 arg = "The state";
306             }
307             desc = maybeAddParamDescription(antOption, desc);
308             sb.append(format("    /**%n     * %s%n     * @param %s %s%n", StringEscapeUtils.escapeHtml4(desc), antOption.getName(),
309                     StringEscapeUtils.escapeHtml4(arg)));
310         } else {
311             sb.append(format("    /**%n     * %s%n", StringEscapeUtils.escapeHtml4(desc)));
312         }
313         if (antOption.isDeprecated()) {
314             sb.append(format("     * @deprecated %s%n", StringEscapeUtils.escapeHtml4(antOption.getDeprecated())));
315         }
316         return sb.append(format("     */%n")).toString();
317     }
318 
319     /**
320      * Get the signature of the attribute function.
321      *
322      * @return the signature of the attribute function.
323      */
324     public static String getAttributeFunctionName(final AntOption antOption) {
325         return "set" +
326                 WordUtils.capitalize(antOption.getName()) +
327                 (antOption.hasArg() ? "(String " : "(boolean ") +
328                 antOption.getName() +
329                 ")";
330     }
331 }