tinyos.dlrc.views.NodeContentProvider.java Source code

Java tutorial

Introduction

Here is the source code for tinyos.dlrc.views.NodeContentProvider.java

Source

/*
 * Dlrc 2, NesC development in Eclipse.
 * Copyright (C) 2009 DLRC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Web:  http://tos-ide.ethz.ch
 * Mail: tos-ide@tik.ee.ethz.ch
 */
package tinyos.dlrc.views;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Queue;
import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.progress.UIJob;

import tinyos.dlrc.Debug;
import tinyos.dlrc.TinyOSPlugin;
import tinyos.dlrc.editors.NesCIcons;
import tinyos.dlrc.ep.parser.ASTNodeFilterFactory;
import tinyos.dlrc.ep.parser.IASTModel;
import tinyos.dlrc.ep.parser.IASTModelAttribute;
import tinyos.dlrc.ep.parser.IASTModelElement;
import tinyos.dlrc.ep.parser.IASTModelNode;
import tinyos.dlrc.ep.parser.IASTModelNodeConnection;
import tinyos.dlrc.ep.parser.IASTModelNodeFilter;
import tinyos.dlrc.ep.parser.IASTModelPath;
import tinyos.dlrc.ep.parser.IDeclaration;
import tinyos.dlrc.ep.parser.IFileRegion;
import tinyos.dlrc.ep.parser.INesCParserFactory;
import tinyos.dlrc.ep.parser.Tag;
import tinyos.dlrc.ep.parser.TagSet;
import tinyos.dlrc.jobs.CancelingJob;
import tinyos.dlrc.jobs.FerryJob;
import tinyos.dlrc.jobs.ResolveConnectionJob;
import tinyos.dlrc.model.ProjectModel;

/**
 * A {@link ITreeContentProvider} that uses an {@link IASTModel} as base and
 * shows the contents of {@link IASTModelNode}s.<br>
 * Note: a {@link NodeContentProvider} should be used only on one
 * {@link TreeViewer}.
 * @author Benjamin Sigg
 */
public class NodeContentProvider implements ITreeContentProvider {
    public static final String PROPERTY_LABEL = "label";
    public static final String PROPERTY_ICON = "icon";
    public static final String PROPERTY_ORDER = "order";

    private IASTModelNodeFilter rootFilter;

    private Element[] roots;
    private INesCParserFactory factory;
    private TreeViewer viewer;

    /** whether to use the backup model only to resolve paths */
    private boolean backupForPathResolvingOnly = false;

    private boolean expandBaseTree = false;

    private SetContentJob setContent = new SetContentJob();

    //private RefreshJob refresh = null;

    private List<ModelInfo> models = new ArrayList<ModelInfo>();

    private Map<Integer, ProjectModel> backups = new HashMap<Integer, ProjectModel>();

    private boolean disposed = false;

    private AddJob addJob = new AddJob();

    /**
     * Creates a content provider that shows all nodes whose tagset is
     * a superset of <code>tags</code> as roots.
     * @param tags the subset of tags
     */
    public NodeContentProvider(TagSet tags) {
        this(ASTNodeFilterFactory.subset(tags));
    }

    /**
     * Creates a content that shows all nodes which pass <code>rootFilter</code>
     * as roots.
     * @param rootFilter the filter for roots
     */
    public NodeContentProvider(IASTModelNodeFilter rootFilter) {
        if (rootFilter == null)
            throw new IllegalArgumentException("rootFilter must not be null");
        this.rootFilter = rootFilter;
        this.factory = TinyOSPlugin.getDefault().getParserFactory();
    }

    /**
     * Gets the filter which is used to find the roots of the tree.
     * @return the roots
     */
    public IASTModelNodeFilter getRootFilter() {
        return rootFilter;
    }

    /**
     * Tells whether <code>element</code> is currently expanded in the view. This
     * method is intended to be called from a non-ui thread.
     * @param element the element to check
     * @return <code>true</code> if the element is expanded
     */
    public boolean isExpanded(final Element element) {
        try {
            if (viewer == null)
                return false;

            class Query implements Runnable {
                boolean answer;

                public void run() {
                    if (viewer != null && !viewer.getTree().isDisposed()) {
                        answer = viewer.getExpandedState(element);
                    }
                }
            }

            Query query = new Query();
            viewer.getTree().getDisplay().syncExec(query);
            return query.answer;
        } catch (SWTException ex) {
            return false;
        }
    }

