/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.ui.internal.text.editor;
import com.google.common.collect.MapMaker;
import com.google.dart.engine.utilities.source.SourceRange;
import com.google.dart.tools.core.utilities.general.SourceRangeFactory;
import com.google.dart.tools.core.utilities.io.FileUtilities;
import com.google.dart.tools.internal.corext.refactoring.util.ReflectionUtils;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.internal.util.PartListenerAdapter;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.ITextEditor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Helper for auto-saving {@link ITextEditor}s.
*/
public class AutoSaveHelper {
private static class CloseTask extends Task {
private final IFile file;
public CloseTask(IFile file) {
this.file = file;
}
@Override
void execute() throws Exception {
instance.pathMapCloseFile(file);
}
}
private static class EditorInfo implements Serializable {
private final long timeStamp;
private final String content;
private final int selectionStart;
private final int selectionLength;
public EditorInfo(String content, int selectionStart, int selectionLength) {
this.timeStamp = System.currentTimeMillis();
this.content = content;
this.selectionStart = selectionStart;
this.selectionLength = selectionLength;
}
}
private static class OpenTask extends Task {
private final IFile file;
public OpenTask(IFile file) {
this.file = file;
}
@Override
void execute() throws Exception {
instance.pathMapOpenFile(file);
}
}
private static class SaveTask extends Task {
private final IFile file;
private final EditorInfo info;
public SaveTask(IFile file, EditorInfo info) {
this.file = file;
this.info = info;
}
@Override
void execute() throws Exception {
String path = getStringPath(file);
Integer id = instance.pathMap.get(path);
if (id != null) {
editorInfoWrite(id, info);
}
}
}
private abstract static class Task {
abstract void execute() throws Exception;
}
private static final long VERSION = 0xDEAD0001;
private static final AutoSaveHelper instance = new AutoSaveHelper();
public static void reconciled(IEditorInput input, ISourceViewer viewer, SourceRange selection) {
IFile file = getInputFile(input);
if (file != null && viewer != null) {
IDocument document = viewer.getDocument();
if (document != null) {
String content = document.get();
if (selection == null) {
selection = SourceRangeFactory.forStartLength(0, 0);
}
EditorInfo info = new EditorInfo(content, selection.getOffset(), selection.getLength());
instance.taskQueue.add(new SaveTask(file, info));
}
}
}
/**
* Starts Thread to run main loop. Should be called only one time.
*/
public static void start() {
final IWorkbench workbench = PlatformUI.getWorkbench();
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
// if workbench is still starting, reschedule this Runnable
if (workbench.isStarting()) {
Display.getDefault().asyncExec(this);
return;
}
// OK, workbench started, editors opened.
// Now we can restore content of editors.
instance.start(workbench);
}
});
}
/**
* Deletes {@link EditorInfo} into file with name "id".
*/
private static void editorInfoDelete(Integer id) {
File saveFolder = getSaveFolder();
File saveFile = new File(saveFolder, id.toString());
if (saveFile.exists()) {
saveFile.delete();
}
}
/**
* @return the {@link EditorInfo} from file with given "id", may be <code>null</code> if file does
* not exist.
*/
private static EditorInfo editorInfoRead(Integer id) throws Exception {
File saveFolder = getSaveFolder();
File saveFile = new File(saveFolder, id.toString());
if (saveFile.exists()) {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(saveFile));
try {
return (EditorInfo) ois.readObject();
} finally {
ois.close();
}
}
return null;
}
/**
* Writes given {@link EditorInfo} into file with name "id".
*/
private static void editorInfoWrite(Integer id, EditorInfo info) throws Exception {
File saveFolder = getSaveFolder();
File saveFile = new File(saveFolder, id.toString());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(saveFile));
try {
oos.writeObject(info);
} finally {
oos.close();
}
}
/**
* @return the input {@link IFile}, may be <code>null</code>.
*/
private static IFile getInputFile(IEditorInput input) {
if (input instanceof IFileEditorInput) {
return ((IFileEditorInput) input).getFile();
}
return null;
}
/**
* @return the input {@link IFile}, may be <code>null</code>.
*/
private static IFile getInputFile(IWorkbenchPart part) {
if (part instanceof ITextEditor) {
ITextEditor editor = (ITextEditor) part;
IEditorInput input = editor.getEditorInput();
return getInputFile(input);
}
return null;
}
/**
* @return the "auto save" folder in plugin state location.
*/
private static File getSaveFolder() {
IPath stateLocation = DartToolsPlugin.getDefault().getStateLocation();
return stateLocation.append("autoSave").toFile();
}
/**
* @return the {@link String} presentation of {@link IFile} path, good to use as argument for
* {@link IWorkspaceRoot#getFile(IPath)}.
*/
private static String getStringPath(IFile file) {
return file.getFullPath().toPortableString();
}
private final BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<Task>();
private final AtomicInteger lastId = new AtomicInteger();
private final Map<String, Integer> pathMap = new MapMaker().makeMap();
/**
* Removes given {@link IFile} from the list of opened files.
*/
private void pathMapCloseFile(IFile file) throws Exception {
String path = getStringPath(file);
Integer id = pathMap.remove(path);
if (id != null) {
pathMapWrite();
editorInfoDelete(id);
}
}
/**
* We remove "autoSave" folder on normal shutdown, we don't need to restore anything.
*/
private void pathMapDelete() {
try {
File saveFolder = getSaveFolder();
FileUtilities.deleteContents(saveFolder);
} catch (Throwable e) {
}
}
private File pathMapGetFile() {
File saveFolder = getSaveFolder();
return new File(saveFolder, "map");
}
/**
* Remembers in {@link #pathMap} that given {@link IFile} was opened.
*/
private void pathMapOpenFile(IFile file) throws Exception {
String path = getStringPath(file);
if (!pathMap.containsKey(path)) {
int id = lastId.getAndIncrement();
pathMap.put(path, id);
pathMapWrite();
}
}
/**
* Writes {@link #pathMap}.
*/
private void pathMapWrite() throws Exception {
File saveFile = pathMapGetFile();
saveFile.getParentFile().mkdirs();
saveFile.delete();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(saveFile));
try {
oos.writeLong(VERSION);
oos.writeObject(pathMap);
} finally {
oos.close();
}
}
private void restoreEditors(IWorkbenchPage activePage) {
try {
File mapFile = pathMapGetFile();
if (mapFile.exists()) {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(mapFile));
try {
// check version
if (ois.readLong() != VERSION) {
return;
}
// load map
@SuppressWarnings("unchecked")
Map<String, Integer> map = (Map<String, Integer>) ois.readObject();
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
for (Entry<String, Integer> entry : map.entrySet()) {
String pathStr = entry.getKey();
Integer id = entry.getValue();
try {
EditorInfo info = editorInfoRead(id);
if (info != null) {
IFile file = workspaceRoot.getFile(new Path(pathStr));
if (file.exists()) {
IEditorPart newEditor = IDE.openEditor(activePage, file);
if (newEditor instanceof AbstractTextEditor
&& file.getLocalTimeStamp() < info.timeStamp) {
ISourceViewer viewer = ReflectionUtils.invokeMethod(
newEditor,
"getSourceViewer()");
IDocument document = viewer.getDocument();
if (!document.get().equals(info.content)) {
document.set(info.content);
}
viewer.setSelectedRange(info.selectionStart, info.selectionLength);
}
}
}
} catch (Throwable e) {
DartToolsPlugin.log(e);
}
}
} finally {
ois.close();
}
}
} catch (Throwable e) {
DartToolsPlugin.log(e);
}
}
/**
* Main loop of {@link AutoSaveHelper}, executes {@link Task}.
*/
private void runTasksLoop() {
while (true) {
try {
Task task = taskQueue.take();
try {
task.execute();
} catch (Throwable e) {
DartToolsPlugin.log(e);
}
} catch (InterruptedException e) {
}
}
}
/**
* Start {@link AutoSaveHelper} with given started {@link IWorkbench}.
*/
private void start(IWorkbench workbench) {
// prepare page
IWorkbenchPage activePage;
{
// may be workbench already closed
if (workbench == null || workbench.getActiveWorkbenchWindow() == null) {
return;
}
activePage = workbench.getActiveWorkbenchWindow().getActivePage();
if (activePage == null) {
return;
}
}
// do restore
restoreEditors(activePage);
// remember all open editors
{
IEditorReference[] editorReferences = activePage.getEditorReferences();
for (IEditorReference reference : editorReferences) {
try {
IEditorInput input = reference.getEditorInput();
IFile file = getInputFile(input);
if (file != null) {
taskQueue.add(new OpenTask(file));
}
} catch (Throwable e) {
DartToolsPlugin.log(e);
}
}
}
// delete all saved data
pathMapDelete();
// add shutdown listener
workbench.addWorkbenchListener(new IWorkbenchListener() {
@Override
public void postShutdown(IWorkbench workbench) {
pathMapDelete();
}
@Override
public boolean preShutdown(IWorkbench workbench, boolean forced) {
return true;
}
});
// add part open/close listener
activePage.addPartListener(new PartListenerAdapter() {
@Override
public void partClosed(IWorkbenchPart part) {
IFile file = getInputFile(part);
if (file != null) {
taskQueue.add(new CloseTask(file));
}
}
@Override
public void partOpened(IWorkbenchPart part) {
IFile file = getInputFile(part);
if (file != null) {
taskQueue.add(new OpenTask(file));
}
}
});
// run tasks loop
{
Thread thread = new Thread() {
@Override
public void run() {
instance.runTasksLoop();
}
};
thread.setDaemon(true);
thread.start();
}
}
}