package org.xmind.ui.internal.editor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.core.runtime.IPath;
import org.xmind.core.util.FileUtils;
import org.xmind.ui.editor.EditorHistoryItem;
import org.xmind.ui.editor.IEditorHistory;
import org.xmind.ui.editor.IEditorHistory.IEditorHistoryListener;
import org.xmind.ui.editor.IEditorHistoryItem;
import org.xmind.ui.internal.MindMapUIPlugin;
import org.xmind.ui.util.Logger;
/**
* @author Ren Siu
* @author Frank Shaka
* @since 3.6.50
*/
public class EditorHistoryPersistenceHelper
implements IEditorHistoryLoader, IEditorHistoryListener {
private static final String VALUE_SEPARATOR = "#$#"; //$NON-NLS-1$
private static final String KEY_PREFIX = "item."; //$NON-NLS-1$
private static final String PINNED_KEY_HISTORY_ITEM_PREFIX = "pinned.editor.history.item."; //$NON-NLS-1$
private static final String UNPINNED_KEY_HISTORY_ITEM_PREFIX = "unpinned.editor.history.item."; //$NON-NLS-1$
private static final String PINNED_KEY_PREFIX = "pinned.item."; //$NON-NLS-1$
private static final String THUMBNAIL_PREFIX = "thumbnail."; //$NON-NLS-1$
private static final String OLD_FILE_NAME = "workbookHistory.properties"; //$NON-NLS-1$
private static final String FILE_NAME = ".workbookHistory.properties"; //$NON-NLS-1$
private static final String THUMBNAIL_DIR_NAME = ".thumbnailHistory"; //$NON-NLS-1$
private static final Object END_OF_QUEUE = new Object();
private static class EditorHistoryState {
private final URI[] unpinnedInputURIs;
private final URI[] pinnedInputURIs;
private final Map<URI, URI> thumbnailURIs;
private final Map<URI, IEditorHistoryItem> editorHistoryItems;
/**
*
*/
private EditorHistoryState(EditorHistoryImpl service) {
this.unpinnedInputURIs = service
.getUnpinnedInputURIs(IEditorHistory.MAX_UNPINNED_SIZE);
this.pinnedInputURIs = service.getPinnedInputURIs();
this.thumbnailURIs = new HashMap<URI, URI>();
this.editorHistoryItems = new HashMap<URI, IEditorHistoryItem>();
for (URI uri : pinnedInputURIs) {
URI thumbnailURI = service.getThumbnail(uri);
if (thumbnailURI != null) {
thumbnailURIs.put(uri, thumbnailURI);
}
IEditorHistoryItem item = service.getItem(uri);
if (item != null)
editorHistoryItems.put(uri, item);
}
for (URI uri : unpinnedInputURIs) {
URI thumbnailURI = service.getThumbnail(uri);
if (thumbnailURI != null) {
thumbnailURIs.put(uri, thumbnailURI);
}
IEditorHistoryItem item = service.getItem(uri);
if (item != null)
editorHistoryItems.put(uri, item);
}
}
/**
* @param service
* @return
*/
public static EditorHistoryState createFrom(EditorHistoryImpl service) {
return new EditorHistoryState(service);
}
/**
* @return
*/
public URI[] getPinnedInputURIs() {
return pinnedInputURIs;
}
/**
* @return
*/
public URI[] getUnpinnedInputURIs() {
return unpinnedInputURIs;
}
/**
* @param input
* @return
*/
public URI getThumbnail(URI input) {
return thumbnailURIs.get(input);
}
/**
* @param input
* @return
*/
public IEditorHistoryItem getEditorHistoryItem(URI input) {
return editorHistoryItems.get(input);
}
}
private final IPath basePath;
private final BlockingQueue<Object> stateQueue;
private Thread thread;
private EditorHistoryImpl service;
/**
*
*/
public EditorHistoryPersistenceHelper(IPath basePath) {
this.basePath = basePath;
this.stateQueue = new LinkedBlockingQueue<Object>();
this.thread = null;
this.service = null;
}
public void setService(EditorHistoryImpl service) {
IEditorHistory oldService = this.service;
if (service == oldService)
return;
this.service = service;
if (oldService != null) {
oldService.removeEditorHistoryListener(this);
}
if (service != null) {
service.addEditorHistoryListener(this);
}
if (oldService == null && service != null) {
startThread();
} else if (oldService != null && service == null) {
stopThread();
}
}
/*
* (non-Javadoc)
* @see org.xmind.ui.internal.editor.IEditorHistoryLoader#load(org.xmind.ui.
* internal.editor.IEditorHistoryLoader.IEditorHistoryLoaderCallback)
*/
@Override
public void load(IEditorHistoryLoaderCallback callback) {
Properties historyRepository = load();
for (int index = 0; index < historyRepository.size(); index++) {
String unpinnedKey = KEY_PREFIX + index;
String pinnedKey = PINNED_KEY_PREFIX + index;
String pinnedThumbnailKey = THUMBNAIL_PREFIX + pinnedKey;
String unpinnedThumbnailKey = THUMBNAIL_PREFIX + unpinnedKey;
String pinedEditorHistoryItemKey = PINNED_KEY_HISTORY_ITEM_PREFIX
+ index;
String unPinedEditorHistoryItemKey = UNPINNED_KEY_HISTORY_ITEM_PREFIX
+ index;
String unpinnedInputURI = historyRepository
.getProperty(unpinnedKey);
String pinnedInputURI = historyRepository.getProperty(pinnedKey);
String unpinnedThumbnailURI = historyRepository
.getProperty(unpinnedThumbnailKey);
String pinnedThumbnailURI = historyRepository
.getProperty(pinnedThumbnailKey);
unpinnedInputURI = fixFileUri(unpinnedInputURI);
pinnedInputURI = fixFileUri(pinnedInputURI);
unpinnedThumbnailURI = fixFileUri(unpinnedThumbnailURI);
pinnedThumbnailURI = fixFileUri(pinnedThumbnailURI);
String pinedItemJson = historyRepository
.getProperty(pinedEditorHistoryItemKey);
IEditorHistoryItem pinnedItem = EditorHistoryItem
.readEditorHistoryItem(pinnedInputURI, pinedItemJson);
String unpinedItemJson = historyRepository
.getProperty(unPinedEditorHistoryItemKey);
IEditorHistoryItem unpinedItem = EditorHistoryItem
.readEditorHistoryItem(unpinnedInputURI, unpinedItemJson);
try {
if (unpinnedInputURI != null) {
URI unpinnedURI = new URI(unpinnedInputURI);
callback.inputURILoaded(unpinnedURI);
if (unpinedItem != null)
callback.editorHistoryItemsLoaded(unpinnedURI,
unpinedItem);
if (unpinnedThumbnailURI != null
&& !unpinnedInputURI.isEmpty()) {
callback.thumbnailURILoaded(unpinnedURI,
new URI(unpinnedThumbnailURI));
}
}
} catch (URISyntaxException e) {
}
try {
if (pinnedInputURI != null) {
URI pinnedURI = new URI(pinnedInputURI);
callback.pinnedInputURILoaded(pinnedURI);
if (pinnedItem != null)
callback.editorHistoryItemsLoaded(pinnedURI,
pinnedItem);
if (pinnedThumbnailURI != null
&& !pinnedInputURI.isEmpty()) {
callback.thumbnailURILoaded(pinnedURI,
new URI(pinnedThumbnailURI));
}
}
} catch (URISyntaxException e) {
}
}
}
private String fixFileUri(String uri) {
if (uri != null && uri.startsWith("file:")) { //$NON-NLS-1$
String specialPart = uri.substring(5);
boolean error = specialPart.startsWith("//") //$NON-NLS-1$
&& !specialPart.startsWith("///"); //$NON-NLS-1$
if (error) {
return "file:" + "/" + specialPart; //$NON-NLS-1$ //$NON-NLS-2$
}
} else if (uri != null && uri.startsWith("seawind:")) {//$NON-NLS-1$
String specialPart = uri.substring(8);
boolean error = specialPart.startsWith("//") //$NON-NLS-1$
&& !specialPart.startsWith("///"); //$NON-NLS-1$
if (error) {
return "seawind:" + "/" + specialPart; //$NON-NLS-1$ //$NON-NLS-2$
}
}
return uri;
}
/*
* (non-Javadoc)
* @see
* org.xmind.ui.internal.editor.IEditorHistoryLoader#saveThumbnail(java.io.
* InputStream)
*/
@Override
public URI saveThumbnail(InputStream thumbnailData) throws IOException {
File thumbnailDir = getThumbnailDir();
if (thumbnailDir == null)
return null;
if (!thumbnailDir.exists()) {
thumbnailDir.mkdirs();
}
String thumbnailName = UUID.randomUUID().toString();
File thumbnailFile = new File(thumbnailDir, thumbnailName);
OutputStream output = new FileOutputStream(thumbnailFile);
try {
FileUtils.transfer(thumbnailData, output, false);
} finally {
output.close();
}
return thumbnailFile.toURI();
}
private File getThumbnailDir() {
IPath basePath = MindMapUIPlugin.getDefault().getStateLocation();
if (basePath == null)
return null;
IPath filePath = basePath.append(THUMBNAIL_DIR_NAME);
return filePath.toFile();
}
/*
* (non-Javadoc)
* @see org.xmind.ui.internal.editor.IEditorHistoryLoader#dispose()
*/
@Override
public void dispose() {
setService(null);
}
private void startThread() {
Thread thread = new Thread(new Runnable() {
public void run() {
runLoop();
}
});
thread.setName("EditorHistoryPersistenceThread"); //$NON-NLS-1$
thread.setPriority(Thread.MIN_PRIORITY);
thread.setDaemon(true);
this.thread = thread;
thread.start();
/*
* Manually trigger a save operation on startup, for there might be some
* changes to editor history before this earlyStartup method is called.
* For example, files are opened on startup via double click in Finder.
*/
editorHistoryChanged();
}
private void stopThread() {
if (service != null)
service.removeEditorHistoryListener(this);
stateQueue.offer(END_OF_QUEUE);
Thread thread = this.thread;
this.thread = null;
if (thread != null) {
thread.interrupt();
}
}
public void editorHistoryChanged() {
if (service == null)
return;
EditorHistoryState state = EditorHistoryState.createFrom(service);
stateQueue.offer(state);
}
private void runLoop() {
try {
while (thread != null) {
Object state = stateQueue.take();
if (state == END_OF_QUEUE)
break;
save((EditorHistoryState) state);
Thread.sleep(0);
}
} catch (InterruptedException e) {
// interruption means stop
}
}
private void save(EditorHistoryState persistable) {
Properties repository = new Properties();
URI[] pinnedInputURIs = persistable.getPinnedInputURIs();
URI[] unpinnedInputURIs = persistable.getUnpinnedInputURIs();
//Push pinned items
for (int index = 0; index < pinnedInputURIs.length; index++) {
URI input = pinnedInputURIs[index];
if (input != null) {
String key = PINNED_KEY_PREFIX + index;
repository.setProperty(key, input.toString());
String pinnedEditorHistoryItemKey = PINNED_KEY_HISTORY_ITEM_PREFIX
+ index;
IEditorHistoryItem pinnedItem = persistable
.getEditorHistoryItem(input);
if (pinnedItem != null)
repository.setProperty(pinnedEditorHistoryItemKey,
pinnedItem.toJson());
URI thumbnail = persistable.getThumbnail(input);
String thumbnailKey = THUMBNAIL_PREFIX + key;
if (thumbnail != null)
repository.setProperty(thumbnailKey, thumbnail.toString());
}
}
// Push unpinned items
for (int index = 0; index < unpinnedInputURIs.length; index++) {
URI input = unpinnedInputURIs[index];
if (input != null) {
String key = KEY_PREFIX + index;
repository.setProperty(key, input.toString());
String unpinedEditorHistoryItemKey = UNPINNED_KEY_HISTORY_ITEM_PREFIX
+ index;
IEditorHistoryItem unpinnedItem = persistable
.getEditorHistoryItem(input);
if (unpinnedItem != null)
repository.setProperty(unpinedEditorHistoryItemKey,
unpinnedItem.toJson());
URI thumbnail = persistable.getThumbnail(input);
String thumbnailKey = THUMBNAIL_PREFIX + key;
if (thumbnail != null)
repository.setProperty(thumbnailKey, thumbnail.toString());
}
}
// Save to properties file
File file = getHistoryFile();
if (file != null) {
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try {
OutputStreamWriter out = new OutputStreamWriter(
new FileOutputStream(file), "UTF-8"); //$NON-NLS-1$
// FileWriter writer = new FileWriter(file);
try {
repository.store(out,
"Generated by org.xmind.ui.internal.editor.EditorHistoryService"); //$NON-NLS-1$
} finally {
out.close();
}
} catch (IOException e) {
Logger.log(e, "Failed to save workbook history to " //$NON-NLS-1$
+ file.getAbsolutePath());
}
}
File oldHistoryFile = getOldHistoryFile();
if (oldHistoryFile != null && oldHistoryFile.exists()) {
oldHistoryFile.delete();
}
}
public Properties load() {
Properties repository = new Properties();
// Load form properties file
File file = getHistoryFile();
if (file != null && file.exists()) {
try {
InputStreamReader reader = new InputStreamReader(
new FileInputStream(file), "UTF-8"); //$NON-NLS-1$
// FileReader reader = new FileReader(file);
try {
repository.load(reader);
} finally {
reader.close();
}
} catch (IOException e) {
Logger.log(e, "Failed to load workbook history from " //$NON-NLS-1$
+ file.getAbsolutePath());
}
} else {
repository = loadOldHistoryRepository();
}
return repository;
}
private Properties loadOldHistoryRepository() {
Properties repository = new Properties();
// Load form old properties file
File file = getOldHistoryFile();
if (file != null && file.exists()) {
try {
FileReader reader = new FileReader(file);
try {
repository.load(reader);
} finally {
reader.close();
}
} catch (IOException e) {
Logger.log(e, "Failed to load workbook history from " //$NON-NLS-1$
+ file.getAbsolutePath());
}
int count = 0;
List<String> items = new ArrayList<String>();
// Parse properties
int size = repository.size();
for (int i = 0; i < size; i++) {
if (count >= IEditorHistory.MAX_UNPINNED_SIZE)
break;
String key = KEY_PREFIX + i;
String value = repository.getProperty(key);
if (value == null)
continue;
items.add(value);
repository.remove(key);
count++;
}
//Compatible with the old version
Set<Object> oldVersionKeys = repository.keySet();
for (Object key : oldVersionKeys) {
if (count >= IEditorHistory.MAX_UNPINNED_SIZE)
break;
String input = (String) key;
String info = repository.getProperty(input);
items.add(input + VALUE_SEPARATOR + info);
count++;
}
//Transfer old format into new format
repository = new Properties();
int countForPinned = 0;
int countForUnpinned = 0;
for (String inputAndInfo : items) {
int index = inputAndInfo.indexOf(VALUE_SEPARATOR);
if (index < 0)
continue;
String input = inputAndInfo.substring(0, index);
String info = inputAndInfo
.substring(index + VALUE_SEPARATOR.length());
if (info == null || info.isEmpty())
continue;
String thumbnail = extractThumbnailFromOldInfo(info);
String thumbnailKey = THUMBNAIL_PREFIX;
boolean isPinned = isPinnedBasedOldInfo(info);
if (isPinned) {
String pinnedKey = PINNED_KEY_PREFIX + countForPinned;
thumbnailKey = THUMBNAIL_PREFIX + pinnedKey;
repository.setProperty(pinnedKey, input);
countForPinned++;
} else {
String unpinnedKey = KEY_PREFIX + countForUnpinned;
thumbnailKey = THUMBNAIL_PREFIX + unpinnedKey;
repository.setProperty(unpinnedKey, input);
countForUnpinned++;
}
if (thumbnail != null && !thumbnail.isEmpty()) {
URI thumbnailURI = new File(thumbnail).toURI();
repository.setProperty(thumbnailKey,
thumbnailURI.toString());
}
}
}
return repository;
}
private static String extractThumbnailFromOldInfo(String info) {
String thumbnail = info;
if (info != null) {
int sepPos = info.indexOf(',');
if (sepPos > 0) {
thumbnail = info.substring(0, sepPos);
}
}
return thumbnail;
}
private static boolean isPinnedBasedOldInfo(String info) {
return info != null && info.endsWith(",favor"); //$NON-NLS-1$
}
private File getHistoryFile() {
if (basePath == null)
return null;
IPath filePath = basePath.append(FILE_NAME);
return filePath.toFile();
}
private File getOldHistoryFile() {
if (basePath == null)
return null;
IPath filePath = basePath.append(OLD_FILE_NAME);
return filePath.toFile();
}
}