/* * 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 java.awt.Color; import com.igormaznitsa.mindmap.plugins.api.AbstractImporter; import java.io.File; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipFile; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.Icon; import org.apache.commons.io.IOUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.igormaznitsa.meta.annotation.MustNotContainNull; import com.igormaznitsa.meta.common.utils.Assertions; import com.igormaznitsa.mindmap.model.ExtraFile; import com.igormaznitsa.mindmap.model.ExtraLink; import com.igormaznitsa.mindmap.model.ExtraNote; import com.igormaznitsa.mindmap.model.ExtraTopic; import com.igormaznitsa.mindmap.model.MMapURI; 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.StandardTopicAttribute; import com.igormaznitsa.mindmap.swing.panel.utils.Utils; public class XMind2MindMapImporter extends AbstractImporter { private static final Icon ICO = ImageIconServiceProvider.findInstance().getIconForId(IconID.POPUP_IMPORT_XMIND2MM); private static final Logger LOGGER = LoggerFactory.getLogger(XMind2MindMapImporter.class); private static final class XMindStyle { private final Color foreground; private final Color background; private final Color border; private XMindStyle(@Nonnull final Element style) { Color back = null; Color front = null; Color bord = null; for (final Element t : Utils.findDirectChildrenForName(style, "topic-properties")) { final String colorFill = t.getAttribute("svg:fill"); final String colorText = t.getAttribute("fo:color"); final String colorBorder = t.getAttribute("border-line-color"); if (colorFill != null) { back = Utils.html2color(colorFill, false); } if (colorText != null) { front = Utils.html2color(colorText, false); } if (colorBorder != null) { bord = Utils.html2color(colorBorder, false); } } this.foreground = front; this.background = back; this.border = bord; } private void attachTo(@Nonnull final Topic topic) { if (this.background != null) { topic.setAttribute(StandardTopicAttribute.ATTR_FILL_COLOR.getText(), Utils.color2html(this.background, false)); } if (this.foreground != null) { topic.setAttribute(StandardTopicAttribute.ATTR_TEXT_COLOR.getText(), Utils.color2html(this.foreground, false)); } if (this.border != null) { topic.setAttribute(StandardTopicAttribute.ATTR_BORDER_COLOR.getText(), Utils.color2html(this.border, false)); } } } private static final class XMindStyles { private final Map<String, XMindStyle> stylesMap = new HashMap<String, XMindStyle>(); private XMindStyles(@Nonnull final ZipFile zipFile) { try { final InputStream stylesXml = Utils.findInputStreamForResource(zipFile, "styles.xml"); if (stylesXml != null) { final Document parsedStyles = Utils.loadXmlDocument(stylesXml, null, true); final Element root = parsedStyles.getDocumentElement(); if ("xmap-styles".equals(root.getTagName())) { for (final Element styles : Utils.findDirectChildrenForName(root, "styles")) { for (final Element style : Utils.findDirectChildrenForName(styles, "style")) { final String id = style.getAttribute("id"); if (!id.isEmpty() && "topic".equals(style.getAttribute("type"))) { this.stylesMap.put(id, new XMindStyle(style)); } } } } } } catch (Exception ex) { LOGGER.error("Can't extract XMIND styles", ex); } } private void setStyle(@Nonnull final String styleId, @Nonnull final Topic topic) { final XMindStyle foundStyle = this.stylesMap.get(styleId); if (foundStyle != null) { foundStyle.attachTo(topic); } } } private static void throwWrongFormat() { throw new IllegalArgumentException("Wrong or unsupported XMind file format"); } @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.XMind2MindMap.openDialogTitle"), "xmind", "XMind files (.XMIND)", Texts.getString("MMDImporters.ApproveImport")); if (file == null) { return null; } final ZipFile zipFile = new ZipFile(file); final XMindStyles styles = new XMindStyles(zipFile); final InputStream contentStream = Utils.findInputStreamForResource(zipFile, "content.xml"); if (contentStream == null) { throwWrongFormat(); } final Document document = Utils.loadXmlDocument(Assertions.assertNotNull(contentStream), null, true); final Element rootElement = document.getDocumentElement(); if (!rootElement.getTagName().equals("xmap-content")) { throwWrongFormat(); } final List<Element> sheets = Utils.findDirectChildrenForName(document.getDocumentElement(), "sheet"); final MindMap result; if (sheets.isEmpty()) { result = new MindMap(null, true); Assertions.assertNotNull(result.getRoot()).setText("Empty"); } else { result = convertSheet(styles, zipFile, sheets.get(0)); } return result; } @Nonnull private MindMap convertSheet(@Nonnull final XMindStyles styles, @Nonnull final ZipFile file, @Nonnull final Element sheet) throws Exception { final MindMap resultedMap = new MindMap(null, true); resultedMap.setAttribute(MindMapPanel.ATTR_SHOW_JUMPS, "true"); final Topic rootTopic = Assertions.assertNotNull(resultedMap.getRoot()); rootTopic.setText("Empty sheet"); final Map<String, Topic> topicIdMap = new HashMap<String, Topic>(); final Map<String, String> linksBetweenTopics = new HashMap<String, String>(); final List<Element> rootTopics = Utils.findDirectChildrenForName(sheet, "topic"); if (!rootTopics.isEmpty()) { convertTopic(file, styles, resultedMap, null, rootTopic, rootTopics.get(0), topicIdMap, linksBetweenTopics); } for (final Element l : Utils.findDirectChildrenForName(sheet, "relationships")) { for (final Element r : Utils.findDirectChildrenForName(l, "relationship")) { final String end1 = r.getAttribute("end1"); final String end2 = r.getAttribute("end2"); if (!linksBetweenTopics.containsKey(end1)) { final Topic startTopic = topicIdMap.get(end1); final Topic endTopic = topicIdMap.get(end2); if (startTopic != null && endTopic != null) { startTopic.setExtra(ExtraTopic.makeLinkTo(resultedMap, endTopic)); } } } } for (final Map.Entry<String, String> e : linksBetweenTopics.entrySet()) { final Topic startTopic = topicIdMap.get(e.getKey()); final Topic endTopic = topicIdMap.get(e.getValue()); if (startTopic != null && endTopic != null) { startTopic.setExtra(ExtraTopic.makeLinkTo(resultedMap, endTopic)); } } return resultedMap; } @Nonnull private static String extractTopicTitle(@Nonnull final Element topic) { final List<Element> title = Utils.findDirectChildrenForName(topic, "title"); return title.isEmpty() ? "" : title.get(0).getTextContent(); } @Nonnull @MustNotContainNull private static List<Element> getChildTopics(@Nonnull final Element topic) { List<Element> result = new ArrayList<Element>(); for (final Element c : Utils.findDirectChildrenForName(topic, "children")) { for (Element t : Utils.findDirectChildrenForName(c, "topics")) { result.addAll(Utils.findDirectChildrenForName(t, "topic")); } } return result; } private static void convertTopic(@Nonnull ZipFile zipFile, @Nonnull final XMindStyles styles, @Nonnull final MindMap map, @Nullable final Topic parent, @Nullable Topic pregeneratedOne, @Nonnull final Element topicElement, @Nonnull Map<String, Topic> idTopicMap, @Nonnull final Map<String, String> linksBetweenTopics) throws Exception { final Topic topicToProcess; if (pregeneratedOne == null) { topicToProcess = Assertions.assertNotNull(parent).makeChild("", null); } else { topicToProcess = pregeneratedOne; } topicToProcess.setText(extractTopicTitle(topicElement)); final String theTopicId = topicElement.getAttribute("id"); idTopicMap.put(theTopicId, topicToProcess); final String styleId = topicElement.getAttribute("style-id"); if (!styleId.isEmpty()) { styles.setStyle(styleId, topicToProcess); } final String attachedImage = extractFirstAttachedImageAsBase64(zipFile, topicElement); if (attachedImage != null && !attachedImage.isEmpty()) { topicToProcess.setAttribute(ImageVisualAttributePlugin.ATTR_KEY, attachedImage); } final String xlink = topicElement.getAttribute("xlink:href"); if (!xlink.isEmpty()) { if (xlink.startsWith("file:")) { try { topicToProcess.setExtra(new ExtraFile(new MMapURI(new File(xlink.substring(5)).toURI()))); } catch (Exception ex) { LOGGER.error("Can't convert file link : " + xlink, ex); } } else if (xlink.startsWith("xmind:#")) { linksBetweenTopics.put(theTopicId, xlink.substring(7)); } else { try { topicToProcess.setExtra(new ExtraLink(new MMapURI(URI.create(xlink)))); } catch (Exception ex) { LOGGER.error("Can't convert link : " + xlink, ex); } } } final String extractedNote = extractNote(topicElement); if (!extractedNote.isEmpty()) { topicToProcess.setExtra(new ExtraNote(extractedNote)); } for (final Element c : getChildTopics(topicElement)) { convertTopic(zipFile, styles, map, topicToProcess, null, c, idTopicMap, linksBetweenTopics); } } @Nullable private static String extractFirstAttachedImageAsBase64(@Nonnull final ZipFile file, @Nonnull final Element topic) { String result = null; for (final Element e : Utils.findDirectChildrenForName(topic, "xhtml:img")) { final String link = e.getAttribute("xhtml:src"); if (!link.isEmpty()) { if (link.startsWith("xap:")) { InputStream inStream = null; try { inStream = Utils.findInputStreamForResource(file, link.substring(4)); if (inStream!=null) { result = Utils.rescaleImageAndEncodeAsBase64(inStream, -1); if (result != null) break; } } catch (final Exception ex) { LOGGER.error("Can't decode attached image : "+link, ex); } finally{ IOUtils.closeQuietly(inStream); } } } } return result; } @Nonnull private static String extractNote(@Nonnull final Element topic) { final StringBuilder result = new StringBuilder(); for (final Element note : Utils.findDirectChildrenForName(topic, "notes")) { final String plain = extractTextContentFrom(note, "plain"); final String html = extractTextContentFrom(note, "html"); if (result.length() > 0) { result.append('\n'); } if (!plain.isEmpty()) { result.append(plain); } else if (!html.isEmpty()) { result.append(html); } } return result.toString(); } @Nonnull private static String extractTextContentFrom(@Nonnull final Element element, @Nonnull final String tag) { final StringBuilder result = new StringBuilder(); for (final Element c : Utils.findDirectChildrenForName(element, tag)) { final String found = c.getTextContent(); if (found != null && !found.isEmpty()) { result.append(found.replace("\r", "")); } } return result.toString(); } @Override @Nullable public String getMnemonic() { return "xmind"; } @Override @Nonnull public String getName(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { return Texts.getString("MMDImporters.XMind2MindMap.Name"); } @Override @Nonnull public String getReference(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { return Texts.getString("MMDImporters.XMind2MindMap.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 4; } }