package org.robotframework.ide.eclipse.main.plugin.tableeditor.source;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.rf.ide.core.project.ImportSearchPaths.PathsProvider;
import org.rf.ide.core.testdata.RobotParser;
import org.rf.ide.core.testdata.RobotParser.RobotParserConfig;
import org.rf.ide.core.testdata.model.RobotFile;
import org.rf.ide.core.testdata.model.RobotFileOutput;
import org.rf.ide.core.testdata.model.RobotProjectHolder;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFile;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
public class RobotDocument extends Document {
private static final int DELAY = 200;
private static final int LIMIT = 500;
private final AtomicBoolean hasNewestVersion = new AtomicBoolean(false);
private final Semaphore parsingSemaphore = new Semaphore(1, true);
private final Semaphore parsingFinishedSemaphore = new Semaphore(1, true);
private boolean reparseInSameThread = true;
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
private final Supplier<RobotSuiteFile> fileModelSupplier;
private RobotParser parser;
private File file;
private RobotFileOutput output;
private final List<IRobotDocumentParsingListener> parseListeners = new ArrayList<>();
private ScheduledFuture<?> scheduledOperation;
public RobotDocument(final Supplier<RobotSuiteFile> fileModelSupplier) {
this.fileModelSupplier = fileModelSupplier;
}
@VisibleForTesting
public RobotDocument(final RobotParser parser, final File file) {
this.fileModelSupplier = new Supplier<RobotSuiteFile>() {
@Override
public RobotSuiteFile get() {
return null;
}
};
this.parser = parser;
this.file = file;
}
public void addFirstDocumentListener(final IDocumentListener documentListener) {
Assert.isNotNull(documentListener);
final List<IDocumentListener> allListeners = new ArrayList<>();
allListeners.add(documentListener);
for (final Object listener : getDocumentListeners()) {
allListeners.add((IDocumentListener) listener);
removeDocumentListener((IDocumentListener) listener);
}
for (final IDocumentListener listener : allListeners) {
addDocumentListener(listener);
}
}
public void addParseListener(final IRobotDocumentParsingListener listener) {
parseListeners.add(listener);
}
public void removeParseListener(final IRobotDocumentParsingListener listener) {
parseListeners.remove(listener);
}
public boolean hasNewestModel() {
return hasNewestVersion.get();
}
@Override
protected void fireDocumentAboutToBeChanged(final DocumentEvent event) {
createParserIfNeeded();
reparseInSameThread = getNumberOfLines() < LIMIT;
if (!reparseInSameThread & hasNewestVersion.getAndSet(false)) {
try {
// it will be acquired only by the first event
parsingSemaphore.acquire();
} catch (final InterruptedException e) {
throw new IllegalStateException("Document reparsing interrupted!", e);
}
}
super.fireDocumentAboutToBeChanged(event);
}
private void createParserIfNeeded() {
if (parser == null) {
final RobotSuiteFile suiteFile = fileModelSupplier.get();
parser = createParser(suiteFile);
file = suiteFile.getFile() == null ? new File(suiteFile.getName())
: suiteFile.getFile().getLocation().toFile();
reparse();
}
}
@Override
protected void fireDocumentChanged(final DocumentEvent event) {
if (reparseInSameThread) {
// short documents can be reparsed in the same thread as this does not
// affect performance too much
reparse();
} else {
reparseInSeparateThread();
}
super.fireDocumentChanged(event);
}
private synchronized void reparse() {
output = parser.parseEditorContent(get(), file);
for (final IRobotDocumentParsingListener listener : parseListeners) {
listener.reparsingFinished(output);
}
hasNewestVersion.set(true);
}
private void reparseInSeparateThread() {
if (scheduledOperation != null) {
scheduledOperation.cancel(true);
}
final Runnable parsingRunnable = new Runnable() {
@Override
public void run() {
reparse();
parsingSemaphore.release();
}
};
scheduledOperation = executor.schedule(parsingRunnable, DELAY, TimeUnit.MILLISECONDS);
}
private Future<RobotFileOutput> getNewestOutput() {
return new Future<RobotFileOutput>() {
@Override
public boolean cancel(final boolean mayInterruptIfRunning) {
// not supported
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return hasNewestVersion.get();
}
@Override
public RobotFileOutput get() throws InterruptedException, ExecutionException {
// we don't want situation, when two threads acquire parsingSemaphore and parsing
// task cannot be performed
parsingFinishedSemaphore.acquire();
parsingSemaphore.acquire();
try {
return output;
} finally {
parsingSemaphore.release();
parsingFinishedSemaphore.release();
}
}
@Override
public RobotFileOutput get(final long timeout, final TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
throw new IllegalStateException("Operation not supported");
}
};
}
/**
* Gets newest parsed model. Waits for reparsing end if needed. IllegalStateException is thrown
* when waiting has been interrupted.
*
* @return
* @throws InterruptedException
*/
public RobotFile getNewestModel() throws InterruptedException {
final RobotFileOutput newestFileOutput = getNewestFileOutput();
return newestFileOutput == null ? new RobotFile(null) : newestFileOutput.getFileModel();
}
/**
* Gets newest parsed file output. Waits for reparsing end if needed. IllegalStateException is
* thrown when waiting has been interrupted.
*
* @return
* @throws InterruptedException
*/
public RobotFileOutput getNewestFileOutput() throws InterruptedException {
try {
return getNewestOutput().get();
} catch (final ExecutionException e) {
throw new IllegalStateException("Parsing the file coulnd't be finished", e.getCause());
}
}
private static RobotParser createParser(final RobotSuiteFile model) {
final RobotProjectHolder holder = isNonFileModel(model) ? new RobotProjectHolder()
: model.getProject().getRobotProjectHolder();
final PathsProvider pathsProvider = isNonFileModel(model) ? null : model.getProject().createPathsProvider();
return RobotParser.create(holder, RobotParserConfig.allImportsLazy(), pathsProvider);
}
private static boolean isNonFileModel(final RobotSuiteFile model) {
// e.g. history revision
return model.getFile() == null;
}
public static interface IRobotDocumentParsingListener {
void reparsingFinished(RobotFileOutput parsedOutput);
}
}