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.lang3.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.Markup;
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.annotation.processing.SupportedSourceVersion;
43  import javax.lang.model.SourceVersion;
44  import javax.lang.model.element.ExecutableElement;
45  import javax.lang.model.element.PackageElement;
46  import javax.lang.model.element.TypeElement;
47  import javax.tools.FileObject;
48  import javax.tools.StandardLocation;
49  import javax.xml.parsers.DocumentBuilder;
50  import javax.xml.parsers.DocumentBuilderFactory;
51  import javax.xml.parsers.ParserConfigurationException;
52  import javax.xml.transform.OutputKeys;
53  import javax.xml.transform.Transformer;
54  import javax.xml.transform.TransformerException;
55  import javax.xml.transform.TransformerFactory;
56  import javax.xml.transform.dom.DOMSource;
57  import javax.xml.transform.stream.StreamResult;
58  import java.io.IOException;
59  import java.io.Writer;
60  import java.util.ArrayList;
61  import java.util.Arrays;
62  import java.util.Comparator;
63  import java.util.HashSet;
64  import java.util.List;
65  import java.util.Locale;
66  import java.util.Map;
67  import java.util.Set;
68  
69  @SupportedSourceVersion(SourceVersion.RELEASE_8)
70  @SupportedAnnotationTypes({
71      "org.apache.myfaces.tobago.apt.annotation.Tag",
72      "org.apache.myfaces.tobago.apt.annotation.TagAttribute",
73      "org.apache.myfaces.tobago.apt.annotation.Taglib"})
74  @SupportedOptions({
75      TaglibGenerator.TARGET_TAGLIB})
76  public class TaglibGenerator extends AbstractGenerator {
77  
78    static final String TARGET_TAGLIB = "targetTaglib";
79  
80    private Set<String> tagSet = new HashSet<>();
81    private Set<String> attributeSet = new HashSet<>();
82    private String currentTag;
83  
84    private String targetTaglib;
85  
86    @Override
87    public void configure() {
88      final Map<String, String> options = processingEnv.getOptions();
89      targetTaglib = options.get(TARGET_TAGLIB);
90  
91      info("Generating the *.tld and *.taglib.xml");
92      info("Options:");
93      info(TARGET_TAGLIB + ": " + targetTaglib);
94    }
95  
96    @Override
97    public void generate()
98        throws IOException, TransformerException, ParserConfigurationException, ClassNotFoundException {
99      for (final PackageElement packageElement : getPackages()) {
100       final Taglib taglibAnnotation = packageElement.getAnnotation(Taglib.class);
101 
102       createTaglib(taglibAnnotation, packageElement);
103     }
104   }
105 
106   protected void createTaglib(final Taglib taglibAnnotation, final PackageElement packageElement)
107       throws ParserConfigurationException, ClassNotFoundException, IOException, TransformerException {
108     resetDuplicateList();
109     final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
110     dbf.setValidating(false);
111 
112     // building the XML document
113 
114     final DocumentBuilder parser = dbf.newDocumentBuilder();
115     final Document document = parser.newDocument();
116 
117     final Element taglib = createTaglib(document, taglibAnnotation);
118     final String description = processingEnv.getElementUtils().getDocComment(packageElement);
119 
120     addComment("The next tags are commented because of MYFACES-3537. "
121         + "The application will not run with MyFaces before 2.0.14/2.1.8. "
122         + "This also affects WebSphere 8.5", taglib, document);
123     addComment("<description>" + description + "</description>", taglib, document);
124     addComment("<display-name>" + taglibAnnotation.displayName() + "</display-name>", taglib, document);
125 
126 /* XXX disabled, because of the bug explained in the comment above.
127     if (description != null) {
128       addLeafCDATAElement(description, "description", taglib, document);
129     }
130     addLeafTextElement(taglibAnnotation.displayName(), "display-name", taglib, document);
131 */
132 
133     addLeafTextElement(taglibAnnotation.uri(), "namespace", taglib, document);
134 
135     // XXX hack: should be configurable or generated from annotations.
136     if ("http://myfaces.apache.org/tobago/component".equals(taglibAnnotation.uri())) {
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(
172       final Document document, final Element taglib, final String functionName, final String functionClass,
173       final 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 (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     if (description.length() > 0) {
305       addLeafCDATAElement(description.toString(), "description", element, document);
306     }
307   }
308 
309   private String deprecationComment(final String string) {
310     if (string == null) {
311       return null;
312     }
313     String result = string;
314     final String deprecated = "@deprecated";
315     final int begin = result.indexOf(deprecated);
316     if (begin > -1) {
317       result = result.substring(begin + deprecated.length());
318       final int end = result.indexOf("@");
319       if (end > -1) {
320         result = result.substring(0, end);
321       }
322       return result.trim();
323     } else {
324       return null;
325     }
326   }
327 
328   private TypeElement getInterfaceDeclaration(final String name) {
329     for (final TypeElement type : getTypes()) {
330       if (name.equals(type.getQualifiedName().toString())) {
331         return type;
332       }
333     }
334     return null;
335   }
336 
337   private String createDescription(final UIComponentTag componentTag) {
338     final StringBuilder description = new StringBuilder();
339     description.append("<p><b>UIComponentClass: </b>");
340     description.append(componentTag.uiComponent());
341     description.append("</p>");
342     description.append("<p><b>RendererType: </b>");
343     description.append("<ul>");
344     boolean first = true;
345     for (final String rendererType : componentTag.rendererType()) {
346       description.append("<li>");
347       description.append(rendererType);
348       if (first) {
349         description.append(" (default)");
350       }
351       description.append("</li>");
352       first = false;
353     }
354     description.append("</ul>");
355     description.append("</p>");
356     final Facet[] facets = componentTag.facets();
357     if (facets.length > 0) {
358       description.append("<p><b>Supported facets:</b></p>");
359       description.append("<dl>");
360       for (final Facet facet : facets) {
361         description.append("<dt><b>");
362         description.append(facet.name());
363         description.append("</b></dt>");
364         description.append("<dd>");
365         description.append(facet.description());
366         description.append("</dd>");
367       }
368       description.append("</dl>");
369     }
370     final Markup[] markups = componentTag.markups();
371     if (markups.length > 0) {
372       description.append("<p><b>Supported markups:</b></p>");
373       description.append("<dl>");
374       for (final Markup markup : markups) {
375         description.append("<dt><b>");
376         description.append(markup.name());
377         description.append("</b></dt>");
378         description.append("<dd>");
379         description.append(markup.description());
380         description.append("</dd>");
381       }
382       description.append("</dl>");
383     }
384     return description.toString();
385   }
386 
387   protected void addAttributes(
388       final TypeElement typeElement, final Element tagElement, final Document document)
389       throws ClassNotFoundException {
390 
391     for (final javax.lang.model.element.Element element : getAllMembers(typeElement)) {
392       if (element instanceof ExecutableElement) {
393         final ExecutableElement executableElement = (ExecutableElement) element;
394         if (executableElement.getAnnotation(TagAttribute.class) == null
395             && executableElement.getAnnotation(UIComponentTagAttribute.class) == null) {
396           continue;
397         }
398         addAttribute(executableElement, tagElement, document);
399       }
400     }
401   }
402 
403   private List<? extends javax.lang.model.element.Element> getAllMembers(final TypeElement type) {
404     final List<? extends javax.lang.model.element.Element> members
405         = new ArrayList<javax.lang.model.element.Element>(processingEnv.getElementUtils().getAllMembers(type));
406     members.sort(Comparator.comparing(d -> d.getSimpleName().toString()));
407     return members;
408   }
409 
410   private void resetDuplicateList() {
411     tagSet = new HashSet<>();
412   }
413 
414   private void resetAttributeDuplicateList() {
415     attributeSet = new HashSet<>();
416   }
417 
418   protected void addAttribute(
419       final ExecutableElement element, final Element tagElement, final Document document)
420       throws ClassNotFoundException {
421     final TagAttribute tagAttribute = element.getAnnotation(TagAttribute.class);
422     if (tagAttribute != null) {
423       final String simpleName = element.getSimpleName().toString();
424       if (simpleName.startsWith("set") || simpleName.startsWith("get")) {
425         final Element attribute = document.createElement("attribute");
426         String attributeName = simpleName.substring(3, 4).toLowerCase(Locale.ENGLISH) + simpleName.substring(4);
427         if (tagAttribute.name().length() > 0) {
428           attributeName = tagAttribute.name();
429         }
430         checkAttributeDuplicates(attributeName);
431         addDescription(element, attribute, document, false);
432         addLeafTextElement(attributeName, "name", attribute, document);
433 
434         addLeafTextElement(Boolean.toString(tagAttribute.required()), "required", attribute, document);
435         final UIComponentTagAttribute componentTagAttribute = element.getAnnotation(UIComponentTagAttribute.class);
436         addAttributeType(attribute, tagAttribute, componentTagAttribute, document);
437         tagElement.appendChild(attribute);
438       } else {
439         throw new IllegalArgumentException("Only setter allowed found: " + simpleName);
440       }
441     }
442   }
443 
444   protected void addComment(final String text, final Element parent, final Document document) {
445     final Comment comment = document.createComment(text);
446     parent.appendChild(comment);
447   }
448 
449   protected void addLeafTextElement(
450       final String text, final String node, final Element parent, final Document document) {
451     final Element element = document.createElement(node);
452     element.appendChild(document.createTextNode(text));
453     parent.appendChild(element);
454   }
455 
456   protected void addLeafCDATAElement(
457       final String text, final String node, final Element parent, final Document document) {
458     final Element element = document.createElement(node);
459     element.appendChild(document.createCDATASection(text));
460     parent.appendChild(element);
461   }
462 
463   protected Element createTaglib(final Document document, final Taglib taglibAnnotation) {
464     final Element taglib;
465     taglib = document.createElement("facelet-taglib");
466     taglib.setAttribute("id", taglibAnnotation.shortName());
467     taglib.setAttribute("xmlns", "http://java.sun.com/xml/ns/javaee");
468     taglib.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
469     taglib.setAttribute("xsi:schemaLocation",
470         "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd");
471     taglib.setAttribute("version", "2.0");
472     return taglib;
473   }
474 
475   protected void addTagContent(
476       final TypeElement typeElement, final Element tagElement, final Document document, final boolean deprecated,
477       final Tag annotationTag) {
478     if (deprecated) {
479       addLeafTextElement(annotationTag.deprecatedName(), "tag-name", tagElement, document);
480     } else {
481       addLeafTextElement(annotationTag.name(), "tag-name", tagElement, document);
482     }
483 
484     final UIComponentTag componentTag = typeElement.getAnnotation(UIComponentTag.class);
485     if (componentTag != null) {
486       final Element componentElement = document.createElement("component");
487       tagElement.appendChild(componentElement);
488       addLeafTextElement(
489           AnnotationUtils.componentType(componentTag), "component-type", componentElement, document);
490       if (componentTag.rendererType().length > 0) {
491         addLeafTextElement(componentTag.rendererType()[0], "renderer-type", componentElement, document);
492       }
493       addLeafTextElement(componentTag.faceletHandler(), "handler-class", componentElement, document);
494     }
495 
496     final SimpleTag simpleTag = typeElement.getAnnotation(SimpleTag.class);
497     if (simpleTag != null) {
498       addLeafTextElement(simpleTag.faceletHandler(), "handler-class", tagElement, document);
499     }
500 
501     final ValidatorTag validatorTag = typeElement.getAnnotation(ValidatorTag.class);
502     if (validatorTag != null) {
503       final Element validatorElement = document.createElement("validator");
504       tagElement.appendChild(validatorElement);
505       addLeafTextElement(validatorTag.validatorId(), "validator-id", validatorElement, document);
506       if (StringUtils.isNotBlank(validatorTag.faceletHandler())) {
507         addLeafTextElement(validatorTag.faceletHandler(), "handler-class", validatorElement, document);
508       }
509     }
510   }
511 
512   protected void addAttributeType(
513       final Element attribute, final TagAttribute tagAttribute, final UIComponentTagAttribute componentTagAttribute,
514       final Document document) {
515     if (!tagAttribute.rtexprvalue()) {
516       if (componentTagAttribute != null) {
517         if (componentTagAttribute.expression().isMethodExpression()) {
518           // todo
519         } else if (componentTagAttribute.expression().isValueExpression()) {
520           String clazz;
521           if (componentTagAttribute.type().length == 1) {
522             clazz = componentTagAttribute.type()[0];
523             final Class wrapper = ClassUtils.getWrapper(clazz);
524             if (wrapper != null) {
525               clazz = wrapper.getName(); // primitive types aren't allowed here
526       /*                } else {
527                       XXX what is with inner classes and arrays?
528                       if (clazz.endsWith("[]")) {
529                         Class.forName(clazz.substring(0, clazz.length() - 2)); // type check
530                       } else {
531                         Class.forName(clazz); // type check
532                       }
533       */
534             }
535           } else {
536             clazz = "java.lang.Object";
537           }
538           addLeafTextElement(clazz, "type", attribute, document);
539         }
540       } else {
541         addLeafTextElement(tagAttribute.type(), "type", attribute, document);
542       }
543     }
544   }
545 }