com.cisco.yangide.core.indexing.DeltaProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.cisco.yangide.core.indexing.DeltaProcessor.java

Source

/*******************************************************************************
 * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others.  All rights reserved.
 *  
 *  This program and the accompanying materials are made available under the
 *  terms of the Eclipse Public License v1.0 which accompanies this distribution,
 *  and is available at http://www.eclipse.org/legal/epl-v10.html
 *  
 *******************************************************************************/
package com.cisco.yangide.core.indexing;

import java.util.ArrayList;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.JavaProject;

import com.cisco.yangide.core.CoreUtil;
import com.cisco.yangide.core.ElementChangedEvent;
import com.cisco.yangide.core.IYangElementChangedListener;
import com.cisco.yangide.core.IYangElementDelta;
import com.cisco.yangide.core.OpenableElementInfo;
import com.cisco.yangide.core.YangCorePlugin;
import com.cisco.yangide.core.YangModelException;
import com.cisco.yangide.core.model.YangElement;
import com.cisco.yangide.core.model.YangElementType;
import com.cisco.yangide.core.model.YangModel;
import com.cisco.yangide.core.model.YangModelManager;
import com.cisco.yangide.core.model.YangProject;

/**
 * @author Konstantin Zaitsev
 * @date Jun 25, 2014
 */
@SuppressWarnings("restriction")
public class DeltaProcessor implements IResourceChangeListener, IElementChangedListener {
    static class OutputsInfo {
        int outputCount;
        IPath[] paths;
        int[] traverseModes;

        OutputsInfo(IPath[] paths, int[] traverseModes, int outputCount) {
            this.paths = paths;
            this.traverseModes = traverseModes;
            this.outputCount = outputCount;
        }
    }

    /*
     * Collection of listeners for Java element deltas
     */
    public IYangElementChangedListener[] elementChangedListeners = new IYangElementChangedListener[5];
    public int[] elementChangedListenerMasks = new int[5];
    public int elementChangedListenerCount = 0;

    public int overridenEventType = -1;
    private YangModelManager manager;
    private boolean isFiring;
    public ArrayList<IYangElementDelta> yangModelDeltas = new ArrayList<IYangElementDelta>();
    private YangElementDelta currentDelta;

    private final static int IGNORE = 0;
    private final static int SOURCE = 1;
    private final static int BINARY = 2;

    /**
     * @param yangModelManager
     */
    public DeltaProcessor(YangModelManager manager) {
        this.manager = manager;
    }

    /*
     * Need to clone defensively the listener information, in case some listener is reacting to some
     * notification iteration by adding/changing/removing any of the other (for example, if it
     * deregisters itself).
     */
    public synchronized void addElementChangedListener(IYangElementChangedListener listener, int eventMask) {
        for (int i = 0; i < this.elementChangedListenerCount; i++) {
            if (this.elementChangedListeners[i] == listener) {

                // only clone the masks, since we could be in the middle of notifications and one
                // listener decide to change
                // any event mask of another listeners (yet not notified).
                int cloneLength = this.elementChangedListenerMasks.length;
                System.arraycopy(this.elementChangedListenerMasks, 0,
                        this.elementChangedListenerMasks = new int[cloneLength], 0, cloneLength);
                this.elementChangedListenerMasks[i] |= eventMask; // could be different
                return;
            }
        }
        // may need to grow, no need to clone, since iterators will have cached original arrays and
        // max boundary and we only add to the end.
        int length;
        if ((length = this.elementChangedListeners.length) == this.elementChangedListenerCount) {
            System.arraycopy(this.elementChangedListeners, 0,
                    this.elementChangedListeners = new IYangElementChangedListener[length * 2], 0, length);
            System.arraycopy(this.elementChangedListenerMasks, 0,
                    this.elementChangedListenerMasks = new int[length * 2], 0, length);
        }
        this.elementChangedListeners[this.elementChangedListenerCount] = listener;
        this.elementChangedListenerMasks[this.elementChangedListenerCount] = eventMask;
        this.elementChangedListenerCount++;
    }

