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