TldGenerateMojo.java
/*
* The MIT License
* Copyright © 2004-2014 Fabrizio Giustina
* Copyright © 2022-2026 Web-Legacy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package io.github.weblegacy.maven.plugin.taglib;
import io.github.weblegacy.maven.plugin.taglib.util.XmlHelper;
import io.github.weblegacy.tlddoc.tagfileparser.Attribute;
import io.github.weblegacy.tlddoc.tagfileparser.Directive;
import io.github.weblegacy.tlddoc.tagfileparser.javacc.ParseException;
import io.github.weblegacy.tlddoc.tagfileparser.javacc.TagFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;
import org.jdom2.input.DOMBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Generates tld files from directories of jsp 2.0 tag files.
*
* @author Fabrizio Giustina
*/
@Mojo(name = "tldgenerate", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
public class TldGenerateMojo extends AbstractMojo {
/**
* Directory containing tag files. Subdirectories are also processed.
*/
@Parameter(defaultValue = "src/main/resources/META-INF/tags/")
private File tagDir;
/**
* Output dir for tld files.
*/
@Parameter(defaultValue = "${project.build.outputDirectory}/META-INF", required = true)
private File outputDir;
/**
* Version added to tld files, defaults to project version.
*/
@Parameter(property = "project.version", required = true)
private String version;
/**
* Detailed configuration for taglibs for tld generation. Starting with version 2.4 you can
* configure multiple taglibs with this attribute, and for each taglib you can add both tagfiles
* dir than classes with EL functions (note that EL function support is preliminary, the
* resulting tld does not include anything in the "description" attribute (that would require
* parsing javadocs from sources.
*
* <pre>
* <taglibs>
* <taglib>
* <description>A test tld that contains functions</description>
* <shortName>test</shortName>
* <uri>testuri</uri>
* <outputname>testtaglib</outputname>
* <functionClasses>
* <functionClass>org.apache.commons.lang.StringUtils</functionClass>
* </functionClasses>
* <tagdir>src/tagfiles</tagdir>
* </taglib>
* </taglibs>
* </pre>
*/
@Parameter
private List<Taglib> taglibs;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().debug(MessageFormat.format(Messages.getString("Taglib.generating.tld"),
tagDir.getAbsolutePath()));
try {
generateTlds();
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
/**
* Generate the tlds.
*
* @throws MojoExecutionException if an error occurs during execution
* @throws IOException if an I/O error occurs
*/
private void generateTlds() throws IOException, MojoExecutionException {
List<Taglib> taglibsList = new ArrayList<>();
if (taglibs != null) {
for (Taglib taglib : taglibs) {
// a drink to get the old-style embedding tests work,
// I should migrate to the new IT test infrastructure
Taglib tlib = new Taglib();
try {
PropertyUtils.copyProperties(tlib, taglib);
} catch (IllegalAccessException | NoSuchMethodException
| InvocationTargetException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
taglibsList.add(tlib);
}
}
// old (pre 2.4) behavior, create taglib configurations from dir
if (taglibsList.isEmpty() && tagDir.isDirectory()) {
// handle tag files. Add any directory containing .tag or .tagx files
List<File> tags = FileUtils.getFiles(tagDir, "**/*.tag", null);
tags.addAll(FileUtils.getFiles(tagDir, "**/*.tagx", null));
if (!tags.isEmpty()) {
Set<File> directories = new HashSet<>();
for (File tag : tags) {
directories.add(tag.getParentFile());
}
for (File dir : directories) {
Taglib tlib = new Taglib();
tlib.setTagdir(dir);
tlib.setShortName(dir.getName());
tlib.setOutputname(dir.getName() + ".tld");
tlib.setUri(dir.getName());
tlib.setShortName("Tag library for tag file directory " + dir.getName());
taglibsList.add(tlib);
}
} else {
getLog().warn(MessageFormat.format(
Messages.getString("Taglib.generating.notfound"),
tagDir.getAbsolutePath()));
}
}
try {
for (Taglib taglib : taglibsList) {
doTaglib(taglib);
}
} catch (IOException | MojoExecutionException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
/**
* Generate tld-file from the taglib.
*
* @param taglib the taglib
*
* @throws MojoExecutionException if an error occurs during execution
* @throws IOException if an I/O error occurs
*/
private void doTaglib(Taglib taglib) throws MojoExecutionException, IOException {
Document doc = getTldDocument(taglib, XmlHelper.getDocumentBuilder());
if (taglib.getShortName() == null) {
throw new MojoExecutionException("Missing \"shortName\" parameter for taglib "
+ taglib);
}
String tldName = taglib.getOutputname();
if (tldName == null) {
tldName = taglib.getShortName() + ".tld";
}
if (!tldName.endsWith(".tld")) {
tldName = tldName + ".tld";
}
File outputFile = new File(outputDir, tldName);
if (!(outputDir.mkdirs() || outputDir.isDirectory())) {
throw new IOException("Unable to create output directory "
+ outputDir.getAbsolutePath());
}
getLog().info(MessageFormat.format(Messages.getString("Taglib.generating.file"),
tldName, taglib.getShortName()));
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
outputter.output(new DOMBuilder().build(doc), fos);
}
}
/**
* Read whole taglib-files an generate a tld-xml.
*
* @param taglib the taglib
* @param documentBuilder DocumentBuilder instance
*
* @return generated tld-xml
*/
protected Document getTldDocument(Taglib taglib, DocumentBuilder documentBuilder) {
Document result = documentBuilder.newDocument();
Element taglibElement = createRootTaglibNode(result, taglib.getDescription(),
taglib.getShortName(), taglib.getUri());
if (taglib.getTagdir() != null && taglib.getTagdir().isDirectory()) {
String path = taglib.getTagdir().getAbsolutePath().replace(File.separatorChar, '/');
int index = path.indexOf("/META-INF/");
if (index != -1) {
path = path.substring(index);
} else {
path = "unknown";
}
if (!path.endsWith("/")) {
path += "/";
}
File[] files = taglib.getTagdir().listFiles();
if (files == null) {
files = new File[0];
}
for (File tag : files) {
if (!tag.isDirectory()
&& (tag.getName().toLowerCase().endsWith(".tag")
|| tag.getName().toLowerCase().endsWith(".tagx"))) {
String tagName = tag.getName().substring(0, tag.getName().lastIndexOf('.'));
String tagPath = path + tag.getName();
Element tagFileElement = result.createElement("tag-file");
Element nameElement = result.createElement("name");
nameElement.appendChild(result.createTextNode(tagName));
tagFileElement.appendChild(nameElement);
Element pathElement = result.createElement("path");
pathElement.appendChild(result.createTextNode(tagPath));
tagFileElement.appendChild(pathElement);
taglibElement.appendChild(tagFileElement);
// tag-file Subelements
// * description (optional) A description of the tag.
// * display-name (optional) Name intended to be displayed by tools.
// * icon (optional) Icon that can be used by tools.
// * name The unique tag name.
// * path Where to find the tag file implementing this tag,
// relative to the root of the web application or the
// root of the JAR file for a tag library packaged in
// a JAR. This must begin with /WEB-INF/tags/ if the
// tag file resides in the WAR, or /META-INF/tags/ if
// the tag file resides in a JAR.
// example (optional) Informal description of an example use of the tag.
// tag-extension (optional) Extensions that provide extra information about the
// tag for tools.
try (InputStream is = new FileInputStream(tag)) {
TagFile tagFile = TagFile.parse(is);
for (Directive directive : tagFile.getDirectives()) {
if ("tag".equals(directive.getDirectiveName())) {
for (Attribute attribute : directive.getAttributes()) {
if ("description".equals(attribute.getName())
|| "display-name".equals(attribute.getName())
|| "example".equals(attribute.getName())) {
Element element = result.createElement(attribute.getName());
element.appendChild(
result.createTextNode(attribute.getValue()));
tagFileElement.appendChild(element);
}
}
}
}
} catch (IOException | ParseException e) {
getLog().error(e);
}
}
}
}
Map<String, Integer> duplicateFunctions = new HashMap<>();
if (taglib.getFunctionClasses() != null) {
for (String functionClassString : taglib.getFunctionClasses()) {
Class<?> functionClass;
try {
functionClass = Class.forName(functionClassString);
} catch (ClassNotFoundException e) {
getLog().error(
"Unable to load function class "
+ functionClassString
+ ": "
+ e.getClass().getName()
+ " "
+ e.getMessage(),
e);
continue;
}
Method[] declaredMethods = functionClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if (!Modifier.isStatic(method.getModifiers())
|| !Modifier.isPublic(method.getModifiers())) {
// not a public static method
continue;
}
Element tagFileElement = result.createElement("function");
String functionName = method.getName();
if (duplicateFunctions.containsKey(functionName)) {
int currentNum = duplicateFunctions.get(functionName);
duplicateFunctions.put(functionName, currentNum + 1);
functionName = functionName + currentNum;
} else {
duplicateFunctions.put(functionName, 1);
}
Element nameElement = result.createElement("name");
nameElement.appendChild(result.createTextNode(functionName));
tagFileElement.appendChild(nameElement);
Element classElement = result.createElement("function-class");
classElement.appendChild(result.createTextNode(
method.getDeclaringClass().getName()));
tagFileElement.appendChild(classElement);
StringBuilder parameterTypesString = new StringBuilder();
Class<?>[] parameterTypes = method.getParameterTypes();
for (int p = 0; p < parameterTypes.length; p++) {
Class<?> param = parameterTypes[p];
parameterTypesString.append(nonPrimitiveName(param.getCanonicalName()));
if (parameterTypes.length - 1 > p) {
parameterTypesString.append(", ");
}
}
Element signatureElement = result.createElement("function-signature");
signatureElement.appendChild(result.createTextNode(nonPrimitiveName(method
.getReturnType()
.getCanonicalName())
+ " "
+ method.getName()
+ "("
+ parameterTypesString.toString()
+ ")"));
tagFileElement.appendChild(signatureElement);
taglibElement.appendChild(tagFileElement);
}
}
}
return result;
}
private String nonPrimitiveName(String string) {
if (string == null) {
return null;
}
switch (string) {
case "int":
return Integer.class.getName();
case "boolean":
return Boolean.class.getName();
case "double":
return Double.class.getName();
case "long":
return Long.class.getName();
case "char":
return Character.class.getName();
default:
return string;
}
}
/**
* Creates the root-element of an empty tld-xml.
*
* @param result resulting tld-xml
* @param description description of the taglib
* @param shortName short-name of the taglib
* @param uri uri of the taglib
*
* @return the root-element
*/
protected Element createRootTaglibNode(Document result, String description, String shortName,
String uri) {
Element taglibElement = result.createElementNS(
"http://java.sun.com/xml/ns/javaee", "taglib");
taglibElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns",
"http://java.sun.com/xml/ns/javaee");
taglibElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xsi",
"http://www.w3.org/2001/XMLSchema-instance");
taglibElement.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation", "http://java.sun.com/xml/ns/javaee "
+ "http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd");
taglibElement.setAttribute("version", "2.1");
result.appendChild(taglibElement);
Element descriptionElement = result.createElement("description");
descriptionElement.appendChild(result.createTextNode(description));
taglibElement.appendChild(descriptionElement);
Element tlibVersionElement = result.createElement("tlib-version");
tlibVersionElement.appendChild(result.createTextNode(version));
taglibElement.appendChild(tlibVersionElement);
Element shortNameElement = result.createElement("short-name");
shortNameElement.appendChild(result.createTextNode(shortName));
taglibElement.appendChild(shortNameElement);
Element uriElement = result.createElement("uri");
uriElement.appendChild(result.createTextNode(uri));
taglibElement.appendChild(uriElement);
return taglibElement;
}
}