Skip to content

Content of file EMFFormsViewContextFixture.java

/*******************************************************************************
 * Copyright (c) 2019 Christian W. Damus and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 * Christian W. Damus - initial API and implementation
 ******************************************************************************/
package org.eclipse.emf.ecp.view.test.common.spi;

import static org.hamcrest.CoreMatchers.not;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecp.view.spi.model.VElement;
import org.eclipse.emfforms.spi.common.report.AbstractReport;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.core.services.view.EMFFormsContextListener;
import org.eclipse.emfforms.spi.core.services.view.EMFFormsViewContext;
import org.eclipse.emfforms.spi.core.services.view.EMFFormsViewServiceManager;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;

/**
 * A convenient fixture for JUnit testing with a mock {@link EMFFormsViewContext}.
 *
 * @param <T> the type of view context to mock up
 *
 * @since 1.22
 */
public class EMFFormsViewContextFixture<T extends EMFFormsViewContext> extends TestWatcher {

	private final Class<T> contextType;
	private final Supplier<? extends VElement> viewSupplier;
	private final Supplier<? extends EObject> domainModelSupplier;

	private T viewContext;

	private IEclipseContext e4Context;

	private final Map<Class<?>, Object> lazyServices = new HashMap<>();
	private final Set<T> childContexts = new HashSet<>();
	private final Set<EMFFormsContextListener> capturedListeners = new HashSet<>();

	/**
	 * Initializes me.
	 */
	protected EMFFormsViewContextFixture(Class<T> contextType,
		Supplier<? extends VElement> viewSupplier, Supplier<? extends EObject> domainModelSupplier) {

		super();

		this.contextType = contextType;
		this.viewSupplier = viewSupplier;
		this.domainModelSupplier = domainModelSupplier;
	}

	/**
	 * Initializes me.
	 */
	protected EMFFormsViewContextFixture(Class<T> contextType, Object owner) {
		this(contextType, fieldSupplier(owner, ViewModel.class, VElement.class),
			fieldSupplier(owner, DomainModel.class, EObject.class));
	}

	//
	// Creation
	//

	/**
	 * Create a new fixture instance.
	 */
	public static EMFFormsViewContextFixture<EMFFormsViewContext> create() {
		return create(EMFFormsViewContext.class);
	}

	/**
	 * Create a new fixture instance.
	 */
	public static <T extends EMFFormsViewContext> EMFFormsViewContextFixture<T> create(Class<T> contextType) {
		return create(contextType, () -> mock(VElement.class), () -> mock(EObject.class));
	}

	/**
	 * Create a new fixture instance.
	 */
	public static EMFFormsViewContextFixture<EMFFormsViewContext> create(
		Supplier<? extends VElement> viewSupplier, Supplier<? extends EObject> domainModelSupplier) {

		return create(EMFFormsViewContext.class, viewSupplier, domainModelSupplier);
	}

	/**
	 * Create a new fixture instance.
	 */
	public static <T extends EMFFormsViewContext> EMFFormsViewContextFixture<T> create(Class<T> contextType,
		Supplier<? extends VElement> viewSupplier, Supplier<? extends EObject> domainModelSupplier) {

		return new EMFFormsViewContextFixture<>(contextType, viewSupplier, domainModelSupplier);
	}

	/**
	 * Create a new fixture instance.
	 */
	public static EMFFormsViewContextFixture<EMFFormsViewContext> create(Object owner) {
		return create(EMFFormsViewContext.class, owner);
	}

	/**
	 * Create a new fixture instance.
	 */
	public static <T extends EMFFormsViewContext> EMFFormsViewContextFixture<T> create(Class<T> contextType,
		Object owner) {

		return new EMFFormsViewContextFixture<>(contextType, owner);
	}

	//
	// Test fixture protocol
	//

	public final VElement getViewModel() {
		return viewContext.getViewModel();
	}

	public final EObject getDomainModel() {
		return viewContext.getDomainModel();
	}

	public final T getViewContext() {
		return viewContext;
	}

	public final <S> S getService(Class<S> serviceType) {
		return viewContext.getService(serviceType);
	}

	public final <S> void putService(Class<S> serviceType, S service) {
		// This we get picked up by the mock view service
		e4Context.set(serviceType, service);

		initService(service);
	}

	private void initService(Object service) {
		// ViewModelService requires "instantiation"
		if (!instantiate(service, true)) {
			instantiate(service, false);
		}
	}

	private boolean instantiate(Object service, boolean withContext) {
		final Method instantiate;
		try {
			if (withContext) {
				instantiate = service.getClass().getMethod("instantiate", contextType);
				instantiate.invoke(service, viewContext);
				return true;
			}
			instantiate = service.getClass().getMethod("instantiate");
			instantiate.invoke(service);
			return true;
		} catch (NoSuchMethodException | IllegalAccessException e) {
			// These are normal when it's not a ViewModelService
		} catch (SecurityException | IllegalArgumentException | InvocationTargetException e) {
			// These are not normal, but we've made the effort and will let the test proceed
			e4Context.get(ReportService.class).report(new AbstractReport(e));
		}
		return false;
	}

