Eclipse hat diese schöne Aufräum-Funktion “Organize Imports”, bei der unbenutzte Imports in Java-Klassen entfernt werden. In einem Projekt mit vielen JSP-Dateien stand ich vor einer ähnlichen Aufgabe: Die JSP-Tag Importe aufzuräumen.
JSP Tag-Importe werden üblicherweise mit einem speziellen Tag eingebunden, das die folgende Form hat und einer Tag-Library einen Namespace-Prefix zuordnet:
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib tagdir="/WEB-INF/tags/mytags" prefix="mytags"%>
Später können Funktionen dieser Bibliothek mit dem Namespace adressiert werden. Dies geht sowohl mit Tags als auch mit Funktionen in JSTL-Anweisungen. Ausserdem kann der Namespace über eine URI oder ein tagdir definiert werden.
<c:if test="${fn:contains(myvariable, 'searchtext')}">
<%-- do something --%>
</c:if>
Man darf auch ein und dieselbe Bibliothek mit verschiedenen Prefixen importieren, was bei der späteren Aufräumarbeit berücksichtigt werden sollte:
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="j" %>
<c:if test="${...}">
<%-- do something --%>
</c:if>
<j:if test="${...}">
<%-- do something --%>
</j:if>
Auch wollte ich Importe, die nicht benutzt werden, ermitteln.
Als Grundlage diente der Jericho HTML-Parser. Er kam von den wenigen einfachen Html-Parsern, die ich ausprobierte, am besten mit JSP-Code zurecht. Anschließend musste ich noch die deklarierenden Tags mit Regex zerlegen und alle importierten Tag-Libraries nachhalten.
Der dabei entstandene Code ist der erste Entwurf die Anforderungen in einem kleinen Zeitfenster zu lösen. Daher steckt die meiste Logik in einer langen, mit IF-Blöcken versehenen Methode und die Hilfsklassen sind alle statisch eingebunden.
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.htmlparser.jericho.Element;
import net.htmlparser.jericho.Source;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Prüft doppelte und / oder ungenutzte Taglibrary-Importe in JSPs.
*/
public class CheckJspImports {
private static final Log LOG = LogFactory.getLog(CheckJspImports.class);
public static void main(final String[] args) throws IOException {
final File f = new File("/home/workspace/src/main/webapp");
process(f);
}
static final class JspFileFilter implements FileFilter {
public static final JspFileFilter INSTANCE = new JspFileFilter();
public boolean accept(final File pathname) {
if (pathname.isDirectory() && pathname.canRead()) {
return true;
}
if (pathname.isFile() && pathname.canRead() && FilenameUtils.isExtension(pathname.getName().toLowerCase(), new String[] { "jsp", "jspx", "tag" })) {
return true;
}
return false;
}
}
public static void process(final File f) throws IOException {
if (f.isDirectory()) {
final File[] subFiles = f.listFiles(JspFileFilter.INSTANCE);
for (final File subFile : subFiles) {
process(subFile);
}
} else if (f.isFile()) {
new CheckJspImports().work(f);
}
}
public void work(final File f) throws IOException {
LOG.info("Processing file " + f);
final String content = FileUtils.readFileToString(f);
final Source source = new Source(content);
final List<Element> elementList = source.getAllElements();
for (final Element element : elementList) {
processElement(element);
}
processContent(content);
for (final Taglib taglib : taglibs) {
if (!taglib.used) {
LOG.warn("Taglib \"" + taglib + "\" wurde nicht benutzt!");
}
}
}
// Pattern für prefix-Attribut in Taglib-Import
static final Pattern taglibPrefixPattern = Pattern.compile(" prefix\s*=\s*\"([^\"]+)\"", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
// Pattern für tagdir-Attribut in Taglib-Import
static final Pattern taglibTagdirPattern = Pattern.compile(" tagdir\s*=\s*\"([^\"]+)\"", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
// Pattern für uri-Attribut in Taglib-Import
static final Pattern taglibUriPattern = Pattern.compile(" uri\s*=\s*\"([^\"]+)\"", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
// Sammlung aller Taglibs
private final List<Taglib> taglibs = new ArrayList<CheckJspImports.Taglib>();
// Sammlung aller Taglibs nach Prefix (z. B. "c" -> [taglib http://java.sun.com/jsp/jstl/core])
private final Map<String, Taglib> taglibsByPrefix = new HashMap<String, CheckJspImports.Taglib>();
// Sammlung aller Taglibs nach Uri (z. B. "http://java.sun.com/jsp/jstl/core" -> [taglib http://java.sun.com/jsp/jstl/core])
private final Map<String, Taglib> taglibsByUri = new HashMap<String, CheckJspImports.Taglib>();
// Sammlung aller Taglibs nach Tagdir (z. B. "/WEB-INF/tags/mytags" -> [taglib /WEB-INF/tags/mytags]
private final Map<String, Taglib> taglibsByTagdir = new HashMap<String, CheckJspImports.Taglib>();
// Servertags nach Taglib-Definitionen durchforsten
// <%@ taglib tagdir="/WEB-INF/tags/mytags" prefix="mytags"%>
// namespaces, uri und tagdir merken
// namespaces der Tags checken und Benutzung ermitteln
private void processElement(final Element element) {
if (element != null) {
LOG.debug("-------------------------------------------------------------------------------");
final String elementContent = element.toString();
LOG.debug(elementContent);
// Behandlung Taglib
if (elementContent.startsWith("<%@") && elementContent.endsWith("%>") && elementContent.contains("taglib")) {
LOG.debug("Found taglib declaration!");
String prefix = null;
String uri = null;
String tagdir = null;
// prefix="mytags:"
Matcher m = taglibPrefixPattern.matcher(elementContent);
if (m.find()) {
prefix = m.group(1);
LOG.debug("\tprefix=\"" + prefix + "\"");
}
// tagdir="/WEB-INF/tags/mytags"
m = taglibTagdirPattern.matcher(elementContent);
if (m.find()) {
tagdir = m.group(1);
LOG.debug("\ttagdir=\"" + tagdir + "\"");
}
// uri="http://java.sun.com/jsp/jstl/functions"
m = taglibUriPattern.matcher(elementContent);
if (m.find()) {
uri = m.group(1);
LOG.debug("\turi=\"" + uri + "\"");
}
if (StringUtils.isNotBlank(prefix) && (StringUtils.isNotBlank(tagdir) || StringUtils.isNotBlank(uri))) {
final Taglib taglib = new Taglib(prefix, tagdir, uri, element);
taglibsByPrefix.put(taglib.prefix, taglib);
taglibs.add(taglib);
Taglib existing;
if (taglib.tagdir != null) {
existing = taglibsByTagdir.get(taglib.tagdir);
if (existing != null) {
LOG.warn("Found duplicate taglibs: " + taglib + " and " + existing);
} else {
taglibsByTagdir.put(taglib.tagdir, taglib);
}
}
if (taglib.uri != null) {
existing = taglibsByUri.get(taglib.uri);
if (existing != null) {
LOG.warn("Found duplicate taglibs: " + taglib + " and " + existing);
} else {
taglibsByUri.put(taglib.uri, taglib);
}
}
}
}
// Behandlung HTML-Tag mit Prefix
// z. B: <c:if ...>
// Hier wird ein Namespace eingesetz, also prüfen, ob es eines der Tag-Libraries ist (zum Nachhalten der Benutzung)
final String name = element.getName();
final int indexOfSeparator = name.indexOf(':');
if (indexOfSeparator > -1) {
final String prefix = name.substring(0, indexOfSeparator);
final Taglib taglib = taglibsByPrefix.get(prefix);
if (taglib != null) {
taglib.used = true;
}
}
// Rekursiv Kindelemente des aktuellen Tags verarbeiten
if (element.getChildElements() != null) {
for (final Element child : element.getChildElements()) {
processElement(child);
}
}
}
}
// Pattern um JSTL-Ausdrücke zu ermitteln
// z. B. ${....}
static final Pattern jstlPattern = Pattern.compile("\$\{\s*([^\}]*)\s*\}", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
// Pattern um Prefixe in JSTL-Ausdrücke zu ermitteln
// z. B. ${fn:length()}
static final Pattern jstlPrefixPattern = Pattern.compile("(\w+):\w+", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
// JSTL Nutzungen auf gesamter Seite checken (notfalls Regex) und Namespaces als Funktion checken
// Hierzu zuerst alle Kommentare <%-- --> entfernen, denn diese sollen ignoriert werden.
// Dann per Regex ${} fischen
private void processContent(final String content) {
if (StringUtils.isNotBlank(content)) {
final StringBuilder cleanContent = new StringBuilder(content);
int indexOfCommentStart;
while ((indexOfCommentStart = cleanContent.indexOf("<%--")) > -1) {
if (indexOfCommentEnd > indexOfCommentStart && indexOfCommentEnd < cleanContent.length()) {
cleanContent.delete(indexOfCommentStart, indexOfCommentEnd);
indexOfCommentStart++;
} else {
break;
}
}
final Matcher m = jstlPattern.matcher(cleanContent.toString());
while (m.find()) {
String jstl = m.group(1);
LOG.debug("Found jstl: " + jstl);
// Parameter in Quotes entfernen. Da drin stehen keine Prefixe.
// z. B. ${'xxx'}
jstl = jstl.replaceAll("\"[^\"]+\"", "");
jstl = jstl.replaceAll("'[^']+'", "");
LOG.debug("\tclean: " + jstl);
final Matcher prefixM = jstlPrefixPattern.matcher(jstl);
while (prefixM.find()) {
final String prefix = prefixM.group(1);
LOG.debug("\tprefix: " + prefix);
final Taglib taglib = taglibsByPrefix.get(prefix);
if (taglib != null) {
taglib.used = true;
}
}
}
}
}
/**
* Bean zum Repräsentieren einer Taglibrary.
*/
static class Taglib {
public String tagdir;
public String prefix;
public String uri;
public Element declaringElement;
// Wird auf true gesetzt, wenn die Taglib mindestens an einer Stelle verwendet wurde
public boolean used = false;
public Taglib(final String prefix, final String tagdir, final String uri, final Element declaringElement) {
super();
this.prefix = prefix;
this.tagdir = tagdir;
this.uri = uri;
this.declaringElement = declaringElement;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Taglib [prefix=" + prefix + (tagdir != null ? ", tagdir=" + tagdir : "") + (uri != null ? ", uri=" + uri : "") + "]";
}
}
}