/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2011 Samuel Marshall. */ package com.leafdigital.ui.checker; import java.io.File; import java.net.*; import java.util.*; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.*; import javax.tools.Diagnostic; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; import com.leafdigital.ui.api.*; import com.sun.source.util.*; /** * Annotation processor that checks the UIHandler annotations to make sure * they really define all the correct callbacks. */ @SupportedAnnotationTypes("com.leafdigital.ui.api.*") @SupportedSourceVersion(SourceVersion.RELEASE_6) @SupportedOptions("xmlroot") public class UIHandlerProcessor extends AbstractProcessor { private static class Handler { TypeElement element; String[] xmlFiles; private Handler(TypeElement element, String[] xmlFiles) { this.element = element; this.xmlFiles = xmlFiles; } } private static class XmlData extends DefaultHandler { Set<String> requiredCallbacks = new HashSet<String>(); Set<String> availableIds = new HashSet<String>(); Map<String, Set<String>> callbacks; private XmlData(Map<String, Set<String>> callbacks) { this.callbacks = callbacks; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // See if there's an id attribute String id = attributes.getValue("id"); if(id != null) { availableIds.add(id); } // See if there's any required callback attributes Set<String> callbackAttributes = callbacks.get(qName); if(callbackAttributes != null) { for(String callbackAttribute : callbackAttributes) { String callback = attributes.getValue(callbackAttribute); if(callback != null) { requiredCallbacks.add(callback); } } } } } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // Initialise data Map<String, Set<String>> callbacks = new HashMap<String, Set<String>>(); List<Handler> handlers = new LinkedList<Handler>(); Set<Element> actions = new HashSet<Element>(); // Loop around, gathering all data for(TypeElement annotation : annotations) { String name = annotation.getSimpleName().toString(); if(name.equals("UICallback")) { processCallback(annotation, roundEnv, callbacks); } else if(name.equals("UIHandler")) { processHandler(annotation, roundEnv, handlers); } else if(name.equals("UIAction")) { processAction(annotation, roundEnv, actions); } else { throw new Error("wtf: " + name); } } finalCheck(callbacks, handlers, actions); return true; } /** * @param annotation Annotation * @param roundEnv Environment * @param callbacks List of callbacks (to add to) */ private void processCallback(TypeElement annotation, RoundEnvironment roundEnv, Map<String, Set<String>> callbacks) { // Loop through all elements annotated with @UICallback for(Element element : roundEnv.getElementsAnnotatedWith(annotation)) { // Get the method name String methodName = element.getSimpleName().toString(); if(!methodName.startsWith("set")) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@UICallback can only be applied to set methods", element); continue; } // Get the interface name Element parent = element.getEnclosingElement(); if(parent.getKind() != ElementKind.INTERFACE) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@UICallback can only be applied to method within an interface", element); continue; } String interfaceName = parent.getSimpleName().toString(); // Store in map (without the 'set') Set<String> set = callbacks.get(interfaceName); if(set == null) { set = new HashSet<String>(); callbacks.put(interfaceName, set); } set.add(methodName.substring(3)); } } /** * @param annotation Annotation * @param roundEnv Environment * @param handlers List of handlers (to add to) */ private void processHandler(TypeElement annotation, RoundEnvironment roundEnv, List<Handler> handlers) { // Loop through all elements annotated with @UIHandler for(Element element : roundEnv.getElementsAnnotatedWith(annotation)) { // Get the list of xml files and add handlers.add(new Handler((TypeElement)element, element.getAnnotation(UIHandler.class).value())); } } /** * @param annotation Annotation * @param roundEnv Environment * @param actions Set of actions (to add to) */ private void processAction(TypeElement annotation, RoundEnvironment roundEnv, Set<Element> actions) { for(Element element : roundEnv.getElementsAnnotatedWith(annotation)) { // Add all actions to the list except runtime ones, because we don't // check those ones if(!element.getAnnotation(UIAction.class).runtime()) { actions.add(element); } } } /** * Now do the final check, loading all the xml files and so on. * @param callbacks List of known callback methods * @param handlers List of required handlers * @param actions List of action methods that must be used */ private void finalCheck(Map<String, Set<String>> callbacks, List<Handler> handlers, Set<Element> actions) { Trees trees = null; try { // This is Sun-specific API. Only works in javac, not in Eclipse or // other builder. trees = Trees.instance(processingEnv); } catch(Exception e) { // Leave it null (check later) } SAXParserFactory factory = SAXParserFactory.newInstance(); // Track IDs that are required and found. This has to be stored up for // the end because there are some parts of the code where the id value // is only set by certain subclasses and not others. Map<Element, String> requiredIds = new HashMap<Element, String>(); Set<Element> foundIds = new HashSet<Element>(); // Track methods that don't have @UIAction so we only warn once Set<Element> warnedMethods = new HashSet<Element>(); for(Handler handler : handlers) { // Get information about elements of the type (fields, methods) Map<String, Element> uiFields = new HashMap<String, Element>(); Map<String, Element> methods = new HashMap<String, Element>(); fillClassData(handler.element, uiFields, methods); // Get file location of Java source and get its folder File folder; if(trees != null) { // Sun-specific version TreePath tp = trees.getPath(handler.element); URI fileUri; try { fileUri = new URI("file:///").resolve( tp.getCompilationUnit().getSourceFile().toUri()); } catch(URISyntaxException ex) { throw new Error("wtf: " + ex.getMessage()); } folder = (new File(fileUri)).getParentFile(); } else { // Generic version requires source path option String path = processingEnv.getOptions().get("src"); if(path == null) { throw new Error("Annotation processor requires option 'src' (set it" + " to absolute path of source folder) when not using Sun javac" + " compiler"); } folder = new File(path); if(!new File(folder, "com").exists()) { throw new Error("Annotation processor option 'src' appears to be " + " incorrect (set to absolute path of source folder)"); } // Find outer class TypeElement reference = handler.element; while(reference.getEnclosingElement().getKind() == ElementKind.CLASS) { reference = (TypeElement)reference.getEnclosingElement(); } // Get absolute path of class String name = reference.getQualifiedName().toString(); // Remove class name name = name.replaceAll("\\.[^.]+$", ""); // Turn . into / name = name.replace('.', '/'); folder = new File(folder, name); } // Find XML files and make a list of required callbacks, available ids XmlData xmlData = new XmlData(callbacks); boolean fileErrors = false; for(String xmlFile : handler.xmlFiles) { File xml = new File(folder, xmlFile + ".xml"); if(!xml.exists()) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "XML file '" + xmlFile + ".xml' not found", handler.element); fileErrors = true; continue; } try { SAXParser parser = factory.newSAXParser(); parser.parse(xml, xmlData); } catch(Exception e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Error parsing XML file '" + xmlFile + ".xml': " + e.getMessage(), handler.element); fileErrors = true; } } // If there's a file error, stop because other errors are likely // caused by it if(fileErrors) { continue; } // Check all the fields are present in xml for(Map.Entry<String, Element> entry : uiFields.entrySet()) { String requiredId = entry.getKey(); Element idElement = entry.getValue(); // If the id is not in this current xml file if(!xmlData.availableIds.contains(requiredId)) { // If we haven't already found an id for this field... if (!foundIds.contains(idElement)) { // ...then add it to the required list requiredIds.put(idElement, requiredId); } } else { // It is in the current xml file, so let's remember that we found it // and also note that it's not required any more foundIds.add(idElement); requiredIds.remove(idElement); } } // Check all the callbacks are present in code for(String requiredCallback : xmlData.requiredCallbacks) { if(methods.containsKey(requiredCallback)) { Element methodElement = methods.get(requiredCallback); // This method was used, so remove it from the required action list actions.remove(methodElement); // Check if it has the annotation if(methodElement.getAnnotation(UIAction.class) == null && !warnedMethods.contains(methodElement)) { processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Callback referenced in XML should be marked with @UIAction", methodElement); warnedMethods.add(methodElement); } } else { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Missing callback '" + requiredCallback + "'", handler.element); } } } for(Map.Entry<Element, String> entry : requiredIds.entrySet()) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "No element with id '" + entry.getValue() + "' in attached XML files", entry.getKey()); } for(Element element : actions) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "No callback for method in attached XML files" + " (remove @UIAction or set runtime=true)", element); } } /** * Fulls all the method and field data from a class. Recursively includes * superclasses. * @param element Element of class * @param uiFields UI field map * @param methods Method map */ private void fillClassData(TypeElement element, Map<String, Element> uiFields, Map<String, Element> methods) { TypeMirror parent = element.getSuperclass(); if(parent.getKind() != TypeKind.NONE) { fillClassData( (TypeElement)processingEnv.getTypeUtils().asElement(parent), uiFields, methods); } for(Element e : element.getEnclosedElements()) { // Check UI fields String name = e.getSimpleName().toString(); if(e.getKind() == ElementKind.FIELD && name.endsWith("UI")) { if(!e.getModifiers().contains(Modifier.PUBLIC)) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "UI field must be declared as public", e); } else { // Remove UI suffix from name String searchName = name.substring(0, name.length()-2); // Is it an array? If so, look for element 0 if(e.asType().getKind().equals(TypeKind.ARRAY)) { searchName += "0"; } // Store name uiFields.put(searchName, e); } } else if(e.getKind() == ElementKind.METHOD) { if(e.getModifiers().contains(Modifier.PUBLIC)) { // Store name methods.put(name, e); } } } } }