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(String string) {
309     if (string == null) {
310       return null;
311     }
312     final String deprecated = "@deprecated";
313     final int begin = string.indexOf(deprecated);
314     if (begin > -1) {
315       string = string.substring(begin + deprecated.length());
316       final int end = string.indexOf("@");
317       if (end > -1) {
318         string = string.substring(0, end);
319       }
320       return string.trim();
321     } else {
322       return null;
323     }
324   }
325 
326   private TypeElement getInterfaceDeclaration(final String name) {
327     for (final TypeElement type : getTypes()) {
328       if (name.equals(type.getQualifiedName().toString())) {
329         return type;
330       }
331     }
332     return null;
333   }
334 
335   private String createDescription(final UIComponentTag componentTag) {
336     final StringBuilder description = new StringBuilder();
337     description.append("<p><b>UIComponentClass: </b>");
338     description.append(componentTag.uiComponent());
339     description.append("</p>");
340     description.append("<p><b>RendererType: </b>");
341     description.append(componentTag.rendererType());
342     description.append("</p>");
343     final Facet[] facets = componentTag.facets();
344     if (facets.length > 0) {
345       description.append("<p><b>Supported facets:</b></p>");
346       description.append("<dl>");
347       for (final Facet facet : facets) {
348         description.append("<dt><b>");
349         description.append(facet.name());
350         description.append("</b></dt>");
351         description.append("<dd>");
352         description.append(facet.description());
353         description.append("</dd>");
354       }
355       description.append("</dl>");
356     }
357     return description.toString();
358   }
359 
360   protected void addAttributes(
361       final TypeElement typeElement, final Element tagElement, final Document document)
362       throws ClassNotFoundException {
363 
364     for (final javax.lang.model.element.Element element : getAllMembers(typeElement)) {
365       if (element instanceof ExecutableElement) {
366         final ExecutableElement executableElement = (ExecutableElement) element;
367         if (executableElement.getAnnotation(TagAttribute.class) == null
368             && executableElement.getAnnotation(UIComponentTagAttribute.class) == null) {
369           continue;
370         }
371         addAttribute(executableElement, tagElement, document);
372       }
373     }
374   }
375 
376   private List<? extends javax.lang.model.element.Element> getAllMembers(final TypeElement type) {
377     final List<? extends javax.lang.model.element.Element> members
378         = new ArrayList<javax.lang.model.element.Element>(processingEnv.getElementUtils().getAllMembers(type));
379     Collections.sort(members, new Comparator<javax.lang.model.element.Element>() {
380       @Override
381       public int compare(final javax.lang.model.element.Element d1, final javax.lang.model.element.Element d2) {
382         return d1.getSimpleName().toString().compareTo(d2.getSimpleName().toString());
383       }
384     });
385     return members;
386   }
387 
388   private void resetDuplicateList() {
389     tagSet = new HashSet<String>();
390   }
391 
392   private void resetAttributeDuplicateList() {
393     attributeSet = new HashSet<String>();
394   }
395 
396   protected void addAttribute(
397       final ExecutableElement element, final Element tagElement, final Document document)
398       throws ClassNotFoundException {
399     final TagAttribute tagAttribute = element.getAnnotation(TagAttribute.class);
400     if (tagAttribute != null) {
401       final String simpleName = element.getSimpleName().toString();
402       if (simpleName.startsWith("set") || simpleName.startsWith("get")) {
403         final Element attribute = document.createElement("attribute");
404         String attributeName = simpleName.substring(3, 4).toLowerCase(Locale.ENGLISH) + simpleName.substring(4);
405         if (tagAttribute.name().length() > 0) {
406           attributeName = tagAttribute.name();
407         }
408         checkAttributeDuplicates(attributeName);
409         addDescription(element, attribute, document, false);
410         addLeafTextElement(attributeName, "name", attribute, document);
411 
412         addLeafTextElement(Boolean.toString(tagAttribute.required()), "required", attribute, document);
413         final UIComponentTagAttribute componentTagAttribute = element.getAnnotation(UIComponentTagAttribute.class);
414         addAttributeType(attribute, tagAttribute, componentTagAttribute, document);
415         tagElement.appendChild(attribute);
416       } else {
417         throw new IllegalArgumentException("Only setter allowed found: " + simpleName);
418       }
419     }
420   }
421 
422   protected void addComment(final String text, final org.w3c.dom.Element parent, final Document document) {
423     final Comment comment = document.createComment(text);
424     parent.appendChild(comment);
425   }
426 
427   protected void addLeafTextElement(
428       final String text, final String node, final org.w3c.dom.Element parent, final Document document) {
429     final org.w3c.dom.Element element = document.createElement(node);
430     element.appendChild(document.createTextNode(text));
431     parent.appendChild(element);
432   }
433 
434   protected void addLeafCDATAElement(
435       final String text, final String node, final org.w3c.dom.Element parent, final Document document) {
436     final org.w3c.dom.Element element = document.createElement(node);
437     element.appendChild(document.createCDATASection(text));
438     parent.appendChild(element);
439   }
440 
441   protected Element createTaglib(final Document document, Taglib taglibAnnotation) {
442     final Element taglib;
443     taglib = document.createElement("facelet-taglib");
444     taglib.setAttribute("id", taglibAnnotation.shortName());
445     taglib.setAttribute("xmlns", "http://java.sun.com/xml/ns/javaee");
446     taglib.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
447     taglib.setAttribute("xsi:schemaLocation",
448         "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd");
449     taglib.setAttribute("version", "2.0");
450     return taglib;
451   }
452 
453   protected void addTagContent(
454       final TypeElement typeElement, final Element tagElement, final Document document, final boolean deprecated,
455       final Tag annotationTag) {
456     if (deprecated) {
457       addLeafTextElement(annotationTag.deprecatedName(), "tag-name", tagElement, document);
458     } else {
459       addLeafTextElement(annotationTag.name(), "tag-name", tagElement, document);
460     }
461 
462     final UIComponentTag componentTag = typeElement.getAnnotation(UIComponentTag.class);
463     if (componentTag != null) {
464       final Element componentElement = document.createElement("component");
465       tagElement.appendChild(componentElement);
466       addLeafTextElement(
467           AnnotationUtils.componentType(componentTag), "component-type", componentElement, document);
468       if (StringUtils.isNotBlank(componentTag.rendererType())) {
469         addLeafTextElement(componentTag.rendererType(), "renderer-type", componentElement, document);
470       }
471       addLeafTextElement(componentTag.faceletHandler(), "handler-class", componentElement, document);
472     }
473 
474     final SimpleTag simpleTag = typeElement.getAnnotation(SimpleTag.class);
475     if (simpleTag != null) {
476       addLeafTextElement(simpleTag.faceletHandler(), "handler-class", tagElement, document);
477     }
478 
479     final ValidatorTag validatorTag = typeElement.getAnnotation(ValidatorTag.class);
480     if (validatorTag != null) {
481       final Element validatorElement = document.createElement("validator");
482       tagElement.appendChild(validatorElement);
483       addLeafTextElement(validatorTag.validatorId(), "validator-id", validatorElement, document);
484       if (StringUtils.isNotBlank(validatorTag.faceletHandler())) {
485         addLeafTextElement(validatorTag.faceletHandler(), "handler-class", validatorElement, document);
486       }
487     }
488   }
489 
490   protected void addAttributeType(
491       final Element attribute, final TagAttribute tagAttribute, final UIComponentTagAttribute componentTagAttribute,
492       final Document document) {
493     if (!tagAttribute.rtexprvalue()) {
494       if (componentTagAttribute != null) {
495         if (componentTagAttribute.expression().isMethodExpression()) {
496           // todo
497         } else if (componentTagAttribute.expression().isValueExpression()) {
498           String clazz;
499           if (componentTagAttribute.type().length == 1) {
500             clazz = componentTagAttribute.type()[0];
501             final Class wrapper = ClassUtils.getWrapper(clazz);
502             if (wrapper != null) {
503               clazz = wrapper.getName(); // primitive types aren't allowed here
504       /*                } else {
505                       XXX what is with inner classes and arrays?
506                       if (clazz.endsWith("[]")) {
507                         Class.forName(clazz.substring(0, clazz.length() - 2)); // type check
508                       } else {
509                         Class.forName(clazz); // type check
510                       }
511       */
512             }
513           } else {
514             clazz = "java.lang.Object";
515           }
516           addLeafTextElement(clazz, "type", attribute, document);
517         }
518       } else {
519         addLeafTextElement(tagAttribute.type(), "type", attribute, document);
520       }
521     }
522   }
523 }