View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.apt.processor;
21  
22  import org.apache.commons.io.IOUtils;
23  import org.apache.commons.lang.StringUtils;
24  import org.apache.myfaces.tobago.apt.AnnotationUtils;
25  import org.apache.myfaces.tobago.apt.annotation.ExtensionTag;
26  import org.apache.myfaces.tobago.apt.annotation.Facet;
27  import org.apache.myfaces.tobago.apt.annotation.Preliminary;
28  import org.apache.myfaces.tobago.apt.annotation.SimpleTag;
29  import org.apache.myfaces.tobago.apt.annotation.Tag;
30  import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
31  import org.apache.myfaces.tobago.apt.annotation.Taglib;
32  import org.apache.myfaces.tobago.apt.annotation.UIComponentTag;
33  import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
34  import org.apache.myfaces.tobago.apt.annotation.ValidatorTag;
35  import org.apache.myfaces.tobago.apt.generate.ClassUtils;
36  import org.w3c.dom.Comment;
37  import org.w3c.dom.Document;
38  import org.w3c.dom.Element;
39  
40  import javax.annotation.processing.SupportedAnnotationTypes;
41  import javax.annotation.processing.SupportedOptions;
42  import javax.lang.model.element.ExecutableElement;
43  import javax.lang.model.element.PackageElement;
44  import javax.lang.model.element.TypeElement;
45  import javax.tools.FileObject;
46  import javax.tools.StandardLocation;
47  import javax.xml.parsers.DocumentBuilder;
48  import javax.xml.parsers.DocumentBuilderFactory;
49  import javax.xml.parsers.ParserConfigurationException;
50  import javax.xml.transform.OutputKeys;
51  import javax.xml.transform.Transformer;
52  import javax.xml.transform.TransformerException;
53  import javax.xml.transform.TransformerFactory;
54  import javax.xml.transform.dom.DOMSource;
55  import javax.xml.transform.stream.StreamResult;
56  import java.io.IOException;
57  import java.io.Writer;
58  import java.util.ArrayList;
59  import java.util.Arrays;
60  import java.util.Collections;
61  import java.util.Comparator;
62  import java.util.HashSet;
63  import java.util.List;
64  import java.util.Locale;
65  import java.util.Map;
66  import java.util.Set;
67  
68  @SupportedAnnotationTypes({
69      "org.apache.myfaces.tobago.apt.annotation.Tag",
70      "org.apache.myfaces.tobago.apt.annotation.TagAttribute",
71      "org.apache.myfaces.tobago.apt.annotation.Taglib"})
72  @SupportedOptions({
73      TaglibGenerator.TARGET_TAGLIB})
74  public class TaglibGenerator extends AbstractGenerator {
75  
76    static final String TARGET_TAGLIB = "targetTaglib";
77  
78    private Set<String> tagSet = new HashSet<String>();
79    private Set<String> attributeSet = new HashSet<String>();
80    private String currentTag;
81  
82    private String targetTaglib;
83  
84    @Override
85    public void configure() {
86      final Map<String, String> options = processingEnv.getOptions();
87      targetTaglib = options.get(TARGET_TAGLIB);
88  
89      info("Generating the *.tld and *.taglib.xml");
90      info("Options:");
91      info(TARGET_TAGLIB + ": " + targetTaglib);
92    }
93  
94    @Override
95    public void generate()
96        throws IOException, TransformerException, ParserConfigurationException, ClassNotFoundException {
97      for (final PackageElement packageElement : getPackages()) {
98        final Taglib taglibAnnotation = packageElement.getAnnotation(Taglib.class);
99  
100       createTaglib(taglibAnnotation, packageElement);
101     }
102   }
103 
104   protected void createTaglib(final Taglib taglibAnnotation, final PackageElement packageElement)
105       throws ParserConfigurationException, ClassNotFoundException, IOException, TransformerException {
106     resetDuplicateList();
107     final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
108     dbf.setValidating(false);
109 
110     // building the XML document
111 
112     final DocumentBuilder parser = dbf.newDocumentBuilder();
113     final Document document = parser.newDocument();
114 
115     final Element taglib = createTaglib(document);
116     final String description = processingEnv.getElementUtils().getDocComment(packageElement);
117 
118     addComment("The next tags are commented because of MYFACES-3537. "
119         + "The application will not run with MyFaces before 2.0.14/2.1.8. "
120         + "This also affects WebSphere 8.5", taglib, document);
121     addComment("<description>" + description + "</description>", taglib, document);
122     addComment("<display-name>" + taglibAnnotation.displayName() + "</display-name>", taglib, document);
123 
124 /* XXX disabled, because of the bug explained in the comment above.
125     if (description != null) {
126       addLeafCDATAElement(description, "description", taglib, document);
127     }
128     addLeafTextElement(taglibAnnotation.displayName(), "display-name", taglib, document);
129 */
130 
131     addLeafTextElement(taglibAnnotation.uri(), "namespace", taglib, document);
132 
133     // XXX hack: should be configurable or generated from annotations.
134     if ("http://myfaces.apache.org/tobago/component".equals(taglibAnnotation.uri())) {
135       addFunction(document, taglib, "columnPartition", "org.apache.myfaces.tobago.layout.ColumnPartition",
136           "org.apache.myfaces.tobago.layout.ColumnPartition valueOf(java.lang.String)");
137 
138       for (int i = 1; i < 10; i++) {
139         addFunction(document, taglib, "format" + i, "org.apache.myfaces.tobago.util.MessageFormat",
140             "java.lang.String format(java.lang.String"+ StringUtils.repeat(", java.lang.Object", i) +")");
141       }
142     }
143 
144     for (final TypeElement typeElement : getTypes()) {
145       if (processingEnv.getElementUtils().getPackageOf(typeElement).equals(packageElement)) {
146         appendTag(typeElement, taglib, document);
147       }
148     }
149     document.appendChild(taglib);
150 
151     // writing the XML document
152 
153     Writer writer = null;
154     try {
155       String target = targetTaglib;
156       target = StringUtils.isNotBlank(target) ? target + '/' : "";
157       final String name = target + taglibAnnotation.name() + ".taglib.xml";
158       final FileObject resource = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", name);
159       info("Writing to file: " + resource.toUri());
160       writer = resource.openWriter();
161 
162       final TransformerFactory transFactory = TransformerFactory.newInstance();
163       transFactory.setAttribute("indent-number", 2);
164       final Transformer transformer = transFactory.newTransformer();
165       transformer.setOutputProperty(OutputKeys.INDENT, "yes");
166       transformer.transform(new DOMSource(document), new StreamResult(writer));
167     } finally {
168       IOUtils.closeQuietly(writer);
169     }
170   }
171 
172   private void addFunction(Document document, Element taglib, String functionName, String functionClass,
173                            String functionSignature) {
174     final Element function = document.createElement("function");
175     taglib.appendChild(function);
176     addLeafTextElement(functionName, "function-name", function, document);
177     addLeafTextElement(functionClass, "function-class", function, document);
178     addLeafTextElement(functionSignature, "function-signature", function, document);
179   }
180 
181   protected void appendTag(
182       final TypeElement typeElement, final Element parent, final Document document)
183       throws ClassNotFoundException {
184     final Tag annotationTag = typeElement.getAnnotation(Tag.class);
185     if (annotationTag != null) {
186       checkDuplicates(annotationTag.name());
187       resetAttributeDuplicateList();
188       // TODO configure replacement
189 //      final String className;
190 //      if (typeElement.getAnnotation(SimpleTag.class) != null
191 // || typeElement.getAnnotation(ValidatorTag.class) != null) {
192 //        className = AnnotationUtils.generatedTagName(typeElement);
193 //      } else if (typeElement.getAnnotation(ExtensionTag.class) != null) {
194 //        className = typeElement.getQualifiedName().toString();
195 //      } else if (typeElement.getAnnotation(UIComponentTag.class) != null) {
196 //        className = "org.apache.myfaces.tobago.internal.taglib." + StringUtils.capitalize(annotationTag.name())
197 //            + "Tag";
198 //      } else {
199 //        throw new RuntimeException("Not supported: " + typeElement.getQualifiedName());
200 //      }
201 //      info("Replacing: " + typeElement.getQualifiedName() + " -> " + className);
202       final Element tag = createTag(typeElement, annotationTag, document, false);
203       addAttributes(typeElement, tag, document);
204       parent.appendChild(tag);
205       if (annotationTag.deprecatedName() != null && annotationTag.deprecatedName().length() > 0) {
206         final Element deprecatedTag = createTag(typeElement, annotationTag, document, true);
207         addAttributes(typeElement, deprecatedTag, document);
208         parent.appendChild(deprecatedTag);
209       }
210     }
211   }
212 
213   protected Element createTag(
214       final TypeElement typeElement, final Tag annotationTag, final Document document,
215       final boolean deprecated) {
216     final Element tagElement = document.createElement("tag");
217     addDescription(typeElement, tagElement, document, deprecated);
218     addTagContent(typeElement, tagElement, document, deprecated, annotationTag);
219     return tagElement;
220   }
221 
222   private void checkAttributeDuplicates(final String attributeName) {
223     if (attributeSet.contains(attributeName)) {
224       throw new IllegalArgumentException("Attribute " + attributeName + " in tag " + currentTag + " already defined!");
225     } else {
226       attributeSet.add(attributeName);
227     }
228   }
229 
230   private void checkDuplicates(final String tagName) {
231     currentTag = tagName;
232     if (tagSet.contains(tagName)) {
233       throw new IllegalArgumentException("tag with name " + tagName + " already defined!");
234     } else {
235       tagSet.add(tagName);
236     }
237   }
238 
239   protected void addDescription(
240       final javax.lang.model.element.Element typeElement, final Element element, final Document document,
241       final boolean deprecated) {
242     final StringBuilder description = new StringBuilder();
243     final Deprecated deprecatedAnnotation = typeElement.getAnnotation(Deprecated.class);
244     String comment = processingEnv.getElementUtils().getDocComment(typeElement);
245     final String deprecationComment = deprecationComment(comment);
246 
247     if (deprecatedAnnotation != null || deprecationComment != null) {
248       description.append("<p>**** @deprecated. Will be removed in a future version **** </p>");
249     }
250     if (deprecated) {
251       final Tag annotationTag = typeElement.getAnnotation(Tag.class);
252       description.append("<p>**** @deprecated. Will be removed in a future version. Use ");
253       description.append(annotationTag.name());
254       description.append(" instead. **** </p>");
255     }
256     if (deprecationComment != null) {
257       description.append("<p>").append(deprecationComment).append("</p>");
258     }
259 
260     final Preliminary preliminary = typeElement.getAnnotation(Preliminary.class);
261     if (preliminary != null) {
262       description.append("<p>**** Preliminary. Maybe subject to changed in a future version");
263       if (preliminary.value().length() > 0) {
264         description.append(": ");
265         description.append(preliminary.value());
266       }
267       description.append(" **** </p>");
268     }
269     if (comment != null) {
270       // remove @param section
271       final int index = comment.indexOf(" @");
272       if (index != -1) {
273         comment = comment.substring(0, index);
274       }
275       comment = comment.trim();
276       if (comment.length() > 0) {
277         //description.append("<p>");
278         description.append(comment);
279         //description.append("</p>");
280       }
281     }
282     final UIComponentTag componentTag = typeElement.getAnnotation(UIComponentTag.class);
283     if (componentTag != null) {
284       description.append(createDescription(componentTag));
285     }
286     final UIComponentTagAttribute attributeTag = typeElement.getAnnotation(UIComponentTagAttribute.class);
287     if (attributeTag != null) {
288       if (null != attributeTag.type() && attributeTag.type().length > 0) {
289         description.append("<br />Type: <code>")
290             .append(attributeTag.type().length == 1 ? attributeTag.type()[0] : Arrays.toString(attributeTag.type()))
291             .append("</code>");
292       }
293       if (StringUtils.isNotEmpty(attributeTag.defaultValue())) {
294         description.append("<br />Default: <code>")
295             .append(attributeTag.defaultValue())
296             .append("</code>");
297       }
298       if (attributeTag.allowedValues().length > 0) {
299         description.append("<br />Allowed Values: <code>")
300             .append(Arrays.toString(attributeTag.allowedValues()))
301             .append("</code>");
302       }
303     }
304     final ExtensionTag extensionTag = typeElement.getAnnotation(ExtensionTag.class);
305     if (extensionTag != null) {
306       final String baseName = extensionTag.baseClassName();
307       description.append("<p><b>Extended tag: </b>");
308       description.append(baseName);
309       description.append("</p>");
310 
311       final TypeElement declaration = getInterfaceDeclaration(baseName + "Declaration");
312       if (declaration != null) {
313         final UIComponentTag baseComponentTag = declaration.getAnnotation(UIComponentTag.class);
314         if (baseComponentTag != null) {
315           description.append(createDescription(baseComponentTag));
316         }
317       }
318     }
319     if (description.length() > 0) {
320       addLeafCDATAElement(description.toString(), "description", element, document);
321     }
322   }
323 
324   private String deprecationComment(String string) {
325     if (string == null) {
326       return null;
327     }
328     final String deprecated = "@deprecated";
329     final int begin = string.indexOf(deprecated);
330     if (begin > -1) {
331       string = string.substring(begin + deprecated.length());
332       final int end = string.indexOf("@");
333       if (end > -1) {
334         string = string.substring(0, end);
335       }
336       return string.trim();
337     } else {
338       return null;
339     }
340   }
341 
342   private TypeElement getInterfaceDeclaration(final String name) {
343     for (final TypeElement type : getTypes()) {
344       if (name.equals(type.getQualifiedName().toString())) {
345         return type;
346       }
347     }
348     return null;
349   }
350 
351   private String createDescription(final UIComponentTag componentTag) {
352     final StringBuilder description = new StringBuilder();
353     description.append("<p><b>UIComponentClass: </b>");
354     description.append(componentTag.uiComponent());
355     description.append("</p>");
356     description.append("<p><b>RendererType: </b>");
357     description.append(componentTag.rendererType());
358     description.append("</p>");
359     final Facet[] facets = componentTag.facets();
360     if (facets.length > 0) {
361       description.append("<p><b>Supported facets:</b></p>");
362       description.append("<dl>");
363       for (final Facet facet : facets) {
364         description.append("<dt><b>");
365         description.append(facet.name());
366         description.append("</b></dt>");
367         description.append("<dd>");
368         description.append(facet.description());
369         description.append("</dd>");
370       }
371       description.append("</dl>");
372     }
373     return description.toString();
374   }
375 
376   protected void addAttributes(
377       final TypeElement typeElement, final Element tagElement, final Document document)
378       throws ClassNotFoundException {
379 
380     for (final javax.lang.model.element.Element element : getAllMembers(typeElement)) {
381       if (element instanceof ExecutableElement) {
382         final ExecutableElement executableElement = (ExecutableElement) element;
383         if (executableElement.getAnnotation(TagAttribute.class) == null
384             && executableElement.getAnnotation(UIComponentTagAttribute.class) == null) {
385           continue;
386         }
387         addAttribute(executableElement, tagElement, document);
388       }
389     }
390   }
391 
392   private List<? extends javax.lang.model.element.Element> getAllMembers(final TypeElement type) {
393     final List<? extends javax.lang.model.element.Element> members
394         = new ArrayList<javax.lang.model.element.Element>(processingEnv.getElementUtils().getAllMembers(type));
395     Collections.sort(members, new Comparator<javax.lang.model.element.Element>() {
396       @Override
397       public int compare(final javax.lang.model.element.Element d1, final javax.lang.model.element.Element d2) {
398         return d1.getSimpleName().toString().compareTo(d2.getSimpleName().toString());
399       }
400     });
401     return members;
402   }
403 
404   private void resetDuplicateList() {
405     tagSet = new HashSet<String>();
406   }
407 
408   private void resetAttributeDuplicateList() {
409     attributeSet = new HashSet<String>();
410   }
411 
412   protected void addAttribute(
413       final ExecutableElement element, final Element tagElement, final Document document)
414       throws ClassNotFoundException {
415     final TagAttribute tagAttribute = element.getAnnotation(TagAttribute.class);
416     if (tagAttribute != null) {
417       final String simpleName = element.getSimpleName().toString();
418       if (simpleName.startsWith("set") || simpleName.startsWith("get")) {
419         final Element attribute = document.createElement("attribute");
420         String attributeName = simpleName.substring(3, 4).toLowerCase(Locale.ENGLISH) + simpleName.substring(4);
421         if (tagAttribute.name().length() > 0) {
422           attributeName = tagAttribute.name();
423         }
424         checkAttributeDuplicates(attributeName);
425         addDescription(element, attribute, document, false);
426         addLeafTextElement(attributeName, "name", attribute, document);
427 
428         addLeafTextElement(Boolean.toString(tagAttribute.required()), "required", attribute, document);
429         final UIComponentTagAttribute componentTagAttribute = element.getAnnotation(UIComponentTagAttribute.class);
430         addAttributeType(attribute, tagAttribute, componentTagAttribute, document);
431         tagElement.appendChild(attribute);
432       } else {
433         throw new IllegalArgumentException("Only setter allowed found: " + simpleName);
434       }
435     }
436   }
437 
438   protected void addComment(final String text, final org.w3c.dom.Element parent, final Document document) {
439     final Comment comment = document.createComment(text);
440     parent.appendChild(comment);
441   }
442 
443   protected void addLeafTextElement(
444       final String text, final String node, final org.w3c.dom.Element parent, final Document document) {
445     final org.w3c.dom.Element element = document.createElement(node);
446     element.appendChild(document.createTextNode(text));
447     parent.appendChild(element);
448   }
449 
450   protected void addLeafCDATAElement(
451       final String text, final String node, final org.w3c.dom.Element parent, final Document document) {
452     final org.w3c.dom.Element element = document.createElement(node);
453     element.appendChild(document.createCDATASection(text));
454     parent.appendChild(element);
455   }
456 
457   protected Element createTaglib(final Document document) {
458     final Element taglib;
459     taglib = document.createElement("facelet-taglib");
460     taglib.setAttribute("xmlns", "http://java.sun.com/xml/ns/javaee");
461     taglib.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
462     taglib.setAttribute("xsi:schemaLocation",
463         "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd");
464     taglib.setAttribute("version", "2.0");
465     return taglib;
466   }
467 
468   protected void addTagContent(
469       final TypeElement typeElement, final Element tagElement, final Document document, final boolean deprecated,
470       final Tag annotationTag) {
471     if (deprecated) {
472       addLeafTextElement(annotationTag.deprecatedName(), "tag-name", tagElement, document);
473     } else {
474       addLeafTextElement(annotationTag.name(), "tag-name", tagElement, document);
475     }
476 
477     final UIComponentTag componentTag = typeElement.getAnnotation(UIComponentTag.class);
478     if (componentTag != null) {
479       final Element componentElement = document.createElement("component");
480       tagElement.appendChild(componentElement);
481       addLeafTextElement(
482           AnnotationUtils.componentType(componentTag), "component-type", componentElement, document);
483       if (StringUtils.isNotBlank(componentTag.rendererType())) {
484         addLeafTextElement(componentTag.rendererType(), "renderer-type", componentElement, document);
485       }
486       addLeafTextElement(componentTag.faceletHandler(), "handler-class", componentElement, document);
487     }
488 
489     final ExtensionTag extensionTag = typeElement.getAnnotation(ExtensionTag.class);
490     if (extensionTag != null) {
491       final Element componentElement = document.createElement("component");
492       tagElement.appendChild(componentElement);
493       addLeafTextElement(extensionTag.componentType(), "component-type", componentElement, document);
494       addLeafTextElement(extensionTag.rendererType(), "renderer-type", componentElement, document);
495       addLeafTextElement(extensionTag.faceletHandler(), "handler-class", componentElement, document);
496     }
497 
498     final SimpleTag simpleTag = typeElement.getAnnotation(SimpleTag.class);
499     if (simpleTag != null) {
500       addLeafTextElement(simpleTag.faceletHandler(), "handler-class", tagElement, document);
501     }
502 
503     final ValidatorTag validatorTag = typeElement.getAnnotation(ValidatorTag.class);
504     if (validatorTag != null) {
505       final Element validatorElement = document.createElement("validator");
506       tagElement.appendChild(validatorElement);
507       addLeafTextElement(validatorTag.validatorId(), "validator-id", validatorElement, document);
508       if (StringUtils.isNotBlank(validatorTag.faceletHandler())) {
509         addLeafTextElement(validatorTag.faceletHandler(), "handler-class", validatorElement, document);
510       }
511     }
512   }
513 
514   protected void addAttributeType(
515       final Element attribute, final TagAttribute tagAttribute, final UIComponentTagAttribute componentTagAttribute,
516       final Document document) {
517     if (!tagAttribute.rtexprvalue()) {
518       if (componentTagAttribute != null) {
519         if (componentTagAttribute.expression().isMethodExpression()) {
520           // todo
521         } else if (componentTagAttribute.expression().isValueExpression()) {
522           String clazz;
523           if (componentTagAttribute.type().length == 1) {
524             clazz = componentTagAttribute.type()[0];
525             final Class wrapper = ClassUtils.getWrapper(clazz);
526             if (wrapper != null) {
527               clazz = wrapper.getName(); // primitive types aren't allowed here
528       /*                } else {
529                       XXX what is with inner classes and arrays?
530                       if (clazz.endsWith("[]")) {
531                         Class.forName(clazz.substring(0, clazz.length() - 2)); // type check
532                       } else {
533                         Class.forName(clazz); // type check
534                       }
535       */
536             }
537           } else {
538             clazz = "java.lang.Object";
539           }
540           addLeafTextElement(clazz, "type", attribute, document);
541         }
542       } else {
543         addLeafTextElement(tagAttribute.type(), "type", attribute, document);
544       }
545     }
546   }
547 }