/* * 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.meta.common.utils.Assertions.assertNotNull; import java.awt.Color; import java.io.ByteArrayInputStream; import com.igormaznitsa.mindmap.plugins.api.AbstractImporter; import java.io.File; import java.io.InputStream; import java.net.URISyntaxException; 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.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.common.utils.Assertions; import com.igormaznitsa.meta.common.utils.GetUtils; import com.igormaznitsa.mindmap.model.Extra; 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 Novamind2MindMapImporter extends AbstractImporter { private static final Icon ICO = ImageIconServiceProvider.findInstance().getIconForId(IconID.POPUP_IMPORT_NOVAMIND2MM); private static final Logger LOGGER = LoggerFactory.getLogger(Novamind2MindMapImporter.class); private static final class Manifest { private final class Resource { private final String url; Resource(@Nonnull final String url) { this.url = url; } @Nonnull String getUrl() { return this.url; } @Nullable byte[] extractResourceBody() { byte[] result = null; final String path = "Resources/" + url; try { result = Utils.toByteArray(zipFile, path); } catch (Exception ex) { LOGGER.error("Can't extract resource data : " + path, ex); } return result; } } private final ZipFile zipFile; private final Map<String, Resource> resourceMap = new HashMap<String, Resource>(); private Manifest(@Nonnull final ZipFile zipFile, @Nonnull final String manifestPath) { this.zipFile = zipFile; try { final InputStream resourceIn = Utils.findInputStreamForResource(zipFile, manifestPath); if (resourceIn != null) { final Document document = Utils.loadXmlDocument(resourceIn, null, true); final Element main = document.getDocumentElement(); if ("manifest".equals(main.getTagName())) { for (final Element e : Utils.findDirectChildrenForName(main, "resources")) { for (final Element r : Utils.findDirectChildrenForName(e, "resource")) { final String id = r.getAttribute("id"); final String url = r.getAttribute("url"); if (!id.isEmpty() && !url.isEmpty()) { resourceMap.put(id, new Resource(url)); } } } } else { LOGGER.warn("Can't find manifest tag, looks like that format changed"); } } } catch (final Exception ex) { LOGGER.error("Can't parse resources list", ex); } } @Nullable private String findResourceImage(@Nonnull final String id) { String result = null; final Resource resource = findResource(id); if (resource != null) { final byte[] imageFile = resource.extractResourceBody(); if (imageFile != null) { try { result = Utils.rescaleImageAndEncodeAsBase64(new ByteArrayInputStream(imageFile),-1); } catch (Exception ex) { LOGGER.error("Can't find or convert image resource : " + resource.getUrl(), ex); } } } return result; } @Nullable private Resource findResource(@Nonnull final String id) { return this.resourceMap.get(id); } } private static final class ParsedContent { private static final class TopicReference { private final String id; private final ContentTopic linkedTopic; private final Color colorBorder; private final Color colorText; private final Color colorFill; private final List<TopicReference> children = new ArrayList<TopicReference>(); private TopicReference(@Nonnull final Element topicNode, @Nonnull final Map<String, ContentTopic> topicMap) { this.id = topicNode.getAttribute("id"); this.linkedTopic = topicMap.get(topicNode.getAttribute("topic-ref")); final Element subTopics = Utils.findFirstElement(topicNode, "sub-topics"); if (subTopics != null) { for (final Element t : Utils.findDirectChildrenForName(subTopics, "topic-node")) { this.children.add(new TopicReference(t, topicMap)); } } Color tmpColorBackground = null; Color tmpColorText = null; Color tmpColorBorder = null; final Element topicNodeView = Utils.findFirstElement(topicNode, "topic-node-view"); if (topicNodeView != null) { final Element style = Utils.findFirstElement(topicNodeView, "topic-node-style"); if (style != null) { final Element fillStyle = Utils.findFirstElement(style, "fill-style"); final Element lineStyle = Utils.findFirstElement(style, "line-style"); if (fillStyle != null) { final Element solidColor = Utils.findFirstElement(fillStyle, "solid-color"); if (solidColor != null) { tmpColorBackground = Utils.html2color(solidColor.getAttribute("color"), false); if (tmpColorBackground != null) { tmpColorText = Utils.makeContrastColor(tmpColorBackground); } } } if (lineStyle != null) { tmpColorBorder = Utils.html2color(lineStyle.getAttribute("color"), false); } } } this.colorBorder = tmpColorBorder; this.colorText = tmpColorText; this.colorFill = tmpColorBackground; } @Nullable Color getColorBorder() { return this.colorBorder; } @Nullable Color getColorBackground() { return this.colorFill; } @Nullable Color getColorText() { return this.colorText; } @Nonnull String getId() { return this.id; } @Nullable ContentTopic getContentTopic() { return this.linkedTopic; } @Nonnull @MustNotContainNull public List<TopicReference> getChildren() { return this.children; } } private static final class ContentTopic { private final String id; private final String richText; private final String notes; private final List<String> linkUrls; private final String imageResourceId; private ContentTopic(@Nonnull final String id, @Nonnull final Element nodeElement) { this.id = id; this.imageResourceId = extractImageId(nodeElement); this.notes = extractNotes(nodeElement); this.linkUrls = extractLinkUrls(nodeElement); this.richText = extractRichTextBlock(nodeElement); } @Nonnull String getId() { return this.id; } @Nullable String getImageResourceId() { return this.imageResourceId; } @Nullable String getNotes() { return this.notes; } @Nullable String getRichText() { return this.richText; } @Nonnull @MustNotContainNull List<String> getLinkUrls() { return this.linkUrls; } @Nonnull private static String extractRichText(@Nonnull final Element richText) { final StringBuilder result = new StringBuilder(); for (final Element r : Utils.findDirectChildrenForName(richText, "text-run")) { NodeList list = r.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { final Node n = list.item(i); if (n.getNodeType() == Node.ELEMENT_NODE) { if (n.getNodeName().equals("br")) { result.append('\n'); } else { result.append(n.getTextContent()); } } else if (n.getNodeType() == Node.TEXT_NODE) { result.append(n.getTextContent()); } } } return result.toString(); } @Nullable private static String extractImageId(@Nonnull final Element node) { final Element imageElement = Utils.findFirstElement(node, "top-image"); final String resourceRef = imageElement == null ? "" : imageElement.getAttribute("resource-ref"); return resourceRef.isEmpty() ? null : resourceRef; } @Nullable private static String extractNotes(@Nonnull final Element node) { final StringBuilder result = new StringBuilder(); for (final Element e : Utils.findDirectChildrenForName(node, "notes")) { final String rtext = extractRichTextBlock(e); if (rtext != null) { result.append(rtext); } } return result.length() == 0 ? null : result.toString(); } @Nullable private static String extractRichTextBlock(@Nonnull final Element element) { final StringBuilder result = new StringBuilder(); for (final Element rc : Utils.findDirectChildrenForName(element, "rich-text")) { result.append(extractRichText(rc)); } return result.length() == 0 ? null : result.toString(); } @Nonnull @MustNotContainNull private static List<String> extractLinkUrls(@Nonnull final Element node) { final List<String> result = new ArrayList<String>(); for (final Element links : Utils.findDirectChildrenForName(node, "links")) { for (final Element l : Utils.findDirectChildrenForName(links, "link")) { final String url = l.getAttribute("url"); if (!url.isEmpty()) { result.add(url); } } } return result; } } private final Map<String, ContentTopic> topicsMap = new HashMap<String, ContentTopic>(); private final Map<String, String> linksBetweenTopics = new HashMap<String, String>(); private final TopicReference rootRef; @Nullable TopicReference findForTopicId(@Nonnull TopicReference startTopicRef, @Nonnull final String contentTopicId) { TopicReference result = null; if (contentTopicId.equals(Assertions.assertNotNull(startTopicRef.getContentTopic()).getId())) { result = startTopicRef; } else { for (final ParsedContent.TopicReference c : startTopicRef.getChildren()) { result = findForTopicId(c, contentTopicId); if (result != null) { break; } } } return result; } @Nullable TopicReference getRootTopic() { return this.rootRef; } @Nonnull Map<String, String> getLinksBetweenTopics() { return this.linksBetweenTopics; } ParsedContent(@Nonnull final ZipFile file, @Nonnull final String path) { TopicReference mapRoot = null; try { final InputStream resourceIn = Utils.findInputStreamForResource(file, path); if (resourceIn != null) { final Document document = Utils.loadXmlDocument(resourceIn, null, true); final Element main = document.getDocumentElement(); if ("document".equals(main.getTagName())) { for (final Element e : Utils.findDirectChildrenForName(main, "topics")) { for (final Element r : Utils.findDirectChildrenForName(e, "topic")) { final String id = r.getAttribute("id"); this.topicsMap.put(id, new ContentTopic(id, r)); } } final Element maps = Utils.findFirstElement(main, "maps"); if (maps != null) { final Element firstMap = Utils.findFirstElement(maps, "map"); if (firstMap != null) { final Element rootTopicNode = Utils.findFirstElement(firstMap, "topic-node"); if (rootTopicNode != null) { mapRoot = new TopicReference(rootTopicNode, this.topicsMap); } for (final Element l : Utils.findDirectChildrenForName(firstMap, "link-lines")) { for (final Element tn : Utils.findDirectChildrenForName(l, "topic-node")) { for (final Element lld : Utils.findDirectChildrenForName(tn, "link-line-data")) { this.linksBetweenTopics.put(lld.getAttribute("start-topic-node-ref"), lld.getAttribute("end-topic-node-ref")); } } } } else { mapRoot = null; } } else { mapRoot = null; } } else { LOGGER.warn("Can't find document, looks like that format changed"); } } } catch (final Exception ex) { LOGGER.error("Can't parse resources list", ex); } this.rootRef = mapRoot; } } @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.Novamind2MindMap.openDialogTitle"), "nm5", "Novamind files (.NM5)", Texts.getString("MMDImporters.ApproveImport")); if (file == null) { return null; } final ZipFile zipFile = new ZipFile(file); final Manifest manifest = new Manifest(zipFile, "manifest.xml"); final ParsedContent content = new ParsedContent(zipFile, "content.xml"); final MindMap result = new MindMap(null, true); result.setAttribute(MindMapPanel.ATTR_SHOW_JUMPS, "true"); assertNotNull(result.getRoot()).setText("Empty map"); final ParsedContent.TopicReference rootRef = content.getRootTopic(); if (rootRef != null) { final Map<String, Topic> mapIdToTopic = new HashMap<String, Topic>(); convertContentTopicIntoMMTopic(result, null, rootRef, manifest, mapIdToTopic); for (final Map.Entry<String, String> link : content.getLinksBetweenTopics().entrySet()) { final Topic from = mapIdToTopic.get(link.getKey()); final Topic to = mapIdToTopic.get(link.getValue()); if (from != null && to != null) { from.setExtra(ExtraTopic.makeLinkTo(result, to)); } } processURLLinks(result, content, rootRef, mapIdToTopic); } return result; } private static void processURLLinks(@Nonnull final MindMap map, @Nonnull final ParsedContent model, @Nonnull final ParsedContent.TopicReference topicRef, @Nonnull final Map<String, Topic> mapTopicRefToTopics) { final Topic topic = mapTopicRefToTopics.get(topicRef.getId()); if (topic != null) { final ParsedContent.ContentTopic ctopic = Assertions.assertNotNull(topicRef.getContentTopic()); final List<String> urls = ctopic.getLinkUrls(); if (!urls.isEmpty()) { final List<Topic> insideLinksToTopics = new ArrayList<Topic>(); final List<MMapURI> insideLinksToURLs = new ArrayList<MMapURI>(); final List<MMapURI> insideLinksToFiles = new ArrayList<MMapURI>(); for (final String s : urls) { if (s.startsWith("novamind://topic/")) { final String targetTopicId = s.substring(17); final ParsedContent.TopicReference reference = model.findForTopicId(assertNotNull(model.getRootTopic()), targetTopicId); if (reference != null) { final Topic destTopic = mapTopicRefToTopics.get(reference.getId()); if (destTopic != null) { insideLinksToTopics.add(destTopic); } } } else { MMapURI uri; try { uri = new MMapURI(s); if (!uri.isAbsolute()) { uri = null; } } catch (final URISyntaxException ex) { uri = null; } if (uri == null) { try { insideLinksToFiles.add(new MMapURI(new File(s).toURI())); } catch (Exception ex) { LOGGER.warn("Can't convert file link : " + s); } } else { insideLinksToURLs.add(uri); } } } if (insideLinksToTopics.size() == 1 && !topic.getExtras().containsKey(Extra.ExtraType.TOPIC)) { topic.setExtra(ExtraTopic.makeLinkTo(map, insideLinksToTopics.get(0))); } else { for (final Topic linkTo : insideLinksToTopics) { final Topic local = topic.makeChild("Linked to topic", null); local.setExtra(ExtraTopic.makeLinkTo(map, linkTo)); } } if (insideLinksToURLs.size() == 1 && !topic.getExtras().containsKey(Extra.ExtraType.LINK)) { topic.setExtra(new ExtraLink(insideLinksToURLs.get(0))); } else { for (final MMapURI uri : insideLinksToURLs) { final Topic local = topic.makeChild("URL link", null); local.setExtra(new ExtraLink(uri)); } } if (insideLinksToFiles.size() == 1 && !topic.getExtras().containsKey(Extra.ExtraType.FILE)) { topic.setExtra(new ExtraFile(insideLinksToFiles.get(0))); } else { for (final MMapURI file : insideLinksToFiles) { final Topic local = topic.makeChild("File link", null); local.setExtra(new ExtraFile(file)); } } } for (final ParsedContent.TopicReference c : topicRef.getChildren()) { processURLLinks(map, model, c, mapTopicRefToTopics); } } } private static void convertContentTopicIntoMMTopic(@Nonnull final MindMap map, @Nullable final Topic parent, @Nonnull final ParsedContent.TopicReference node, @Nonnull final Manifest manifest, @Nonnull final Map<String, Topic> mapRefToTopic) { final Topic processing; if (parent == null) { processing = assertNotNull(map.getRoot()); } else { processing = parent.makeChild("<ID not found>", null); } if (node.getColorBackground() != null) { processing.setAttribute(StandardTopicAttribute.ATTR_FILL_COLOR.getText(), Utils.color2html(node.getColorBackground(), false)); } if (node.getColorBorder() != null) { processing.setAttribute(StandardTopicAttribute.ATTR_BORDER_COLOR.getText(), Utils.color2html(node.getColorBorder(), false)); } if (node.getColorText() != null) { processing.setAttribute(StandardTopicAttribute.ATTR_TEXT_COLOR.getText(), Utils.color2html(node.getColorText(), false)); } final ParsedContent.ContentTopic data = node.getContentTopic(); if (data != null) { mapRefToTopic.put(node.getId(), processing); processing.setText(GetUtils.ensureNonNull(data.getRichText(), "")); final String imageResourceId = data.getImageResourceId(); if (imageResourceId != null) { final String imageBody = manifest.findResourceImage(imageResourceId); if (imageBody != null) { processing.setAttribute(ImageVisualAttributePlugin.ATTR_KEY, imageBody); } } if (data.getNotes() != null) { processing.setExtra(new ExtraNote(data.getNotes())); } for (final ParsedContent.TopicReference c : node.getChildren()) { convertContentTopicIntoMMTopic(map, processing, c, manifest, mapRefToTopic); } } } @Override @Nullable public String getMnemonic() { return "novamind"; } @Override @Nonnull public String getName(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { return Texts.getString("MMDImporters.Novamind2MindMap.Name"); } @Override @Nonnull public String getReference(@Nonnull final MindMapPanel panel, @Nullable final Topic actionTopic, @Nonnull @MustNotContainNull final Topic[] selectedTopics) { return Texts.getString("MMDImporters.Novamind2MindMap.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 5; } }