package sk.stuba.fiit.perconik.activity.listeners.ui.text;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentMap;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import org.eclipse.core.filebuffers.IFileBuffer;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ui.IWorkbenchPart;
import sk.stuba.fiit.perconik.activity.listeners.ActivityListener;
import sk.stuba.fiit.perconik.core.annotations.Version;
import sk.stuba.fiit.perconik.core.listeners.DocumentListener;
import sk.stuba.fiit.perconik.core.listeners.FileBufferListener;
import sk.stuba.fiit.perconik.data.events.Event;
import sk.stuba.fiit.perconik.eclipse.jface.text.Documents;
import sk.stuba.fiit.perconik.eclipse.ui.Parts;
import sk.stuba.fiit.perconik.utilities.concurrent.TimeValue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static com.google.common.cache.CacheBuilder.newBuilder;
import static sk.stuba.fiit.perconik.activity.listeners.ui.text.TextDifferenceListener.Action.DIFFERENCE;
import static sk.stuba.fiit.perconik.data.content.StructuredContents.key;
import static sk.stuba.fiit.perconik.utilities.MoreStrings.toLowerCase;
import static sk.stuba.fiit.perconik.utilities.concurrent.TimeValue.of;
/**
* TODO
*
* @author Pavol Zbell
* @since 1.0
*/
@Version("0.1.0.alpha")
public final class TextDifferenceListener extends AbstractTextListener implements DocumentListener, FileBufferListener {
// TODO note that difference must be initiated by user after startup to be sent on shutdown
// TODO note that this listener does not handle text differences in consoles
static final TimeValue differenceEventPause = of(500, MILLISECONDS);
static final TimeValue differenceEventWindow = of(10, SECONDS);
static final int cacheConcurrencyLevel = 4;
static final int cacheInitialCapacity = 16;
static final long cacheMaximumSize = 128;
private final TextDocumentEvents events;
public TextDifferenceListener() {
this.events = new TextDocumentEvents(this);
}
enum Action implements ActivityListener.Action {
DIFFERENCE;
private final String name;
private final String path;
private Action() {
this.name = actionName("eclipse", "text", this);
this.path = actionPath(this.name);
}
public String getName() {
return this.name;
}
public String getPath() {
return this.path;
}
}
Event build(final long time, final Action action, final LinkedList<TextDocumentEvent> sequence, final IWorkbenchPart part, final String original, final String revision) {
Event data = this.build(time, action, part);
data.put(key("sequence", "first", "timestamp"), sequence.getFirst().time);
data.put(key("sequence", "last", "timestamp"), sequence.getLast().time);
data.put(key("sequence", "count"), sequence.size());
data.put(key("content", "original", "text"), original);
data.put(key("content", "revision", "text"), revision);
return data;
}
void process(final long time, final Action action, final LinkedList<TextDocumentEvent> sequence, final String original, final String revision) {
IDocument document = sequence.getLast().document;
IWorkbenchPart part = Parts.forDocument(document);
this.send(action.getPath(), this.intern(this.build(time, action, sequence, part, original, revision)));
}
static final class TextDocumentEvents extends ContinuousEvent<TextDifferenceListener, TextDocumentEvent> {
final Cache<IDocument, String> cache;
TextDocumentEvents(final TextDifferenceListener listener) {
super(listener, "text-difference", differenceEventPause, differenceEventWindow);
CacheBuilder<Object, Object> builder = newBuilder();
builder.concurrencyLevel(cacheConcurrencyLevel);
builder.initialCapacity(cacheInitialCapacity);
builder.maximumSize(cacheMaximumSize);
builder.ticker(this.listener.getTimeContext().elapsedTimeTicker());
builder.weakKeys();
final Log log = this.log;
builder.removalListener(new RemovalListener<IDocument, String>() {
@SuppressWarnings({"synthetic-access", "unqualified-field-access"})
public void onRemoval(final RemovalNotification<IDocument, String> notification) {
if (log.isEnabled()) {
IDocument document = notification.getKey();
RemovalCause cause = notification.getCause();
if (cause != RemovalCause.EXPLICIT && cause != RemovalCause.REPLACED) {
log.print("%s: document %x removed (%s) from cache", identifier, document.hashCode(), toLowerCase(cause));
}
}
}
});
this.cache = builder.build();
}
boolean update(final IDocument document, final String text, final boolean force) {
ConcurrentMap<IDocument, String> map = this.cache.asMap();
String previous;
boolean result;
if (force) {
previous = map.put(document, text);
result = !text.equals(previous);
} else {
previous = map.putIfAbsent(document, text);
result = previous == null;
}
if (result) {
if (this.log.isEnabled()) {
String operation = force && previous != null ? "updated" : "put";
String forced = force ? " (forced)" : "";
this.log.print("%s: document %x of %d characters %s in cache%s", this.identifier, document.hashCode(), text.length(), operation, forced);
}
}
return result;
}
@Override
protected boolean accept(final LinkedList<TextDocumentEvent> sequence, final TextDocumentEvent event) {
if (sequence.isEmpty()) {
IDocument document = event.document;
String text = document.get();
this.update(document, text, false);
}
return true;
}
@Override
protected boolean continuous(final LinkedList<TextDocumentEvent> sequence, final TextDocumentEvent event) {
return sequence.getLast().isContinuousWith(event);
}
@Override
protected void process(final LinkedList<TextDocumentEvent> sequence) {
assert sequence.getFirst().document.equals(sequence.getLast().document);
TextDocumentEvent last = sequence.getLast();
IDocument document = last.document;
String original = this.cache.getIfPresent(document);
String revision = document.get();
if (original == null) {
if (this.log.isEnabled()) {
this.log.print("%s: text for document %x not cached, nothing to process", this.identifier, document.hashCode());
}
this.update(document, revision, true);
return;
}
if (original.equals(revision)) {
if (this.log.isEnabled()) {
this.log.print("%s: document %x texts equal, nothing to process", this.identifier, document.hashCode());
}
return;
}
if (this.log.isEnabled()) {
this.log.print("%s: document %x texts not equal -> difference", this.identifier, document.hashCode());
}
this.update(document, revision, true);
this.listener.handleDocumentEvents(sequence, original, revision);
}
}
void handleDocumentEvents(final LinkedList<TextDocumentEvent> sequence, final String original, final String revision) {
this.execute(new Runnable() {
public void run() {
TextDocumentEvent event = sequence.getLast();
process(event.time, DIFFERENCE, sequence, original, revision);
}
});
}
public void documentAboutToBeChanged(final DocumentEvent event) {
IDocument document = event.getDocument();
this.events.update(document, document.get(), false);
}
public void documentChanged(final DocumentEvent event) {
final long time = this.currentTime();
this.events.push(new TextDocumentEvent(time, event));
}
public void bufferCreated(final IFileBuffer buffer) {
// ignore
}
public void bufferDisposed(final IFileBuffer buffer) {
// ignore
}
public void bufferContentAboutToBeReplaced(final IFileBuffer buffer) {
// ignore
}
public void bufferContentReplaced(final IFileBuffer buffer) {
// ignore
}
public void stateChanging(final IFileBuffer buffer) {
// ignore
}
public void stateChangeFailed(final IFileBuffer buffer) {
// ignore
}
public void stateValidationChanged(final IFileBuffer buffer, final boolean stateValidated) {
// ignore
}
public void dirtyStateChanged(final IFileBuffer buffer, final boolean dirty) {
final long time = this.currentTime();
if (dirty) {
return;
}
IDocument document = Documents.fromFileBuffer(buffer);
if (document == null) {
if (this.log.isEnabled()) {
this.log.print("%s: document not found for %s -> ignore", "text-difference", buffer);
}
return;
}
// ensures that difference events are forced to process on document save,
// as a side effect this also partially handles correct shutdown behavior
this.events.push(new TextDocumentEvent(time, document, true));
}
public void underlyingFileMoved(final IFileBuffer buffer, final IPath path) {
// ignore
}
public void underlyingFileDeleted(final IFileBuffer buffer) {
// ignore
}
}