    /**
     * Tells whether the base tree should automatically be expanded.
     * @return <code>true</code> if the tree should be expanded
     */
    public boolean isExpandBaseTree() {
        return expandBaseTree;
    }

    /**
     * Sets the resolved values of <code>element</code>. This method starts
     * its own thread to run in the ui-thread, <code>callback</code> will
     * be called once this method has completed its job.
     * @param element the element to update
     * @param resolved the new resolved state of the element
     * @param node the new node of the element, may be <code>null</code>
     * @param connection the new connection of the element, may be <code>null</code>
     * @param children the new children of the element
     * @param callback to be called once the element is updated, can be <code>null</code>
     */
    public void setContent(Element element, boolean resolved, IASTModelNode node,
            IASTModelNodeConnection connection, Element[] children, ResolveCallback callback) {
        setContent.dispatch(new Content(element, resolved, node, connection, children, callback));
    }

    private void updateViewer(Element parent, Element[] oldChildren, Element[] newChildren, boolean labelChanged,
            boolean iconChanged) {
        try {
            if (viewer == null || viewer.getControl().isDisposed())
                return;

            boolean childrenChanged = false;

            if (oldChildren != null && newChildren != null) {
                // find those children that have been removed
                List<Element> removed = new ArrayList<Element>();
                for (Element check : oldChildren) {
                    boolean found = false;
                    for (Element child : newChildren) {
                        if (child == check) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        removed.add(check);
                        check.setViewer(null);
                    }
                }

                if (removed.size() > 0) {
                    childrenChanged = true;
                    TreePath[] paths = new TreePath[removed.size()];
                    for (int i = 0; i < paths.length; i++) {
                        paths[i] = removed.get(i).getTreePath();
                    }

                    viewer.remove(paths);
                }

                // add new children at correct position
                List<Element> added = new ArrayList<Element>();
                for (int i = 0; i < newChildren.length; i++) {
                    Element check = newChildren[i];
                    boolean found = false;

                    for (Element child : oldChildren) {
                        if (child == check) {
                            found = true;
                            break;
                        }
                    }

                    if (!found) {
                        added.add(check);
                        check.setViewer(viewer);
                    }
                }

                if (added.size() > 0) {
                    childrenChanged = true;
                    if (parent == null) {
                        // viewer.add( new TreePath( new Object[]{} ), added.toArray() );

                        // TODO this call depends on some internal behavior of TreeViewer,
                        // "input" is not really the root.
                        viewer.add(viewer.getInput(), added.toArray());
                    } else {
                        viewer.add(parent.getTreePath(), added.toArray());
                    }
                }
            } else {
                if (oldChildren != null) {
                    childrenChanged = true;
                    TreePath[] paths = new TreePath[oldChildren.length];
                    for (int i = 0; i < paths.length; i++) {
                        paths[i] = oldChildren[i].getTreePath();
                    }

                    viewer.remove(paths);
                }

                if (newChildren != null) {
                    childrenChanged = true;
                    if (parent == null) {
                        viewer.add(new TreePath(new Object[] {}), newChildren);
                    } else {
                        viewer.add(parent.getTreePath(), newChildren);
                    }
                }
            }

            List<String> properties = new ArrayList<String>();
            if (iconChanged)
                properties.add(PROPERTY_ICON);

            if (labelChanged)
                properties.add(PROPERTY_LABEL);

            if (!childrenChanged)
                properties.add(PROPERTY_ORDER);

            String[] propertiesArray = properties.toArray(new String[properties.size()]);

            if (parent != null) {
                viewer.update(getPathElement(parent), propertiesArray);
            } else {
                viewer.update(viewer.getInput(), propertiesArray);
            }
        } catch (SWTException ex) {
            // Sometime something is disposed...
            Debug.error(ex);
        }
    }

    /**
     * Sets whether the base tree should be automatically expanded or not.
     * @param expand <code>true</code> if expansion should be done automatically
     */
    public void setExpandBaseTree(boolean expand) {
        expandBaseTree = expand;
    }

    /**
     * Sets a backup model that will be used to search nodes that can't
     * be found in the ordinary source of this provider.
     * @param model the index of the model for which the backup will be used
     * @param backup the backup, can be <code>null</code>
     */
    public void setBackup(int model, ProjectModel backup) {
        if (backup == null) {
            backups.remove(models);
        } else {
            backups.put(model, backup);
        }

        if (model < models.size()) {
            models.get(model).backup = backup;
        }
    }

