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("&", "&").replace("<", "<").replace("\"", """); 439 } 440 441}