| /* |
| * $Id: $ |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.struts.annotations.taglib.apt; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.FileWriter; |
| import java.io.OutputStreamWriter; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| import javax.annotation.processing.AbstractProcessor; |
| import javax.annotation.processing.RoundEnvironment; |
| import javax.annotation.processing.SupportedAnnotationTypes; |
| import javax.lang.model.element.AnnotationMirror; |
| import javax.lang.model.element.AnnotationValue; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.element.Modifier; |
| import javax.lang.model.element.TypeElement; |
| import javax.lang.model.type.NoType; |
| import javax.lang.model.util.ElementFilter; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.transform.OutputKeys; |
| import javax.xml.transform.Result; |
| import javax.xml.transform.Source; |
| import javax.xml.transform.Transformer; |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.dom.DOMSource; |
| import javax.xml.transform.stream.StreamResult; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Text; |
| |
| import freemarker.template.Configuration; |
| import freemarker.template.DefaultObjectWrapper; |
| import freemarker.template.Template; |
| |
| @SupportedAnnotationTypes({TagAnnotationProcessor.TAG, TagAnnotationProcessor.TAG_ATTRIBUTE, TagAnnotationProcessor.TAG_SKIP_HIERARCHY}) |
| public class TagAnnotationProcessor extends AbstractProcessor { |
| public static final String TAG = "org.apache.struts2.views.annotations.StrutsTag"; |
| public static final String TAG_ATTRIBUTE = "org.apache.struts2.views.annotations.StrutsTagAttribute"; |
| public static final String TAG_SKIP_HIERARCHY = "org.apache.struts2.views.annotations.StrutsTagSkipInheritance"; |
| |
| private Map<String, Tag> tags = new TreeMap<>(); |
| |
| @Override |
| public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { |
| // make sure all paramters were set |
| checkOptions(); |
| |
| // tags |
| TypeElement tagAnnotationType = processingEnv.getElementUtils().getTypeElement(TAG); |
| TypeElement attributeAnnotationType = processingEnv.getElementUtils().getTypeElement(TAG_ATTRIBUTE); |
| TypeElement skipAnnotationType = processingEnv.getElementUtils().getTypeElement(TAG_SKIP_HIERARCHY); |
| Set<? extends javax.lang.model.element.Element> tagDeclarations = roundEnv.getElementsAnnotatedWith(tagAnnotationType); |
| Set<? extends javax.lang.model.element.Element> attributesDeclarations = roundEnv.getElementsAnnotatedWith(attributeAnnotationType); |
| Set<? extends javax.lang.model.element.Element> skipDeclarations = roundEnv.getElementsAnnotatedWith(skipAnnotationType); |
| |
| // find Tags |
| for (javax.lang.model.element.Element element : tagDeclarations) { |
| Map<String, Object> values = getValues(element, tagAnnotationType); |
| TypeElement type = (TypeElement) element; |
| Tag tag = new Tag(); |
| tag.setDescription((String) values.get("description")); |
| tag.setName((String) values.get("name")); |
| tag.setTldBodyContent((String) values.get("tldBodyContent")); |
| tag.setTldTagClass((String) values.get("tldTagClass")); |
| tag.setDeclaredType(type.getQualifiedName().toString()); |
| tag.setAllowDynamicAttributes((Boolean) values.get("allowDynamicAttributes")); |
| // add to map |
| tags.put(type.getQualifiedName().toString(), tag); |
| } |
| |
| //find attributes to be skipped |
| for (javax.lang.model.element.Element declaration : skipDeclarations) { |
| //types will be ignored when hierarchy is scanned |
| if (declaration instanceof ExecutableElement) { |
| ExecutableElement methodDeclaration = (ExecutableElement) declaration; |
| String typeName = ((TypeElement) methodDeclaration.getEnclosingElement()).getQualifiedName().toString(); |
| String methodName = methodDeclaration.getSimpleName().toString(); |
| String name = String.valueOf(Character.toLowerCase(methodName |
| .charAt(3))) |
| + methodName.substring(4); |
| Tag tag = tags.get(typeName); |
| if(tag != null) { |
| //if it is on an abstract class, there is not tag for it at this point |
| tags.get(typeName).addSkipAttribute(name); |
| } |
| } |
| } |
| |
| // find Tags Attributes |
| for (javax.lang.model.element.Element declaration : attributesDeclarations) { |
| // type |
| ExecutableElement methodDeclaration = (ExecutableElement) declaration; |
| String typeName = ((TypeElement) methodDeclaration.getEnclosingElement()).getQualifiedName().toString(); |
| Map<String, Object> values = getValues(methodDeclaration, |
| attributeAnnotationType); |
| // create Attribute and apply values found |
| TagAttribute attribute = new TagAttribute(); |
| String name = (String) values.get("name"); |
| if (name == null || name.length() == 0) { |
| // get name from method |
| String methodName = methodDeclaration.getSimpleName().toString(); |
| name = String.valueOf(Character.toLowerCase(methodName |
| .charAt(3))) |
| + methodName.substring(4); |
| } |
| values.put("name", name); |
| populateTagAttributes(attribute, values); |
| // add to map |
| Tag parentTag = tags.get(typeName); |
| if (parentTag != null){ |
| tags.get(typeName).addTagAttribute(attribute); |
| }else { |
| // an abstract or base class |
| parentTag = new Tag(); |
| parentTag.setDeclaredType(typeName); |
| parentTag.setInclude(false); |
| parentTag.addTagAttribute(attribute); |
| tags.put(typeName, parentTag); |
| } |
| } |
| |
| // we can't process the hierarchy on the first pass because |
| // apt does not guarantee that the base classes will be processed |
| // before their subclasses |
| for (Map.Entry<String, Tag> entry : tags.entrySet()) { |
| processHierarchy(entry.getValue()); |
| } |
| |
| // save |
| saveAsXml(); |
| saveTemplates(); |
| return true; |
| } |
| |
| private static void populateTagAttributes(TagAttribute attribute, Map<String, Object> values) { |
| attribute.setRequired((Boolean) values.get("required")); |
| attribute.setRtexprvalue((Boolean) values.get("rtexprvalue")); |
| attribute.setDefaultValue((String) values.get("defaultValue")); |
| attribute.setType((String) values.get("type")); |
| attribute.setDescription((String) values.get("description")); |
| attribute.setName((String) values.get("name")); |
| } |
| |
| private void processHierarchy(Tag tag) { |
| TypeElement type = processingEnv.getElementUtils().getTypeElement(tag.getDeclaredType()); |
| List<String> skipAttributes = tag.getSkipAttributes(); |
| while (type !=null && !(type instanceof NoType) && getAnnotation(type, TAG_SKIP_HIERARCHY) == null) { |
| Tag parentTag = tags.get(type.getQualifiedName().toString()); |
| // copy parent annotations to this tag |
| if(parentTag != null) { |
| for(TagAttribute attribute : parentTag.getAttributes()) { |
| if(!skipAttributes.contains(attribute.getName())){ |
| tag.addTagAttribute(attribute); |
| } |
| } |
| } else { |
| // Maybe the parent class is already compiled |
| addTagAttributesFromParent(tag, type); |
| } |
| type = (TypeElement) processingEnv.getTypeUtils().asElement(type.getSuperclass()); |
| } |
| } |
| |
| private void addTagAttributesFromParent(Tag tag, TypeElement type) { |
| for (ExecutableElement method : ElementFilter.methodsIn(processingEnv.getElementUtils().getAllMembers(type))) { |
| AnnotationMirror annotation = getAnnotation(method, TAG_ATTRIBUTE); |
| if (method.getModifiers().contains(Modifier.PUBLIC) && annotation != null) { |
| String name = String.valueOf(Character.toLowerCase(method.getSimpleName() |
| .charAt(3))) |
| + method.getSimpleName().subSequence(4, method.getSimpleName().length()); |
| if (!tag.getSkipAttributes().contains(name)) { |
| Map<String, Object> values = new HashMap<>(); |
| for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : processingEnv.getElementUtils().getElementValuesWithDefaults(annotation).entrySet()) { |
| values.put(entry.getKey().getSimpleName().toString(), entry.getValue().getValue()); |
| } |
| TagAttribute attribute = new TagAttribute(); |
| populateTagAttributes(attribute, values); |
| attribute.setName(name); |
| tag.addTagAttribute(attribute); |
| } |
| } |
| } |
| } |
| |
| private AnnotationMirror getAnnotation(javax.lang.model.element.Element element, String annotationName) { |
| TypeElement annotation = processingEnv.getElementUtils().getTypeElement(annotationName); |
| if (element != null && element.getAnnotationMirrors() != null) { |
| for (AnnotationMirror mirror : element.getAnnotationMirrors()) { |
| if (mirror.getAnnotationType().asElement().equals(annotation)) { |
| return mirror; |
| } |
| } |
| } |
| return null; |
| } |
| |
| private void checkOptions() { |
| if (getOption("tlibVersion") == null) |
| throw new IllegalArgumentException("'tlibVersion' is missing"); |
| if (getOption("jspVersion") == null) |
| throw new IllegalArgumentException("'jspVersion' is missing"); |
| if (getOption("shortName") == null) |
| throw new IllegalArgumentException("'shortName' is missing"); |
| if (getOption("description") == null) |
| throw new IllegalArgumentException("'description' is missing"); |
| if (getOption("displayName") == null) |
| throw new IllegalArgumentException("'displayName' is missing"); |
| if (getOption("uri") == null) |
| throw new IllegalArgumentException("'uri' is missing"); |
| if (getOption("outTemplatesDir") == null) |
| throw new IllegalArgumentException("'outTemplatesDir' is missing"); |
| if (getOption("outFile") == null) |
| throw new IllegalArgumentException("'outFile' is missing"); |
| } |
| |
| private void saveTemplates() { |
| // freemarker configuration |
| Configuration config = new Configuration(); |
| config.setClassForTemplateLoading(getClass(), ""); |
| config.setObjectWrapper(new DefaultObjectWrapper()); |
| |
| try { |
| // load template |
| Template template = config.getTemplate("tag.ftl"); |
| String rootDir = (new File(getOption("outTemplatesDir"))) |
| .getAbsolutePath(); |
| for (Tag tag : tags.values()) { |
| if (tag.isInclude()) { |
| // model |
| HashMap<String, Tag> root = new HashMap<>(); |
| root.put("tag", tag); |
| |
| try (BufferedWriter writer = new BufferedWriter(new FileWriter( |
| new File(rootDir, tag.getName() + ".html")))){ |
| template.process(root, writer); |
| } |
| } |
| } |
| } catch (Exception e) { |
| // oops we cannot throw checked exceptions |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void saveAsXml() { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| DocumentBuilder builder; |
| |
| try { |
| // create xml document |
| builder = factory.newDocumentBuilder(); |
| Document document = builder.newDocument(); |
| document.setXmlVersion("1.0"); |
| |
| // taglib |
| Element tagLib = document.createElement("taglib"); |
| tagLib.setAttribute("xmlns", "http://java.sun.com/xml/ns/j2ee"); |
| tagLib.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); |
| tagLib.setAttribute("xsi:schemaLocation", "http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"); |
| tagLib.setAttribute("version", getOption("jspVersion")); |
| document.appendChild(tagLib); |
| // tag lib attributes |
| appendTextNode(document, tagLib, "description", |
| getOption("description"), true); |
| appendTextNode(document, tagLib, "display-name", |
| getOption("displayName"), false); |
| appendTextNode(document, tagLib, "tlib-version", |
| getOption("tlibVersion"), false); |
| appendTextNode(document, tagLib, "short-name", |
| getOption("shortName"), false); |
| appendTextNode(document, tagLib, "uri", getOption("uri"), false); |
| |
| // create tags |
| for (Map.Entry<String, Tag> entry : tags.entrySet()) { |
| Tag tag = entry.getValue(); |
| if (tag.isInclude()) |
| createElement(document, tagLib, tag); |
| } |
| |
| // save to file |
| TransformerFactory tf = TransformerFactory.newInstance(); |
| tf.setAttribute("indent-number", 2); |
| Transformer transformer = tf.newTransformer(); |
| // if tiger would just format it :( |
| // formatting bug in tiger |
| // (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446) |
| |
| transformer.setOutputProperty(OutputKeys.INDENT, "yes"); |
| transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); |
| |
| //create output directory if it does not exists |
| File outputFile = new File(getOption("outFile")); |
| File parentDir = outputFile.getParentFile(); |
| if (!parentDir.exists()) |
| parentDir.mkdirs(); |
| |
| Source source = new DOMSource(document); |
| Result result = new StreamResult(new OutputStreamWriter( |
| new FileOutputStream(outputFile))); |
| transformer.transform(source, result); |
| } catch (Exception e) { |
| // oops we cannot throw checked exceptions |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private String getOption(String name) { |
| // there is a bug in the 1.5 apt implementation: |
| // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6258929 |
| // this is a hack-around |
| if (processingEnv.getOptions().containsKey(name)){ |
| return processingEnv.getOptions().get(name); |
| } |
| for (Map.Entry<String, String> entry : processingEnv.getOptions() |
| .entrySet()) { |
| String key = entry.getKey(); |
| String[] splitted = key.split("="); |
| if (splitted[0].equals("-A" + name)) |
| return splitted[1]; |
| } |
| return null; |
| } |
| |
| private static void createElement(Document doc, Element tagLibElement, Tag tag) { |
| Element tagElement = doc.createElement("tag"); |
| tagLibElement.appendChild(tagElement); |
| appendTextNode(doc, tagElement, "description", tag.getDescription(), |
| true); |
| appendTextNode(doc, tagElement, "name", tag.getName(), false); |
| appendTextNode(doc, tagElement, "tag-class", tag.getTldTagClass(), |
| false); |
| appendTextNode(doc, tagElement, "body-content", |
| tag.getTldBodyContent(), false); |
| |
| // save attributes |
| for (TagAttribute attribute : tag.getAttributes()) { |
| createElement(doc, tagElement, attribute); |
| } |
| |
| appendTextNode(doc, tagElement, "dynamic-attributes", String.valueOf(tag.isAllowDynamicAttributes()), false); |
| } |
| |
| private static void createElement(Document doc, Element tagElement, TagAttribute attribute) { |
| Element attributeElement = doc.createElement("attribute"); |
| tagElement.appendChild(attributeElement); |
| appendTextNode(doc, attributeElement, "description", attribute |
| .getDescription(), true); |
| appendTextNode(doc, attributeElement, "name", attribute.getName(), |
| false); |
| appendTextNode(doc, attributeElement, "required", String |
| .valueOf(attribute.isRequired()), false); |
| appendTextNode(doc, attributeElement, "rtexprvalue", String |
| .valueOf(attribute.isRtexprvalue()), false); |
| } |
| |
| private static void appendTextNode(Document doc, Element element, String name, |
| String text, boolean cdata) { |
| Text textNode = cdata ? doc.createCDATASection(text) : doc |
| .createTextNode(text); |
| Element newElement = doc.createElement(name); |
| newElement.appendChild(textNode); |
| element.appendChild(newElement); |
| } |
| |
| /** |
| * Get values of annotation |
| * |
| * @param declaration The annotation declaration |
| * @param type |
| * The type of the annotation |
| * @return name->value map of annotation values |
| */ |
| private Map<String, Object> getValues(javax.lang.model.element.Element element, TypeElement type) { |
| Map<String, Object> values = new TreeMap<>(); |
| Collection<? extends AnnotationMirror> annotations = element |
| .getAnnotationMirrors(); |
| // iterate over the mirrors. |
| |
| for (AnnotationMirror mirror : annotations) { |
| // if the mirror in this iteration is for our note declaration... |
| if (mirror.getAnnotationType().asElement().equals(type)) { |
| for (Entry<? extends ExecutableElement, ? extends AnnotationValue> elementEntry : processingEnv.getElementUtils().getElementValuesWithDefaults(mirror).entrySet()) { |
| values.put(elementEntry.getKey().getSimpleName().toString(), elementEntry.getValue().getValue()); |
| } |
| } |
| } |
| |
| return values; |
| } |
| |
| } |