/*
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and above are dual-licensed
* under the Eclipse Public License (EPL), which is available at
* http://www.eclipse.org/legal/epl-v10.html and the GNU Lesser General Public
* License (LGPL), which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors: XMind Ltd. - initial API and implementation
*/
package org.xmind.ui.internal.imports.freemind;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmind.core.Core;
import org.xmind.core.IBoundary;
import org.xmind.core.IFileEntry;
import org.xmind.core.IHtmlNotesContent;
import org.xmind.core.IImage;
import org.xmind.core.INotes;
import org.xmind.core.IParagraph;
import org.xmind.core.IRelationship;
import org.xmind.core.ISheet;
import org.xmind.core.ISpan;
import org.xmind.core.ITextSpan;
import org.xmind.core.ITopic;
import org.xmind.core.IWorkbook;
import org.xmind.core.io.ResourceMappingManager;
import org.xmind.core.io.freemind.FreeMindConstants;
import org.xmind.core.io.freemind.FreeMindResourceMappingManager;
import org.xmind.core.style.IStyle;
import org.xmind.core.style.IStyleSheet;
import org.xmind.core.style.IStyled;
import org.xmind.core.util.DOMUtils;
import org.xmind.core.util.HyperlinkUtils;
import org.xmind.ui.internal.MindMapUIPlugin;
import org.xmind.ui.internal.imports.ImportMessages;
import org.xmind.ui.internal.imports.ImporterUtils;
import org.xmind.ui.io.MonitoredInputStream;
import org.xmind.ui.resources.FontUtils;
import org.xmind.ui.style.Styles;
import org.xmind.ui.wizards.MindMapImporter;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
public class FreeMindImporter extends MindMapImporter
implements FreeMindConstants, ErrorHandler {
private class LinkPoint {
ITopic end1;
ITopic end2;
public LinkPoint(Element ele) {
String id1 = att(ele, "ID"); //$NON-NLS-1$
end1 = findLinkTopic(id1);
Element linkNode = child(ele, "arrowlink"); //$NON-NLS-1$
String id2 = att(linkNode, "DESTINATION"); //$NON-NLS-1$
end2 = findLinkTopic(id2);
}
private ITopic findLinkTopic(String id) {
if (idMap == null || idMap.isEmpty())
return null;
String topicId = idMap.get(id);
return getTargetWorkbook().findTopic(topicId);
}
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj != null && !(obj instanceof LinkPoint))
return false;
LinkPoint that = (LinkPoint) obj;
return this.end1 == that.end1 && this.end2 == that.end2;
}
}
private class NotesImporter {
IHtmlNotesContent content;
IParagraph currentParagraph = null;
Stack<IStyle> styleStack = new Stack<IStyle>();
public NotesImporter(IHtmlNotesContent content) {
this.content = content;
}
public void importFrom(Element htmlEle) throws InterruptedException {
checkInterrupted();
String tagName = DOMUtils.getLocalName(htmlEle.getTagName());
boolean isParagraph = "p".equalsIgnoreCase(tagName) //$NON-NLS-1$
|| "li".equalsIgnoreCase(tagName); //$NON-NLS-1$
IStyle style = pushStyle(htmlEle,
isParagraph ? IStyle.PARAGRAPH : IStyle.TEXT);
if (isParagraph)
addParagraph();
NodeList nl = htmlEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
short nodeType = node.getNodeType();
if (nodeType == Node.TEXT_NODE) {
addText(node.getTextContent());
} else if (nodeType == Node.ELEMENT_NODE) {
importFrom((Element) node);
}
}
popStyle(style);
}
private void addText(String text) {
if (text == null)
return;
text = text.trim();
if ("".equals(text)) //$NON-NLS-1$
return;
ITextSpan textSpan = content.createTextSpan(text);
addSpan(textSpan);
}
private void addSpan(ISpan span) {
if (currentParagraph == null)
addParagraph();
currentParagraph.addSpan(span);
registerStyle(span, Styles.FontFamily, Styles.FontSize,
Styles.TextColor, Styles.FontWeight, Styles.TextDecoration,
Styles.FontStyle);
}
private void addParagraph() {
currentParagraph = content.createParagraph();
content.addParagraph(currentParagraph);
registerStyle(currentParagraph, Styles.TextAlign);
}
private void registerStyle(IStyled host, String... keys) {
for (IStyle style : styleStack) {
registerStyle(host, style, keys);
}
}
private void registerStyle(IStyled host, IStyle style, String... keys) {
for (String key : keys) {
String value = style.getProperty(key);
if (value != null)
FreeMindImporter.this.registerStyle(host, key, value);
}
}
private IStyle pushStyle(Element ele, String type) {
IStyle style = getStyleSheet().createStyle(type);
receiveStyle(ele, style);
if (style.isEmpty())
style = null;
else
styleStack.push(style);
return style;
}
private void receiveStyle(Element ele, IStyle style) {
String alignStyle = att(ele, "style"); //$NON-NLS-1$
if (alignStyle != null) {
String align = FreeMindImporter.this.parseAlign(alignStyle);
style.setProperty(Styles.TextAlign, align);
}
String name = ele.getTagName();
if ("b".equalsIgnoreCase(name)) //$NON-NLS-1$
style.setProperty(Styles.FontWeight, Styles.FONT_WEIGHT_BOLD);
else if ("i".equalsIgnoreCase(name)) //$NON-NLS-1$
style.setProperty(Styles.FontStyle, Styles.FONT_STYLE_ITALIC);
else if ("u".equalsIgnoreCase(name)) //$NON-NLS-1$
style.setProperty(Styles.TextDecoration,
Styles.TEXT_DECORATION_UNDERLINE);
else if ("font".equalsIgnoreCase(name)) { //$NON-NLS-1$
String fontFamily = att(ele, "face"); //$NON-NLS-1$
if (fontFamily != null)
style.setProperty(Styles.FontWeight, fontFamily);
String color = att(ele, "color"); //$NON-NLS-1$
if (color != null)
style.setProperty(Styles.TextColor, color);
String size = att(ele, "size"); //$NON-NLS-1$
if (size != null) {
String fontSize = FreeMindImporter.this.parseSize(size);
style.setProperty(Styles.FontSize, fontSize + "pt"); //$NON-NLS-1$
}
}
}
private void popStyle(IStyle style) {
if (style == null)
return;
if (!styleSheet.isEmpty() && styleStack.peek() == style)
styleStack.pop();
}
}
private static ResourceMappingManager mappings = null;
private List<Element> linkEles = null;
private Map<String, String> idMap = new HashMap<String, String>();
private Map<IStyled, IStyle> styleMap = new HashMap<IStyled, IStyle>();
private Map<ITopic, String> topicHyperMap = null;
private IStyleSheet styleSheet = null;
private StringBuilder topicText = null;
public FreeMindImporter(String sourcePath, IWorkbook targetWorkbook) {
super(sourcePath, targetWorkbook);
}
public FreeMindImporter(String sourcePath) {
super(sourcePath);
}
public void build() throws InvocationTargetException, InterruptedException {
MindMapUIPlugin.getDefault().getUsageDataCollector()
.increase("ImportFromFreeMindCount"); //$NON-NLS-1$
try {
DocumentBuilder builder = getDocumentBuilder();
builder.setErrorHandler(this);
Document doc;
InputStream in = new FileInputStream(getSourcePath());
try {
in = new MonitoredInputStream(in, getMonitor());
doc = builder.parse(in);
} finally {
builder.setErrorHandler(null);
try {
in.close();
} catch (IOException e) {
}
}
checkInterrupted();
Element rootElement = doc.getDocumentElement();
loadSheet(rootElement);
checkInterrupted();
arrangeStyles();
checkInterrupted();
dealTopicHyperlinks();
} catch (Throwable e) {
throw new InvocationTargetException(e);
} finally {
idMap = null;
styleMap = null;
}
postBuilded();
}
private void dealTopicHyperlinks() {
if (topicHyperMap == null || topicHyperMap.isEmpty())
return;
for (Entry<ITopic, String> entry : topicHyperMap.entrySet()) {
ITopic topic = entry.getKey();
String nodeId = entry.getValue();
if (idMap == null || idMap.isEmpty())
break;
String topicId = idMap.get(nodeId);
if (topicId == null)
continue;
topic.setHyperlink("xmind:#" + topicId); //$NON-NLS-1$
}
}
private void arrangeStyles() throws InterruptedException {
IStyleSheet targetStyleSheet = getTargetWorkbook().getStyleSheet();
for (Entry<IStyled, IStyle> entry : styleMap.entrySet()) {
checkInterrupted();
IStyled styleOwned = entry.getKey();
IStyle style = entry.getValue();
IStyle importStyle = targetStyleSheet.importStyle(style);
if (importStyle != null)
styleOwned.setStyleId(importStyle.getId());
}
}
private void loadSheet(Element rootEle) throws InterruptedException {
checkInterrupted();
ISheet sheet = getTargetWorkbook().createSheet();
sheet.setTitleText("sheet1"); //$NON-NLS-1$
Element nodeEle = child(rootEle, "node"); //$NON-NLS-1$
if (nodeEle != null)
loadTopic(sheet.getRootTopic(), nodeEle);
else
sheet.getRootTopic()
.setTitleText(ImportMessages.Importer_CentralTopic);
if (linkEles != null && !linkEles.isEmpty())
loadRelationship();
addTargetSheet(sheet);
}
private void loadRelationship() {
for (Element linkEle : linkEles) {
if (linkEle == null)
continue;
LinkPoint link = new LinkPoint(linkEle);
ITopic end1 = link.end1;
ITopic end2 = link.end2;
if (end1 != null && end2 != null) {
IRelationship relationship = getTargetWorkbook()
.createRelationship(end1, end2);
relationship.setEnd1Id(end1.getId());
relationship.setEnd2Id(end2.getId());
Element arrowEle = child(linkEle, "arrowlink"); //$NON-NLS-1$
loadLinkShape(relationship, arrowEle);
loadLinkColor(relationship, arrowEle);
}
}
}
private void loadLinkColor(IRelationship relationship, Element arrowEle) {
String color = att(arrowEle, "COLOR"); //$NON-NLS-1$
if (color == null)
return;
registerStyle(relationship, Styles.LineColor, color);
}
private void loadLinkShape(IRelationship relationship, Element arrowEle) {
String startArrow = att(arrowEle, "STARTARROW"); //$NON-NLS-1$
String startShape = parseArrowShape(startArrow);
registerStyle(relationship, Styles.ArrowBeginClass, startShape);
String endArrow = att(arrowEle, "ENDARROW"); //$NON-NLS-1$
String endShape = parseArrowShape(endArrow);
registerStyle(relationship, Styles.ArrowEndClass, endShape);
}
private String parseArrowShape(String shape) {
if ("Default".equals(shape)) //$NON-NLS-1$
return "org.xmind.arrowShape.normal"; //$NON-NLS-1$
return "org.xmind.arrowShape.none"; //$NON-NLS-1$
}
private void loadTopic(ITopic topic, Element nodeEle)
throws InterruptedException {
checkInterrupted();
String id = att(nodeEle, "ID"); //$NON-NLS-1$
if (id != null) {
idMap.put(id, topic.getId());
}
checkInterrupted();
String text = att(nodeEle, "TEXT"); //$NON-NLS-1$
if (text == null)
text = ImporterUtils.getDefaultTopicTitle(topic);
else if (text.contains("../../../../")) { //$NON-NLS-1$
int index = text.indexOf("../../../../"); //$NON-NLS-1$
String path = text.substring(index + "../../../../".length(), text //$NON-NLS-1$
.length() - 2);
loadImage(topic, path);
text = ImporterUtils.getDefaultTopicTitle(topic);
}
topic.setTitleText(text);
checkInterrupted();
String folded = att(nodeEle, "FOLDED"); //$NON-NLS-1$
if (folded != null && "true".equals(folded)) //$NON-NLS-1$
topic.setFolded(true);
checkInterrupted();
if (!topic.isRoot()) {
Element ele = child(nodeEle, "cloud"); //$NON-NLS-1$
if (ele != null) {
IBoundary boundary = getTargetWorkbook().createBoundary();
int index = topic.getIndex();
boundary.setStartIndex(index);
boundary.setEndIndex(index);
topic.getParent().addBoundary(boundary);
}
}
checkInterrupted();
String link = att(nodeEle, "LINK"); //$NON-NLS-1$
if (link != null) {
if (link.startsWith("../../../../")) { //$NON-NLS-1$
String hyperlink = link.substring("../../../../".length()); //$NON-NLS-1$
topic.setHyperlink("file:" + hyperlink); //$NON-NLS-1$
} else if (link.startsWith("#")) { //$NON-NLS-1$
String nodeId = link.substring(1);
if (topicHyperMap == null)
topicHyperMap = new HashMap<ITopic, String>();
topicHyperMap.put(topic, nodeId);
} else {
topic.setHyperlink(link);
}
}
checkInterrupted();
String backgroundColor = att(nodeEle, "BACKGROUND_COLOR"); //$NON-NLS-1$
registerStyle(topic, Styles.FillColor, backgroundColor);
checkInterrupted();
String foregroundColor = att(nodeEle, "COLOR"); //$NON-NLS-1$
registerStyle(topic, Styles.TextColor, foregroundColor);
checkInterrupted();
Element fontEle = child(nodeEle, "font"); //$NON-NLS-1$
loadFont(fontEle, topic);
checkInterrupted();
Element linkNode = child(nodeEle, "arrowlink"); //$NON-NLS-1$
if (linkNode != null) {
if (linkEles == null)
linkEles = new ArrayList<Element>();
linkEles.add(nodeEle);
}
Iterator<Element> iconIter = children(nodeEle, "icon"); //$NON-NLS-1$
while (iconIter.hasNext()) {
checkInterrupted();
Element iconEle = iconIter.next();
String builtIn = att(iconEle, "BUILTIN"); //$NON-NLS-1$
if (builtIn != null) {
String markerId = getTransferred("marker", builtIn, null); //$NON-NLS-1$
if (markerId == null)
markerId = "other-question"; //$NON-NLS-1$
topic.addMarker(markerId);
}
}
checkInterrupted();
Element hookEle = child(nodeEle, "hook"); //$NON-NLS-1$
if (hookEle != null) {
String name = att(hookEle, "NAME"); //$NON-NLS-1$
if ("accessories/plugins/NodeNote.properties".equals(name)) { //$NON-NLS-1$
Element notesEle = child(hookEle, "text"); //$NON-NLS-1$
if (notesEle != null)
loadNotesContent(topic, notesEle);
}
}
Iterator<Element> notesIter = children(nodeEle, "richcontent"); //$NON-NLS-1$
while (notesIter.hasNext()) {
checkInterrupted();
Element richEle = notesIter.next();
String type = att(richEle, "TYPE"); //$NON-NLS-1$
if (type != null && "NOTE".equals(type)) { //$NON-NLS-1$
Element htmlEle = child(richEle, "html"); //$NON-NLS-1$
if (htmlEle != null) {
IHtmlNotesContent notesContent = (IHtmlNotesContent) getTargetWorkbook()
.createNotesContent(INotes.HTML);
NotesImporter notesImport = new NotesImporter(notesContent);
notesImport.importFrom(htmlEle);
topic.getNotes().setContent(INotes.HTML, notesContent);
}
} else if ("NODE".equals(type)) { //$NON-NLS-1$
topicText = null;
Element htmlEle = child(richEle, "html"); //$NON-NLS-1$
if (htmlEle != null) {
loadNode(topic, htmlEle);
}
if (topicText != null) {
text = topicText.toString().trim();
} else {
text = ImporterUtils.getDefaultTopicTitle(topic);
}
topic.setTitleText(text);
}
}
Iterator<Element> nodeIter = children(nodeEle, "node"); //$NON-NLS-1$
while (nodeIter.hasNext()) {
checkInterrupted();
Element subNodeEle = nodeIter.next();
ITopic subTopic = getTargetWorkbook().createTopic();
topic.add(subTopic);
loadTopic(subTopic, subNodeEle);
}
}
private void loadNotesContent(ITopic topic, Element ele) {
String text = ele.getTextContent().trim();
if (text == null)
return;
IHtmlNotesContent notesContent = (IHtmlNotesContent) getTargetWorkbook()
.createNotesContent(INotes.HTML);
ITextSpan span = notesContent.createTextSpan(text);
IParagraph paragraph = notesContent.createParagraph();
paragraph.addSpan(span);
notesContent.addParagraph(paragraph);
topic.getNotes().setContent(INotes.HTML, notesContent);
}
private void loadNode(ITopic topic, Element element)
throws InterruptedException {
checkInterrupted();
String tagName = DOMUtils.getLocalName(element.getTagName());
if ("img".equalsIgnoreCase(tagName)) { //$NON-NLS-1$
String src = att(element, "src"); //$NON-NLS-1$
if (src.startsWith("../../../../")) { //$NON-NLS-1$
String path = src.substring("../../../../".length()); //$NON-NLS-1$
loadImage(topic, path);
} else {
String path = getSourcePath().substring(0,
getSourcePath().lastIndexOf("\\") + 1) + src;//$NON-NLS-1$
loadImage(topic, path);
}
} else if ("p".equalsIgnoreCase(tagName) //$NON-NLS-1$
|| "li".equalsIgnoreCase(tagName)) { //$NON-NLS-1$
String text = element.getTextContent().trim();
if (text != null) {
if (topicText == null)
topicText = new StringBuilder();
topicText.append(text);
topicText.append('\n');
}
String align = att(element, "style"); //$NON-NLS-1$
if (align != null) {
String value = parseAlign(align);
registerStyle(topic, Styles.TextAlign, value);
}
} else if ("b".equalsIgnoreCase(tagName)) //$NON-NLS-1$
registerStyle(topic, Styles.FontWeight, Styles.FONT_WEIGHT_BOLD);
else if ("i".equalsIgnoreCase(tagName)) //$NON-NLS-1$
registerStyle(topic, Styles.FontStyle, Styles.FONT_STYLE_ITALIC);
else if ("font".equalsIgnoreCase(tagName)) { //$NON-NLS-1$
String fontFamily = att(element, "face"); //$NON-NLS-1$
if (fontFamily != null)
registerStyle(topic, Styles.FontWeight, fontFamily);
String color = att(element, "color"); //$NON-NLS-1$
if (color != null)
registerStyle(topic, Styles.TextColor, color);
String size = att(element, "size"); //$NON-NLS-1$
if (size != null)
registerStyle(topic, Styles.FontSize, parseSize(size));
}
Element[] children = DOMUtils.getChildElements(element);
for (Element ele : children) {
loadNode(topic, ele);
}
}
private void loadImage(ITopic topic, String path)
throws InterruptedException {
checkInterrupted();
IFileEntry imgEntry = loadAttachment(path);
if (imgEntry != null) {
IImage image = topic.getImage();
image.setSource(HyperlinkUtils.toAttachmentURL(imgEntry.getPath()));
}
}
private IFileEntry loadAttachment(String path) throws InterruptedException {
checkInterrupted();
try {
IFileEntry entry = getTargetWorkbook().getManifest()
.createAttachmentFromFilePath(path);
return entry;
} catch (IOException e) {
log(e, "failed to create attachment from: " + path); //$NON-NLS-1$
}
return null;
}
private void loadFont(Element fontEle, ITopic topic) {
if (fontEle == null)
return;
String fontName = att(fontEle, "NAME"); //$NON-NLS-1$
String availableFontName = FontUtils.getAAvailableFontNameFor(fontName);
fontName = availableFontName != null ? availableFontName : fontName;
if (fontName != null) {
registerStyle(topic, Styles.FontFamily, fontName);
}
String size = att(fontEle, "SIZE"); //$NON-NLS-1$
if (size != null) {
registerStyle(topic, Styles.FontSize, size);
}
String italic = att(fontEle, "ITALIC"); //$NON-NLS-1$
if ("true".equalsIgnoreCase(italic)) { //$NON-NLS-1$
registerStyle(topic, Styles.FontStyle, Styles.FONT_STYLE_ITALIC);
}
String bold = att(fontEle, "BOLD"); //$NON-NLS-1$
if ("true".equalsIgnoreCase(bold)) //$NON-NLS-1$
registerStyle(topic, Styles.FontWeight, Styles.FONT_WEIGHT_BOLD);
}
private void registerStyle(IStyled styleOwned, String key, String value) {
if (value == null)
return;
IStyle style = styleMap.get(styleOwned);
if (style == null) {
style = getStyleSheet().createStyle(styleOwned.getStyleType());
getStyleSheet().addStyle(style, IStyleSheet.NORMAL_STYLES);
styleMap.put(styleOwned, style);
}
style.setProperty(key, value);
}
private String parseSize(String size) {
if ("2".equals(size)) //$NON-NLS-1$
return "10"; //$NON-NLS-1$
else if ("3".equals(size)) //$NON-NLS-1$
return "12"; //$NON-NLS-1$
else if ("4".equals(size)) //$NON-NLS-1$
return "14"; //$NON-NLS-1$
else if ("5".equals(size)) //$NON-NLS-1$
return "18"; //$NON-NLS-1$
else if ("6".equals(size)) //$NON-NLS-1$
return "24"; //$NON-NLS-1$
return "8"; //$NON-NLS-1$
}
private String parseAlign(String align) {
if (align.endsWith(Styles.ALIGN_CENTER))
return Styles.ALIGN_CENTER;
else if (align.endsWith(Styles.ALIGN_RIGHT))
return Styles.ALIGN_RIGHT;
return Styles.ALIGN_LEFT;
}
private IStyleSheet getStyleSheet() {
if (styleSheet == null)
styleSheet = Core.getStyleSheetBuilder().createStyleSheet();
return styleSheet;
}
private String getTransferred(String type, String sourceId,
String defaultId) {
if (sourceId != null) {
ResourceMappingManager mappings = getMappings();
if (mappings != null) {
String destination = mappings.getDestination(type, sourceId);
if (destination != null)
return destination;
}
}
return defaultId;
}
private ResourceMappingManager getMappings() {
if (mappings == null)
mappings = createMappings();
return mappings;
}
private ResourceMappingManager createMappings() {
return FreeMindResourceMappingManager.getInstance();
}
private void checkInterrupted() throws InterruptedException {
if (getMonitor().isCanceled())
throw new InterruptedException();
}
private DocumentBuilder getDocumentBuilder()
throws ParserConfigurationException {
return DOMUtils.getDefaultDocumentBuilder();
}
private static Element child(Element parentEle, String childTag) {
return children(parentEle, childTag).next();
}
private static Iterator<Element> children(final Element parentEle,
final String childTag) {
return new Iterator<Element>() {
String tag = DOMUtils.getLocalName(childTag);
Iterator<Element> it = DOMUtils.childElementIter(parentEle);
Element next = findNext();
public void remove() {
}
private Element findNext() {
while (it.hasNext()) {
Element ele = it.next();
if (DOMUtils.getLocalName(ele.getTagName())
.equalsIgnoreCase(tag)) {
return ele;
}
}
return null;
}
public Element next() {
Element result = next;
next = findNext();
return result;
}
public boolean hasNext() {
return next != null;
}
};
}
private static String att(Element ele, String attName) {
if (ele.hasAttribute(attName))
return ele.getAttribute(attName);
attName = DOMUtils.getLocalName(attName);
NamedNodeMap atts = ele.getAttributes();
for (int i = 0; i < atts.getLength(); i++) {
Node att = atts.item(i);
if (attName.equalsIgnoreCase(
DOMUtils.getLocalName(att.getNodeName()))) {
return att.getNodeValue();
}
}
return null;
}
public void error(SAXParseException exception) throws SAXException {
log(exception, null);
}
public void fatalError(SAXParseException exception) throws SAXException {
log(exception, null);
}
public void warning(SAXParseException exception) throws SAXException {
log(exception, null);
}
}