~/home of geeks

Check JSP-Imports

· 1187 Wörter · 6 Minute(n) Lesedauer

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 : "") + "]";
        }
    }
}