    public ProjectModel getBackup(int model) {
        return backups.get(model);
    }

    public ProjectModel getBackup(IASTModel model) {
        List<ModelInfo> models = this.models;
        if (models == null)
            return null;
        for (ModelInfo info : models) {
            if (info.model == model) {
                return info.backup;
            }
        }
        return null;
    }

    public void setBackupForPathResolvingOnly(boolean backupForPathResolvingOnly) {
        this.backupForPathResolvingOnly = backupForPathResolvingOnly;
    }

    public boolean isBackupForPathResolvingOnly() {
        return backupForPathResolvingOnly;
    }

    public Object[] getChildren(Object parentElement) {
        return ((Element) parentElement).getChildren();
    }

    public Object getParent(Object element) {
        return ((Element) element).getParent();
    }

    public boolean hasChildren(Object element) {
        return ((Element) element).hasChildren();
    }

    public Object[] getElements(Object inputElement) {
        return getElements();
    }

    public Object[] getElements() {
        if (roots != null)
            return roots;

        List<Element> result = new ArrayList<Element>();
        for (ModelInfo info : models) {
            IASTModelNode[] nodes = info.model.getNodes(rootFilter);
            for (int i = 0, n = nodes.length; i < n; i++) {
                IASTModelNode node = nodes[i];
                result.add(new Element(null, node, viewer, info));
            }
        }

        roots = result.toArray(new Element[result.size()]);

        return roots;
    }

    public Element getElement(IASTModelElement element) {
        if (roots == null)
            return null;

        for (Element root : roots) {
            Element result = root.getElement(element);
            if (result != null)
                return result;
        }
        return null;
    }

    public void dispose() {
        disposed = true;
        roots = null;
        viewer = null;
        models.clear();
    }

    public boolean isDisposed() {
        return disposed;
    }

    /**
     * Can be called by clients to inform this {@link NodeContentProvider} that
     * <code>element</code> has been removed from the underlying {@link IASTModel}s.
     * This content provider will inform its {@link Viewer} about the removal
     * of the node.<br>
     * This method is intended to be called from the UI thread.
     * @param element the node that was removed
     */
    public void removed(IASTModelElement element) {
        Element node;

        while ((node = getElement(element)) != null) {
            Element parent = node.getParent();
            if (parent == null) {
                if (roots != null) {
                    for (int i = 0; i < roots.length; i++) {
                        if (roots[i] == node) {
                            TreePath path = node.getTreePath();
                            roots = remove(roots, i);
                            fireRemoved(viewer, path, node);
                        }
                    }
                }
            } else {
                parent.removeChild(node);
            }
        }
    }

    /**
     * Can be called by clients to inform this {@link NodeContentProvider} that
     * <code>element</code> has been added to the underlying {@link IASTModel}s.
     * This content provider will inform its {@link Viewer} about the
     * new element.<br>
     * This method is thread safe.
     * @param model the model where <code>element</code> was inserted
     * @param element the new element
     */
    public void addRoot(IASTModel model, IASTModelElement element) {
        addJob.dispatch(model, element);
    }

    /**
     * Informs the <code>viewer</code> that <code>element</code> has been
     * added, <code>element</code> is a root in this content provider.
     * @param viewer the viewer
     * @param element the new root
     */
    protected void addRoot(TreeViewer viewer, Element element) {
        Object[] path = getRootPath(element);
        Object self = getPathElement(element);

        if (path.length == 0) {
            viewer.add(viewer.getInput(), self);
        } else {
            viewer.add(new TreePath(path), self);
        }
    }

    /**
     * Called when a root element was removed.
     * @param viewer the viewer to inform
     * @param path to the removed element
     * @param removed the element that was removed
     */
    protected void fireRemoved(TreeViewer viewer, TreePath path, Element removed) {
        viewer.remove(path);
    }

    /**
     * Called if an element was removed from its parent.
     * @param viewer the viewer to inform
     * @param parent the parent element
     * @param path path to the removed element
     * @param removed the element that was removed
     */
    protected void fireRemoved(TreeViewer viewer, Element parent, TreePath path, Element removed) {
        viewer.remove(path);
    }

