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.lang3.StringUtils;
23  import org.apache.myfaces.tobago.apt.AnnotationUtils;
24  import org.apache.myfaces.tobago.apt.annotation.ConverterTag;
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     if (description != null) {
127       addLeafCDATAElement(description, "description", taglib, document);
128     }
129     addLeafTextElement(taglibAnnotation.displayName(), "display-name", taglib, document);
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       for (int i = 1; i < 10; i++) {
136         addFunction(document, taglib, "format" + i, "org.apache.myfaces.tobago.util.MessageFormat",
137             "java.lang.String format(java.lang.String"+ StringUtils.repeat(", java.lang.Object", i) +")");
138       }
139     }
140 
141     for (final TypeElement typeElement : getTypes()) {
142       if (processingEnv.getElementUtils().getPackageOf(typeElement).equals(packageElement)) {
143         appendTag(typeElement, taglib, document);
144       }
145     }
146     document.appendChild(taglib);
147 
148     // writing the XML document
149 
150       String target = targetTaglib;
151       target = StringUtils.isNotBlank(target) ? target + '/' : "";
152       final String name = target + taglibAnnotation.name() + ".taglib.xml";
153       final FileObject resource = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", name);
154       info("Writing to file: " + resource.toUri());
155 
156     try (Writer writer = resource.openWriter()) {
157       final TransformerFactory transFactory = TransformerFactory.newInstance();
158       transFactory.setAttribute("indent-number", 2);
159       final Transformer transformer = transFactory.newTransformer();
160       transformer.setOutputProperty(OutputKeys.INDENT, "yes");
161       transformer.transform(new DOMSource(document), new StreamResult(writer));
162     }
163   }
164 
165   private void addFunction(
166       final Document document, final Element taglib, final String functionName, final String functionClass,
167       final String functionSignature) {
168     final Element function = document.createElement("function");
169     taglib.appendChild(function);
170     addLeafTextElement(functionName, "function-name", function, document);
171     addLeafTextElement(functionClass, "function-class", function, document);
172     addLeafTextElement(functionSignature, "function-signature", function, document);
173   }
174 
175   protected void appendTag(
176       final TypeElement typeElement, final Element parent, final Document document)
177       throws ClassNotFoundException {
178     final Tag annotationTag = typeElement.getAnnotation(Tag.class);
179     if (annotationTag != null) {
180       checkDuplicates(annotationTag.name());
181       resetAttributeDuplicateList();
182       // TODO configure replacement
183 //      final String className;
184 //      if (typeElement.getAnnotation(SimpleTag.class) != null
185 // || typeElement.getAnnotation(ValidatorTag.class) != null) {
186 //        className = AnnotationUtils.generatedTagName(typeElement);
187 //      } else if (typeElement.getAnnotation(ExtensionTag.class) != null) {
188 //        className = typeElement.getQualifiedName().toString();
189 //      } else if (typeElement.getAnnotation(UIComponentTag.class) != null) {
190 //        className = "org.apache.myfaces.tobago.internal.taglib." + StringUtils.capitalize(annotationTag.name())
191 //            + "Tag";
192 //      } else {
193 //        throw new RuntimeException("Not supported: " + typeElement.getQualifiedName());
194 //      }
195 //      info("Replacing: " + typeElement.getQualifiedName() + " -> " + className);
196       final Element tag = createTag(typeElement, annotationTag, document, false);
197       addAttributes(typeElement, tag, document);
198       parent.appendChild(tag);
199       if (annotationTag.deprecatedName() != null && annotationTag.deprecatedName().length() > 0) {
200         final Element deprecatedTag = createTag(typeElement, annotationTag, document, true);
201         addAttributes(typeElement, deprecatedTag, document);
202         parent.appendChild(deprecatedTag);
203       }
204     }
205   }
206 
207   protected Element createTag(
208       final TypeElement typeElement, final Tag annotationTag, final Document document,
209       final boolean deprecated) {
210     final Element tagElement = document.createElement("tag");
211     addDescription(typeElement, tagElement, document, deprecated);
212     addTagContent(typeElement, tagElement, document, deprecated, annotationTag);
213     return tagElement;
214   }
215 
216   private void checkAttributeDuplicates(final String attributeName) {
217     if (attributeSet.contains(attributeName)) {
218       throw new IllegalArgumentException("Attribute " + attributeName + " in tag " + currentTag + " already defined!");
219     } else {
220       attributeSet.add(attributeName);
221     }
222   }
223 
224   private void checkDuplicates(final String tagName) {
225     currentTag = tagName;
226     if (tagSet.contains(tagName)) {
227       throw new IllegalArgumentException("tag with name " + tagName + " already defined!");
228     } else {
229       tagSet.add(tagName);
230     }
231   }
232 
233   protected void addDescription(
234       final javax.lang.model.element.Element typeElement, final Element element, final Document document,
235       final boolean deprecated) {
236     final StringBuilder description = new StringBuilder();
237     final Deprecated deprecatedAnnotation = typeElement.getAnnotation(Deprecated.class);
238     String comment = processingEnv.getElementUtils().getDocComment(typeElement);
239     final String deprecationComment = deprecationComment(comment);
240 
241     if (deprecatedAnnotation != null || deprecationComment != null) {
242       description.append("<p>**** @deprecated. Will be removed in a future version **** </p>");
243     }
244     if (deprecated) {
245       final Tag annotationTag = typeElement.getAnnotation(Tag.class);
246       description.append("<p>**** @deprecated. Will be removed in a future version. Use ");
247       description.append(annotationTag.name());
248       description.append(" instead. **** </p>");
249     }
250     if (deprecationComment != null) {
251       description.append("<p>").append(deprecationComment).append("</p>");
252     }
253 
254     final Preliminary preliminary = typeElement.getAnnotation(Preliminary.class);
255     if (preliminary != null) {
256       description.append("<p>**** Preliminary. Maybe subject to changed in a future version");
257       if (preliminary.value().length() > 0) {
258         description.append(": ");
259         description.append(preliminary.value());
260       }
261       description.append(" **** </p>");
262     }
263     if (comment != null) {
264       // remove @param section
265       final int index = comment.indexOf(" @");
266       if (index != -1) {
267         comment = comment.substring(0, index);
268       }
269       comment = comment.trim();
270       if (comment.length() > 0) {
271         //description.append("<p>");
272         description.append(comment);
273         //description.append("</p>");
274       }
275     }
276     final UIComponentTag componentTag = typeElement.getAnnotation(UIComponentTag.class);
277     if (componentTag != null) {
278       description.append(createDescription(componentTag));
279     }
280     final UIComponentTagAttribute attributeTag = typeElement.getAnnotation(UIComponentTagAttribute.class);
281     if (attributeTag != null) {
282       if (attributeTag.type().length > 0) {
283         description.append("<br />Type: <code>")
284             .append(attributeTag.type().length == 1 ? attributeTag.type()[0] : Arrays.toString(attributeTag.type()))
285             .append("</code>");
286       }
287       if (StringUtils.isNotEmpty(attributeTag.defaultValue())) {
288         description.append("<br />Default: <code>")
289             .append(attributeTag.defaultValue())
290             .append("</code>");
291       }
292       if (attributeTag.allowedValues().length > 0) {
293         description.append("<br />Allowed Values: <code>")
294             .append(Arrays.toString(attributeTag.allowedValues()))
295             .append("</code>");
296       }
297     }
298     if (description.length() > 0) {
299       addLeafCDATAElement(description.toString(), "description", element, document);
300     }
301   }
302 
303   private String deprecationComment(final String string) {
304     if (string == null) {
305       return null;
306     }
307     String result = string;
308     final String deprecated = "@deprecated";
309     final int begin = result.indexOf(deprecated);
310     if (begin > -1) {
311       result = result.substring(begin + deprecated.length());
312       final int end = result.indexOf("@");
313       if (end > -1) {
314         result = result.substring(0, end);
315       }
316       return result.trim();
317     } else {
318       return null;
319     }
320   }
321 
322   private TypeElement getInterfaceDeclaration(final String name) {
323     for (final TypeElement type : getTypes()) {
324       if (name.equals(type.getQualifiedName().toString())) {
325         return type;
326       }
327     }
328     return null;
329   }
330 
331   private String createDescription(final UIComponentTag componentTag) {
332     final StringBuilder description = new StringBuilder();
333     description.append("<p><b>UIComponentClass: </b>");
334     description.append(componentTag.uiComponent());
335     description.append("</p>");
336     description.append("<p><b>RendererType: </b>");
337     description.append("<ul>");
338     boolean first = true;
339     for (final String rendererType : componentTag.rendererType()) {
340       description.append("<li>");
341       description.append(rendererType);
342       if (first) {
343         description.append(" (default)");
344       }
345       description.append("</li>");
346       first = false;
347     }
348     description.append("</ul>");
349     description.append("</p>");
350     final Facet[] facets = componentTag.facets();
351     if (facets.length > 0) {
352       description.append("<p><b>Supported facets:</b></p>");
353       description.append("<dl>");
354       for (final Facet facet : facets) {
355         description.append("<dt><b>");
356         description.append(facet.name());
357         description.append("</b></dt>");
358         description.append("<dd>");
359         description.append(facet.description());
360         description.append("</dd>");
361       }
362       description.append("</dl>");
363     }
364     final Markup[] markups = componentTag.markups();
365     if (markups.length > 0) {
366       description.append("<p><b>Supported markups:</b></p>");
367       description.append("<dl>");
368       for (final Markup markup : markups) {
369         description.append("<dt><b>");
370         description.append(markup.name());
371         description.append("</b></dt>");
372         description.append("<dd>");
373         description.append(markup.description());
374         description.append("</dd>");
375       }
376       description.append("</dl>");
377     }
378     return description.toString();
379   }
380 
381   protected void addAttributes(
382       final TypeElement typeElement, final Element tagElement, final Document document)
383       throws ClassNotFoundException {
384 
385     for (final javax.lang.model.element.Element element : getAllMembers(typeElement)) {
386       if (element instanceof ExecutableElement) {
387         final ExecutableElement executableElement = (ExecutableElement) element;
388         if (executableElement.getAnnotation(TagAttribute.class) == null
389             && executableElement.getAnnotation(UIComponentTagAttribute.class) == null) {
390           continue;
391         }
392         addAttribute(executableElement, tagElement, document);
393       }
394     }
395   }
396 
397   private List<? extends javax.lang.model.element.Element> getAllMembers(final TypeElement type) {
398     final List<? extends javax.lang.model.element.Element> members
399         = new ArrayList<javax.lang.model.element.Element>(processingEnv.getElementUtils().getAllMembers(type));
400     members.sort(Comparator.comparing(d -> d.getSimpleName().toString()));
401     return members;
402   }
403 
404   private void resetDuplicateList() {
405     tagSet = new HashSet<>();
406   }
407 
408   private void resetAttributeDuplicateList() {
409     attributeSet = new HashSet<>();
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 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 Element parent, final Document document) {
445     final 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 Element parent, final Document document) {
452     final Element element = document.createElement(node);
453     element.appendChild(document.createCDATASection(text));
454     parent.appendChild(element);
455   }
456 
457   protected Element createTaglib(final Document document, final Taglib taglibAnnotation) {
458     final Element taglib;
459     taglib = document.createElement("facelet-taglib");
460     taglib.setAttribute("id", taglibAnnotation.shortName());
461     taglib.setAttribute("xmlns", "http://java.sun.com/xml/ns/javaee");
462     taglib.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
463     taglib.setAttribute("xsi:schemaLocation",
464         "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd");
465     taglib.setAttribute("version", "2.0");
466     return taglib;
467   }
468 
469   protected void addTagContent(
470       final TypeElement typeElement, final Element tagElement, final Document document, final boolean deprecated,
471       final Tag annotationTag) {
472     if (deprecated) {
473       addLeafTextElement(annotationTag.deprecatedName(), "tag-name", tagElement, document);
474     } else {
475       addLeafTextElement(annotationTag.name(), "tag-name", tagElement, document);
476     }
477 
478     final UIComponentTag componentTag = typeElement.getAnnotation(UIComponentTag.class);
479     if (componentTag != null) {
480       final Element componentElement = document.createElement("component");
481       tagElement.appendChild(componentElement);
482       addLeafTextElement(
483           AnnotationUtils.componentType(componentTag), "component-type", componentElement, document);
484       if (componentTag.rendererType().length > 0) {
485         addLeafTextElement(componentTag.rendererType()[0], "renderer-type", componentElement, document);
486       }
487       addLeafTextElement(componentTag.faceletHandler(), "handler-class", componentElement, document);
488     }
489 
490     final SimpleTag simpleTag = typeElement.getAnnotation(SimpleTag.class);
491     if (simpleTag != null) {
492       addLeafTextElement(simpleTag.faceletHandler(), "handler-class", tagElement, document);
493     }
494 
495     final ConverterTag converterTag = typeElement.getAnnotation(ConverterTag.class);
496     if (converterTag != null) {
497       final Element converterElement = document.createElement("converter");
498       tagElement.appendChild(converterElement);
499       addLeafTextElement(converterTag.converterId(), "converter-id", converterElement, document);
500       if (StringUtils.isNotBlank(converterTag.faceletHandler())) {
501         addLeafTextElement(converterTag.faceletHandler(), "handler-class", converterElement, document);
502       }
503     }
504 
505     final ValidatorTag validatorTag = typeElement.getAnnotation(ValidatorTag.class);
506     if (validatorTag != null) {
507       final Element validatorElement = document.createElement("validator");
508       tagElement.appendChild(validatorElement);
509       addLeafTextElement(validatorTag.validatorId(), "validator-id", validatorElement, document);
510       if (StringUtils.isNotBlank(validatorTag.faceletHandler())) {
511         addLeafTextElement(validatorTag.faceletHandler(), "handler-class", validatorElement, document);
512       }
513     }
514   }
515 
516   protected void addAttributeType(
517       final Element attribute, final TagAttribute tagAttribute, final UIComponentTagAttribute componentTagAttribute,
518       final Document document) {
519     if (!tagAttribute.rtexprvalue()) {
520       if (componentTagAttribute != null) {
521         if (componentTagAttribute.expression().isMethodExpression()) {
522           // todo
523         } else if (componentTagAttribute.expression().isValueExpression()) {
524           String clazz;
525           if (componentTagAttribute.type().length == 1) {
526             clazz = componentTagAttribute.type()[0];
527             final Class wrapper = ClassUtils.getWrapper(clazz);
528             if (wrapper != null) {
529               clazz = wrapper.getName(); // primitive types aren't allowed here
530       /*                } else {
531                       XXX what is with inner classes and arrays?
532                       if (clazz.endsWith("[]")) {
533                         Class.forName(clazz.substring(0, clazz.length() - 2)); // type check
534                       } else {
535                         Class.forName(clazz); // type check
536                       }
537       */
538             }
539           } else {
540             clazz = "java.lang.Object";
541           }
542           addLeafTextElement(clazz, "type", attribute, document);
543         }
544       } else {
545         addLeafTextElement(tagAttribute.type(), "type", attribute, document);
546       }
547     }
548   }
549 }