All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.junitpioneer.jupiter.AbstractEntryBasedExtension Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * Copyright 2016-2022 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v2.0 which
 * accompanies this distribution and is available at
 *
 * http://www.eclipse.org/legal/epl-v20.html
 */

package org.junitpioneer.jupiter;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
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.Set;
import java.util.function.Function;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junitpioneer.internal.PioneerUtils;

/**
 * An abstract base class for entry-based extensions, where entries (key-value
 * pairs) can be cleared or set.
 *
 * @param  The entry key type.
 * @param  The entry value type.
 * @param  The clear annotation type.
 * @param  The set annotation type.
 */
abstract class AbstractEntryBasedExtension
		implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, AfterAllCallback {

	@Override
	public void beforeAll(ExtensionContext context) {
		applyForAllContexts(context);
	}

	@Override
	public void beforeEach(ExtensionContext context) {
		applyForAllContexts(context);
	}

	private void applyForAllContexts(ExtensionContext originalContext) {
		/*
		 * We cannot use PioneerAnnotationUtils#findAllEnclosingRepeatableAnnotations(ExtensionContext, Class) or the
		 * like as clearing and setting might interfere. Therefore, we have to apply the extension from the outermost
		 * to the innermost ExtensionContext.
		 */
		List contexts = PioneerUtils.findAllContexts(originalContext);
		Collections.reverse(contexts);
		contexts.forEach(currentContext -> clearAndSetEntries(currentContext, originalContext));
	}

	private void clearAndSetEntries(ExtensionContext currentContext, ExtensionContext originalContext) {
		currentContext.getElement().ifPresent(element -> {
			Set entriesToClear;
			Map entriesToSet;

			try {
				entriesToClear = findEntriesToClear(element);
				entriesToSet = findEntriesToSet(element);
				preventClearAndSetSameEntries(entriesToClear, entriesToSet.keySet());
			}
			catch (IllegalStateException ex) {
				throw new ExtensionConfigurationException("Don't clear/set the same entry more than once.", ex);
			}

			if (entriesToClear.isEmpty() && entriesToSet.isEmpty())
				return;

			reportWarning(currentContext);
			storeOriginalEntries(originalContext, entriesToClear, entriesToSet.keySet());
			clearEntries(entriesToClear);
			setEntries(entriesToSet);
		});
	}

	private Set findEntriesToClear(AnnotatedElement element) {
		return findAnnotations(element, getClearAnnotationType())
				.map(clearKeyMapper())
				.collect(PioneerUtils.distinctToSet());
	}

	private Map findEntriesToSet(AnnotatedElement element) {
		return findAnnotations(element, getSetAnnotationType()).collect(toMap(setKeyMapper(), setValueMapper()));
	}

	private  Stream findAnnotations(AnnotatedElement element, Class clazz) {
		return AnnotationSupport.findRepeatableAnnotations(element, clazz).stream();
	}

	@SuppressWarnings("unchecked")
	private Class getClearAnnotationType() {
		return (Class) getActualTypeArgumentAt(2);
	}

	@SuppressWarnings("unchecked")
	private Class getSetAnnotationType() {
		return (Class) getActualTypeArgumentAt(3);
	}

	private Type getActualTypeArgumentAt(int index) {
		ParameterizedType abstractEntryBasedExtensionType = (ParameterizedType) getClass().getGenericSuperclass();
		return abstractEntryBasedExtensionType.getActualTypeArguments()[index];
	}

	private void preventClearAndSetSameEntries(Collection entriesToClear, Collection entriesToSet) {
		String duplicateEntries = entriesToClear
				.stream()
				.filter(entriesToSet::contains)
				.map(Object::toString)
				.collect(joining(", "));
		if (!duplicateEntries.isEmpty())
			throw new IllegalStateException(
				"Cannot clear and set the following entries at the same time: " + duplicateEntries);
	}

	private void storeOriginalEntries(ExtensionContext context, Collection entriesToClear,
			Collection entriesToSet) {
		getStore(context).put(getStoreKey(context), new EntriesBackup(entriesToClear, entriesToSet));
	}

	private void clearEntries(Collection entriesToClear) {
		entriesToClear.forEach(this::clearEntry);
	}

	private void setEntries(Map entriesToSet) {
		entriesToSet.forEach(this::setEntry);
	}

	@Override
	public void afterEach(ExtensionContext context) {
		restoreForAllContexts(context);
	}

	@Override
	public void afterAll(ExtensionContext context) {
		restoreForAllContexts(context);
	}

	private void restoreForAllContexts(ExtensionContext originalContext) {
		// restore from innermost to outermost
		PioneerUtils.findAllContexts(originalContext).forEach(__ -> restoreOriginalEntries(originalContext));
	}

	private void restoreOriginalEntries(ExtensionContext originalContext) {
		getStore(originalContext)
				.getOrDefault(getStoreKey(originalContext), EntriesBackup.class, new EntriesBackup())
				.restoreBackup();
	}

	private Store getStore(ExtensionContext context) {
		return context.getStore(Namespace.create(getClass()));
	}

	private Object getStoreKey(ExtensionContext context) {
		return context.getUniqueId();
	}

	private class EntriesBackup {

		private final Set entriesToClear = new HashSet<>();
		private final Map entriesToSet = new HashMap<>();

		public EntriesBackup() {
			// empty backup
		}

		public EntriesBackup(Collection entriesToClear, Collection entriesToSet) {
			Stream.concat(entriesToClear.stream(), entriesToSet.stream()).forEach(entry -> {
				V backup = AbstractEntryBasedExtension.this.getEntry(entry);
				if (backup == null)
					this.entriesToClear.add(entry);
				else
					this.entriesToSet.put(entry, backup);
			});
		}

		public void restoreBackup() {
			entriesToClear.forEach(AbstractEntryBasedExtension.this::clearEntry);
			entriesToSet.forEach(AbstractEntryBasedExtension.this::setEntry);
		}

	}

	/**
	 * @return Mapper function to get the key from a clear annotation.
	 */
	protected abstract Function clearKeyMapper();

	/**
	 * @return Mapper function to get the key from a set annotation.
	 */
	protected abstract Function setKeyMapper();

	/**
	 * @return Mapper function to get the value from a set annotation.
	 */
	protected abstract Function setValueMapper();

	/**
	 * Removes the entry indicated by the specified key.
	 */
	protected abstract void clearEntry(K key);

	/**
	 * Gets the entry indicated by the specified key.
	 */
	protected abstract V getEntry(K key);

	/**
	 * Sets the entry indicated by the specified key.
	 */
	protected abstract void setEntry(K key, V value);

	/**
	 * Reports a warning about potentially unsafe practices.
	 */
	protected void reportWarning(ExtensionContext context) {
		// nothing reported by default
	}

}