/** * Copyright 2014 SAP AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.spotter.eclipse.ui.model; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.lpe.common.config.ConfigParameterDescription; import org.lpe.common.util.system.LpeSystemUtils; import org.spotter.eclipse.ui.Activator; import org.spotter.eclipse.ui.ServiceClientWrapper; import org.spotter.eclipse.ui.handlers.HandlerMediatorHelper; import org.spotter.eclipse.ui.handlers.IHandlerMediator; import org.spotter.eclipse.ui.listeners.IItemChangedListener; import org.spotter.eclipse.ui.listeners.IItemPropertiesChangedListener; import org.spotter.eclipse.ui.model.xml.IModelWrapper; import org.spotter.shared.environment.model.XMConfiguration; /** * A basic implementation for an extension item. * * @author Denis Knoepfle * */ public class ExtensionItem implements IExtensionItem { private class ConnectionUpdater implements Runnable { private volatile boolean isCancelled = false; public void cancel() { this.isCancelled = true; } @Override public void run() { try { Boolean newConnection = ExtensionItem.this.modelWrapper.testConnection(); onConnectionUpdateComplete(this, newConnection, null); } catch (Exception e) { onConnectionUpdateComplete(this, null, e); } } } private static final String MSG_CONN_PENDING = "Connection test pending..."; private static final String MSG_CONN_AVAILABLE = "Connection OK"; private static final String MSG_CONN_UNAVAILABLE = "No connection"; private static final String MSG_CONN_INVALID = "Invalid state"; private static final Image IMG_CONN_PENDING = Activator.getImage("icons/pending.gif"); private static final Image IMG_CONN_AVAILABLE = Activator.getImage("icons/tick.png"); private static final Image IMG_CONN_UNAVAILABLE = Activator.getImage("icons/cross.png"); private static final Image IMG_CONN_INVALID = Activator.getImage("icons/exclamation.png"); private final List<IItemChangedListener> itemChangedListeners; private final List<IItemPropertiesChangedListener> propertiesChangedListeners; private final IHandlerMediator handlerMediatorHelper; private final List<IExtensionItem> childrenItems; private IExtensionItem parentItem; private final IModelWrapper modelWrapper; private final Map<String, ConfigParameterDescription> remainingDescriptions; private final String editorId; private ServiceClientWrapper client; private long lastCacheClearTime; private Map<String, ConfigParameterDescription> paramsMap; private volatile Boolean connection; private volatile ConnectionUpdater connectionUpdater; private boolean ignoreConnection; private boolean isPending; private String extensionDescription; private String errorMsg; /** * Creates an extension item with no children and no model. * * @param editorId * the id of the editor this extension is assigned to */ public ExtensionItem(String editorId) { this(null, null, editorId); } /** * Creates an extension item with no parent. This is a convenience * constructor as the item is assigned a parent later anyway when being * added to another extension item as a child. * * @param modelWrapper * the model wrapper * @param editorId * the id of the editor this extension is assigned to */ public ExtensionItem(IModelWrapper modelWrapper, String editorId) { this(null, modelWrapper, editorId); } /** * Creates an extension item in the hierarchy under the given parent. * * @param parent * the parent of this item * @param modelWrapper * the model wrapper for this item * @param editorId * the id of the editor this extension is assigned to */ public ExtensionItem(IExtensionItem parent, IModelWrapper modelWrapper, String editorId) { this.itemChangedListeners = new ArrayList<IItemChangedListener>(); this.propertiesChangedListeners = new ArrayList<IItemPropertiesChangedListener>(); this.handlerMediatorHelper = new HandlerMediatorHelper(); this.childrenItems = new ArrayList<IExtensionItem>(); this.parentItem = parent; this.editorId = editorId; this.client = null; this.lastCacheClearTime = 0; this.modelWrapper = modelWrapper; if (modelWrapper == null || modelWrapper.getXMLModel() == null) { this.remainingDescriptions = new HashMap<String, ConfigParameterDescription>(); } else { String projectName = modelWrapper.getProjectName(); if (projectName != null) { this.client = Activator.getDefault().getClient(projectName); this.lastCacheClearTime = client.getLastClearTime(); } this.remainingDescriptions = computeConfigurableExtensionConfigParams(); ConfigParameterDescription extDesc = getExtensionConfigParam(ConfigParameterDescription.EXT_DESCRIPTION_KEY); if (extDesc != null) { this.extensionDescription = extDesc.getDefaultValue(); } } setConnection(null); setConnectionUpdater(null); this.ignoreConnection = false; this.isPending = modelWrapper == null ? false : true; this.errorMsg = null; } private synchronized Boolean getConnection() { return connection; } private synchronized void setConnection(Boolean connection) { this.connection = connection; } private synchronized ConnectionUpdater getConnectionUpdater() { return connectionUpdater; } private synchronized void setConnectionUpdater(ConnectionUpdater connectionUpdater) { this.connectionUpdater = connectionUpdater; } private synchronized void onConnectionUpdateComplete(ConnectionUpdater updater, Boolean newConnection, Exception exception) { if (!updater.isCancelled) { if (exception == null) { setConnection(newConnection); } else { errorMsg = exception.getMessage(); } isPending = false; fireItemAppearanceChangedOnUIThread(); } } @Override public String getText() { if (modelWrapper == null || modelWrapper.getXMLModel() == null) { return ""; } String customName = modelWrapper.getName(); String extensionName = modelWrapper.getExtensionName(); if (extensionName == null) { extensionName = "unnamed extension"; } if (customName != null && !customName.isEmpty()) { return customName + " (" + extensionName + ")"; } else { return extensionName; } } @Override public String getToolTip() { if (isConnectionIgnored()) { return extensionDescription != null ? extensionDescription : ""; } if (isPending) { return MSG_CONN_PENDING; } String tooltip = MSG_CONN_INVALID + (errorMsg == null ? "" : ": " + errorMsg); Boolean currentConnection = getConnection(); if (currentConnection != null) { tooltip = currentConnection ? MSG_CONN_AVAILABLE : MSG_CONN_UNAVAILABLE; } return tooltip; } @Override public Image getImage() { if (isConnectionIgnored()) { return null; } if (isPending) { return IMG_CONN_PENDING; } Image image = IMG_CONN_INVALID; Boolean currentConnection = getConnection(); if (currentConnection != null) { image = currentConnection ? IMG_CONN_AVAILABLE : IMG_CONN_UNAVAILABLE; } return image; } @Override public String getEditorId() { return editorId; } @Override public String toString() { String text = getText(); return text.isEmpty() ? "ExtensionItem {no model}" : text; } @Override public boolean isConnectionIgnored() { return ignoreConnection; } @Override public void setIgnoreConnection(boolean ignoreConnection) { this.ignoreConnection = ignoreConnection; } @Override public IModelWrapper getModelWrapper() { return modelWrapper; } @Override public void propertyDirty(Object propertyItem) { fireItemPropertyChanged(propertyItem); } @Override public void addItemChangedListener(IItemChangedListener listener) { itemChangedListeners.add(listener); } @Override public void removeItemChangedListener(IItemChangedListener listener) { itemChangedListeners.remove(listener); } @Override public void addItemPropertiesChangedListener(IItemPropertiesChangedListener listener) { propertiesChangedListeners.add(listener); } @Override public void removeItemPropertiesChangedListener(IItemPropertiesChangedListener listener) { propertiesChangedListeners.remove(listener); } @Override public synchronized void updateConnectionStatus() { if (isConnectionIgnored() || modelWrapper == null) { return; } ConnectionUpdater currentUpdater = getConnectionUpdater(); if (currentUpdater != null) { currentUpdater.cancel(); } isPending = true; errorMsg = null; setConnection(null); fireItemAppearanceChanged(); currentUpdater = new ConnectionUpdater(); setConnectionUpdater(currentUpdater); LpeSystemUtils.submitTask(currentUpdater); } @Override public void addConfigParamUsingDescription(ConfigParameterDescription desc) { List<XMConfiguration> xmConfigList = modelWrapper.getConfig(); if (xmConfigList == null) { xmConfigList = new ArrayList<XMConfiguration>(); modelWrapper.setConfig(xmConfigList); } XMConfiguration xmConfig = new XMConfiguration(); xmConfig.setKey(desc.getName()); xmConfig.setValue(desc.getDefaultValue()); xmConfigList.add(xmConfig); if (!desc.isMandatory()) { remainingDescriptions.remove(xmConfig.getKey()); } fireItemPropertiesChanged(); fireItemAppearanceChanged(); } @Override public void removeConfigParam(ConfigParamPropertyItem item) { if (modelWrapper.getConfig() != null) { XMConfiguration conf = item.getXMConfig(); if (modelWrapper.getConfig().remove(conf)) { ConfigParameterDescription desc = item.getConfigParameterDescription(); if (!desc.isMandatory()) { remainingDescriptions.put(conf.getKey(), desc); } fireItemPropertyRemoved(item); } } } @Override public void removeNonMandatoryConfigParams() { List<XMConfiguration> xmConfigList = modelWrapper.getConfig(); List<XMConfiguration> removeLater = new ArrayList<XMConfiguration>(); if (xmConfigList != null) { for (XMConfiguration conf : xmConfigList) { ConfigParameterDescription desc = getExtensionConfigParam(conf.getKey()); if (!desc.isMandatory()) { // TODO: recalculate remainingDescription after cache clear removeLater.add(conf); remainingDescriptions.put(conf.getKey(), desc); } } xmConfigList.removeAll(removeLater); fireItemPropertiesChanged(); } } @Override public void removed(boolean propagate) { modelWrapper.removed(); if (propagate) { for (IExtensionItem child : childrenItems) { child.removed(propagate); } } } /** * Copies this item including its children. Any attached handlers or * listeners will not be copied. * * @return a copy of this item */ @Override public IExtensionItem copyItem() { ExtensionItem copy = new ExtensionItem(getParent(), getModelWrapper().copy(), getEditorId()); copy.setIgnoreConnection(isConnectionIgnored()); // copy children items as well if (hasItems()) { for (IExtensionItem child : getItems()) { IExtensionItem childCopy = child.copyItem(); copy.addItem(childCopy); } } return copy; } @Override public IExtensionItem getItem(int index) { return childrenItems.get(index); } @Override public int getItemIndex(IExtensionItem item) { return childrenItems.lastIndexOf(item); } @Override public void addItem(IExtensionItem item) { doAddItem(item); fireItemChildAdded(this, item); } @Override public void addItem(int index, IExtensionItem item) { doAddItem(item); if (!moveItem(item, index)) { fireItemChildAdded(this, item); } } private void doAddItem(IExtensionItem item) { childrenItems.add(item); item.setParent(this); item.setIgnoreConnection(isConnectionIgnored()); if (getModelWrapper() != null) { List<?> modelContainingList = getModelWrapper().getChildren(); item.getModelWrapper().setXMLModelContainingList(modelContainingList); } item.getModelWrapper().added(); } @Override public boolean moveItem(IExtensionItem item, int destinationIndex) { boolean success = false; int index = getItemIndex(item); if (index != -1 && index != destinationIndex && destinationIndex >= 0 && destinationIndex < getItemCount()) { childrenItems.remove(index); if (destinationIndex < getItemCount()) { childrenItems.add(destinationIndex, item); } else { childrenItems.add(item); } item.getModelWrapper().moved(destinationIndex); fireItemChildAdded(this, item); success = true; } return success; } @Override public void removeItem(int index, boolean propagate) { IExtensionItem item = childrenItems.remove(index); item.removed(propagate); fireItemChildRemoved(this, item); } @Override public void removeItem(IExtensionItem item, boolean propgate) { childrenItems.remove(item); item.removed(propgate); fireItemChildRemoved(this, item); } @Override public IExtensionItem[] getItems() { return childrenItems.toArray(new IExtensionItem[childrenItems.size()]); } @Override public boolean hasItems() { return !childrenItems.isEmpty(); } @Override public int getItemCount() { return childrenItems.size(); } @Override public void setParent(IExtensionItem parent) { this.parentItem = parent; } @Override public IExtensionItem getParent() { return parentItem; } @Override public boolean hasParent(IExtensionItem parent) { if (getParent() != null) { if (getParent().equals(parent)) { return true; } else { return getParent().hasParent(parent); } } return false; } @Override public void setError(String errorMessage) { if (errorMessage != null) { this.connection = null; this.errorMsg = errorMessage; } } @Override public void setChildrenError(String errorMessage) { for (IExtensionItem item : childrenItems) { item.setError(errorMessage); item.fireItemAppearanceChanged(); item.setChildrenError(errorMessage); } } @Override public void updateChildrenConnections() { for (IExtensionItem item : childrenItems) { item.updateConnectionStatus(); item.updateChildrenConnections(); } } @Override public ConfigParameterDescription getExtensionConfigParam(String key) { if (paramsMap == null || hasCacheCleared()) { initParamsMap(); return paramsMap == null ? null : paramsMap.get(key); } return paramsMap.get(key); } private boolean hasCacheCleared() { if (client == null) { return false; } long clearTime = client.getLastClearTime(); return lastCacheClearTime < clearTime; } @Override public Collection<ConfigParameterDescription> getConfigurableExtensionConfigParams() { return remainingDescriptions.values(); } @Override public boolean hasConfigurableExtensionConfigParams() { return !remainingDescriptions.isEmpty(); } @Override public void fireItemAppearanceChanged() { fireItemAppearanceChanged(this); } @Override public void fireItemPropertiesChanged() { for (IItemPropertiesChangedListener listener : propertiesChangedListeners) { listener.propertiesChanged(); } } @Override public void fireItemAppearanceChanged(IExtensionItem item) { for (IItemChangedListener listener : itemChangedListeners) { listener.appearanceChanged(item); } if (parentItem != null) { parentItem.fireItemAppearanceChanged(item); } } @Override public void fireItemChildAdded(IExtensionItem parent, IExtensionItem item) { for (IItemChangedListener listener : itemChangedListeners) { listener.childAdded(parent, item); } if (parentItem != null) { parentItem.fireItemChildAdded(parent, item); } } @Override public void fireItemChildRemoved(IExtensionItem parent, IExtensionItem item) { for (IItemChangedListener listener : itemChangedListeners) { listener.childRemoved(parent, item); } if (parentItem != null) { parentItem.fireItemChildRemoved(parent, item); } } private void fireItemPropertyRemoved(Object propertyItem) { for (IItemPropertiesChangedListener listener : propertiesChangedListeners) { listener.itemPropertyRemoved(propertyItem); } } private void fireItemPropertyChanged(Object propertyItem) { for (IItemPropertiesChangedListener listener : propertiesChangedListeners) { listener.itemPropertyChanged(propertyItem); } } /** * Fires an item appearance changed event using the UI-Thread. This helper * method can be used by non-UI-Threads to ensure that the listeners are * executing their code on the UI thread. */ private void fireItemAppearanceChangedOnUIThread() { Display display = Display.getDefault(); display.asyncExec(new Runnable() { @Override public void run() { fireItemAppearanceChanged(); } }); } private void initParamsMap() { paramsMap = null; Set<ConfigParameterDescription> params = modelWrapper.getExtensionConfigParams(); if (params == null) { // can not initialize the params map return; } paramsMap = new HashMap<String, ConfigParameterDescription>(); for (ConfigParameterDescription desc : params) { paramsMap.put(desc.getName(), desc); } if (client != null) { lastCacheClearTime = client.getLastClearTime(); } } /** * Creates a set of keys that are used within the model's config list. * * @return set of used keys within configuration */ private Set<String> createConfigParamKeysSet() { Set<String> keysSet = new HashSet<String>(); if (modelWrapper.getConfig() != null) { for (XMConfiguration conf : modelWrapper.getConfig()) { keysSet.add(conf.getKey()); } } return keysSet; } /** * Computes a map containing all non-mandatory configuration parameter * descriptions that are editable and have not been added yet. The * descriptions are accessible by their name as key. * * @return map containing all non-used and non-mandatory configuration * parameter descriptions that are editable */ private Map<String, ConfigParameterDescription> computeConfigurableExtensionConfigParams() { Map<String, ConfigParameterDescription> configurable = new HashMap<String, ConfigParameterDescription>(); Set<String> usedKeysSet = createConfigParamKeysSet(); Set<ConfigParameterDescription> extensionConfigParams = modelWrapper.getExtensionConfigParams(); if (extensionConfigParams == null) { return configurable; } for (ConfigParameterDescription desc : extensionConfigParams) { if (desc.isEditable() && !desc.isMandatory() && !usedKeysSet.contains(desc.getName())) { configurable.put(desc.getName(), desc); } } return configurable; } @Override public boolean canHandle(String commandId) { return handlerMediatorHelper.canHandle(commandId); } @Override public Object getHandler(String commandId) { return handlerMediatorHelper.getHandler(commandId); } @Override public void addHandler(String commandId, Object handler) { handlerMediatorHelper.addHandler(commandId, handler); } @Override public void removeHandler(String commandId) { handlerMediatorHelper.removeHandler(commandId); } }