RoundMirror.java

/**
 * Powerunit - A JDK1.8 test framework
 * Copyright (C) 2014 Mathieu Boretti.
 *
 * This file is part of Powerunit
 *
 * Powerunit 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.
 *
 * Powerunit 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 Powerunit. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.powerunit.extensions.matchers.provideprocessor;

import static ch.powerunit.extensions.matchers.common.CommonUtils.asStandardMethodName;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;

import static javax.lang.model.util.ElementFilter.typesIn;
import static javax.lang.model.util.ElementFilter.fieldsIn;
import static javax.lang.model.util.ElementFilter.methodsIn;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic.Kind;

import ch.powerunit.extensions.matchers.AddToMatcher;
import ch.powerunit.extensions.matchers.AddToMatchers;
import ch.powerunit.extensions.matchers.IgnoreInMatcher;
import ch.powerunit.extensions.matchers.ProvideMatchers;
import ch.powerunit.extensions.matchers.common.AbstractRoundMirrorReferenceToProcessingEnv;
import ch.powerunit.extensions.matchers.provideprocessor.dsl.DSLMethod;
import ch.powerunit.extensions.matchers.provideprocessor.extension.AutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.beanmatchers.DefaultBeanMatchersAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.hamcrestdate.LocalDateMatchersAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.hamcrestdate.LocalDateTimeMatchersAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.hamcrestdate.LocalTimeMatchersAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.hamcrestdate.ZonedDateTimeMatchersAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.hamcrestutility.CollectionHamcrestUtilityAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.jackson.DefaultJsonNodeJacksonAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.extension.spotify.JsonStringSpotifyAutomatedExtension;
import ch.powerunit.extensions.matchers.provideprocessor.fields.AbstractFieldDescription;
import ch.powerunit.extensions.matchers.provideprocessor.fields.FieldDSLMethod;

public class RoundMirror extends AbstractRoundMirrorReferenceToProcessingEnv {

	private final Collection<AutomatedExtension> AUTOMATED_EXTENSIONS;
	private final Set<? extends Element> elementsWithPM;
	private final Map<Class<?>, Set<? extends Element>> elementsWithOtherAnnotations;
	private final Map<String, ProvidesMatchersAnnotatedElementMirror> alias = new HashMap<>();

	public RoundMirror(RoundEnvironment roundEnv, ProcessingEnvironment processingEnv) {
		super(roundEnv, processingEnv);
		this.elementsWithOtherAnnotations = new HashMap<>();
		this.elementsWithPM = roundEnv.getElementsAnnotatedWith(ProvideMatchers.class);
		elementsWithOtherAnnotations.put(IgnoreInMatcher.class,
				new HashSet<>(roundEnv.getElementsAnnotatedWith(IgnoreInMatcher.class)));
		elementsWithOtherAnnotations.put(AddToMatcher.class,
				new HashSet<>(roundEnv.getElementsAnnotatedWith(AddToMatcher.class)));
		elementsWithOtherAnnotations.put(AddToMatchers.class,
				new HashSet<>(roundEnv.getElementsAnnotatedWith(AddToMatchers.class)));
		AUTOMATED_EXTENSIONS = getDefaultExtension().stream().filter(AutomatedExtension::isPresent)
				.collect(collectingAndThen(toList(), Collections::unmodifiableList));
	}

	private final List<AutomatedExtension> getDefaultExtension() {
		return Arrays.asList(new LocalDateMatchersAutomatedExtension(this),
				new LocalDateTimeMatchersAutomatedExtension(this), new LocalTimeMatchersAutomatedExtension(this),
				new ZonedDateTimeMatchersAutomatedExtension(this),
				new CollectionHamcrestUtilityAutomatedExtension(this), new JsonStringSpotifyAutomatedExtension(this),
				new DefaultBeanMatchersAutomatedExtension(this), new DefaultJsonNodeJacksonAutomatedExtension(this));
	}

	public Collection<ProvidesMatchersAnnotatedElementMirror> parse() {
		ProvidesMatchersElementVisitor providesMatchersElementVisitor = new ProvidesMatchersElementVisitor(this);
		elementsWithPM.stream().filter(e -> roundEnv.getRootElements().contains(e))
				.map(e -> e.accept(providesMatchersElementVisitor, null)).filter(Optional::isPresent)
				.map(t -> new ProvidesMatchersAnnotatedElementMirror(t.get(), this))
				.forEach(a -> alias.put(a.getFullyQualifiedNameOfClassAnnotated(), a));

		doErrorforAllElements();
		return alias.values();
	}

	private void doErrorforAllElements() {
		elementsWithOtherAnnotations.entrySet().stream().forEach(e -> doErrorForElement(e.getValue(), e.getKey()));
	}

	private void doErrorForElement(Set<? extends Element> elements, Class<?> aa) {
		elements.stream().forEach(e -> getMessager().printMessage(Kind.ERROR, "Annotation @" + aa.getName()
				+ " not supported at this location ; The surrounding class is not annotated with @ProvideMatchers. Since version 0.2.0 of powerunit-extension-matchers this is considered as an error.",
				e, findAnnotationMirrorFor(e, aa)));
	}

	private AnnotationMirror findAnnotationMirrorFor(Element e, Class<?> aa) {
		String aaName = aa.getName().toString();
		return e.getAnnotationMirrors().stream()
				.filter(a -> a.getAnnotationType().equals(getElementUtils().getTypeElement(aaName).asType())).findAny()
				.orElse(null);
	}

	public boolean removeFromIgnoreList(Element e) {
		return elementsWithOtherAnnotations.values().stream().map(t -> t.remove(e)).filter(t -> t).findAny()
				.orElse(false);
	}

	public Matchable getByName(String name) {
		return Optional.ofNullable((Matchable) alias.get(name)).orElseGet(() -> Optional
				.ofNullable(getElementUtils().getTypeElement(name)).map(this::lookupMatchableByType).orElse(null));

	}

	public Matchable lookupMatchableByType(TypeElement type) {
		return Optional.ofNullable(getElementUtils().getTypeElement(getQualifiedName(type) + "Matchers"))
				.map(m -> lookupMatchableByTypeAndMatchers(type, m)).orElse(null);
	}

	private Matchable lookupMatchableByTypeAndMatchers(TypeElement type, TypeElement guestMatcher) {
		List<? extends Element> guestMatcherEnclosed = guestMatcher.getEnclosedElements();
		List<TypeElement> types = typesIn(guestMatcherEnclosed);
		Optional<Long> compatibilityField = types.stream().filter(t -> isSimpleName(t, "Metadata"))
				.flatMap(t -> fieldsIn(t.getEnclosedElements()).stream()).filter(t -> isSimpleName(t, "COMPATIBILITY"))
				.map(VariableElement::getConstantValue).filter(Objects::nonNull).filter(Long.class::isInstance)
				.map(Long.class::cast).findAny();
		if (!compatibilityField.isPresent()) {
			return null;// NOT FOUND
		}
		// In future, verify compatiblity
		String guestMatcherName = getSimpleName(type) + "Matcher";
		if (!types.stream().anyMatch(t -> isSimpleName(t, guestMatcherName))) {
			return null;// NOT FOUND
		}
		String shortMethodClassName = asStandardMethodName(getSimpleName(type));
		String withSameValue = shortMethodClassName + "WithSameValue";
		boolean hasSameValue = methodsIn(guestMatcherEnclosed).stream().filter(t -> isSimpleName(t, withSameValue))
				.anyMatch(this::isStatic);
		return Matchable.of(getQualifiedName(guestMatcher), shortMethodClassName, guestMatcherName, hasSameValue,
				compatibilityField.get());
	}

	public AnnotationMirror getProvideMatchersAnnotation(Element e) {
		return getElementUtils().getAllAnnotationMirrors(e).stream()
				.filter(a -> a.getAnnotationType().equals(provideMatchersMirror)).findAny().orElse(null);
	}

	public Collection<Supplier<DSLMethod>> getDSLMethodFor(ProvidesMatchersAnnotatedElementData target) {
		return AUTOMATED_EXTENSIONS.stream().map(ae -> ae.accept(target)).flatMap(Collection::stream).collect(toList());
	}

	public Collection<FieldDSLMethod> getFieldDSLMethodFor(AbstractFieldDescription target) {
		return AUTOMATED_EXTENSIONS.stream().map(ae -> ae.accept(target)).flatMap(Collection::stream).collect(toList());
	}

}