    public synchronized void removeElementChangedListener(IYangElementChangedListener listener) {

        for (int i = 0; i < this.elementChangedListenerCount; i++) {

            if (this.elementChangedListeners[i] == listener) {

                // need to clone defensively since we might be in the middle of listener
                // notifications (#fire)
                int length = this.elementChangedListeners.length;
                IYangElementChangedListener[] newListeners = new IYangElementChangedListener[length];
                System.arraycopy(this.elementChangedListeners, 0, newListeners, 0, i);
                int[] newMasks = new int[length];
                System.arraycopy(this.elementChangedListenerMasks, 0, newMasks, 0, i);

                // copy trailing listeners
                int trailingLength = this.elementChangedListenerCount - i - 1;
                if (trailingLength > 0) {
                    System.arraycopy(this.elementChangedListeners, i + 1, newListeners, i, trailingLength);
                    System.arraycopy(this.elementChangedListenerMasks, i + 1, newMasks, i, trailingLength);
                }

                // update manager listener state (#fire need to iterate over original listeners
                // through a local variable to hold onto
                // the original ones)
                this.elementChangedListeners = newListeners;
                this.elementChangedListenerMasks = newMasks;
                this.elementChangedListenerCount--;
                return;
            }
        }
    }

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        try {
            int eventType = this.overridenEventType == -1 ? event.getType() : this.overridenEventType;
            IResource resource = event.getResource();
            IResourceDelta delta = event.getDelta();

            switch (eventType) {
            case IResourceChangeEvent.PRE_DELETE:
                if (resource.getType() == IResource.PROJECT && YangCorePlugin.isYangProject((IProject) resource)) {
                    deleting((IProject) resource);
                }
                return;

            case IResourceChangeEvent.PRE_REFRESH:
                // nothing to do on refresh
                return;

            case IResourceChangeEvent.POST_CHANGE:
                if (isAffectedBy(delta)) {
                    try {
                        stopDeltas();

                        // generate Yang deltas from resource changes
                        IYangElementDelta translatedDelta = processResourceDelta(delta);
                        if (translatedDelta != null) {
                            this.yangModelDeltas.add(translatedDelta);
                        }
                    } finally {
                        // necessary
                        startDeltas();
                    }
                    // TODO KOS: need implement notification for type hierarchy
                    fire(null, ElementChangedEvent.POST_CHANGE);
                }
                return;

            case IResourceChangeEvent.PRE_BUILD:
                // nothing to do on pre-build
                return;

            case IResourceChangeEvent.POST_BUILD:
                // nothing to do on post build
                return;
            }
        } finally {
            overridenEventType = -1;
        }
    }

    private void deleting(IProject project) {

        try {
            // discard indexing jobs that belong to this project so that the project can be
            // deleted without interferences from the index manager
            this.manager.indexManager.discardJobs(project.getName());

            YangProject yangProject = (YangProject) YangCorePlugin.create(project);
            yangProject.close();

            removeFromParentInfo(yangProject);
        } catch (YangModelException e) {
            // yang project doesn't exist: ignore
        }
    }

    /*
     * Turns the firing mode to on. That is, deltas that are/have been registered will be fired.
     */
    private void startDeltas() {
        this.isFiring = true;
    }

    /*
     * Turns the firing mode to off. That is, deltas that are/have been registered will not be fired
     * until deltas are started again.
     */
    private void stopDeltas() {
        this.isFiring = false;
    }

    /*
     * Converts a <code>IResourceDelta</code> rooted in a <code>Workspace</code> into the
     * corresponding set of <code>IJavaElementDelta</code>, rooted in the relevant
     * <code>JavaModel</code>s.
     */
    private IYangElementDelta processResourceDelta(IResourceDelta changes) {

        try {
            YangModel model = this.manager.getYangModel();
            if (!model.isOpen()) {
                // force opening of yang model so that java element delta are reported
                try {
                    model.open(null);
                } catch (YangModelException e) {
                    return null;
                }
            }
            // this.currentElement = null;

            // get the workspace delta, and start processing there.
            IResourceDelta[] deltas = changes.getAffectedChildren(
                    IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED,
                    IContainer.INCLUDE_HIDDEN);
            for (int i = 0; i < deltas.length; i++) {
                traverseDelta(deltas[i], null);
            }
            return this.currentDelta;
        } finally {
            this.currentDelta = null;
        }
    }

    private void removeFromParentInfo(YangElement child) {

        YangElement parent = (YangElement) child.getParent();
        if (parent != null && parent.isOpen()) {
            try {
                OpenableElementInfo info = (OpenableElementInfo) parent.getElementInfo(null);
                info.removeChild(child);
            } catch (YangModelException e) {
                // do nothing - we already checked if open
            }
        }
    }

    /*
     * Fire Java Model delta, flushing them after the fact after post_change notification. If the
     * firing mode has been turned off, this has no effect.
     */
    public void fire(IYangElementDelta customDelta, int eventType) {
        if (!this.isFiring) {
            return;
        }

        IYangElementDelta deltaToNotify;
        if (customDelta == null) {
            deltaToNotify = null; // TODO KOS: need merge delta for notify
            // mergeDeltas(this.yangModelDeltas);
        } else {
            deltaToNotify = customDelta;
        }

        // Notification
        // Important: if any listener reacts to notification by updating the listeners list or mask,
        // these lists will
        // be duplicated, so it is necessary to remember original lists in a variable (since field
        // values may change under us)
        IYangElementChangedListener[] listeners;
        int[] listenerMask;
        int listenerCount;
        synchronized (this) {
            listeners = this.elementChangedListeners;
            listenerMask = this.elementChangedListenerMasks;
            listenerCount = this.elementChangedListenerCount;
        }

        switch (eventType) {
        case ElementChangedEvent.POST_CHANGE:
            firePostChangeDelta(deltaToNotify, listeners, listenerMask, listenerCount);
            // fireReconcileDelta(listeners, listenerMask, listenerCount);
            break;
        }
    }

    private void firePostChangeDelta(IYangElementDelta deltaToNotify, IYangElementChangedListener[] listeners,
            int[] listenerMask, int listenerCount) {

        // post change deltas
        if (deltaToNotify != null) {
            // flush now so as to keep listener reactions to post their own deltas for subsequent
            // iteration
            this.yangModelDeltas = new ArrayList<IYangElementDelta>();

            notifyListeners(deltaToNotify, ElementChangedEvent.POST_CHANGE, listeners, listenerMask, listenerCount);
        }
    }

    private void notifyListeners(IYangElementDelta deltaToNotify, int eventType,
            IYangElementChangedListener[] listeners, int[] listenerMask, int listenerCount) {
        final ElementChangedEvent extraEvent = new ElementChangedEvent(deltaToNotify, eventType);
        for (int i = 0; i < listenerCount; i++) {
            if ((listenerMask[i] & eventType) != 0) {
                final IYangElementChangedListener listener = listeners[i];
                // wrap callbacks with Safe runnable for subsequent listeners to be called when some
                // are causing grief
                SafeRunner.run(new ISafeRunnable() {
                    @Override
                    public void handleException(Throwable exception) {
                        YangCorePlugin.log(exception,
                                "Exception occurred in listener of Java element change notification");
                    }

                    @Override
                    public void run() throws Exception {
                        listener.elementChanged(extraEvent);
                    }
                });
            }
        }
    }

    /*
     * Converts an <code>IResourceDelta</code> and its children into the corresponding
     * <code>IYangElementDelta</code>s.
     */
    private void traverseDelta(IResourceDelta delta, OutputsInfo outputsInfo) {
        // process current delta
        boolean processChildren = true;
        if (delta.getResource().getType() == IResource.PROJECT) {
            processChildren = updateCurrentDeltaAndIndex(delta);
        } else if (delta.getResource().getType() == IResource.FILE) {
            // skip non YANG files
            if (!CoreUtil.isYangLikeFileName(delta.getResource().getFullPath().toString())) {
                return;
            }
            processChildren = updateCurrentDeltaAndIndex(delta);
        }

        // process children if needed
        if (processChildren) {

            // get the project's output locations and traverse mode
            if (outputsInfo == null) {
                outputsInfo = outputsInfo(delta.getResource());
            }

            IResourceDelta[] children = delta.getAffectedChildren();
            int length = children.length;
            for (int i = 0; i < length; i++) {
                IResourceDelta child = children[i];
                IResource childRes = child.getResource();

                // is childRes in the output folder and is it filtered out ?
                boolean isResFilteredFromOutput = isResFilteredFromOutput(outputsInfo, childRes);

                if (!isResFilteredFromOutput) {
                    traverseDelta(child, outputsInfo);
                }
            }
        } // else resource delta will be added by parent

    }

    /*
     * Update the current delta (i.e. add/remove/change the given element) and update the
     * correponding index. Returns whether the children of the given delta must be processed.
     */
    public boolean updateCurrentDeltaAndIndex(IResourceDelta delta) {
        YangElement element;
        switch (delta.getKind()) {
        case IResourceDelta.ADDED:
            element = YangCorePlugin.create(delta.getResource());
            updateIndex(element, delta);
            elementAdded(element, delta);
            return false;
        case IResourceDelta.REMOVED:
            element = YangCorePlugin.create(delta.getResource());
            updateIndex(element, delta);
            elementRemoved(element, delta);
            return false;
        case IResourceDelta.CHANGED:
            int flags = delta.getFlags();
            if ((flags & IResourceDelta.CONTENT) != 0 || (flags & IResourceDelta.ENCODING) != 0) {
                // content or encoding has changed
                element = YangCorePlugin.create(delta.getResource());
                if (element == null) {
                    return false;
                }
                updateIndex(element, delta);
                contentChanged(element);
            } else if (delta.getResource().getType() == IResource.PROJECT) {
                if ((flags & IResourceDelta.OPEN) != 0) {
                    // project has been opened or closed
                    IProject res = (IProject) delta.getResource();
                    element = YangCorePlugin.create(res);

                    if (res.isOpen()) {
                        addToParentInfo(element);
                        this.manager.indexManager.indexAll(res);
                    } else {
                        close(element);
                        removeFromParentInfo(element);
                        this.manager.indexManager.discardJobs(element.getName());
                        this.manager.indexManager.removeIndexFamily(res);

                    }
                    return false; // when a project is open/closed don't process children
                }
            }
            return true;
        }
        return true;
    }

    @SuppressWarnings("incomplete-switch")
    private void updateIndex(YangElement element, IResourceDelta delta) {

        IndexManager indexManager = this.manager.indexManager;
        if (indexManager == null) {
            return;
        }

        switch (element.getElementType()) {
        case YANG_PROJECT:
            switch (delta.getKind()) {
            case IResourceDelta.ADDED:
                indexManager.indexAll(element.getResource().getProject());
                break;
            case IResourceDelta.REMOVED:
                indexManager.removeIndexFamily(element.getResource().getProject());
                break;
            }
            break;
        case YANG_FILE:
            IFile file = (IFile) delta.getResource();
            switch (delta.getKind()) {
            case IResourceDelta.CHANGED:
                // no need to index if the content has not changed
                int flags = delta.getFlags();
                if ((flags & IResourceDelta.CONTENT) == 0 && (flags & IResourceDelta.ENCODING) == 0) {
                    break;
                }
            case IResourceDelta.ADDED:
                indexManager.addSource(file);
                break;
            case IResourceDelta.REMOVED:
                indexManager.remove(file);
                break;
            }
        }
    }

    /*
     * Processing for an element that has been added:<ul> <li>If the element is a project, do
     * nothing, and do not process children, as when a project is created it does not yet have any
     * natures - specifically a java nature. <li>If the elemet is not a project, process it as added
     * (see <code>basicElementAdded</code>. </ul> Delta argument could be null if processing an
     * external JAR change
     */
    private void elementAdded(YangElement element, IResourceDelta delta) {
        YangElementType elementType = element.getElementType();

        if (elementType == YangElementType.YANG_PROJECT) {
            // project add is handled by JavaProject.configure() because
            // when a project is created, it does not yet have a java nature
            if (delta != null) {
                IProject project = (IProject) delta.getResource();
                if (YangCorePlugin.isYangProject(project)) {
                    addToParentInfo(element);
                    close(element);
                    currentDelta().added(element);
                }
            }
        } else {
            if (delta == null || (delta.getFlags() & IResourceDelta.MOVED_FROM) == 0) {
                // regular element addition
                addToParentInfo(element);
                close(element);
                currentDelta().added(element);
            } else {
                // element is moved
                addToParentInfo(element);
                close(element);
                currentDelta().added(element);
            }
        }
    }

    /*
     * Generic processing for a removed element:<ul> <li>Close the element, removing its structure
     * from the cache <li>Remove the element from its parent's cache of children <li>Add a REMOVED
     * entry in the delta </ul> Delta argument could be null if processing an external JAR change
     */
    private void elementRemoved(YangElement element, IResourceDelta delta) {

        if (delta == null || (delta.getFlags() & IResourceDelta.MOVED_TO) == 0) {
            close(element);
            removeFromParentInfo(element);
            currentDelta().removed(element);
        } else {
            // element is moved
            close(element);
            removeFromParentInfo(element);
            currentDelta().removed(element);
        }
        if (element.getElementType() == YangElementType.YANG_MODEL) {
            this.manager.indexManager.reset();
        }
    }

    private void contentChanged(YangElement element) {
        close(element);
        currentDelta().changed(element, IYangElementDelta.F_CONTENT);
    }

    private void addToParentInfo(YangElement child) {
        YangElement parent = (YangElement) child.getParent();
        if (parent != null && parent.isOpen()) {
            try {
                OpenableElementInfo info = (OpenableElementInfo) parent.getElementInfo(null);
                info.addChild(child);
            } catch (YangModelException e) {
                // do nothing - we already checked if open
            }
        }
    }

    /*
     * Closes the given element, which removes it from the cache of open elements.
     */
    private void close(YangElement element) {
        try {
            element.close();
        } catch (YangModelException e) {
            // do nothing
        }
    }

    private YangElementDelta currentDelta() {
        if (this.currentDelta == null) {
            this.currentDelta = new YangElementDelta(this.manager.getYangModel());
        }
        return this.currentDelta;
    }

    /*
     * Returns whether a given delta contains some information relevant to the JavaModel, in
     * particular it will not consider SYNC or MARKER only deltas.
     */
    private boolean isAffectedBy(IResourceDelta rootDelta) {
        if (rootDelta != null) {
            // use local exception to quickly escape from delta traversal
            class FoundRelevantDeltaException extends RuntimeException {
                private static final long serialVersionUID = 7137113252936111023L;
            }
            try {
                rootDelta.accept(new IResourceDeltaVisitor() {
                    @Override
                    public boolean visit(IResourceDelta delta) /* throws CoreException */ {
                        switch (delta.getKind()) {
                        case IResourceDelta.ADDED:
                        case IResourceDelta.REMOVED:
                            throw new FoundRelevantDeltaException();
                        case IResourceDelta.CHANGED:
                            // if any flag is set but SYNC or MARKER, this delta should be
                            // considered
                            if (delta.getAffectedChildren().length == 0 // only check leaf delta
                                    // nodes
                                    && (delta.getFlags() & ~(IResourceDelta.SYNC | IResourceDelta.MARKERS)) != 0) {
                                throw new FoundRelevantDeltaException();
                            }
                        }
                        return true;
                    }
                }, IContainer.INCLUDE_HIDDEN);
            } catch (FoundRelevantDeltaException e) {
                return true;
            } catch (CoreException e) { // ignore delta if not able to traverse
            }
        }
        return false;
    }

    private OutputsInfo outputsInfo(IResource res) {
        try {
            JavaProject proj = (JavaProject) JavaCore.create(res.getProject());
            if (proj != null) {
                IPath projectOutput = proj.getOutputLocation();
                int traverseMode = IGNORE;
                if (proj.getProject().getFullPath().equals(projectOutput)) { // case of
                    // proj==bin==src
                    return new OutputsInfo(new IPath[] { projectOutput }, new int[] { SOURCE }, 1);
                }
                IClasspathEntry[] classpath = proj.getResolvedClasspath();
                IPath[] outputs = new IPath[classpath.length + 1];
                int[] traverseModes = new int[classpath.length + 1];
                int outputCount = 1;
                outputs[0] = projectOutput;
                traverseModes[0] = traverseMode;
                for (int i = 0, length = classpath.length; i < length; i++) {
                    IClasspathEntry entry = classpath[i];
                    IPath entryPath = entry.getPath();
                    IPath output = entry.getOutputLocation();
                    if (output != null) {
                        outputs[outputCount] = output;
                        // check case of src==bin
                        if (entryPath.equals(output)) {
                            traverseModes[outputCount++] = (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE)
                                    ? SOURCE
                                    : BINARY;
                        } else {
                            traverseModes[outputCount++] = IGNORE;
                        }
                    }

                    // check case of src==bin
                    if (entryPath.equals(projectOutput)) {
                        traverseModes[0] = (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) ? SOURCE : BINARY;
                    }
                }
                return new OutputsInfo(outputs, traverseModes, outputCount);
            }
        } catch (JavaModelException e) {
            // java project doesn't exist: ignore
        }
        return null;
    }

    /*
     * Returns whether the given resource is in one of the given output folders and if it is
     * filtered out from this output folder.
     */
    private boolean isResFilteredFromOutput(OutputsInfo info, IResource res) {
        if (info != null) {
            IPath resPath = res.getFullPath();
            for (int i = 0; i < info.outputCount; i++) {
                if (info.paths[i].isPrefixOf(resPath)
                        && (info.traverseModes[i] == IGNORE || info.traverseModes[i] == BINARY)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void elementChanged(org.eclipse.jdt.core.ElementChangedEvent event) {
        IJavaElementDelta delta = event.getDelta();
        processJavaDeltas(delta.getAffectedChildren());
    }

    private boolean processJavaDeltas(IJavaElementDelta[] affectedChildren) {

        for (IJavaElementDelta d : affectedChildren) {
            IJavaElement element = d.getElement();
            if (element instanceof IPackageFragmentRoot) {
                IPath path = ((IPackageFragmentRoot) element).getPath();

                if (path != null && path.toFile().exists() && path.lastSegment().toLowerCase().endsWith(".jar")) {

                    switch (d.getKind()) {
                    case IJavaElementDelta.ADDED:
                    case IJavaElementDelta.CHANGED:
                        this.manager.indexManager.addJarFile(element.getJavaProject().getProject(), path);
                        break;
                    case IJavaElementDelta.REMOVED:
                        this.manager.indexManager.indexAll(element.getJavaProject().getProject());
                        return false;
                    }
                }
            }
            if (!processJavaDeltas(d.getAffectedChildren())) {
                return false;
            }
        }
        return true;
    }
}