/*
* Copyright 2016 Igor Maznitsa.
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.igormaznitsa.mindmap.plugins.importers;
import static com.igormaznitsa.mindmap.swing.panel.StandardTopicAttribute.ATTR_FILL_COLOR;
import static com.igormaznitsa.mindmap.swing.panel.StandardTopicAttribute.ATTR_TEXT_COLOR;
import java.awt.Color;
import com.igormaznitsa.mindmap.plugins.api.AbstractImporter;
import java.io.File;
import java.io.FileInputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.Icon;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.meta.annotation.ReturnsOriginal;
import com.igormaznitsa.meta.common.utils.Assertions;
import com.igormaznitsa.mindmap.model.ExtraNote;
import com.igormaznitsa.mindmap.model.ExtraTopic;
import com.igormaznitsa.mindmap.model.MindMap;
import com.igormaznitsa.mindmap.model.Topic;
import com.igormaznitsa.mindmap.swing.panel.DialogProvider;
import com.igormaznitsa.mindmap.swing.panel.MindMapPanel;
import com.igormaznitsa.mindmap.swing.panel.Texts;
import com.igormaznitsa.mindmap.swing.services.IconID;
import com.igormaznitsa.mindmap.swing.services.ImageIconServiceProvider;
import com.igormaznitsa.mindmap.model.logger.Logger;
import com.igormaznitsa.mindmap.model.logger.LoggerFactory;
import com.igormaznitsa.mindmap.plugins.attributes.images.ImageVisualAttributePlugin;
import com.igormaznitsa.mindmap.swing.panel.ui.AbstractCollapsableElement;
import com.igormaznitsa.mindmap.swing.panel.utils.Utils;
public class Freemind2MindMapImporter extends AbstractImporter {
private static final Icon ICO = ImageIconServiceProvider.findInstance().getIconForId(IconID.POPUP_EXPORT_FREEMIND);
private static final Logger LOGGER = LoggerFactory.getLogger(Freemind2MindMapImporter.class);
private static final Set<String> TOKEN_NEEDS_NEXT_LINE = new HashSet<String>(Arrays.asList("br", "div", "p", "li"));
private enum RichContentType {
NODE, NOTE
}
private static final class RichContent {
private final RichContentType type;
private final String text;
private final String[] imageUrls;
private RichContent(@Nonnull final RichContentType type, @Nonnull final String text, @Nonnull @MustNotContainNull final List<String> foundImageUrls) {
this.type = type;
this.text = text;
this.imageUrls = foundImageUrls.toArray(new String[foundImageUrls.size()]);
}
@Nonnull
@MustNotContainNull
private String[] getFoundImageURLs() {
return this.imageUrls;
}
@Nonnull
private RichContentType getType() {
return this.type;
}
@Nonnull
private String getText() {
return this.text;
}
}
@Override
@Nullable
public MindMap doImport(@Nonnull final MindMapPanel panel, @Nonnull final DialogProvider dialogProvider, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) throws Exception {
final File file = this.selectFileForExtension(panel, Texts.getString("MMDImporters.Freemind2MindMap.openDialogTitle"), "mm", "Freemind files (.MM)", Texts.getString("MMDImporters.ApproveImport"));
if (file == null) {
return null;
}
final Document document = Utils.loadXmlDocument(new FileInputStream(file), "UTF-8", true);
final Element rootElement = document.getDocumentElement();
if (!rootElement.getTagName().equals("map")) {
throw new IllegalArgumentException("Not Freemind file");
}
final Map<String, Topic> idTopicMap = new HashMap<String, Topic>();
final Map<String, String> linksMap = new HashMap<String, String>();
final MindMap resultedMap = new MindMap(null, true);
resultedMap.setAttribute(MindMapPanel.ATTR_SHOW_JUMPS, "true");
final List<Element> list = Utils.findDirectChildrenForName(rootElement, "node");
if (list.isEmpty()) {
Assertions.assertNotNull(resultedMap.getRoot()).setText("Empty");
} else {
parseTopic(file.getParentFile(), resultedMap, null, resultedMap.getRoot(), list.get(0), idTopicMap, linksMap);
}
for (final Map.Entry<String, String> l : linksMap.entrySet()) {
final Topic start = idTopicMap.get(l.getKey());
final Topic end = idTopicMap.get(l.getValue());
if (start != null && end != null) {
start.setExtra(ExtraTopic.makeLinkTo(resultedMap, end));
}
}
return resultedMap;
}
@Nonnull
private static String findArrowlinkDestination(@Nonnull final Element element) {
final List<Element> arrows = Utils.findDirectChildrenForName(element, "arrowlink");
return arrows.isEmpty() ? "" : arrows.get(0).getAttribute("DESTINATION");
}
private void parseTopic(@Nonnull final File rootFolder, @Nonnull final MindMap map, @Nullable Topic parent, @Nullable Topic preGeneratedTopic, @Nonnull Element element, @Nonnull final Map<String, Topic> idTopicMap, @Nonnull final Map<String, String> linksMap) {
final String text = element.getAttribute("TEXT");
final String id = element.getAttribute("ID");
final String position = element.getAttribute("POSITION");
final String arrowDestination = findArrowlinkDestination(element);
final String color = element.getAttribute("COLOR");
final List<RichContent> foundRichContent = extractRichContent(element);
final Topic topicToProcess;
if (preGeneratedTopic == null) {
topicToProcess = Assertions.assertNotNull(parent).makeChild(text, null);
if (Assertions.assertNotNull(parent).isRoot()) {
if ("left".equalsIgnoreCase(position)) {
AbstractCollapsableElement.makeTopicLeftSided(topicToProcess, true);
}
}
} else {
topicToProcess = preGeneratedTopic;
}
if (!color.isEmpty()) {
final Color converted = Utils.html2color(color, false);
if (converted != null) {
topicToProcess.setAttribute(ATTR_FILL_COLOR.getText(), Utils.color2html(converted, false));
topicToProcess.setAttribute(ATTR_TEXT_COLOR.getText(), Utils.color2html(Utils.makeContrastColor(converted), false));
}
}
topicToProcess.setText(text);
for (final RichContent r : foundRichContent) {
switch (r.getType()) {
case NODE: {
if (!r.getText().isEmpty()) {
topicToProcess.setText(r.getText().trim());
}
}
break;
case NOTE: {
if (!r.getText().isEmpty()) {
topicToProcess.setExtra(new ExtraNote(r.getText().trim()));
}
}
break;
}
processImageLinkForTopic(rootFolder, topicToProcess, r.getFoundImageURLs());
}
if (!id.isEmpty()) {
idTopicMap.put(id, topicToProcess);
if (!arrowDestination.isEmpty()) {
linksMap.put(id, arrowDestination);
}
}
for (final Element e : Utils.findDirectChildrenForName(element, "node")) {
parseTopic(rootFolder, map, topicToProcess, null, e, idTopicMap, linksMap);
}
}
private static void processImageLinkForTopic(@Nonnull final File rootFolder, @Nonnull final Topic topic, @Nonnull @MustNotContainNull final String[] imageUrls) {
for (final String s : imageUrls) {
try {
URI imageUri = URI.create(s);
final File file;
if (imageUri.isAbsolute()) {
file = new File(imageUri);
} else {
file = new File(rootFolder.toURI().resolve(imageUri));
}
if (file.isFile()) {
topic.setAttribute(ImageVisualAttributePlugin.ATTR_KEY, Utils.rescaleImageAndEncodeAsBase64(file, -1));
break;
}
}
catch (final Exception ex) {
LOGGER.warn("Can't decode or load image for URI : " + s);
}
}
}
@Nonnull
@ReturnsOriginal
private static StringBuilder processHtmlElement(@Nonnull final Node node, @Nonnull final StringBuilder builder, @Nonnull @MustNotContainNull final List<String> imageURLs) {
final NodeList list = node.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
final Node n = list.item(i);
switch (n.getNodeType()) {
case Node.TEXT_NODE: {
builder.append(n.getTextContent());
}
break;
case Node.ELEMENT_NODE: {
final String tag = n.getNodeName();
if ("img".equals(tag)) {
final String source = ((Element) n).getAttribute("src");
if (!source.isEmpty()) {
imageURLs.add(source);
}
}
if (TOKEN_NEEDS_NEXT_LINE.contains(tag)) {
builder.append('\n');
}
processHtmlElement(n, builder, imageURLs);
}
break;
}
}
return builder;
}
@Nonnull
@ReturnsOriginal
private static StringBuilder extractTextFromHtmlElement(@Nonnull final Element element, @Nonnull final StringBuilder buffer, @Nonnull @MustNotContainNull final List<String> imageURLs) {
final List<Element> html = Utils.findDirectChildrenForName(element, "html");
if (!html.isEmpty()) {
final List<Element> body = Utils.findDirectChildrenForName(html.get(0), "body");
if (!body.isEmpty()) {
processHtmlElement(body.get(0), buffer, imageURLs);
}
}
return buffer;
}
@Nonnull
@MustNotContainNull
private static List<RichContent> extractRichContent(@Nonnull final Element richContentElement) {
final List<Element> richContents = Utils.findDirectChildrenForName(richContentElement, "richcontent");
final List<RichContent> result = new ArrayList<RichContent>();
final List<String> foundImageUrls = new ArrayList<String>();
for (final Element e : richContents) {
final String textType = e.getAttribute("TYPE");
try {
foundImageUrls.clear();
final RichContentType type = RichContentType.valueOf(textType);
final String text = extractTextFromHtmlElement(e, new StringBuilder(), foundImageUrls).toString().replace("\r", "");
result.add(new RichContent(type, text, foundImageUrls));
}
catch (IllegalArgumentException ex) {
LOGGER.warn("Unknown node type : " + textType);
}
}
return result;
}
@Override
@Nullable
public String getMnemonic() {
return "freemind";
}
@Override
@Nonnull
public String getName(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) {
return Texts.getString("MMDImporters.Freemind2MindMap.Name");
}
@Override
@Nonnull
public String getReference(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) {
return Texts.getString("MMDImporters.Freemind2MindMap.Reference");
}
@Override
@Nonnull
public Icon getIcon(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) {
return ICO;
}
@Override
public int getOrder() {
return 3;
}
}