package com.gratex.perconik.activity.ide.listeners;
import java.io.IOException;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.IFileBuffer;
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.IResourceDelta;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbenchPart;
import com.gratex.perconik.activity.ide.IdeGitProjects;
import com.gratex.perconik.activity.uaca.IdeUacaProxy;
import com.gratex.perconik.services.uaca.ide.IdeDocumentEventRequest;
import com.gratex.perconik.services.uaca.ide.IdeDocumentEventType;
import sk.stuba.fiit.perconik.core.java.JavaElements;
import sk.stuba.fiit.perconik.core.java.JavaProjects;
import sk.stuba.fiit.perconik.core.listeners.EditorListener;
import sk.stuba.fiit.perconik.core.listeners.FileBufferListener;
import sk.stuba.fiit.perconik.core.listeners.ResourceListener;
import sk.stuba.fiit.perconik.core.listeners.SelectionListener;
import sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaFlag;
import sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaKind;
import sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaResolver;
import sk.stuba.fiit.perconik.eclipse.core.resources.ResourceEventType;
import sk.stuba.fiit.perconik.eclipse.core.resources.ResourceType;
import sk.stuba.fiit.perconik.eclipse.core.runtime.RuntimeCoreException;
import sk.stuba.fiit.perconik.eclipse.swt.widgets.DisplayTask;
import sk.stuba.fiit.perconik.eclipse.ui.Editors;
import static java.util.Arrays.asList;
import static com.google.common.base.Objects.equal;
import static com.gratex.perconik.activity.ide.IdeData.setApplicationData;
import static com.gratex.perconik.activity.ide.IdeData.setEventData;
import static com.gratex.perconik.activity.ide.listeners.Utilities.currentTime;
import static com.gratex.perconik.activity.ide.listeners.Utilities.dereferenceEditor;
import static com.gratex.perconik.activity.ide.listeners.Utilities.isNull;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaFlag.MOVED_TO;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaFlag.OPEN;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaKind.ADDED;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceDeltaKind.REMOVED;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceEventType.POST_CHANGE;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceType.FILE;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceType.PROJECT;
import static sk.stuba.fiit.perconik.eclipse.core.resources.ResourceType.ROOT;
/**
* A listener of IDE document events. This listener handles desired
* events and eventually builds corresponding data transfer objects
* of type {@link IdeDocumentEventRequest} and passes them to the
* {@link IdeUacaProxy} to be transferred into the <i>User Activity Central
* Application</i> for further processing.
*
* <p>Document operation types that this listener is interested in are
* determined by the {@link IdeDocumentEventType} enumeration:
*
* <ul>
* <li>Add - a document is added into the project.
* <li>Close - an opened document is closed.
* <li>Open - a closed document is opened.
* <li>Remove - a document is removed from the project.
* <li>Rename - currently not supported.
* <li>Save - a document is saved.
* <li>Switch to - focus is changed from one document to another
* and editor selections (tabs and text). Note that structured
* selections in package explorer are supported.
* </ul>
*
* <p>Data available in an {@code IdeDocumentEventRequest}:
*
* <ul>
* <li>{@code document} - see {@code IdeDocumentDto} below.
* <li>See {@link IdeListener} for documentation of inherited data.
* </ul>
*
* <p>Data available in an {@code IdeDocumentDto}:
*
* <ul>
* <li>{@code branch} - current Git branch name for the document.
* <li>{@code changesetIdInRcs} - most recent Git commit
* identifier for the document (40 hexadecimal characters),
* for example {@code "984dd5f359532d7d806a92b47ef5bfc39d772d64"}.
* <li>{@code localPath} - path to the document relative to the workspace root,
* for example {@code "com.gratex.perconik.activity/src/com/gratex/perconik/activity/ide/listeners/IdeCommitListener.java"}.
* <li>{@code rcsServer} - see documentation of {@code RcsServerDto}
* in {@link IdeCommitListener} for more details.
* <li>{@code serverPath} - always the same as {@code localPath}.
* </ul>
*
* <p>Note that in case of not editable source code, such as classes from JRE
* system library, fields {@code branchName}, {@code changesetIdInRcs},
* and {@code rcsServer} are unused and set to {@code null}.
*
* @author Pavol Zbell
* @since 1.0
*/
public final class IdeDocumentListener extends IdeListener implements EditorListener, FileBufferListener, ResourceListener, SelectionListener {
// TODO note that switch_to is sometimes generated before open/close
// TODO open is also generated on initial switch to previously opened tab directly after eclipse launch
static final boolean processStructuredSelections = false;
static final Predicate<IResource> resourceDeltaFilter = Predicates.or(asList(OutputLocationFilter.INSTANCE, GitInternalFilter.INSTANCE, GitIgnoreFilter.INSTANCE));
static final Set<ResourceEventType> resourceEventTypes = ImmutableSet.of(POST_CHANGE);
private final Object lock = new Object();
@GuardedBy("lock")
private UnderlyingResource<?> resource;
public IdeDocumentListener() {}
private boolean updateResource(final UnderlyingResource<?> resource) {
if (resource != null) {
synchronized (this.lock) {
if (!resource.equals(this.resource)) {
this.resource = resource;
return true;
}
}
}
return false;
}
static IdeDocumentEventRequest build(final long time, final IFile file) {
return build(time, UnderlyingResource.of(file));
}
static IdeDocumentEventRequest build(final long time, final UnderlyingResource<?> resource) {
final IdeDocumentEventRequest data = new IdeDocumentEventRequest();
resource.setDocumentData(data);
resource.setProjectData(data);
setApplicationData(data);
setEventData(data, time);
return data;
}
private final class ResourceDeltaVisitor extends ResourceDeltaResolver {
private final long time;
private final ResourceEventType type;
private final Predicate<IResource> filter;
private final SetMultimap<IdeDocumentEventType, IFile> operations;
ResourceDeltaVisitor(final long time, final ResourceEventType type) {
assert time >= 0 && type != null;
this.time = time;
this.type = type;
this.filter = resourceDeltaFilter;
this.operations = LinkedHashMultimap.create(3, 2);
}
@Override
protected boolean resolveDelta(final IResourceDelta delta, final IResource resource) {
assert delta != null && resource != null;
if (this.type != POST_CHANGE || this.filter.apply(resource)) {
return false;
}
ResourceDeltaKind kind = ResourceDeltaKind.valueOf(delta.getKind());
ResourceType type = ResourceType.valueOf(resource.getType());
Set<ResourceDeltaFlag> flags = ResourceDeltaFlag.setOf(delta.getFlags());
if (type == PROJECT && (kind == ADDED || kind == REMOVED || flags.contains(OPEN))) {
return false;
}
if (type != FILE) {
return true;
}
if (flags.contains(MOVED_TO)) {
IPath path = delta.getMovedToPath();
IPath other = resource.getFullPath();
if (path != null && other != null && !equal(path.lastSegment(), other.lastSegment())) {
this.operations.put(IdeDocumentEventType.RENAME, (IFile) resource);
return false;
}
}
switch (kind) {
case ADDED:
this.operations.put(IdeDocumentEventType.ADD, (IFile) resource);
break;
case REMOVED:
this.operations.put(IdeDocumentEventType.REMOVE, (IFile) resource);
break;
default:
break;
}
return false;
}
@Override
protected boolean resolveResource(final IResource resource) {
return false;
}
@Override
protected void postVisitOrProbe() {
if (this.operations.containsKey(IdeDocumentEventType.RENAME)) {
this.operations.removeAll(IdeDocumentEventType.ADD);
}
IdeUacaProxy proxy = IdeDocumentListener.this.proxy;
for (Entry<IdeDocumentEventType, IFile> entry: this.operations.entries()) {
proxy.sendDocumentEvent(build(this.time, entry.getValue()), entry.getKey());
}
}
}
private enum OutputLocationFilter implements Predicate<IResource> {
INSTANCE;
public boolean apply(@Nonnull final IResource resource) {
IProject project = resource.getProject();
if (project == null) {
return false;
}
try {
// TODO on POST_CHANGE when project gets deleted it has no more Java nature -> needs PRE_DELETE hook?
if (JavaProjects.inOutputLocation(project, resource)) {
return true;
}
} catch (RuntimeCoreException e) {}
return false;
}
}
private enum GitInternalFilter implements Predicate<IResource> {
INSTANCE;
public boolean apply(@Nonnull final IResource resource) {
IPath path = resource.getLocation();
if (path == null) {
path = resource.getFullPath();
}
for (String segment: path.segments()) {
if (segment.equals(Constants.DOT_GIT)) {
return true;
}
}
return false;
}
}
private enum GitIgnoreFilter implements Predicate<IResource> {
INSTANCE;
public boolean apply(@Nonnull final IResource resource) {
ResourceType type = ResourceType.valueOf(resource.getType());
if (type == ROOT || type == PROJECT) {
return false;
}
IPath path = resource.getLocation();
if (path == null) {
// TODO location is null on project delete and egit repository is null too
// try to resolve this in (project) PRE_DELETE events -> but that code probably
// can not run in parallel (i.e. all pre* code, only post code can....)
path = resource.getFullPath();
}
if (!IdeGitProjects.isMapped(path)) {
return false;
}
try {
return IdeGitProjects.isIgnored(path);
} catch (IOException e) {
return false;
}
}
}
void processResource(final long time, final IResourceChangeEvent event) {
ResourceEventType type = ResourceEventType.valueOf(event.getType());
IResourceDelta delta = event.getDelta();
new ResourceDeltaVisitor(time, type).visitOrProbe(delta, event);
}
void processResource(final long time, final IEditorReference reference, final IdeDocumentEventType type) {
UnderlyingResource<?> resource = UnderlyingResource.from(dereferenceEditor(reference));
if (resource != null) {
this.proxy.sendDocumentEvent(build(time, resource), type);
}
}
void processSelection(final long time, final IWorkbenchPart part, final ISelection selection) {
UnderlyingResource<?> resource = null;
if (processStructuredSelections) {
if (selection instanceof StructuredSelection) {
Object element = ((StructuredSelection) selection).getFirstElement();
resource = UnderlyingResource.resolve(element);
if (resource == null && element instanceof IJavaElement) {
IResource other = JavaElements.resource((IJavaElement) element);
if (other instanceof IFile) {
resource = UnderlyingResource.of((IFile) other);
}
}
}
}
if (isNull(resource) && part instanceof IEditorPart) {
resource = UnderlyingResource.from((IEditorPart) part);
}
if (this.updateResource(resource)) {
this.proxy.sendDocumentEvent(build(time, resource), IdeDocumentEventType.SWITCH_TO);
}
}
@Override
public void postRegister() {
execute(new Runnable() {
@Override
public void run() {
IEditorPart editor = execute(DisplayTask.of(Editors.activeEditorSupplier()));
UnderlyingResource<?> resource = UnderlyingResource.from(editor);
if (resource == null) {
return;
}
IdeDocumentListener.this.proxy.sendDocumentEvent(build(currentTime(), resource), IdeDocumentEventType.OPEN);
}
});
}
public void resourceChanged(final IResourceChangeEvent event) {
final long time = currentTime();
execute(new Runnable() {
public void run() {
processResource(time, event);
}
});
}
public void selectionChanged(final IWorkbenchPart part, final ISelection selection) {
final long time = currentTime();
execute(new Runnable() {
public void run() {
IdeDocumentListener.this.processSelection(time, part, selection);
}
});
}
public void editorOpened(final IEditorReference reference) {
final long time = currentTime();
execute(new Runnable() {
public void run() {
processResource(time, reference, IdeDocumentEventType.OPEN);
}
});
}
// TODO close not working for locally build class files
public void editorClosed(final IEditorReference reference) {
final long time = currentTime();
execute(new Runnable() {
public void run() {
processResource(time, reference, IdeDocumentEventType.CLOSE);
}
});
}
public void editorActivated(final IEditorReference reference) {}
public void editorDeactivated(final IEditorReference reference) {}
public void editorVisible(final IEditorReference reference) {}
public void editorHidden(final IEditorReference reference) {}
public void editorBroughtToTop(final IEditorReference reference) {}
public void editorInputChanged(final IEditorReference reference) {}
public void bufferCreated(final IFileBuffer buffer) {}
public void bufferDisposed(final IFileBuffer buffer) {}
public void bufferContentAboutToBeReplaced(final IFileBuffer buffer) {}
public void bufferContentReplaced(final IFileBuffer buffer) {}
public void stateChanging(final IFileBuffer buffer) {}
public void stateChangeFailed(final IFileBuffer buffer) {}
public void stateValidationChanged(final IFileBuffer buffer, final boolean stateValidated) {}
public void dirtyStateChanged(final IFileBuffer buffer, final boolean dirty) {
final long time = currentTime();
execute(new Runnable() {
public void run() {
if (!dirty) {
IFile file = FileBuffers.getWorkspaceFileAtLocation(buffer.getLocation());
IdeDocumentListener.this.proxy.sendDocumentEvent(build(time, file), IdeDocumentEventType.SAVE);
}
}
});
}
public void underlyingFileMoved(final IFileBuffer buffer, final IPath path) {}
public void underlyingFileDeleted(final IFileBuffer buffer) {}
public Set<ResourceEventType> getEventTypes() {
return resourceEventTypes;
}
}