AntGenerator.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.rat.tools;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.apache.commons.cli.Option;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.text.WordUtils;
import org.apache.rat.OptionCollection;
import org.apache.rat.documentation.options.AntOption;
import org.apache.rat.documentation.options.AntOptionCollection;
import org.apache.rat.utils.CasedString;
import org.apache.rat.utils.CasedString.StringCase;
import static java.lang.String.format;
/**
* A simple tool to convert CLI options into an Ant report base class.
*/
public final class AntGenerator {
/**
* Create a GenerateType for the option
* @param antOption the Ant option to generate the type for.
*/
private static GenerateType getGenerateType(final AntOption antOption) {
String defaultFmt = """
public void add%$1s(String %2$s) {
addArg(%%1$s, %2$s);
}
""";
return switch (antOption.getArgType()) {
case FILE, DIRORARCHIVE -> new GenerateType("FileSet") {
@Override
public String getMethod(final AntOption antOption) {
return format("""
public void addConfiguredFileset(FileSet fileSet) {
for (Resource resource : fileSet) {
if (resource.isFilesystemOnly()) {
addArg("%1$s", ((FileResource) resource).getFile().getAbsolutePath());
}
}
}
""", antOption.keyValue());
}
};
case NONE -> new GenerateType("") {
@Override
public String getMethod(final AntOption antOption) {
return "";
}
};
case STANDARDCOLLECTION -> new GenerateType("Std");
case EXPRESSION -> new GenerateType("Expr");
case COUNTERPATTERN -> new GenerateType("Cntr");
case LICENSEID, FAMILYID -> new GenerateType("Lst");
default -> new GenerateType(antOption.getArgType().getDisplayName()) {
@Override
public String getMethod(final AntOption antOption) {
return String.format(defaultFmt, innerClass, WordUtils.uncapitalize(antOption.getArgName()));
}
};
};
}
private AntGenerator() { }
/**
* Gets the key for the Args array.
* @param option the option to get the key for.
* @return the key for the option.
*/
private static String argsKey(final Option option) {
return StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt());
}
/**
* Creates a base class for an Ant task.
* Requires 3 arguments:
* <ol>
* <li>the package name for the class</li>
* <li>the simple class name</li>
* <li>the directory in which to write the class file.</li>
* </ol>
* @param args the arguments.
* @throws IOException on error.
*/
public static void main(final String[] args) throws IOException {
if (args == null || args.length < 3) {
System.err.println("At least three arguments are required: package, simple class name, target directory.");
return;
}
String packageName = args[0];
String className = args[1];
String destDir = args[2];
List<AntOption> options = AntOptionCollection.INSTANCE.getMappedOptions().toList();
String pkgName = String.join(File.separator, new CasedString(StringCase.DOT, packageName).getSegments());
File file = new File(new File(new File(destDir), pkgName), className + ".java");
file.getParentFile().mkdirs();
try (InputStream template = AntGenerator.class.getResourceAsStream("/Ant.tpl");
FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
OutputStreamWriter customClasses = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
if (template == null) {
throw new RuntimeException("Template /Ant.tpl not found");
}
LineIterator iter = IOUtils.lineIterator(new InputStreamReader(template, StandardCharsets.UTF_8));
while (iter.hasNext()) {
String line = iter.next();
switch (line.trim()) {
case "${static}":
for (Map.Entry<?, ?> entry : AntOptionCollection.getRenameMap().entrySet()) {
writer.append(format(" xlateName.put(\"%s\", \"%s\");%n", entry.getKey(), entry.getValue()));
}
for (Option option : AntOptionCollection.INSTANCE.getUnsupportedOptions()
.getOptions()) {
writer.append(format(" unsupportedArgs.add(\"%s\");%n", argsKey(option)));
}
for (AntOption option : AntOptionCollection.INSTANCE.getMappedOptions().filter(AntOption::isDeprecated).toList()) {
writer.append(format(" deprecatedArgs.put(\"%s\", \"%s\");%n", argsKey(option.getOption()),
format("Use of deprecated option '%s'. %s", option.getName(), option.getDeprecated())));
}
break;
case "${methods}":
writeMethods(writer, options, customClasses);
break;
case "${package}":
writer.append(format("package %s;%n", packageName));
break;
case "${constructor}":
writer.append(format("""
protected %s() {
setDeprecationReporter();
}%n""", className));
break;
case "${class}":
writer.append(format("public abstract class %s extends Task {%n", className));
break;
case "${classes}":
customClasses.flush();
customClasses.close();
writer.write(bos.toString(StandardCharsets.UTF_8));
break;
case "${commonArgs}":
try (InputStream argsTpl = MavenGenerator.class.getResourceAsStream("/Args.tpl")) {
if (argsTpl == null) {
throw new RuntimeException("Args.tpl not found");
}
IOUtils.copy(argsTpl, writer, StandardCharsets.UTF_8);
}
break;
default:
writer.append(line).append(System.lineSeparator());
break;
}
}
}
}
private static void writeMethods(final FileWriter writer, final List<AntOption> options, final Writer customClasses) throws IOException {
for (AntOption antOption : options) {
if (antOption.isAttribute()) {
writer.append(getComment(antOption, true));
if (antOption.isDeprecated()) {
writer.append(" @Deprecated\n");
}
writer.append(format(" public void %s {%n%s%n }%n%n", getAttributeFunctionName(antOption), getAttributeBody(antOption)));
} else {
customClasses.append(getComment(antOption, false));
customClasses.append(format(" public %1$s create%1$s() {%n return new %1$s();%n }%n%n",
antOption.getCasedName().toCase(StringCase.CAMEL)));
customClasses.append(getElementClass(antOption));
}
}
}
private static String getAttributeBody(final AntOption option) {
return option.hasArg() ? format(" setArg(\"%s\", %s);%n", option.keyValue(), option.getName())
: format(" if (%1$s) { setArg(\"%2$s\", null); } else { removeArg(\"%2$s\"); }", option.getName(), option.keyValue());
}
private static String getElementClass(final AntOption option) {
String elementConstructor =
"""
public class %1$s {
%1$s() { }%n""";
String funcName = WordUtils.capitalize(option.getName());
StringBuilder result = new StringBuilder(format(elementConstructor, funcName));
Set<AntOption> implementedOptions = new HashSet<>();
implementedOptions.add(option);
implementedOptions.addAll(option.convertedFrom());
implementedOptions.forEach(antOption -> result.append(getGenerateType(antOption).getMethod(antOption)));
result.append(format(" }%n"));
return result.toString();
}
public static class GenerateType {
/** The inner class name text. */
protected final String innerClass;
GenerateType(final String innerClass) {
this.innerClass = innerClass;
}
public String getMethod(final AntOption antOption) {
String variableName = WordUtils.uncapitalize(antOption.getArgName());
return String.format("""
public void addConfigured%1$s(%1$s %2$s) {
addArg("%3$s", %2$s.value);
}%n""", innerClass, variableName, antOption.keyValue());
}
public String getPattern(final AntOption delegateOption, final AntOption antOption) {
if (delegateOption.isAttribute()) {
String fmt = "<rat:report %s='%s' />";
return format(fmt, delegateOption.getName(), antOption.hasArg() ? antOption.getArgName() : "true");
} else {
String fmt = """
<rat:report>
<%1$s>
<%2$s>%3$s</%2$s>
</%1$s>
</rat:report>
""";
return format(fmt, delegateOption.getName(), innerClass, antOption.getArgName());
}
}
}
private static String maybeAddParamDescription(final AntOption antOption, final String desc) {
if (antOption.getArgName() != null) {
Supplier<String> sup = OptionCollection.ArgumentType.forDisplayName(antOption.getArgName()).map(OptionCollection.ArgumentType::description)
.orElse(null);
if (sup == null) {
throw new IllegalStateException(format("Argument type %s must be in OptionCollection.ARGUMENT_TYPES", antOption.getArgName()));
}
return format("%s Argument%s should be %s%s. (See Argument Types for clarification)", desc, antOption.hasArgs() ? "s" : "",
antOption.hasArgs() ? "" : "a ", antOption.getArgName());
}
return desc;
}
/**
* Get the method comment for this option.
*
* @param addParam if {@code true} the parameter annotation is added.
* @return the Comment block for the function.
*/
private static String getComment(final AntOption antOption, final boolean addParam) {
StringBuilder sb = new StringBuilder();
String desc = antOption.getDescription();
if (desc == null) {
throw new IllegalStateException(format("Description for %s may not be null", antOption.getName()));
}
if (!desc.contains(".")) {
throw new IllegalStateException(format("First sentence of description for %s must end with a '.'", antOption.getName()));
}
if (addParam) {
String arg;
if (antOption.hasArg()) {
arg = desc.substring(desc.indexOf(" ") + 1, desc.indexOf(".") + 1);
arg = WordUtils.capitalize(arg.substring(0, 1)) + arg.substring(1);
} else {
arg = "The state";
}
desc = maybeAddParamDescription(antOption, desc);
sb.append(format(" /**%n * %s%n * @param %s %s%n", StringEscapeUtils.escapeHtml4(desc), antOption.getName(),
StringEscapeUtils.escapeHtml4(arg)));
} else {
sb.append(format(" /**%n * %s%n", StringEscapeUtils.escapeHtml4(desc)));
}
if (antOption.isDeprecated()) {
sb.append(format(" * @deprecated %s%n", StringEscapeUtils.escapeHtml4(antOption.getDeprecated())));
}
return sb.append(format(" */%n")).toString();
}
/**
* Get the signature of the attribute function.
*
* @return the signature of the attribute function.
*/
public static String getAttributeFunctionName(final AntOption antOption) {
return "set" +
WordUtils.capitalize(antOption.getName()) +
(antOption.hasArg() ? "(String " : "(boolean ") +
antOption.getName() +
")";
}
}