ValidateRenderer.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.checker.ElFunction;
import io.github.weblegacy.maven.plugin.taglib.checker.Tag;
import io.github.weblegacy.maven.plugin.taglib.checker.TagAttribute;
import io.github.weblegacy.maven.plugin.taglib.checker.Tld;
import io.github.weblegacy.maven.plugin.taglib.util.JspCheck;
import io.github.weblegacy.maven.plugin.taglib.util.JspClass;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.reporting.AbstractMavenReportRenderer;
/**
* Validates tag handler classes fount in tlds.
*
* @author Fabrizio Giustina
*/
public class ValidateRenderer extends AbstractMavenTaglibReportRenderer {
/**
* Code for Success-Icon.
*/
private static final int ICO_SUCCESS = 0;
/**
* Code for Info-Icon.
*/
private static final int ICO_INFO = 1;
/**
* Code for Warning-Icon.
*/
private static final int ICO_WARNING = 2;
/**
* Code for Error-Icon.
*/
private static final int ICO_ERROR = 3;
/**
* Path to Error-Icon.
*/
private static final String IMAGE_ERROR_SRC = Messages.getString("Validate.image.error");
/**
* Path to Waring-Icon.
*/
private static final String IMAGE_WARNING_SRC = Messages.getString("Validate.image.warning");
/**
* Path to Info-Icon.
*/
private static final String IMAGE_INFO_SRC = Messages.getString("Validate.image.info");
/**
* Path to Success-Icon.
*/
private static final String IMAGE_SUCCESS_SRC = Messages.getString("Validate.image.success");
/**
* List of TLDs to check.
*/
private final Tld[] tlds;
/**
* For logging.
*/
private final Log log;
/**
* The class-loader for the project.
*/
private final ClassLoader projectClassLoader;
/**
* Utility to check for the loaded Jsp-Classes.
*/
private final JspCheck jspCheck;
/**
* The class-constructor.
*
* @param sink the sink to use.
* @param locale the wanted locale to return the report's description, could be
* <code>null</code>.
* @param tlds list of TLDs to check.
* @param log the logger that has been injected into this mojo.
* @param projectClassLoader ClassLoader for all compile-classpaths
*/
public ValidateRenderer(final Sink sink, final Locale locale, final Tld[] tlds, final Log log,
final ClassLoader projectClassLoader) {
super(sink, locale);
this.tlds = tlds;
this.log = log;
this.projectClassLoader = projectClassLoader;
// Load all jsp-classes for all namespaces.
this.jspCheck = new JspCheck(log, projectClassLoader);
}
@Override
public String getTitle() {
return getMessageString("Validate.title");
}
/**
* Check the given tld. Assure that:
* <ul>
* <li>Any tag class is loadable</li>
* <li>the tag class has a setter for any of the declared attribute</li>
* <li>the type declared in the dtd for an attribute (if any) matches the type accepted by the
* getter</li>
* </ul>
*
* @see AbstractMavenReportRenderer#renderBody()
*/
@Override
protected void renderBody() {
sink.body();
startSection(getMessageString("Validate.h1"));
paragraph(getMessageString("Validate.into1"));
paragraph(getMessageString("Validate.intro2"));
sink.list();
for (Tld tld : tlds) {
sink.listItem();
sink.link("#" + tld.getFilename());
sink.text(MessageFormat.format(getMessageString("Validate.listitem.tld"),
StringUtils.defaultIfEmpty(tld.getName(), tld.getShortname()),
tld.getFilename()));
sink.link_();
sink.text(getMessageString("Validate.listitem.uri") + tld.getUri());
sink.listItem_();
}
sink.list_();
endSection();
for (Tld tld : tlds) {
checkTld(tld);
}
sink.body_();
}
/**
* Checks a single tld and returns validation results.
*
* @param tld Tld
*/
private void checkTld(Tld tld) {
// new section for each tld
sink.anchor(tld.getFilename());
sink.anchor_();
startSection(StringUtils.defaultIfEmpty(tld.getName(),
tld.getShortname()) + ' ' + tld.getFilename());
doTags(tld.getTags(), tld.getShortname());
doFunctions(tld.getFunctions(), tld.getShortname());
endSection();
}
/**
* Checks the tags and returns validation results.
*
* @param tags tags of the Tld
* @param shortname shortname of the Tld
*/
private void doTags(Tag[] tags, String shortname) {
if (tags != null && tags.length > 0) {
for (Tag tldItem : tags) {
checkTag(shortname, tldItem);
}
}
}
/**
* Checks the functions and returns validation results.
*
* @param tags functions of the Tld
* @param shortname shortname of the Tld
*/
private void doFunctions(ElFunction[] tags, String shortname) {
if (tags != null && tags.length > 0) {
startSection("EL functions");
startTable();
tableHeader(new String[]{
getMessageString("Validate.header.validated"),
"function",
getMessageString("Validate.header.class"),
getMessageString("Validate.header.signature")
});
for (ElFunction tldItem : tags) {
checkFunction(shortname, tldItem);
}
endTable();
endSection();
}
}
/**
* Checks a function and returns the validation result.
*
* @param prefix prefix of the function
* @param tag the function
*/
private void checkFunction(String prefix, ElFunction tag) {
String className = tag.getFunctionClass();
boolean found = true;
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.projectClassLoader);
try {
Class<?> functionClass = Class.forName(className, true, this.projectClassLoader);
String fullSignature = tag.getFunctionSignature();
String paramsString = tag.getParameters();
String returnvalue = null;
String methodName = StringUtils.trim(StringUtils.substringBefore(fullSignature, "("));
if (Strings.CS.contains(methodName, " ")) {
returnvalue = StringUtils.substringBefore(methodName, " ");
methodName = StringUtils.substringAfter(methodName, " ");
}
String[] params = StringUtils.split(paramsString, ",");
List<Class<?>> parClasses = new ArrayList<>(params.length);
for (String stringClass : params) {
parClasses.add(Class.forName(StringUtils.trim(stringClass), true,
this.projectClassLoader));
}
Method method = functionClass.getMethod(methodName,
parClasses.toArray(Class<?>[]::new));
Class<?> returnType = method.getReturnType();
if (!(returnvalue == null || returnType.getCanonicalName().equals(returnvalue))) {
found = false;
}
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
found = false;
}
Thread.currentThread().setContextClassLoader(currentClassLoader);
sink.tableRow();
sink.tableCell();
figure(found ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
tableCell(prefix + ":" + tag.getName() + "()");
tableCell(className);
tableCell(tag.getFunctionSignature());
sink.tableRow_();
}
/**
* Checks a single tag and returns validation results.
*
* @param prefix prefix of the tag
* @param tag Tag
*/
private void checkTag(String prefix, Tag tag) {
// new subsection for each tag
startSection("<" + prefix + ":" + tag.getName() + ">");
String className = tag.getTagClass();
startTable();
tableHeader(new String[]{
getMessageString("Validate.header.found"),
getMessageString("Validate.header.loadable"),
getMessageString("Validate.header.extends"),
getMessageString("Validate.header.class")
});
boolean found = true;
boolean loadable = true;
boolean extend;
Object tagObject = null;
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.projectClassLoader);
try {
Class<?> tagClass = Class.forName(className, true, this.projectClassLoader);
// extend only true, if tagClass derives from TagSupport or derives from SimpleTag
extend = jspCheck.check(JspClass.TAG_SUPPORT, tagClass)
|| jspCheck.check(JspClass.SIMPLE_TAG, tagClass);
try {
tagObject = tagClass.getDeclaredConstructor().newInstance();
} catch (IllegalAccessException | IllegalArgumentException | InstantiationException
| NoSuchMethodException | SecurityException | InvocationTargetException e) {
loadable = false;
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
found = false;
loadable = false;
extend = false;
}
Thread.currentThread().setContextClassLoader(currentClassLoader);
sink.tableRow();
sink.tableCell();
figure(found ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
sink.tableCell();
figure(loadable ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
sink.tableCell();
figure(extend ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
tableCell(className);
sink.tableRow_();
if (tag.getTeiClass() != null) {
checkTeiClass(tag.getTeiClass());
}
endTable();
TagAttribute[] attributes = tag.getAttributes();
if (tagObject != null && attributes.length > 0) {
startTable();
tableHeader(new String[]{
StringUtils.EMPTY,
getMessageString("Validate.header.attributename"),
getMessageString("Validate.header.tlddeclares"),
getMessageString("Validate.header.tagdeclares")
});
for (TagAttribute attribute : attributes) {
checkAttribute(tagObject, attribute);
}
endTable();
}
endSection();
}
/**
* Check a declared TagExtraInfo class.
*
* @param className TEI class name
*/
private void checkTeiClass(String className) {
boolean found = true;
boolean loadable = true;
boolean extend;
Class<?> teiClass;
try {
teiClass = Class.forName(className, true, this.projectClassLoader);
extend = jspCheck.check(JspClass.TAG_EXTRA_INFO, teiClass);
try {
teiClass.getDeclaredConstructor().newInstance();
} catch (IllegalAccessException | IllegalArgumentException | InstantiationException
| NoSuchMethodException | SecurityException | InvocationTargetException e) {
loadable = false;
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
found = false;
loadable = false;
extend = false;
}
sink.tableRow();
sink.tableCell();
figure(found ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
sink.tableCell();
figure(loadable ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
sink.tableCell();
figure(extend ? ICO_SUCCESS : ICO_ERROR);
sink.tableCell_();
sink.tableCell();
sink.text(className);
sink.tableCell_();
sink.tableRow_();
}
/**
* Checks a single attribute and returns validation results.
*
* @param tag tag handler instance
* @param attribute TagAttribute
*/
private void checkAttribute(Object tag, TagAttribute attribute) {
String tldType = attribute.getType();
String tldName = attribute.getName();
Class<?> tagType = null;
String tagTypeName = null;
List<ValidationError> validationErrors = new ArrayList<>(3);
if (!PropertyUtils.isWriteable(tag, tldName)) {
validationErrors.add(new ValidationError(ValidationError.LEVEL_ERROR,
getMessageString("Validate.error.setternotfound")));
}
// don't check if setter is missing
if (validationErrors.isEmpty()) {
try {
tagType = PropertyUtils.getPropertyType(tag, tldName);
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
// should never happen, since we already checked the writable property
log.warn(e);
}
tagTypeName = tagType == null ? StringUtils.EMPTY : tagType.getName();
if (tldType != null && tagType != null) {
Class<?> tldTypeClass = getClassFromName(tldType);
if (!tagType.isAssignableFrom(tldTypeClass)) {
validationErrors.add(new ValidationError(ValidationError.LEVEL_ERROR,
MessageFormat.format(
getMessageString("Validate.error.attributetypemismatch"),
tldType, tagType.getName())));
}
}
}
// don't check if we already know type is different
if (validationErrors.isEmpty()) {
if (tldType != null && tagType != null && !tldType.equals(tagType.getName())) {
validationErrors.add(new ValidationError(ValidationError.LEVEL_WARNING,
MessageFormat.format(
getMessageString("Validate.error.attributetypeinexactmatch"),
tldType, tagType.getName())));
} else if (tldType == null && !String.class.equals(tagType)) {
validationErrors.add(new ValidationError(ValidationError.LEVEL_INFO,
getMessageString("Validate.error.attributetype")));
}
}
sink.tableRow();
sink.tableCell();
boolean errors = false;
boolean warnings = false;
boolean infos = false;
for (ValidationError error : validationErrors) {
switch (error.getLevel()) {
case ValidationError.LEVEL_ERROR:
errors = true;
break;
case ValidationError.LEVEL_WARNING:
warnings = true;
break;
case ValidationError.LEVEL_INFO:
infos = true;
break;
default:
break;
}
}
final int figure;
if (errors) {
figure = ICO_ERROR;
} else if (warnings) {
figure = ICO_WARNING;
} else if (infos) {
figure = ICO_INFO;
} else {
figure = ICO_SUCCESS;
}
figure(figure);
sink.tableCell_();
sink.tableCell();
sink.text(tldName);
for (ValidationError error : validationErrors) {
sink.lineBreak();
if (error.getLevel() == ValidationError.LEVEL_ERROR) {
sink.bold();
}
sink.text(error.getText());
if (error.getLevel() == ValidationError.LEVEL_ERROR) {
sink.bold_();
}
}
sink.tableCell_();
sink.tableCell();
if (tldType != null) {
sink.text(StringUtils.substringAfter(tldType, "java.lang."));
}
sink.tableCell_();
tableCell(StringUtils.substringAfter(tagTypeName, "java.lang."));
sink.tableRow_();
}
private void figure(int type) {
String text;
String src;
switch (type) {
case ICO_ERROR:
text = getMessageString("Validate.level.error");
src = IMAGE_ERROR_SRC;
break;
case ICO_WARNING:
text = getMessageString("Validate.level.warning");
src = IMAGE_WARNING_SRC;
break;
case ICO_INFO:
text = getMessageString("Validate.level.info");
src = IMAGE_INFO_SRC;
break;
default:
text = getMessageString("Validate.level.success");
src = IMAGE_SUCCESS_SRC;
break;
}
sink.figure();
sink.figureGraphics(src);
sink.figureCaption();
sink.text(text);
sink.figureCaption_();
sink.figure_();
}
/**
* Returns a class from its name, handling primitives.
*
* @param className clss name
*
* @return Class istantiated using Class.forName or the matching primitive.
*/
private Class<?> getClassFromName(String className) {
Class<?> tldTypeClass = tryGettingPrimitiveClass(className);
if (tldTypeClass == null) {
// not a primitive type
try {
if (isArrayClassName(className)) {
tldTypeClass = getArrayClass(className);
} else {
tldTypeClass = Class.forName(className, true, this.projectClassLoader);
}
} catch (ClassNotFoundException e) {
log.error(MessageFormat.format(
Messages.getString("Validate.error.unabletofindclass"), className));
}
}
return tldTypeClass;
}
private Class<?> tryGettingPrimitiveClass(String className) {
if (className == null) {
return null;
}
switch (className) {
case "byte":
return byte.class;
case "short":
return int.class;
case "int":
return int.class;
case "long":
return long.class;
case "float":
return double.class;
case "double":
return double.class;
case "boolean":
return boolean.class;
case "char":
return char.class;
default:
return null;
}
}
/**
* Tests if the given {@code className} as an array.
*
* @param className the className to test
*
* @return {@code true} if the given {@code className} as an array
*/
private boolean isArrayClassName(String className) {
return className.endsWith("[]");
}
/**
* Gets the class of an array with the elements of {@code className}.
*
* @param className elements-class of the array
*
* @return the array-class
*
* @throws ClassNotFoundException if the class is not found
*/
private Class<?> getArrayClass(String className) throws ClassNotFoundException {
String elementClassName = Strings.CS.replace(className, "[]", "");
Class<?> elementClass = tryGettingPrimitiveClass(elementClassName);
if (elementClass == null) {
elementClass = Class.forName(elementClassName);
}
return Array.newInstance(elementClass, 0).getClass();
}
static class ValidationError {
/**
* Level of validation is information.
*/
public static final int LEVEL_INFO = 1;
/**
* Level of validation is warning.
*/
public static final int LEVEL_WARNING = 2;
/**
* Level of validation is error.
*/
public static final int LEVEL_ERROR = 3;
/**
* The level of the validation.
*/
private int level;
/**
* The text of the validation.
*/
private String text;
/**
* The class-constructor.
*
* @param level the level of the validation
* @param text the text of the validation
*/
public ValidationError(int level, String text) {
this.level = level;
this.text = text;
}
/**
* Getter for {@code level}.
*
* @return Returns the level.
*/
public int getLevel() {
return this.level;
}
/**
* Setter for {@code level}.
*
* @param level The level to set.
*/
public void setLevel(int level) {
this.level = level;
}
/**
* Getter for {@code text}.
*
* @return Returns the text.
*/
public String getText() {
return this.text;
}
/**
* Setter for {@code text}.
*
* @param text The text to set.
*/
public void setText(String text) {
this.text = text;
}
}
}