001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2012 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.commons.taglib.processor;
021
022import java.io.IOException;
023import java.io.OutputStreamWriter;
024import java.io.PrintWriter;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.Set;
028import java.util.TreeSet;
029import java.util.logging.Level;
030import java.util.logging.Logger;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034import javax.annotation.Nonnull;
035import javax.annotation.ParametersAreNonnullByDefault;
036import javax.annotation.processing.AbstractProcessor;
037import javax.annotation.processing.Messager;
038import javax.annotation.processing.RoundEnvironment;
039import javax.annotation.processing.SupportedAnnotationTypes;
040import javax.lang.model.element.Element;
041import javax.lang.model.element.ElementKind;
042import javax.lang.model.element.TypeElement;
043import javax.lang.model.type.MirroredTypeException;
044import javax.servlet.jsp.tagext.BodyTag;
045import javax.servlet.jsp.tagext.IterationTag;
046import javax.servlet.jsp.tagext.JspTag;
047import javax.servlet.jsp.tagext.SimpleTag;
048import javax.tools.Diagnostic;
049import javax.tools.FileObject;
050import javax.tools.JavaFileObject;
051import javax.tools.StandardLocation;
052
053import org.shredzone.commons.taglib.annotation.BeanFactoryReference;
054import org.shredzone.commons.taglib.annotation.Tag;
055import org.shredzone.commons.taglib.annotation.TagInfo;
056import org.shredzone.commons.taglib.annotation.TagLib;
057import org.shredzone.commons.taglib.annotation.TagParameter;
058import org.shredzone.commons.taglib.proxy.BodyTagProxy;
059import org.shredzone.commons.taglib.proxy.IterationTagProxy;
060import org.shredzone.commons.taglib.proxy.SimpleTagProxy;
061import org.shredzone.commons.taglib.proxy.TagProxy;
062import org.springframework.util.StringUtils;
063
064/**
065 * A javac processor that scans for tag annotations and creates proxy classes that allows
066 * to use Spring in tag library implementations.
067 *
068 * @author Richard "Shred" Körber
069 */
070@SupportedAnnotationTypes("org.shredzone.commons.taglib.annotation.*")
071@ParametersAreNonnullByDefault
072public class TaglibProcessor extends AbstractProcessor {
073
074    private static final Map<String, String> PROXY_MAP = new HashMap<>();
075    private static final Pattern METHOD_PATTERN = Pattern.compile("^set([^(]+)\\((.+?)\\)$");
076
077    static {
078        PROXY_MAP.put(javax.servlet.jsp.tagext.Tag.class.getName(), TagProxy.class.getName());
079        PROXY_MAP.put(IterationTag.class.getName(), IterationTagProxy.class.getName());
080        PROXY_MAP.put(BodyTag.class.getName(), BodyTagProxy.class.getName());
081        PROXY_MAP.put(SimpleTag.class.getName(), SimpleTagProxy.class.getName());
082    }
083
084    private TaglibBean taglib;
085    private boolean taglibSet = false;
086
087    @Override
088    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
089        taglib = new TaglibBean();
090
091        try {
092            for (Element e : roundEnv.getElementsAnnotatedWith(Tag.class)) {
093                processTag(e);
094            }
095
096            for (Element e : roundEnv.getElementsAnnotatedWith(TagInfo.class)) {
097                processTagInfo(e);
098            }
099
100            for (Element e : roundEnv.getElementsAnnotatedWith(BeanFactoryReference.class)) {
101                processBeanFactoryReference(e);
102            }
103
104            for (Element e : roundEnv.getElementsAnnotatedWith(TagParameter.class)) {
105                processTagParameter(e);
106            }
107
108            for (Element e : roundEnv.getElementsAnnotatedWith(TagLib.class)) {
109                processTagLib(e);
110            }
111
112            if (!taglib.getTags().isEmpty()) {
113                for (TagBean tag : taglib.getTags()) {
114                    generateProxyClass(tag);
115                }
116                generateTaglibTld(taglib.getTldName());
117            }
118
119        } catch (ProcessorException | IOException ex) {
120            Logger.getLogger(TaglibProcessor.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
121            Messager messager = processingEnv.getMessager();
122            messager.printMessage(Diagnostic.Kind.ERROR, ex.getMessage());
123            return false;
124
125        } finally {
126            taglib = null;
127            taglibSet = false;
128        }
129
130        return true;
131    }
132
133    /**
134     * Processes a {@link Tag} annotation.
135     *
136     * @param element
137     *            Program element with that tag
138     */
139    private void processTag(Element element) {
140        Tag tagAnno = element.getAnnotation(Tag.class);
141        String className = element.toString();
142        String tagName = computeTagName(tagAnno.name(), className);
143
144        // Try to evaluate the class name of the tag type
145        String tagTypeClass = null;
146        try {
147            Class<? extends JspTag> tagType = tagAnno.type();
148            tagTypeClass = tagType.getName();
149        } catch(MirroredTypeException ex) {
150            // This is a hack, see http://forums.sun.com/thread.jspa?threadID=791053
151            Logger.getLogger(TaglibProcessor.class.getName()).log(Level.FINE, "use type mirror", ex);
152            tagTypeClass = ex.getTypeMirror().toString();
153        }
154
155        if (!PROXY_MAP.containsKey(tagTypeClass)) {
156            throw new ProcessorException("No proxy for tag type " + tagTypeClass);
157        }
158
159        TagBean tag = new TagBean(tagName, className, tagAnno.bodycontent(), tagTypeClass);
160        tag.setProxyClassName(className + "Proxy");
161
162        if (StringUtils.hasText(tagAnno.bean())) {
163            tag.setBeanName(tagAnno.bean());
164        } else {
165            tag.setBeanName(StringUtils.uncapitalize(StringUtils.unqualify(className)));
166        }
167
168        tag.setTryCatchFinally(tagAnno.tryCatchFinally());
169
170        taglib.addTag(tag);
171    }
172
173    /**
174     * Processes a {@link TagInfo} annotation.
175     *
176     * @param element
177     *            Program element with that tag
178     */
179    private void processTagInfo(Element element) {
180        TagInfo tagAnno = element.getAnnotation(TagInfo.class);
181
182        if (element.getKind().equals(ElementKind.PACKAGE)) {
183            taglib.setInfo(tagAnno.value());
184            return;
185        }
186
187        String className = element.toString();
188
189        TagBean tag = taglib.getTagForClass(className);
190        if (tag == null) {
191            throw new ProcessorException("Missing @Tag on class: " + className);
192        }
193
194        tag.setInfo(tagAnno.value());
195    }
196
197    /**
198     * Processes a {@link BeanFactoryReference} annotation.
199     *
200     * @param element
201     *            Program element with that tag
202     */
203    private void processBeanFactoryReference(Element element) {
204        BeanFactoryReference tagAnno = element.getAnnotation(BeanFactoryReference.class);
205
206        if (element.getKind().equals(ElementKind.PACKAGE)) {
207            if (taglib.getBeanFactoryReference() != null) {
208                throw new ProcessorException("Package @BeanFactoryReference already defined");
209            }
210
211            taglib.setBeanFactoryReference(tagAnno.value());
212            return;
213        }
214
215        String className = element.toString();
216        TagBean tag = taglib.getTagForClass(className);
217        if (tag == null) {
218            throw new ProcessorException("Missing @Tag on class: " + className);
219        }
220
221        tag.setBeanFactoryReference(tagAnno.value());
222    }
223
224    /**
225     * Processes a {@link TagLib} annotation.
226     *
227     * @param element
228     *            Program element with that tag
229     */
230    private void processTagLib(Element element) {
231        if (taglibSet) {
232            throw new ProcessorException("@TagLib already defined");
233        }
234
235        TagLib tagAnno = element.getAnnotation(TagLib.class);
236
237        taglib.setShortname(tagAnno.shortname());
238        taglib.setTlibversion(tagAnno.tlibversion());
239        taglib.setTldName(tagAnno.tld());
240
241        if (StringUtils.hasText(tagAnno.jspversion())) {
242            taglib.setJspversion(tagAnno.jspversion());
243        }
244
245        if (StringUtils.hasText(tagAnno.uri())) {
246            taglib.setUri(tagAnno.uri());
247        }
248
249        taglibSet = true;
250    }
251
252    /**
253     * Processes a {@link TagParameter} annotation.
254     *
255     * @param element
256     *            Program element with that tag
257     */
258    private void processTagParameter(Element element) {
259        TagParameter tagAnno = element.getAnnotation(TagParameter.class);
260        String methodName = element.toString();
261        String className = element.getEnclosingElement().toString();
262
263        TagBean tag = taglib.getTagForClass(className);
264        if (tag == null) {
265            throw new ProcessorException("Missing @Tag on class: " + className);
266        }
267
268        Matcher m = METHOD_PATTERN.matcher(methodName);
269        if (!m.matches()) {
270            throw new ProcessorException("@TagParameter must be used on a setter method: " + methodName);
271        }
272
273        String attrName = StringUtils.uncapitalize(m.group(1));
274        String attrType = m.group(2);
275
276        if (attrType.indexOf(',') >= 0) {
277            throw new ProcessorException("@TagParameter setter only allows one parameter: " + methodName);
278        }
279
280        AttributeBean attr = new AttributeBean(attrName, attrType, tagAnno.required(), tagAnno.rtexprvalue());
281        tag.addAttribute(attr);
282    }
283
284    /**
285     * Computes the name of a tag. If there was a name given in the annotation, it will be
286     * used. Otherwise, a name is derived from the class name of the tag class, with a
287     * "Tag" suffix removed.
288     *
289     * @param annotation
290     *            Tag name, as given in the annotation
291     * @param className
292     *            Name of the tag class
293     * @return Name of the tag
294     */
295    private @Nonnull String computeTagName(String annotation, String className) {
296        String result = annotation;
297        if (!StringUtils.hasText(result)) {
298            result = StringUtils.unqualify(className);
299            if (result.endsWith("Tag")) {
300                result = result.substring(0, result.length() - 3);
301            }
302            result = StringUtils.uncapitalize(result);
303        }
304        return result;
305    }
306
307    /**
308     * Generates a proxy class that connects to Spring and allows all Spring features like
309     * dependency injection in the implementing tag class.
310     *
311     * @param tag
312     *            {@link TagBean} that describes the tag.
313     * @throws IOException
314     *             when the generated Java code could not be saved.
315     */
316    private void generateProxyClass(TagBean tag) throws IOException {
317        String beanFactoryReference = tag.getBeanFactoryReference();
318        if (beanFactoryReference == null) {
319            beanFactoryReference = taglib.getBeanFactoryReference();
320        }
321
322        JavaFileObject src = processingEnv.getFiler().createSourceFile(tag.getProxyClassName());
323
324        String packageName = null;
325        int packPos = tag.getClassName().lastIndexOf('.');
326        if (packPos >= 0) {
327            packageName = tag.getClassName().substring(0, packPos);
328        }
329
330        String proxyClass = PROXY_MAP.get(tag.getType());
331
332        try (PrintWriter out = new PrintWriter(src.openWriter())) {
333            if (packageName != null) {
334                out.printf("package %s;", packageName).println();
335                out.println();
336            }
337
338            out.print("@javax.annotation.Generated(\"");
339            out.print(TaglibProcessor.class.getName());
340            out.println("\")");
341
342            out.printf("public class %s extends %s<%s> %s {",
343                    StringUtils.unqualify(tag.getProxyClassName()),
344                    proxyClass,
345                    tag.getClassName(),
346                    tag.isTryCatchFinally() ? "implements javax.servlet.jsp.tagext.TryCatchFinally" : ""
347            ).println();
348
349            if (beanFactoryReference != null) {
350                out.println("  protected org.springframework.beans.factory.BeanFactory getBeanFactory(javax.servlet.jsp.JspContext jspContext) {");
351                out.printf(
352                        "    java.lang.Object beanFactory = jspContext.findAttribute(\"%s\");",
353                        beanFactoryReference
354                ).println();
355                out.println("    if (beanFactory == null) {");
356                out.printf("      throw new java.lang.NullPointerException(\"attribute '%s' not set\");", beanFactoryReference).println();
357                out.println("    }");
358                out.println("    return (org.springframework.beans.factory.BeanFactory) beanFactory;");
359                out.println("  }");
360            }
361
362            out.println("  protected java.lang.String getBeanName() {");
363            out.printf("    return \"%s\";", tag.getBeanName()).println();
364            out.println("  }");
365
366            for (AttributeBean attr : new TreeSet<>(tag.getAttributes())) {
367                out.printf("  public void set%s(%s _%s) {",
368                        StringUtils.capitalize(attr.getName()),
369                        attr.getType(),
370                        attr.getName()
371                ).println();
372
373                out.printf("    getTargetBean().set%s(_%s);",
374                        StringUtils.capitalize(attr.getName()),
375                        attr.getName()
376                ).println();
377
378                out.println("  }");
379            }
380
381            out.println("}");
382        }
383    }
384
385    /**
386     * Generates a TLD file for the tag library.
387     *
388     * @param tldfile
389     *            name of the TLD file to be generated
390     * @throws IOException
391     *             when the generated TLD file could not be saved.
392     */
393    private void generateTaglibTld(String tldfile) throws IOException {
394        FileObject file = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", tldfile);
395        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(file.openOutputStream(), "UTF-8"))) {
396            out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
397            out.println("<!DOCTYPE taglib PUBLIC \"-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN\" \"http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd\">");
398            out.println("<!-- Generated file, do not edit! -->");
399            out.println("<taglib>");
400            out.printf("  <tlibversion>%s</tlibversion>", taglib.getTlibversion()).println();
401            out.printf("  <jspversion>%s</jspversion>", taglib.getJspversion()).println();
402            out.printf("  <shortname>%s</shortname>", taglib.getShortname()).println();
403            out.printf("  <uri>%s</uri>", escapeXml(taglib.getUri())).println();
404            out.printf("  <info>%s</info>", escapeXml(taglib.getInfo())).println();
405
406            for (TagBean tag : new TreeSet<>(taglib.getTags())) {
407                out.println("  <tag>");
408                out.printf("    <name>%s</name>", tag.getName()).println();
409                out.printf("    <tagclass>%s</tagclass>", tag.getProxyClassName()).println();
410                out.printf("    <bodycontent>%s</bodycontent>", tag.getBodycontent()).println();
411                if (tag.getInfo() != null) {
412                    out.printf("    <info>%s</info>", escapeXml(tag.getInfo())).println();
413                }
414
415                for (AttributeBean attr : new TreeSet<>(tag.getAttributes())) {
416                    out.println("    <attribute>");
417                    out.printf("      <name>%s</name>", attr.getName()).println();
418                    out.printf("      <required>%s</required>", String.valueOf(attr.isRequired())).println();
419                    out.printf("      <rtexprvalue>%s</rtexprvalue>", String.valueOf(attr.isRtexprvalue())).println();
420                    out.println("    </attribute>");
421                }
422
423                out.println("  </tag>");
424            }
425
426            out.println("</taglib>");
427        }
428    }
429
430    /**
431     * Escapes a string so it can be used in XML.
432     *
433     * @param text
434     *            String to be escaped
435     * @return Escaped text
436     */
437    private static @Nonnull String escapeXml(String text) {
438        return text.replace("&", "&amp;").replace("<", "&lt;").replace("\"", "&quot;");
439    }
440
441}