/*******************************************************************************
* 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;
}
}