	public T createContext(String name, VElement viewModel, EObject domainModel) {
		final T result = mock(contextType, name);

		when(result.getService(ReportService.class)).thenReturn(mock(ReportService.class));
		when(result.getService(argThat(not(IEclipseContext.class)))).thenAnswer(
			invocation -> bestEffortService((Class<?>) invocation.getArguments()[0]));
		when(result.getService(IEclipseContext.class)).thenReturn(e4Context);

		when(result.getViewModel()).thenReturn(viewModel);
		when(result.getDomainModel()).thenReturn(domainModel);

		return result;
	}

	private <S> S bestEffortService(Class<S> serviceType) {
		S result = serviceType.cast(lazyServices.get(serviceType));

		if (result == null) {
			result = e4Context.get(serviceType);
			if (result != null) {
				initService(result);
				lazyServices.put(serviceType, result);
			}
		}

		if (result == null) {
			// Try the service manager
			final EMFFormsViewServiceManager mgr = getEMFFormsViewServiceManager();

			// These are de facto lazy and local to this mock
			org.eclipse.emfforms.common.Optional<S> service = mgr.createLocalLazyService(serviceType,
				viewContext);
			if (!service.isPresent()) {
				service = mgr.createLocalImmediateService(serviceType, viewContext);
			}
			if (service.isPresent()) {
				result = service.get();
				initService(result);
				lazyServices.put(serviceType, result);
			}
		}

		return result;
	}

	/**
	 * Get the view-model service manager, or a mock substitute, for delegation of
	 * services via the factory OSGi components.
	 *
	 * @return the best-effort service manager
	 */
	private EMFFormsViewServiceManager getEMFFormsViewServiceManager() {
		EMFFormsViewServiceManager result = (EMFFormsViewServiceManager) lazyServices
			.get(EMFFormsViewServiceManager.class);
		if (result == null) {
			result = e4Context.get(EMFFormsViewServiceManager.class);
			if (result != null) {
				initService(result);
			} else {
				result = mock(EMFFormsViewServiceManager.class);
			}

			// Cache the result to avoid unbounded recursion
			lazyServices.put(EMFFormsViewServiceManager.class, result);
		}

		return result;
	}

	/**
	 * Initialize the legacy services manager, which injects view-model services from
	 * the extension point as service-factory OSGi components.
	 */
	private void initializeLegacyServices() {
		final Object legacyServicesManager = e4Context
			.get("org.eclipse.emf.ecp.view.spi.context.EMFFormsLegacyServicesManager");
		if (legacyServicesManager != null) {
			initService(legacyServicesManager);
		}
	}

	public T createChildContext(VElement parentElement, String name,
		VElement viewModel, EObject domainModel) {

		final T result = createContext(name, viewModel, domainModel);

		if (childContexts.add(result)) {
			capturedListeners.forEach(safeRun(l -> l.childContextAdded(parentElement, result)));
		}

		return result;
	}

	private <V> Consumer<V> safeRun(Consumer<? super V> action) {
		return input -> SafeRunner.run(() -> action.accept(input));
	}

	public void disposeChildContext(T childContext) {
		if (childContexts.remove(childContext)) {
			capturedListeners.forEach(safeRun(l -> l.childContextDisposed(childContext)));
		}
	}

	private static <V> Supplier<V> fieldSupplier(Object owner,
		Class<? extends Annotation> annotationType, Class<V> type) {

		for (final Field next : owner.getClass().getDeclaredFields()) {
			if (next.isAnnotationPresent(annotationType) && type.isAssignableFrom(type)) {
				return () -> {
					final boolean wasAccessible = next.isAccessible();
next.setAccessible(true); try { try { return type.cast(next.get(owner)); } catch (final IllegalAccessException e) { return null; } } finally { next.setAccessible(wasAccessible); } }; } } return () -> null; } // // Test lifecycle // @Override protected void starting(Description description) { final Bundle self = FrameworkUtil.getBundle(EMFFormsViewContextFixture.class); e4Context = EclipseContextFactory.createServiceContext(self.getBundleContext()); viewContext = createContext("root", viewSupplier.get(), domainModelSupplier.get()); // Capture the context listeners of the reveal service to feed them context lifecycle events doAnswer(invocation -> { capturedListeners.add((EMFFormsContextListener) invocation.getArguments()[0]); return null; }).when(viewContext).registerEMFFormsContextListener(any()); initializeLegacyServices(); } @Override protected void finished(Description description) { // Dispose any left-over child contexts new ArrayList<>(childContexts).forEach(this::disposeChildContext); // Dispose the root context. Defensive copy in case any removes itself new ArrayList<>(capturedListeners).forEach(safeRun(EMFFormsContextListener::contextDispose)); capturedListeners.clear(); lazyServices.clear(); e4Context.dispose(); viewContext = null; } // // Nested types // @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ViewModel { // Empty annotation } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DomainModel { // Empty annotation } }