Długi tytuł, czyli będzie ciekawie.

Co chcemy zrobić?

Załóżmy, że chcemy stworzyć sobie narzędzie do raportowania postępów w projekcie. Potrzebujemy czegoś co pozwoli nam na oznaczanie w kodzie informacji typu TODO, czy identyfikacji miejsca w którym mamy buga. Ogólnie istnieją narzędzia takie jak Checkstyle, które potrafią analizując kod źródłowy wyłapać jakieś standardowe wzorce w komentarzach. My chcemy czegoś więcej.
Rozwiązaniem są oczywiście adnotacje. Dla uproszczenia pokażę co i jak przyjmując, że mamy tylko jedną adnotację i możemy nią oznaczać tylko metody. Raport będzie produkowany do logu kompilatora. Oczywiście będziemy potrzebować jakiegoś narzędzia do obróbki adnotacji… w trakcie kompilacji. I o tym będzie ten wpis.

Adnotacja

Zacznijmy od zdefiniowania adnotacji @Todo

Listing 1. Definicja @Todo

package pl.koziolekweb.blog.development.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Todo {

	public String description();
}

Pierwsze zaskoczenie. Otóż wykorzystamy zakres CLASS po to by adnotacja nie była dostępna w trakcie uruchomienia. Istotne jest by można było przetwarzać informacje na etapie kompilacji. Później nie powinny być one dostępne dla klienta. Zresztą po co mu one?

Projekt testowy

Tworzymy nowy projekt i dodajemy do niego jako zależność projekt z adnotacjami. Jest to o tyle ważne, że jeżeli używamy mavena to trzeba w pierwszym projekcie wyłączyć przetwarzanie adnotacji. Dlaczego? O tym innym razem. Tworzymy sobie kilka metod i dodajemy do nich nasze adnotacje…

Procesor właściwy

Procesor adnotacji jest uruchamiany jako serwis SE, czyli w katalogu META-INF/services tworzymy plik javax.annotation.processing.Processor i dodajemy w nim linijkę z informacją o naszej klasie procesora.
Sama klasa jest prosta (w tej wersji!):

Listing 2. Implementacja DevelopmentAnnotationProcessor

package pl.koziolekweb.blog.development;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;

import pl.koziolekweb.blog.development.annotations.Todo;

@SupportedAnnotationTypes("pl.koziolekweb.blog.development.annotations.Todo")
@SupportedSourceVersion(SourceVersion.RELEASE_5)
public class DevelopmentAnnotationProcessor extends AbstractProcessor {

	private Map<name list="">> markedElements;

	private boolean justRun = false;

	public DevelopmentAnnotationProcessor() {

	}

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);
		markedElements = new HashMap<name list="">>();
	}

	public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
		if (justRun)
			return true;
		justRun = true;
		Set extends Element> todoElements = roundEnv.getElementsAnnotatedWith(Todo.class);
		for (Element element : todoElements) {
			if (element.getKind() != ElementKind.METHOD) {
				System.out.println("[WARN] Oznaczony element to nie metodą");
			}
			TypeElement ownerClass = (TypeElement) element.getEnclosingElement();
			Name qualifiedName = ownerClass.getQualifiedName();
			addElementToReport(element, qualifiedName);
		}
		produceReport();
		return true;
	}

	private void produceReport() {
		Set<entry list="">>> markedElementsToReport = markedElements.entrySet();
		if (markedElementsToReport.size() > 0) {
			for (Entry<name list="">> entry : markedElementsToReport) {
				System.out.println(entry.getKey().toString());
				for (Element e : entry.getValue()) {
					System.out.println("\t" + e.toString() + " TODO: " + e.getAnnotation(Todo.class).description());
				}
			}
		}
	}

	private void addElementToReport(Element element, Name qualifiedName) {
		if (markedElements.containsKey(qualifiedName)) {
			List<element> classElements = markedElements.get(qualifiedName);
			classElements.add(element);
		} else {
			List<element> classElements = new LinkedList<element>();
			classElements.add(element);
			markedElements.put(qualifiedName, classElements);
		}
	}

}
</element></element></element></name></entry></name></name>

Prawda, że proste?