    protected Element[] remove(Element[] children, int index) {
        Element[] copy = new Element[children.length - 1];
        if (index > 0)
            System.arraycopy(children, 0, copy, 0, index);
        if (index + 1 < children.length)
            System.arraycopy(children, index + 1, copy, index, children.length - index - 1);
        return copy;
    }

    /**
     * Gets the path that leads to the parent of <code>root</code>.
     * @param root some root element
     * @return the path to the parent of <code>root</code>
     */
    protected Object[] getRootPath(Element root) {
        return new Object[] {};
    }

    /**
     * If the elements of this provider are wrapped into other elements, then
     * this method can be used by subclasses to convert elements to 
     * wrapper.
     * @param element some element
     * @return the wrapper or <code>element</code>
     */
    protected Object getPathElement(Element element) {
        return element;
    }

    protected Image image(ImageDescriptor descriptor, IASTModelAttribute[] attributes) {
        if (descriptor == null)
            return null;

        return NesCIcons.icons().get(descriptor, attributes);
    }

    public void update(Observable arg0, Object arg1) {
        viewer.getControl().getDisplay().asyncExec(new Runnable() {
            public void run() {
                roots = null;
                viewer.refresh();
            }
        });
    }

    public TreeViewer getViewer() {
        return viewer;
    }

    public void setViewer(TreeViewer viewer) {
        this.viewer = viewer;
    }

    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        this.viewer = (TreeViewer) viewer;

