/**
* Copyright 2014 Microsoft Open Technologies Inc.
*
* 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 com.microsoftopentechnologies.intellij.serviceexplorer;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.microsoftopentechnologies.intellij.helpers.UIHelper;
import com.microsoftopentechnologies.intellij.helpers.azure.AzureCmdException;
import com.microsoftopentechnologies.intellij.helpers.collections.ObservableList;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Node {
private static final String CLICK_ACTION = "click";
protected String id;
protected String name;
protected Node parent;
protected ObservableList<Node> childNodes = new ObservableList<Node>();
protected String iconPath;
protected Object viewData;
protected NodeAction clickAction = new NodeAction(this, CLICK_ACTION);
protected List<NodeAction> nodeActions = new ArrayList<NodeAction>();
// marks this node as being in a "loading" state; when this field is true
// the following consequences apply:
// [1] all actions associated with this node get disabled
// [2] click action gets disabled automatically
protected boolean loading = false;
protected PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
private boolean hasRefreshAction;
public Node(String id, String name) {
this(id, name, null, null, false, false);
}
public Node(String id, String name, Node parent, String iconPath, boolean hasRefreshAction) {
this(id, name, parent, iconPath, hasRefreshAction, false);
}
public Node(String id, String name, Node parent, String iconPath, boolean hasRefreshAction, boolean delayActionLoading) {
this.id = id;
this.name = name;
this.parent = parent;
this.iconPath = iconPath;
this.hasRefreshAction = hasRefreshAction;
if (!delayActionLoading) {
loadActions();
}
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
String oldValue = this.name;
this.name = name;
propertyChangeSupport.firePropertyChange("name", oldValue, name);
}
public Node getParent() {
return parent;
}
public ObservableList<Node> getChildNodes() {
return childNodes;
}
public boolean isDirectChild(Node node) {
return childNodes.contains(node);
}
public boolean isDescendant(Node node) {
if (isDirectChild(node))
return true;
for (Node child : childNodes) {
if (child.isDescendant(node))
return true;
}
return false;
}
// Walk up the tree till we find a parent node who's type
// is equal to "clazz".
public Node findParentByType(Class clazz) {
if (parent == null)
return null;
if (parent.getClass().equals(clazz))
return parent;
return parent.findParentByType(clazz);
}
public boolean hasChildNodes() {
return !childNodes.isEmpty();
}
public void removeDirectChildNode(Node childNode) {
if (isDirectChild(childNode)) {
// remove this node's child nodes (so they get an
// opportunity to clean up after them)
childNode.removeAllChildNodes();
// this remove call should cause the NodeListChangeListener object
// registered on it's child nodes to fire
childNodes.remove(childNode);
}
}
public void removeAllChildNodes() {
while (!childNodes.isEmpty()) {
Node node = childNodes.get(0);
// remove this node's child nodes (so they get an
// opportunity to clean up after them)
node.removeAllChildNodes();
// this remove call should cause the NodeListChangeListener object
// registered on it's child nodes to fire
childNodes.remove(0);
}
}
public String getIconPath() {
return iconPath;
}
public void setIconPath(String iconPath) {
String oldValue = this.iconPath;
this.iconPath = iconPath;
propertyChangeSupport.firePropertyChange("iconPath", oldValue, iconPath);
}
public void addChildNode(Node child) {
childNodes.add(child);
}
public void addAction(NodeAction action) {
nodeActions.add(action);
}
// Convenience method to add a new action with a pre-configured listener. If
// an action with the same name already exists then the listener is added
// to that action.
public NodeAction addAction(String name, NodeActionListener actionListener) {
NodeAction nodeAction = getNodeActionByName(name);
if (nodeAction == null) {
addAction(nodeAction = new NodeAction(this, name));
}
nodeAction.addListener(actionListener);
return nodeAction;
}
protected void loadActions() {
// add the click action handler
addClickActionListener(new NodeActionListener() {
@Override
public void actionPerformed(NodeActionEvent e) {
onNodeClick(e);
}
});
// add the refresh node action
if (hasRefreshAction) {
addAction("Refresh", new NodeActionListener() {
@Override
public void actionPerformed(NodeActionEvent e) {
Futures.addCallback(load(), new FutureCallback<List<Node>>() {
@Override
public void onSuccess(List<Node> nodes) {
}
@Override
public void onFailure(Throwable throwable) {
UIHelper.showException("An error occurred while refreshing the service.", throwable);
}
});
}
});
}
// add the other actions
Map<String, Class<? extends NodeActionListener>> actions = initActions();
if (actions != null) {
for (Map.Entry<String, Class<? extends NodeActionListener>> entry : actions.entrySet()) {
try {
// get default constructor
Class<? extends NodeActionListener> listenerClass = entry.getValue();
Constructor constructor = listenerClass.getDeclaredConstructor(getClass());
// create an instance passing this object as a constructor argument
// since we assume that this is an inner class
NodeActionListener actionListener = (NodeActionListener) constructor.newInstance(this);
addAction(entry.getKey(), actionListener);
} catch (InstantiationException e) {
UIHelper.showException(e.getMessage(), e);
} catch (IllegalAccessException e) {
UIHelper.showException(e.getMessage(), e);
} catch (NoSuchMethodException e) {
UIHelper.showException(e.getMessage(), e);
} catch (InvocationTargetException e) {
UIHelper.showException(e.getMessage(), e);
}
}
}
}
// sub-classes are expected to override this method and
// add code for initializing node-specific actions; this
// method is called when the node is being constructed and
// is guaranteed to be called only once per node
// NOTE: The Class<?> objects returned by this method MUST be
// public inner classes of the sub-class. We assume that they are.
protected Map<String, Class<? extends NodeActionListener>> initActions() {
return null;
}
// sub-classes are expected to override this method and
// add a handler for the case when something needs to be
// done when the user left-clicks this node in the tree view
protected void onNodeClick(NodeActionEvent e) {
}
public List<NodeAction> getNodeActions() {
return nodeActions;
}
public NodeAction getNodeActionByName(final String name) {
return Iterators.tryFind(nodeActions.iterator(), new Predicate<NodeAction>() {
@Override
public boolean apply(NodeAction nodeAction) {
return name.compareTo(nodeAction.getName()) == 0;
}
}).orNull();
}
public boolean hasNodeActions() {
return !nodeActions.isEmpty();
}
public void addActions(Iterable<NodeAction> actions) {
for (NodeAction action : actions) {
addAction(action);
}
}
public NodeAction getClickAction() {
return clickAction;
}
public void addClickActionListener(NodeActionListener actionListener) {
clickAction.addListener(actionListener);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.removePropertyChangeListener(listener);
}
public Object getViewData() {
return viewData;
}
public void setViewData(Object viewData) {
this.viewData = viewData;
}
public Project getProject() {
// delegate to parent node if there's one else return null
if (parent != null) {
return parent.getProject();
}
return null;
}
@Override
public String toString() {
return getName();
}
// Sub-classes are expected to override this method if they wish to
// refresh items synchronously. The default implementation does nothing.
protected void refreshItems() throws AzureCmdException {
}
// Sub-classes are expected to override this method if they wish
// to refresh items asynchronously. The default implementation simply
// delegates to "refreshItems" *synchronously* and completes the Future
// with the result of calling getChildNodes.
protected void refreshItems(SettableFuture<List<Node>> future) throws AzureCmdException {
setLoading(true);
try {
refreshItems();
future.set(getChildNodes());
} finally {
setLoading(false);
}
}
protected void runAsBackground(Runnable runnable) {
runAsBackground("", runnable);
}
protected void runAsBackground(final String status, final Runnable runnable) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
ProgressManager.getInstance().run(
new Task.Backgroundable(getProject(), status, false) {
@Override
public void run(ProgressIndicator progressIndicator) {
runnable.run();
}
});
}
});
}
public ListenableFuture<List<Node>> load() {
final SettableFuture<List<Node>> future = SettableFuture.create();
// background tasks via ProgressManager can be scheduled only on the
// dispatch thread
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
ProgressManager.getInstance().run(new BackgroundLoader(
future, getProject(), "Loading " + getName() + "...", false));
}
});
return future;
}
public boolean isLoading() {
return loading;
}
public void setLoading(boolean loading) {
this.loading = loading;
}
class BackgroundLoader extends Task.Backgroundable {
private SettableFuture<List<Node>> future;
public BackgroundLoader(SettableFuture<List<Node>> future, Project project, String title, boolean canBeCancelled) {
super(project, title, canBeCancelled);
this.future = future;
}
@Override
public void run(ProgressIndicator progressIndicator) {
progressIndicator.setIndeterminate(true);
final String nodeName = getName();
setName(nodeName + " (Refreshing...)");
Futures.addCallback(future, new FutureCallback<List<Node>>() {
@Override
public void onSuccess(List<Node> nodes) {
updateName(null);
}
@Override
public void onFailure(Throwable throwable) {
updateName(throwable);
}
private void updateName(final Throwable throwable) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
setName(nodeName);
if (throwable != null) {
UIHelper.showException("An error occurred while loading " + getName() + ".", throwable);
}
}
});
}
});
try {
refreshItems(future);
} catch (AzureCmdException e) {
future.setException(e);
}
}
}
}