| /* |
| * $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.beans.BeanInfo; |
| import java.beans.IntrospectionException; |
| import java.beans.Introspector; |
| import java.beans.PropertyDescriptor; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.FileWriter; |
| import java.io.OutputStreamWriter; |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.TreeMap; |
| import java.util.Map; |
| |
| 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 com.sun.mirror.apt.AnnotationProcessor; |
| import com.sun.mirror.apt.AnnotationProcessorEnvironment; |
| import com.sun.mirror.declaration.AnnotationMirror; |
| import com.sun.mirror.declaration.AnnotationTypeDeclaration; |
| import com.sun.mirror.declaration.AnnotationTypeElementDeclaration; |
| import com.sun.mirror.declaration.AnnotationValue; |
| import com.sun.mirror.declaration.Declaration; |
| import com.sun.mirror.declaration.MethodDeclaration; |
| import com.sun.mirror.declaration.TypeDeclaration; |
| |
| import freemarker.template.Configuration; |
| import freemarker.template.DefaultObjectWrapper; |
| import freemarker.template.Template; |
| |
| public class TagAnnotationProcessor implements AnnotationProcessor { |
| 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 AnnotationProcessorEnvironment environment; |
| private AnnotationTypeDeclaration tagDeclaration; |
| private AnnotationTypeDeclaration tagAttributeDeclaration; |
| private AnnotationTypeDeclaration skipDeclaration; |
| private Map<String, Tag> tags = new TreeMap<String, Tag>(); |
| |
| public TagAnnotationProcessor(AnnotationProcessorEnvironment env) { |
| environment = env; |
| tagDeclaration = (AnnotationTypeDeclaration) environment |
| .getTypeDeclaration(TAG); |
| tagAttributeDeclaration = (AnnotationTypeDeclaration) environment |
| .getTypeDeclaration(TAG_ATTRIBUTE); |
| skipDeclaration = (AnnotationTypeDeclaration) environment |
| .getTypeDeclaration(TAG_SKIP_HIERARCHY); |
| } |
| |
| public void process() { |
| // make sure all paramters were set |
| checkOptions(); |
| |
| // tags |
| Collection<Declaration> tagDeclarations = environment |
| .getDeclarationsAnnotatedWith(tagDeclaration); |
| Collection<Declaration> attributesDeclarations = environment |
| .getDeclarationsAnnotatedWith(tagAttributeDeclaration); |
| Collection<Declaration> skipDeclarations = environment |
| .getDeclarationsAnnotatedWith(skipDeclaration); |
| |
| // find Tags |
| for (Declaration declaration : tagDeclarations) { |
| // type |
| TypeDeclaration typeDeclaration = (TypeDeclaration) declaration; |
| String typeName = typeDeclaration.getQualifiedName(); |
| Map<String, Object> values = getValues(typeDeclaration, |
| tagDeclaration); |
| // create Tag and apply values found |
| 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(typeName); |
| tag.setAllowDynamicAttributes((Boolean) values.get("allowDynamicAttributes")); |
| // add to map |
| tags.put(typeName, tag); |
| } |
| |
| //find attributes to be skipped |
| for (Declaration declaration : skipDeclarations) { |
| //types will be ignored when hierarchy is scanned |
| if (declaration instanceof MethodDeclaration) { |
| MethodDeclaration methodDeclaration = (MethodDeclaration) declaration; |
| String typeName = methodDeclaration.getDeclaringType().getQualifiedName(); |
| String methodName = methodDeclaration.getSimpleName(); |
| 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 (Declaration declaration : attributesDeclarations) { |
| // type |
| MethodDeclaration methodDeclaration = (MethodDeclaration) declaration; |
| String typeName = methodDeclaration.getDeclaringType() |
| .getQualifiedName(); |
| Map<String, Object> values = getValues(methodDeclaration, |
| tagAttributeDeclaration); |
| // 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(); |
| 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 garantees that the base classes will be processed |
| // before their subclasses |
| for (Map.Entry<String, Tag> entry : tags.entrySet()) { |
| processHierarchy(entry.getValue()); |
| } |
| |
| // save |
| saveAsXml(); |
| saveTemplates(); |
| } |
| |
| private 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) { |
| try { |
| Class clazz = Class.forName(tag.getDeclaredType()); |
| List<String> skipAttributes = tag.getSkipAttributes(); |
| //skip hierarchy processing if the class is marked with the skip annotation |
| while(getAnnotation(TAG_SKIP_HIERARCHY, clazz.getAnnotations()) == null |
| && ((clazz = clazz.getSuperclass()) != null)) { |
| Tag parentTag = tags.get(clazz.getName()); |
| // 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, clazz); |
| } |
| } |
| } catch(Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void addTagAttributesFromParent(Tag tag, Class clazz) throws ClassNotFoundException { |
| try { |
| BeanInfo info = Introspector.getBeanInfo(clazz); |
| PropertyDescriptor[] props = info.getPropertyDescriptors(); |
| List<String> skipAttributes = tag.getSkipAttributes(); |
| |
| //iterate over class fields |
| for(int i = 0; i < props.length; ++i) { |
| PropertyDescriptor prop = props[i]; |
| Method writeMethod = prop.getWriteMethod(); |
| |
| //make sure it is public |
| if(writeMethod != null && Modifier.isPublic(writeMethod.getModifiers())) { |
| //can't use the genertic getAnnotation 'cause the class it not on this jar |
| Annotation annotation = getAnnotation(TAG_ATTRIBUTE, writeMethod.getAnnotations()); |
| if(annotation != null && !skipAttributes.contains(prop.getName())) { |
| Map<String, Object> values = getValues(annotation); |
| //create tag |
| TagAttribute attribute = new TagAttribute(); |
| values.put("name", prop.getName()); |
| populateTagAttributes(attribute, values); |
| tag.addTagAttribute(attribute); |
| } |
| } |
| |
| } |
| } catch(Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private Annotation getAnnotation(String typeName, Annotation[] annotations) { |
| for(int i = 0; i < annotations.length; i++) { |
| if(annotations[i].annotationType().getName().equals(typeName)) |
| return annotations[i]; |
| } |
| 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<String, Tag>(); |
| root.put("tag", tag); |
| |
| // save file |
| BufferedWriter writer = new BufferedWriter(new FileWriter( |
| new File(rootDir, tag.getName() + ".html"))); |
| try { |
| template.process(root, writer); |
| } finally { |
| writer.close(); |
| } |
| } |
| } |
| } 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"); |
| |
| Source source = new DOMSource(document); |
| Result result = new StreamResult(new OutputStreamWriter( |
| new FileOutputStream(getOption("outFile")))); |
| 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 (environment.getOptions().containsKey(name)) |
| return environment.getOptions().get(name); |
| |
| for (Map.Entry<String, String> entry : environment.getOptions() |
| .entrySet()) { |
| String key = entry.getKey(); |
| String[] splitted = key.split("="); |
| if (splitted[0].equals("-A" + name)) |
| return splitted[1]; |
| } |
| return null; |
| } |
| |
| private 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 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 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(Declaration declaration, |
| AnnotationTypeDeclaration type) { |
| Map<String, Object> values = new TreeMap<String, Object>(); |
| Collection<AnnotationMirror> annotations = declaration |
| .getAnnotationMirrors(); |
| // iterate over the mirrors. |
| |
| for (AnnotationMirror mirror : annotations) { |
| // if the mirror in this iteration is for our note declaration... |
| if (mirror.getAnnotationType().getDeclaration().equals(type)) { |
| for (AnnotationTypeElementDeclaration annotationType : mirror |
| .getElementValues().keySet()) { |
| Object value = mirror.getElementValues() |
| .get(annotationType).getValue(); |
| Object defaultValue = annotationType.getDefaultValue(); |
| values.put(annotationType.getSimpleName(), |
| value != null ? value : defaultValue); |
| } |
| } |
| } |
| |
| // find default values...painful |
| for (AnnotationTypeElementDeclaration annotationType : type |
| .getMethods()) { |
| AnnotationValue value = annotationType.getDefaultValue(); |
| if (value != null) { |
| String name = annotationType.getSimpleName(); |
| if (!values.containsKey(name)) |
| values.put(name, value.getValue()); |
| } |
| } |
| |
| return values; |
| } |
| |
| /** |
| * Get values of annotation |
| * |
| * @param annotation The annotation |
| * @return name->value map of annotation values |
| * @throws IntrospectionException |
| * @throws InvocationTargetException |
| * @throws IllegalAccessException |
| * @throws IllegalArgumentException |
| */ |
| private Map<String, Object> getValues(Annotation annotation) throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { |
| Map<String, Object> values = new TreeMap<String, Object>(); |
| //if the tag classes were on this project we could just cast to the right type |
| //but they are needed on core |
| Class annotationType = annotation.annotationType(); |
| |
| Method[] methods = annotationType.getMethods(); |
| //iterate over class fields |
| for(int i = 0; i < methods.length; ++i) { |
| Method method = methods[i]; |
| if(method != null && method.getParameterTypes().length == 0) { |
| Object value = method.invoke(annotation, new Object[0]); |
| values.put(method.getName(), value); |
| } |
| } |
| |
| return values; |
| } |
| } |