| /******************************************************************************* |
| * Copyright (c) 2009, 2015 IBM Corporation and others. |
| * |
| * 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: |
| * IBM Corporation - initial API and implementation |
| * Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654 |
| *******************************************************************************/ |
| package org.eclipse.e4.ui.internal.workbench; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| import org.eclipse.e4.core.contexts.IEclipseContext; |
| import org.eclipse.e4.core.services.events.IEventBroker; |
| import org.eclipse.e4.ui.model.application.MApplicationElement; |
| import org.eclipse.e4.ui.model.application.impl.StringToObjectMapImpl; |
| import org.eclipse.e4.ui.model.application.impl.StringToStringMapImpl; |
| import org.eclipse.e4.ui.model.application.ui.MUIElement; |
| import org.eclipse.e4.ui.workbench.UIEvents; |
| import org.eclipse.e4.ui.workbench.UIEvents.EventTags; |
| import org.eclipse.e4.ui.workbench.UIEvents.EventTypes; |
| import org.eclipse.emf.common.notify.Notification; |
| import org.eclipse.emf.ecore.EClass; |
| import org.eclipse.emf.ecore.EStructuralFeature; |
| import org.eclipse.emf.ecore.util.EContentAdapter; |
| import org.osgi.service.log.LogService; |
| |
| /** |
| * Transforms E4 MPart events into 3.x legacy events. |
| */ |
| public class UIEventPublisher extends EContentAdapter { |
| |
| private IEclipseContext context; |
| |
| /** |
| * @param e4Context |
| */ |
| public UIEventPublisher(IEclipseContext e4Context) { |
| this.context = e4Context; |
| } |
| |
| @Override |
| public void notifyChanged(Notification notification) { |
| super.notifyChanged(notification); |
| |
| // Ignore events that did not change the model value |
| if (notification.isTouch()) |
| return; |
| |
| // Format the EMF event as an E4 UIEvent |
| Map<String, Object> argMap = new HashMap<>(); |
| |
| String topic = formatData(notification, argMap); |
| |
| if (topic != null) { |
| IEventBroker eventManager = context.get(IEventBroker.class); |
| eventManager.send(topic, argMap); |
| } |
| } |
| |
| /** |
| * Large hack here. Open to better suggestions |
| * |
| * When the model SET changes occur to the values of maps, the NOTIFIER is the map and not the |
| * element containing the map. (Note that map addition and removal events DO come in with the |
| * NOTIFIER set to the element containing the map) |
| * |
| * In the problematic case, the containing model element can be found in the NOTIFIER's |
| * eContainer. However, the FEATURE (field) of the eContainer containing the map is not readily |
| * available. |
| * |
| * We had considered a strategy where you recursively check all fields of the eContainer for an |
| * object that IS the NOTIFIER, however these events happen a lot and the resulting code would |
| * have very poor performance. Additionally, all I could get from the eContainer was an EMF |
| * DelegatingMap, while the NOTIFIER is an instance of the contained delegated map |
| * (StringToObjectImpl for example). It would appear some reflection tricks to read |
| * protected/private methods would be required to get the correct objects to compare. |
| * |
| * At this time there are only two maps used in our model. A StringToObject map used in |
| * MApplicationElement.transientData a StringToString map used in |
| * MApplicationElement.persistentState. |
| * |
| * The large hack employed is to take this knowledge and hard hard code the topic based on the |
| * type of the NOTIFIER. |
| * |
| * One problem with this approach is if a developer modifies the model to contain another field |
| * using a map events would be generated with the wrong topic information. This will lead to |
| * hard to identify bugs. |
| * |
| * To combat this I have added guard code that explicitly confirms that a StringToObject change |
| * is being originated by transientData and a StringToString change is being originated by |
| * persistentState. If the guard check fails an IllegalArgumentException is thrown with |
| * instructions to modify UIEventPublisher. |
| * |
| * Clearly this is a sub-optimal solution and would like to hear suggestions for improvement. |
| * Likely there is some "simple" EMF wisdom we are missing to make this work with a couple of |
| * annotations, a white swan and a full moon. |
| */ |
| private String formatData(final Notification notification, Map<String, Object> argMap) { |
| MApplicationElement appElement = null; |
| EStructuralFeature feature = null; |
| String attributeName = null; |
| String topic = null; |
| |
| Object notifier = notification.getNotifier(); |
| Object oldValue = null; |
| Object newValue = null; |
| Object position = null; |
| |
| if (notifier instanceof MApplicationElement) { |
| // Most EMF events will be these. Even map add and remove events |
| appElement = (MApplicationElement) notifier; |
| feature = (EStructuralFeature) notification.getFeature(); |
| attributeName = feature.getName(); |
| topic = getTopic(feature, getEventType(notification)); |
| switch (notification.getEventType()) { |
| case Notification.MOVE: |
| // for MOVE, oldValue is actually the source position |
| oldValue = notification.getOldValue(); |
| newValue = notification.getNewValue(); |
| position = notification.getPosition(); |
| break; |
| case Notification.ADD_MANY: |
| newValue = notification.getNewValue(); |
| position = notification.getPosition(); |
| break; |
| case Notification.REMOVE_MANY: |
| oldValue = notification.getOldValue(); |
| position = notification.getNewValue(); |
| break; |
| case Notification.ADD: |
| case Notification.REMOVE: |
| oldValue = notification.getOldValue(); |
| newValue = notification.getNewValue(); |
| position = notification.getPosition(); |
| break; |
| case Notification.SET: |
| case Notification.UNSET: |
| oldValue = notification.getOldValue(); |
| newValue = notification.getNewValue(); |
| break; |
| default: |
| Activator.log(LogService.LOG_ERROR, getClass().getName() |
| + ": unhandled EMF Notification code: " //$NON-NLS-1$ |
| + notification.getEventType()); |
| } |
| } else if (notifier instanceof StringToObjectMapImpl) { |
| // These are SET events on StringToObjectMap only |
| // StringToObjectMap is ONLY used by MApplicationData.transientData |
| appElement = (MApplicationElement) ((StringToObjectMapImpl) notifier).eContainer(); |
| |
| // Guard code to detect if some other model field is using a StringToObjectMap |
| final String key = ((StringToObjectMapImpl) notifier).getKey(); |
| Object storedNewValue = appElement.getTransientData().get(key); |
| Object notificationNewValue = notification.getNewValue(); |
| |
| // Identity check by design. If these are not the same object then the event came from |
| // a different model object than we expected. Warn the developer |
| if (notificationNewValue != storedNewValue) { |
| throw new IllegalArgumentException( |
| "A StringToObjectMap that was NOT MApplicationElement.transientData changed. You must modify UIEventPublisher appropriately"); //$NON-NLS-1$ |
| } |
| |
| attributeName = UIEvents.ApplicationElement.TRANSIENTDATA; |
| topic = getTopic(attributeName, getEventType(notification)); |
| |
| // We need to send MapEntries for the old and new values. |
| oldValue = createMapEntry(key, notification.getOldValue()); |
| newValue = createMapEntry(key, notification.getNewValue()); |
| } else if (notifier instanceof StringToStringMapImpl) { |
| // These are SET events on StringToStringMap only |
| // StringToStringMap is ONLY used by MApplicationData.persistedState |
| appElement = (MApplicationElement) ((StringToStringMapImpl) notifier).eContainer(); |
| |
| // Guard code to detect if some other model field is using a StringToStringMap |
| final String key = ((StringToStringMapImpl) notifier).getKey(); |
| Object storedNewValue = appElement.getPersistedState().get(key); |
| Object notificationNewValue = notification.getNewValue(); |
| |
| // Identity check by design. If these are not the same object then the event came from |
| // a different model object than we expected. Warn the developer |
| if (notificationNewValue != storedNewValue) { |
| throw new IllegalArgumentException( |
| "A StringToStringMap that was NOT MApplicationElement.persistedState changed. You must modify UIEventPublisher appropriately"); //$NON-NLS-1$ |
| } |
| |
| attributeName = UIEvents.ApplicationElement.PERSISTEDSTATE; |
| topic = getTopic(attributeName, getEventType(notification)); |
| oldValue = createMapEntry(key, notification.getOldValue()); |
| newValue = createMapEntry(key, notification.getNewValue()); |
| } else { |
| // Unhandled notification type. Ignore event |
| return null; |
| } |
| |
| argMap.put(EventTags.TYPE, getEventType(notification)); |
| argMap.put(EventTags.ELEMENT, appElement); |
| argMap.put(EventTags.ATTNAME, attributeName); |
| |
| // no need to include UNSET |
| if (notification.getEventType() == Notification.SET |
| || notification.getEventType() == Notification.MOVE |
| || notification.getEventType() == Notification.ADD |
| || notification.getEventType() == Notification.ADD_MANY |
| || notification.getEventType() == Notification.REMOVE |
| || notification.getEventType() == Notification.REMOVE_MANY) { |
| if (newValue != null) { |
| argMap.put(EventTags.NEW_VALUE, newValue); |
| } |
| if (oldValue != null) { |
| argMap.put(EventTags.OLD_VALUE, oldValue); |
| } |
| if (position != null) { |
| argMap.put(EventTags.POSITION, position); |
| } |
| } |
| |
| if (appElement instanceof MUIElement) { |
| argMap.put(EventTags.WIDGET, ((MUIElement) appElement).getWidget()); |
| } |
| |
| return topic; |
| } |
| |
| private String getEventType(Notification notification) { |
| switch (notification.getEventType()) { |
| case Notification.ADD: |
| return EventTypes.ADD; |
| |
| case Notification.ADD_MANY: |
| return EventTypes.ADD_MANY; |
| |
| case Notification.REMOVE: |
| return EventTypes.REMOVE; |
| |
| case Notification.REMOVE_MANY: |
| return EventTypes.REMOVE_MANY; |
| |
| case Notification.MOVE: |
| return EventTypes.MOVE; |
| |
| // case Notification.UNSET: doesn't appear to be generated |
| case Notification.SET: |
| return EventTypes.SET; |
| } |
| |
| return "UNKNOWN"; //$NON-NLS-1$ |
| } |
| |
| private Map.Entry<String, Object> createMapEntry(final String key, final Object value) { |
| return new Map.Entry<String, Object>() { |
| @Override |
| public String getKey() { |
| return key; |
| } |
| |
| @Override |
| public Object getValue() { |
| return value; |
| } |
| |
| @Override |
| public Object setValue(Object value) { |
| throw new UnsupportedOperationException(); |
| } |
| }; |
| } |
| |
| private String getTopic(EStructuralFeature eFeature, String type) { |
| EClass eContainingClass = eFeature.getEContainingClass(); |
| return UIEvents.UIModelTopicBase + UIEvents.TOPIC_SEP |
| + eContainingClass.getEPackage().getName() + UIEvents.TOPIC_SEP |
| + eContainingClass.getName() + UIEvents.TOPIC_SEP + eFeature.getName() |
| + UIEvents.TOPIC_SEP + type; |
| } |
| |
| private String getTopic(String attributeName, String type) { |
| String topicBase = "org/eclipse/e4/ui/model/application/ApplicationElement/"; //$NON-NLS-1$ |
| return topicBase + attributeName + UIEvents.TOPIC_SEP + type; |
| } |
| } |