        if (newInput instanceof IASTModel) {
            setModel((IASTModel) newInput);
        } else if (newInput instanceof IASTModel[]) {
            setModels((IASTModel[]) newInput);
        } else {
            setModels(new IASTModel[] {});
        }
    }

    public void setModel(IASTModel model) {
        if (model == null)
            setModels(new IASTModel[] {});
        else
            setModels(new IASTModel[] { model });
    }

    public void setModels(IASTModel[] models) {
        List<ModelInfo> infos = new ArrayList<ModelInfo>();
        for (int i = 0; i < models.length; i++) {
            ModelInfo info;

            if (i < this.models.size()) {
                info = this.models.get(i);
                infos.add(info);
            } else {
                info = new ModelInfo();
                info.backup = backups.get(i);
                infos.add(info);
            }

            info.model = models[i];
        }

        if (this.models.size() > 0 && models.length > 0 && viewer != null) {
            this.models = infos;

            NodeTreeRefiller refiller = new NodeTreeRefiller(this, roots);
            refiller.refill(infos.toArray(new ModelInfo[infos.size()]));
        } else {
            // just replace
            roots = null;
            this.models = infos;
            if (viewer != null) {
                viewer.refresh();
                if (expandBaseTree) {
                    expandBaseTree();
                }
            }
        }
    }

    /**
     * Adds <code>model</code> to this provider, the viewer is informed
     * about the new roots.
     * @param model the new model
     * @param backup its backup, may be <code>null</code>
     */
    public void addModel(IASTModel model, ProjectModel backup) {
        ModelInfo info = new ModelInfo();
        info.model = model;
        info.backup = backup;

        models.add(info);

        for (IASTModelNode node : model.getNodes(rootFilter)) {
            addRoot(model, node);
        }
    }

    /**
     * Ensures that <code>model</code> is present. If <code>model</code>
     * is missing then it is added, but the viewer is not informed
     * about the new model.
     * @param model the model which must be present
     * @param backup its backup
     * @return <code>true</code> if the model was present
     */
    public boolean ensureModel(IASTModel model, ProjectModel backup) {
        for (ModelInfo info : models) {
            if (info.model == model) {
                return true;
            }
        }

        ModelInfo info = new ModelInfo();
        info.backup = backup;
        info.model = model;

        models.add(info);

        return false;
    }

    /**
     * Forces the view to an update using <code>roots</code> as new roots.
     * @param roots the new roots
     */
    public void setRoots(Element[] roots) {
        Set<Element> noExpansion = selectNoReexpansionElements();
        Element[] oldRoots = this.roots;
        this.roots = roots;

        if (viewer != null) {
            updateViewer(null, oldRoots, roots, false, false);
        }

        if (isExpandBaseTree() && viewer != null) {
            expandBaseTree(noExpansion);
        }
    }

    /**
     * Creates a selection of those elements that are currently in the tree, 
     * have not {@link Tag#NO_BASE_EXPANSION}, and are collapsed. This method
     * is intended to be called from the ui thread.
     */
    public Set<Element> selectNoReexpansionElements() {
        Set<Element> result = new HashSet<Element>();
        if (roots != null) {
            for (Element root : roots) {
                selectNoReexpansionElements(root, result);
            }
        }
        return result;
    }

    private void selectNoReexpansionElements(Element check, Set<Element> result) {
        if (viewer.getExpandedState(check)) {
            Element[] children = check.getBaseChildren();
            if (children != null) {
                for (Element child : children) {
                    selectNoReexpansionElements(child, result);
                }
            }
        } else {
            if (check.getBaseChildren() != null) {
                if (!check.getTags().contains(Tag.NO_BASE_EXPANSION)) {
                    result.add(check);
                }
            }
        }
    }

    /**
     * Expands the base tree: those connections which are not references.
     */
    public void expandBaseTree() {
        expandBaseTree(Collections.<Element>emptySet());
    }

    public void expandBaseTree(Set<Element> noExpansion) {
        if (viewer != null) {
            // make sure the elements are initialized
            getElements();

            if (roots != null) {
                for (Element root : roots) {
                    expandBaseTree(root, noExpansion);
                }
            }
        }
    }

    private void expandBaseTree(final Element root, final Set<Element> noExpansion) {
        Job job = new CancelingJob("Expand") {
            @Override
            public IStatus run(IProgressMonitor monitor) {
                monitor.beginTask("Expand", 1);

                expandBaseTreeNow(root, noExpansion);

                monitor.done();
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.DECORATE);
        job.setSystem(true);
        job.schedule();
    }

    private void expandBaseTreeNow(final Element root, final Set<Element> noExpansion) {
        root.resolve(new ResolveCallback() {
            public void finished(Element element) {
                if (!root.getTags().contains(Tag.NO_BASE_EXPANSION) && !noExpansion.contains(root)) {
                    if (viewer != null && viewer.getControl() != null) {
                        if (!viewer.getControl().isDisposed()) {
                            viewer.setExpandedState(root, true);

                            for (Element child : root.getBaseChildren()) {
                                expandBaseTree(child, noExpansion);
                            }
                        }
                    }
                }
            }
        });
    }

    /**
     * Searches for the node to which <code>connection</code> points.
     * @param element the element in which <code>connection</code> is declared
     * @param connection the connection to resolve
     * @param monitor to report progress
     * @return the node to which <code>connection</code> points or <code>null</code>
     */
    public IASTModelNode resolveNode(Element element, IASTModelNodeConnection connection,
            IProgressMonitor monitor) {
        IASTModelNode node = null;

        IASTModel model = element.getModel();
        ProjectModel backup = element.getBackup();

        if (model != null) {
            node = model.getNode(connection);
        }

        if (model != null && connection != null && node == null && backup != null
                && !isBackupForPathResolvingOnly()) {
            ResolveConnectionJob job = new ResolveConnectionJob(backup, connection);
            job.setPriority(Job.INTERACTIVE);
            backup.runJob(job, monitor);
            node = job.getContent();
        }

        return node;
    }

    /**
     * Information about the model from which an {@link Element} was
     * derived. 
     * @author Benjamin Sigg
     */
    public class ModelInfo {
        public IASTModel model;
        public ProjectModel backup;
    }

    /**
     * A node that wraps around an (imaginary) {@link IASTModelNode}.
     * @author Benjamin Sigg
     */
    public class Element implements Comparable<Element> {
        private TreeViewer viewer;

        private Element parent;

        private IASTModelNode node;
        private IASTModelNodeConnection connection;

        private boolean pathResolved = false;
        private IASTModelPath path;

        private boolean resolved = false;
        private Element[] children;

        private ModelInfo model;

        public Element(Element parent, IASTModelNodeConnection connection, TreeViewer viewer, ModelInfo model) {
            this.parent = parent;
            this.connection = connection;
            this.viewer = viewer;
            this.model = model;
        }

        public Element(Element parent, IASTModelNode node, TreeViewer viewer, ModelInfo model) {
            this.parent = parent;
            this.node = node;
            this.viewer = viewer;
            this.model = model;
        }

        public Element getElement(IASTModelElement element) {
            if (element instanceof IASTModelNode) {
                if (node != null) {
                    if (node.getPath().equals(((IASTModelNode) element).getPath()))
                        return this;
                }
            }
            if (element instanceof IASTModelNodeConnection) {
                if (connection != null) {
                    IASTModelNodeConnection connection2 = (IASTModelNodeConnection) element;
                    if (connection2.getIdentifier().equals(connection.getIdentifier())
                            && connection2.getPath().equals(connection.getPath())
                            && (connection2.getRegion() != null
                                    && connection2.getRegion().equals(connection.getRegion()))) {
                        return this;
                    }
                }
            }

            if (children == null)
                return null;

            for (Element child : children) {
                Element result = child.getElement(element);
                if (result != null)
                    return result;
            }
            return null;
        }

        public IASTModel getModel() {
            return model.model;
        }

        public ProjectModel getBackup() {
            return model.backup;
        }

        public ModelInfo getModelInfo() {
            return model;
        }

        public void setViewer(TreeViewer viewer) {
            this.viewer = viewer;
        }

        public void removeChild(Element child) {
            if (child.getParent() != this)
                throw new IllegalArgumentException("not a child of this node");

            if (children == null)
                return;

            int index = child.indexInParent();
            TreePath path = child.getTreePath();
            children = remove(children, index);

            if (viewer != null) {
                fireRemoved(viewer, this, path, child);
            }
        }

        public int indexInParent() {
            if (parent == null) {
                for (int i = 0; i < roots.length; i++) {
                    if (roots[i] == this)
                        return i;
                }
            } else {
                for (int i = 0; i < parent.children.length; i++) {
                    if (parent.children[i] == this) {
                        return i;
                    }
                }
            }

            return -1;
        }

        public int compareTo(Element o) {
            int myIndex = indexInParent();
            int otherIndex = o.indexInParent();

            if (myIndex < otherIndex)
                return -1;
            if (myIndex > otherIndex)
                return 1;
            return 0;
        }

        public Image getImage() {
            if (connection != null) {
                if (connection.getTags().contains(Tag.AST_CONNECTION_ICON_RESOLVE)) {
                    return image(factory.getImageFor(connection.getTags()), connection.getAttributes());
                } else {
                    resolve();
                    if (node != null)
                        return image(factory.getImageFor(node.getTags()), node.getAttributes());
                    else
                        return image(factory.getImageFor(connection.getTags()), connection.getAttributes());
                }
            }

            return image(factory.getImageFor(getTags()), getAttributes());
        }

        public String getIdentifier() {
            resolve();
            if (node != null)
                return node.getIdentifier();
            else
                return connection.getIdentifier();
        }

        public String getLabel() {
            return getLabel(true);
        }

        public String getLabel(boolean resolve) {
            if (connection != null) {
                if (connection.getTags().contains(Tag.AST_CONNECTION_LABEL_RESOLVE))
                    return connection.getLabel();
            }

            if (resolve) {
                resolve();
            }
            if (node != null)
                return node.getLabel();
            else
                return connection.getLabel();
        }

        public TagSet getTags() {
            if (connection != null)
                return connection.getTags();
            else
                return node.getTags();
        }

        public IASTModelAttribute[] getAttributes() {
            if (connection != null)
                return connection.getAttributes();
            else
                return node.getAttributes();
        }

        public int getDepth() {
            if (parent == null)
                return 0;
            return parent.getDepth() + 1;
        }

        public ElementPath getFullPath() {
            ElementPath path;
            if (parent == null) {
                path = new ElementPath();
                path.add(getPath(), 0);
            } else {
                path = parent.getFullPath();
                path.add(getPath(), parent.samePathIndex(this));
            }

            return path;
        }

        public TreePath getTreePath() {
            LinkedList<Object> list = new LinkedList<Object>();
            Element element = this;
            Element root = this;

            while (element != null) {
                list.addFirst(getPathElement(element));
                root = element;
                element = element.getParent();
            }

            Object[] rootPath = getRootPath(root);
            for (int i = rootPath.length - 1; i >= 0; i--) {
                list.addFirst(rootPath[i]);
            }

            return new TreePath(list.toArray());
        }

        private int samePathIndex(Element child) {
            IASTModelPath path = child.getPath();
            int count = 0;
            for (Element check : getChildren()) {
                if (check == child)
                    return count;

                IASTModelPath checkPath = check.getPath();
                if ((path == null && checkPath == null) || (path != null && path.equals(checkPath)))
                    count++;
            }

            return -1;
        }

        public IASTModelPath getPath() {
            if (pathResolved)
                return path;

            pathResolved = true;
            resolve();

            if (node != null) {
                path = node.getPath();
                return path;
            }

            ProjectModel backup = getBackup();

            if (backup != null) {
                IDeclaration declaration;
                if (backup.secureThread()) {
                    declaration = backup.getDeclaration(connection);
                } else {
                    FerryJob<IDeclaration> job = new FerryJob<IDeclaration>("Resolve connection") {
                        @Override
                        public IStatus run(IProgressMonitor monitor) {
                            monitor.beginTask("resolve connection", 1);
                            ProjectModel backup = getBackup();
                            content = backup.getDeclaration(connection);
                            monitor.done();
                            return Status.OK_STATUS;
                        }
                    };
                    job.setPriority(Job.INTERACTIVE);
                    job.setSystem(true);
                    backup.runJob(job, null);
                    declaration = job.getContent();
                }

                if (declaration != null) {
                    path = declaration.getPath();
                    return path;
                }
            }

            return null;
        }

        public IFileRegion getNodeRegion() {
            resolve();
            if (node != null) {
                return node.getRegion();
            }

            return null;
        }

        public IASTModelNode getNode() {
            return node;
        }

        public IFileRegion getRegion() {
            if (connection != null) {
                IFileRegion region = connection.getRegion();
                if (region != null)
                    return region;
            }

            return getNodeRegion();
        }

        public Element getParent() {
            return parent;
        }

        @Override
        public String toString() {
            return getLabel(false);
        }

        public void resolve() {
            resolve(null);
        }

        public synchronized void resolve(final ResolveCallback callback) {
            if (!resolved) {
                resolved = true;

                Job update = new UpdateElementJob(this, callback);
                update.schedule();
            } else if (callback != null) {
                Job job = new UIJob("Callback") {
                    @Override
                    public IStatus runInUIThread(IProgressMonitor monitor) {
                        monitor.beginTask("Call", IProgressMonitor.UNKNOWN);
                        callback.finished(Element.this);
                        monitor.done();
                        return Status.OK_STATUS;
                    }
                };
                job.setSystem(true);
                job.setPriority(Job.INTERACTIVE);
                job.schedule();
            }
        }

        public boolean hasChildren() {
            resolve();
            return children != null && children.length > 0;
        }

        public boolean isReference() {
            return connection != null && connection.isReference();
        }

        public Element[] getBaseChildren() {
            if (children == null)
                return new Element[] {};

            List<Element> list = new ArrayList<Element>();
            for (Element child : children) {
                if (!child.isReference()) {
                    list.add(child);
                }
            }
            return list.toArray(new Element[list.size()]);
        }

        public Element[] getChildren() {
            resolve();
            return children;
        }

        public IASTModelNode getUnresolvedNode() {
            return node;
        }

        public IASTModelNodeConnection getUnresolvedConnection() {
            return connection;
        }

        /**
         * Completely exchanges the content of this element, also notifies
         * the viewer that this element has changed.
         * @param resolved whether the information is resolved or not
         * @param node the new node
         * @param connection the new connection
         * @param children the new children
         */
        public void setContent(boolean resolved, IASTModelNode node, IASTModelNodeConnection connection,
                Element[] children) {
            Element[] oldChildren = this.children;

            String oldLabel = getLabel();
            Image oldIcon = getImage();

            this.resolved = resolved;
            this.node = node;
            this.connection = connection;
            this.children = children;

            String newLabel = getLabel();
            Image newIcon = getImage();

            if (viewer != null) {
                boolean labelChanged = oldLabel == null ? (newLabel != null) : (!oldLabel.equals(newLabel));
                boolean iconChanged = oldIcon != newIcon;

                updateViewer(this, oldChildren, children, labelChanged, iconChanged);
            }
        }
    }

    private static interface ResolveCallback {
        public void finished(Element element);
    }

    private class UpdateElementJob extends CancelingJob {
        private Element element;
        private ResolveCallback callback;

        public UpdateElementJob(Element element, ResolveCallback callback) {
            super("Resolve '" + element.getLabel(false) + "'");
            this.element = element;
            this.callback = callback;
            setPriority(Job.INTERACTIVE);
        }

        @Override
        public IStatus run(IProgressMonitor monitor) {
            monitor.beginTask("Update", 3);

            IASTModel model = element.getModel();

            if (model != null) {
                IASTModelNode node = element.getUnresolvedNode();
                if (node == null) {
                    node = resolveNode(element, element.getUnresolvedConnection(),
                            new SubProgressMonitor(monitor, 1));
                }

                monitor.worked(1);

                if (node != null) {
                    Element[] children = null;

                    IASTModelNodeConnection[] connections = node.getChildren();
                    if (connections != null) {
                        children = new Element[connections.length];

                        for (int i = 0, n = children.length; i < n; i++) {
                            children[i] = new Element(element, connections[i], viewer, element.getModelInfo());
                        }
                    }

                    setContent(element, true, node, element.getUnresolvedConnection(), children, callback);
                }
            }

            monitor.done();
            return Status.OK_STATUS;
        }
    }

    private class Content {
        private Element element;
        private IASTModelNode node;
        private IASTModelNodeConnection connection;
        private Element[] children;
        private ResolveCallback callback;
        private boolean resolved;

        public Content(Element element, boolean resolved, IASTModelNode node, IASTModelNodeConnection connection,
                Element[] children, ResolveCallback callback) {
            this.resolved = resolved;
            this.element = element;
            this.node = node;
            this.connection = connection;
            this.children = children;
            this.callback = callback;
        }

        public void transmit() {
            element.setContent(resolved, node, connection, children);
            // Debug.info( String.valueOf( node ) );
            if (callback != null)
                callback.finished(element);
        }
    }

    private class SetContentJob extends UIJob {
        private Queue<Content> contents = new LinkedList<Content>();
        private boolean running = false;

        public SetContentJob() {
            super("Update UI");
            setSystem(true);
            setPriority(Job.INTERACTIVE);
        }

        public void dispatch(Content content) {
            synchronized (contents) {
                contents.add(content);
                if (!running) {
                    schedule();
                }
            }
        }

        @Override
        public IStatus runInUIThread(IProgressMonitor monitor) {
            monitor.beginTask("Set Content", IProgressMonitor.UNKNOWN);

            while (true) {
                Content content;
                synchronized (contents) {
                    if (contents.isEmpty()) {
                        running = false;
                        break;
                    } else {
                        content = contents.remove();
                    }
                }

                content.transmit();
            }

            monitor.done();
            return Status.OK_STATUS;
        }
    };

    private class AddJob extends UIJob {
        private final Object lock = new Object();

        private List<IASTModel> models = new ArrayList<IASTModel>();
        private List<IASTModelElement> elements = new ArrayList<IASTModelElement>();

        public AddJob() {
            super("Add Node");
            setSystem(true);
            setPriority(Job.DECORATE);
        }

        public void dispatch(IASTModel model, IASTModelElement element) {
            synchronized (lock) {
                models.add(model);
                elements.add(element);
                this.schedule();
            }
        }

        @Override
        public IStatus runInUIThread(IProgressMonitor monitor) {
            IASTModel[] models;
            IASTModelElement[] elements;

            synchronized (lock) {
                models = this.models.toArray(new IASTModel[this.models.size()]);
                elements = this.elements.toArray(new IASTModelElement[this.elements.size()]);

                this.models.clear();
                this.elements.clear();
            }

            if (isDisposed()) {
                return Status.OK_STATUS;
            }

            monitor.beginTask("add node", models.length);

            if (roots == null) {
                monitor.done();
                return Status.OK_STATUS;
            }

            List<Element> newElements = new ArrayList<Element>(elements.length);

            for (int i = 0; i < models.length; i++) {
                IASTModel model = models[i];
                IASTModelElement element = elements[i];

                Element node = null;

                for (ModelInfo info : NodeContentProvider.this.models) {
                    if (info.model == model) {
                        if (element instanceof IASTModelNodeConnection)
                            node = new Element(null, (IASTModelNodeConnection) element, viewer, info);
                        else if (element instanceof IASTModelNode)
                            node = new Element(null, (IASTModelNode) element, viewer, info);

                        break;
                    }
                }

                if (node != null) {
                    newElements.add(node);
                }

                monitor.worked(1);
            }

            if (newElements.size() > 0) {
                Element[] newRoots = new Element[roots.length + newElements.size()];
                System.arraycopy(roots, 0, newRoots, 0, roots.length);

                for (int i = 0, n = newElements.size(); i < n; i++) {
                    newRoots[roots.length + i] = newElements.get(i);
                }

                roots = newRoots;
                for (Element newElement : newElements) {
                    addRoot(viewer, newElement);
                }
            }

            monitor.done();
            return Status.OK_STATUS;
        }
    }
}