Skip to content

Package: EMFFormsViewContextFixture$DomainModel

EMFFormsViewContextFixture$DomainModel

Coverage

1: /*******************************************************************************
2: * Copyright (c) 2019 Christian W. Damus and others.
3: *
4: * All rights reserved. This program and the accompanying materials
5: * are made available under the terms of the Eclipse Public License 2.0
6: * which accompanies this distribution, and is available at
7: * https://www.eclipse.org/legal/epl-2.0/
8: *
9: * SPDX-License-Identifier: EPL-2.0
10: *
11: * Contributors:
12: * Christian W. Damus - initial API and implementation
13: ******************************************************************************/
14: package org.eclipse.emf.ecp.view.test.common.spi;
15:
16: import static org.hamcrest.CoreMatchers.not;
17: import static org.mockito.Matchers.any;
18: import static org.mockito.Matchers.argThat;
19: import static org.mockito.Mockito.doAnswer;
20: import static org.mockito.Mockito.mock;
21: import static org.mockito.Mockito.when;
22:
23: import java.lang.annotation.Annotation;
24: import java.lang.annotation.ElementType;
25: import java.lang.annotation.Retention;
26: import java.lang.annotation.RetentionPolicy;
27: import java.lang.annotation.Target;
28: import java.lang.reflect.Field;
29: import java.lang.reflect.InvocationTargetException;
30: import java.lang.reflect.Method;
31: import java.util.ArrayList;
32: import java.util.HashMap;
33: import java.util.HashSet;
34: import java.util.Map;
35: import java.util.Set;
36: import java.util.function.Consumer;
37: import java.util.function.Supplier;
38:
39: import org.eclipse.core.runtime.SafeRunner;
40: import org.eclipse.e4.core.contexts.EclipseContextFactory;
41: import org.eclipse.e4.core.contexts.IEclipseContext;
42: import org.eclipse.emf.ecore.EObject;
43: import org.eclipse.emf.ecp.view.spi.model.VElement;
44: import org.eclipse.emfforms.spi.common.report.AbstractReport;
45: import org.eclipse.emfforms.spi.common.report.ReportService;
46: import org.eclipse.emfforms.spi.core.services.view.EMFFormsContextListener;
47: import org.eclipse.emfforms.spi.core.services.view.EMFFormsViewContext;
48: import org.eclipse.emfforms.spi.core.services.view.EMFFormsViewServiceManager;
49: import org.junit.rules.TestWatcher;
50: import org.junit.runner.Description;
51: import org.osgi.framework.Bundle;
52: import org.osgi.framework.FrameworkUtil;
53:
54: /**
55: * A convenient fixture for JUnit testing with a mock {@link EMFFormsViewContext}.
56: *
57: * @param <T> the type of view context to mock up
58: *
59: * @since 1.22
60: */
61: public class EMFFormsViewContextFixture<T extends EMFFormsViewContext> extends TestWatcher {
62:
63:         private final Class<T> contextType;
64:         private final Supplier<? extends VElement> viewSupplier;
65:         private final Supplier<? extends EObject> domainModelSupplier;
66:
67:         private T viewContext;
68:
69:         private IEclipseContext e4Context;
70:
71:         private final Map<Class<?>, Object> lazyServices = new HashMap<>();
72:         private final Set<T> childContexts = new HashSet<>();
73:         private final Set<EMFFormsContextListener> capturedListeners = new HashSet<>();
74:
75:         /**
76:          * Initializes me.
77:          */
78:         protected EMFFormsViewContextFixture(Class<T> contextType,
79:                 Supplier<? extends VElement> viewSupplier, Supplier<? extends EObject> domainModelSupplier) {
80:
81:                 super();
82:
83:                 this.contextType = contextType;
84:                 this.viewSupplier = viewSupplier;
85:                 this.domainModelSupplier = domainModelSupplier;
86:         }
87:
88:         /**
89:          * Initializes me.
90:          */
91:         protected EMFFormsViewContextFixture(Class<T> contextType, Object owner) {
92:                 this(contextType, fieldSupplier(owner, ViewModel.class, VElement.class),
93:                         fieldSupplier(owner, DomainModel.class, EObject.class));
94:         }
95:
96:         //
97:         // Creation
98:         //
99:
100:         /**
101:          * Create a new fixture instance.
102:          */
103:         public static EMFFormsViewContextFixture<EMFFormsViewContext> create() {
104:                 return create(EMFFormsViewContext.class);
105:         }
106:
107:         /**
108:          * Create a new fixture instance.
109:          */
110:         public static <T extends EMFFormsViewContext> EMFFormsViewContextFixture<T> create(Class<T> contextType) {
111:                 return create(contextType, () -> mock(VElement.class), () -> mock(EObject.class));
112:         }
113:
114:         /**
115:          * Create a new fixture instance.
116:          */
117:         public static EMFFormsViewContextFixture<EMFFormsViewContext> create(
118:                 Supplier<? extends VElement> viewSupplier, Supplier<? extends EObject> domainModelSupplier) {
119:
120:                 return create(EMFFormsViewContext.class, viewSupplier, domainModelSupplier);
121:         }
122:
123:         /**
124:          * Create a new fixture instance.
125:          */
126:         public static <T extends EMFFormsViewContext> EMFFormsViewContextFixture<T> create(Class<T> contextType,
127:                 Supplier<? extends VElement> viewSupplier, Supplier<? extends EObject> domainModelSupplier) {
128:
129:                 return new EMFFormsViewContextFixture<>(contextType, viewSupplier, domainModelSupplier);
130:         }
131:
132:         /**
133:          * Create a new fixture instance.
134:          */
135:         public static EMFFormsViewContextFixture<EMFFormsViewContext> create(Object owner) {
136:                 return create(EMFFormsViewContext.class, owner);
137:         }
138:
139:         /**
140:          * Create a new fixture instance.
141:          */
142:         public static <T extends EMFFormsViewContext> EMFFormsViewContextFixture<T> create(Class<T> contextType,
143:                 Object owner) {
144:
145:                 return new EMFFormsViewContextFixture<>(contextType, owner);
146:         }
147:
148:         //
149:         // Test fixture protocol
150:         //
151:
152:         public final VElement getViewModel() {
153:                 return viewContext.getViewModel();
154:         }
155:
156:         public final EObject getDomainModel() {
157:                 return viewContext.getDomainModel();
158:         }
159:
160:         public final T getViewContext() {
161:                 return viewContext;
162:         }
163:
164:         public final <S> S getService(Class<S> serviceType) {
165:                 return viewContext.getService(serviceType);
166:         }
167:
168:         public final <S> void putService(Class<S> serviceType, S service) {
169:                 // This we get picked up by the mock view service
170:                 e4Context.set(serviceType, service);
171:
172:                 initService(service);
173:         }
174:
175:         private void initService(Object service) {
176:                 // ViewModelService requires "instantiation"
177:                 if (!instantiate(service, true)) {
178:                         instantiate(service, false);
179:                 }
180:         }
181:
182:         private boolean instantiate(Object service, boolean withContext) {
183:                 final Method instantiate;
184:                 try {
185:                         if (withContext) {
186:                                 instantiate = service.getClass().getMethod("instantiate", contextType);
187:                                 instantiate.invoke(service, viewContext);
188:                                 return true;
189:                         }
190:                         instantiate = service.getClass().getMethod("instantiate");
191:                         instantiate.invoke(service);
192:                         return true;
193:                 } catch (NoSuchMethodException | IllegalAccessException e) {
194:                         // These are normal when it's not a ViewModelService
195:                 } catch (SecurityException | IllegalArgumentException | InvocationTargetException e) {
196:                         // These are not normal, but we've made the effort and will let the test proceed
197:                         e4Context.get(ReportService.class).report(new AbstractReport(e));
198:                 }
199:                 return false;
200:         }
201:
202:         public T createContext(String name, VElement viewModel, EObject domainModel) {
203:                 final T result = mock(contextType, name);
204:
205:                 when(result.getService(ReportService.class)).thenReturn(mock(ReportService.class));
206:                 when(result.getService(argThat(not(IEclipseContext.class)))).thenAnswer(
207:                         invocation -> bestEffortService((Class<?>) invocation.getArguments()[0]));
208:                 when(result.getService(IEclipseContext.class)).thenReturn(e4Context);
209:
210:                 when(result.getViewModel()).thenReturn(viewModel);
211:                 when(result.getDomainModel()).thenReturn(domainModel);
212:
213:                 return result;
214:         }
215:
216:         private <S> S bestEffortService(Class<S> serviceType) {
217:                 S result = serviceType.cast(lazyServices.get(serviceType));
218:
219:                 if (result == null) {
220:                         result = e4Context.get(serviceType);
221:                         if (result != null) {
222:                                 initService(result);
223:                                 lazyServices.put(serviceType, result);
224:                         }
225:                 }
226:
227:                 if (result == null) {
228:                         // Try the service manager
229:                         final EMFFormsViewServiceManager mgr = getEMFFormsViewServiceManager();
230:
231:                         // These are de facto lazy and local to this mock
232:                         org.eclipse.emfforms.common.Optional<S> service = mgr.createLocalLazyService(serviceType,
233:                                 viewContext);
234:                         if (!service.isPresent()) {
235:                                 service = mgr.createLocalImmediateService(serviceType, viewContext);
236:                         }
237:                         if (service.isPresent()) {
238:                                 result = service.get();
239:                                 initService(result);
240:                                 lazyServices.put(serviceType, result);
241:                         }
242:                 }
243:
244:                 return result;
245:         }
246:
247:         /**
248:          * Get the view-model service manager, or a mock substitute, for delegation of
249:          * services via the factory OSGi components.
250:          *
251:          * @return the best-effort service manager
252:          */
253:         private EMFFormsViewServiceManager getEMFFormsViewServiceManager() {
254:                 EMFFormsViewServiceManager result = (EMFFormsViewServiceManager) lazyServices
255:                         .get(EMFFormsViewServiceManager.class);
256:                 if (result == null) {
257:                         result = e4Context.get(EMFFormsViewServiceManager.class);
258:                         if (result != null) {
259:                                 initService(result);
260:                         } else {
261:                                 result = mock(EMFFormsViewServiceManager.class);
262:                         }
263:
264:                         // Cache the result to avoid unbounded recursion
265:                         lazyServices.put(EMFFormsViewServiceManager.class, result);
266:                 }
267:
268:                 return result;
269:         }
270:
271:         /**
272:          * Initialize the legacy services manager, which injects view-model services from
273:          * the extension point as service-factory OSGi components.
274:          */
275:         private void initializeLegacyServices() {
276:                 final Object legacyServicesManager = e4Context
277:                         .get("org.eclipse.emf.ecp.view.spi.context.EMFFormsLegacyServicesManager");
278:                 if (legacyServicesManager != null) {
279:                         initService(legacyServicesManager);
280:                 }
281:         }
282:
283:         public T createChildContext(VElement parentElement, String name,
284:                 VElement viewModel, EObject domainModel) {
285:
286:                 final T result = createContext(name, viewModel, domainModel);
287:
288:                 if (childContexts.add(result)) {
289:                         capturedListeners.forEach(safeRun(l -> l.childContextAdded(parentElement, result)));
290:                 }
291:
292:                 return result;
293:         }
294:
295:         private <V> Consumer<V> safeRun(Consumer<? super V> action) {
296:                 return input -> SafeRunner.run(() -> action.accept(input));
297:         }
298:
299:         public void disposeChildContext(T childContext) {
300:                 if (childContexts.remove(childContext)) {
301:                         capturedListeners.forEach(safeRun(l -> l.childContextDisposed(childContext)));
302:                 }
303:         }
304:
305:         private static <V> Supplier<V> fieldSupplier(Object owner,
306:                 Class<? extends Annotation> annotationType, Class<V> type) {
307:
308:                 for (final Field next : owner.getClass().getDeclaredFields()) {
309:                         if (next.isAnnotationPresent(annotationType) && type.isAssignableFrom(type)) {
310:                                 return () -> {
311:                                         final boolean wasAccessible = next.isAccessible();
312:                                         next.setAccessible(true);
313:                                         try {
314:                                                 try {
315:                                                         return type.cast(next.get(owner));
316:                                                 } catch (final IllegalAccessException e) {
317:                                                         return null;
318:                                                 }
319:                                         } finally {
320:                                                 next.setAccessible(wasAccessible);
321:                                         }
322:                                 };
323:                         }
324:                 }
325:
326:                 return () -> null;
327:         }
328:
329:         //
330:         // Test lifecycle
331:         //
332:
333:         @Override
334:         protected void starting(Description description) {
335:                 final Bundle self = FrameworkUtil.getBundle(EMFFormsViewContextFixture.class);
336:                 e4Context = EclipseContextFactory.createServiceContext(self.getBundleContext());
337:
338:                 viewContext = createContext("root", viewSupplier.get(), domainModelSupplier.get());
339:
340:                 // Capture the context listeners of the reveal service to feed them context lifecycle events
341:                 doAnswer(invocation -> {
342:                         capturedListeners.add((EMFFormsContextListener) invocation.getArguments()[0]);
343:                         return null;
344:                 }).when(viewContext).registerEMFFormsContextListener(any());
345:
346:                 initializeLegacyServices();
347:         }
348:
349:         @Override
350:         protected void finished(Description description) {
351:                 // Dispose any left-over child contexts
352:                 new ArrayList<>(childContexts).forEach(this::disposeChildContext);
353:
354:                 // Dispose the root context. Defensive copy in case any removes itself
355:                 new ArrayList<>(capturedListeners).forEach(safeRun(EMFFormsContextListener::contextDispose));
356:                 capturedListeners.clear();
357:
358:                 lazyServices.clear();
359:                 e4Context.dispose();
360:
361:                 viewContext = null;
362:         }
363:
364:         //
365:         // Nested types
366:         //
367:
368:         @Retention(RetentionPolicy.RUNTIME)
369:         @Target(ElementType.FIELD)
370:         public @interface ViewModel {
371:                 // Empty annotation
372:         }
373:
374:         @Retention(RetentionPolicy.RUNTIME)
375:         @Target(ElementType.FIELD)
376:         public @interface DomainModel {
377:                 // Empty annotation
378:         }
379:
380: }