package org.krakenapps.docxcod; import static org.krakenapps.docxcod.util.XMLDocHelper.evaluateXPath; import static org.krakenapps.docxcod.util.XMLDocHelper.newDocumentBuilder; import static org.krakenapps.docxcod.util.XMLDocHelper.newXPath; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.xml.xpath.XPath; import org.apache.commons.io.FilenameUtils; import org.krakenapps.docxcod.util.CloseableHelper; import org.krakenapps.docxcod.util.XMLDocHelper; import org.krakenapps.docxcod.util.XMLDocHelper.NodeListWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import freemarker.core.Environment; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; public class ChartDirectiveParser implements OOXMLProcessor { public class ChartUidFunction implements TemplateMethodModelEx { public static final String functionName = "chartUid"; private String rid = null; private int count = 0; @Override public Object exec(@SuppressWarnings("rawtypes") List arguments) throws TemplateModelException { String origRid = arguments.get(0).toString(); if (!origRid.equals(rid)) { rid = origRid; count = 0; } return Integer.toString(count++); } } private static final String CHART_XML_CONTENTTYPE = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"; private static final String DOCXCOD_CHART_XML_EXT = "docxcod_chart_xml"; private static final String CONTENT_TYPES_XML = "[Content_Types].xml"; public class ChartResFunction implements TemplateMethodModelEx { public static final String functionName = "ridHelper"; public int count = 0; private final OOXMLPackage pkg; public ChartResFunction(OOXMLPackage pkg) { this.pkg = pkg; } @Override public Object exec(@SuppressWarnings("rawtypes") List arguments) throws TemplateModelException { String originalRid = arguments.get(0).toString(); String chartUid = arguments.get(1).toString(); String rid = originalRid + "_" + chartUid; Environment ce = Environment.getCurrentEnvironment(); @SuppressWarnings("unchecked") Set<String> knownVariable = ce.getKnownVariableNames(); HashMap<String, Object> localRoot = new HashMap<String, Object>(); for (String k : knownVariable) { localRoot.put(k, ce.getVariable(k)); } appendContentType(pkg); // add ".kraken_chart_xml" to // [Content_Types].xml createCopiedChart(pkg, originalRid, chartUid, localRoot); // create // copied // chart(xml, // xlsx) // must return text content of attr "r:id". return rid; } public String getName() { return functionName; } } private Logger logger = LoggerFactory.getLogger(getClass().getName()); private void createCopiedChart(OOXMLPackage pkg, String originalRid, String chartUid, HashMap<String, Object> localRoot) { String chartXmlPath = appendToRels(pkg, "word/_rels/document.xml.rels", originalRid, chartUid); chartXmlPath = FilenameUtils.concat("word", chartXmlPath); if (chartXmlPath != null) { logger.info("new chart xml filename: {}", chartXmlPath); String embeddedXlsxPath = createChartFromExisting(pkg, chartXmlPath, chartUid); createNewEmbeddedXlsx(pkg, embeddedXlsxPath, chartUid, localRoot); // modify chart from xlsx } } private String makeRelsPath(String chartXmlPath) { String name = FilenameUtils.getName(chartXmlPath); String path = FilenameUtils.getPath(chartXmlPath); return FilenameUtils.concat(FilenameUtils.concat(path, "_rels"), name + ".rels"); } private String createChartFromExisting(OOXMLPackage pkg, String chartXmlPath, String chartUid) { InputStream f = null; try { String embeddedXlsxRid = null; String embeddedXlsxFile = null; f = new FileInputStream(new File(pkg.getDataDir(), chartXmlPath)); Document doc = newDocumentBuilder().parse(f); XPath xpath = newXPath(doc); NodeList nodeList = evaluateXPath(xpath, "//c:externalData", doc); if (nodeList.getLength() != 0) { Node n = nodeList.item(0); Node attrRid = n.getAttributes().getNamedItem("r:id"); if (attrRid == null) throw new IllegalStateException(String.format("no Target externalData in %s", chartXmlPath)); embeddedXlsxRid = attrRid.getTextContent(); } XMLDocHelper.save(doc, new File(pkg.getDataDir(), makeNewChartFilename(chartXmlPath, chartUid)), true); f.close(); f = new FileInputStream(new File(pkg.getDataDir(), makeRelsPath(chartXmlPath))); doc = newDocumentBuilder().parse(f); xpath = newXPath(doc); if (embeddedXlsxRid != null) { NodeList rsNodes = evaluateXPath(xpath, "//:Relationship[@Id='" + embeddedXlsxRid + "']", doc); if (rsNodes.getLength() != 0) { Node n = rsNodes.item(0); embeddedXlsxFile = FilenameUtils.concat("word/charts", n.getAttributes().getNamedItem("Target").getTextContent()); String relTarget = makeXlsxRelTarget(chartXmlPath, makeNewXlsxFilename(embeddedXlsxFile, chartUid)); n.getAttributes().getNamedItem("Target").setTextContent(FilenameUtils.separatorsToUnix(relTarget)); } } XMLDocHelper.save(doc, new File(pkg.getDataDir(), makeRelsPath(makeNewChartFilename(chartXmlPath, chartUid))), true); return embeddedXlsxFile; } catch (Exception e) { e.printStackTrace(); } finally { CloseableHelper.safeClose(f); } return null; } private String makeXlsxRelTarget(String chartXmlPath, String xlsxFilename) { // TODO: use reliable relative path logic return "../embeddings/" + FilenameUtils.getName(xlsxFilename); } private String makeNewChartFilename(String chartXmlPath, String chartUid) { // trim right ".xml" String s = chartXmlPath.substring(0, chartXmlPath.length() - 4); s += "_" + chartUid + "." + DOCXCOD_CHART_XML_EXT; return s; } private String appendToRels(OOXMLPackage pkg, String relPath, String originalRid, String chartUid) { InputStream f = null; try { f = new FileInputStream(new File(pkg.getDataDir(), relPath)); Document doc = newDocumentBuilder().parse(f); XPath xpath = newXPath(doc); NodeList nodeList = evaluateXPath(xpath, "//:Relationship[@Id='" + originalRid + "']", doc); String chartXmlPath = null; if (nodeList.getLength() != 0) { Node n = nodeList.item(0); Node attrTarget = n.getAttributes().getNamedItem("Target"); if (attrTarget == null) throw new IllegalStateException(String.format("no Target attribute in %s with rid %s", relPath, originalRid)); chartXmlPath = attrTarget.getTextContent(); Node attrType = n.getAttributes().getNamedItem("Type"); if (attrType == null) throw new IllegalStateException(String.format("no Type attribute in %s with rid %s", relPath, originalRid)); String type = attrType.getTextContent(); Element newChild = doc.createElement("Relationship"); newChild.setAttribute("Id", originalRid + "_" + chartUid); newChild.setAttribute("Target", FilenameUtils.separatorsToUnix(makeNewChartFilename(chartXmlPath, chartUid))); newChild.setAttribute("Type", type); doc.getFirstChild().appendChild(newChild); } else { return null; } XMLDocHelper.save(doc, new File(pkg.getDataDir(), relPath), true); return chartXmlPath; } catch (Exception e) { e.printStackTrace(); } finally { CloseableHelper.safeClose(f); } return null; } private String makeNewXlsxFilename(String embeddedXlsxFile, String chartUid) { String path = FilenameUtils.getPath(embeddedXlsxFile); String basename = FilenameUtils.getBaseName(embeddedXlsxFile); String ext = FilenameUtils.getExtension(embeddedXlsxFile); return path + basename + "_" + chartUid + "." + ext; } private void createNewEmbeddedXlsx(OOXMLPackage pkg, String embeddedXlsxFile, String chartUid, Map<String, Object> localRoot) { OOXMLPackage xlsx = new OOXMLPackage(); FileInputStream is = null; FileOutputStream os = null; try { is = new FileInputStream(new File(pkg.getDataDir(), embeddedXlsxFile)); xlsx.load(is); os = new FileOutputStream(new File(pkg.getDataDir(), makeNewXlsxFilename(embeddedXlsxFile, chartUid))); xlsx.save(os); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { CloseableHelper.safeClose(is); CloseableHelper.safeClose(os); } } private void appendContentType(OOXMLPackage pkg) { InputStream f = null; try { f = new FileInputStream(new File(pkg.getDataDir(), CONTENT_TYPES_XML)); Document doc = newDocumentBuilder().parse(f); XPath xpath = newXPath(doc); NodeList nodeList = evaluateXPath(xpath, "//:Default[@Extension='" + DOCXCOD_CHART_XML_EXT + "']", doc); if (nodeList.getLength() == 0) { Element newChild = doc.createElement("Default"); newChild.setAttribute("ContentType", CHART_XML_CONTENTTYPE); newChild.setAttribute("Extension", DOCXCOD_CHART_XML_EXT); doc.getFirstChild().appendChild(newChild); } XMLDocHelper.save(doc, new File(pkg.getDataDir(), CONTENT_TYPES_XML), true); } catch (Exception e) { e.printStackTrace(); } finally { CloseableHelper.safeClose(f); } } @Override public void process(OOXMLPackage pkg, Map<String, Object> rootMap) { if (rootMap != null) { rootMap.put(ChartResFunction.functionName, new ChartResFunction(pkg)); rootMap.put(ChartUidFunction.functionName, new ChartUidFunction()); } InputStream f = null; try { f = new FileInputStream(new File(pkg.getDataDir(), "word/document.xml")); Document doc = newDocumentBuilder().parse(f); XPath xpath = newXPath(doc); NodeList nodeList = evaluateXPath(xpath, "//c:chart", doc); for (Node n : new NodeListWrapper(nodeList)) { InsertChartHelperMagicNode(doc, n); } XMLDocHelper.save(doc, new File(pkg.getDataDir(), "word/document.xml"), true); } catch (Exception e) { e.printStackTrace(); } finally { CloseableHelper.safeClose(f); } } private enum AsttpPos { BEFORE, AFTER }; private void InsertChartHelperMagicNode(Document doc, Node chartNode) { Node attrRid = chartNode.getAttributes().getNamedItem("r:id"); String originalRid = attrRid.getTextContent(); logger.info("chart rid: {}", originalRid); appendSiblingToTheParent(chartNode, "w:drawing", AsttpPos.BEFORE, getMagicNode(doc, String.format("#assign curChartUid=chartUid(\'%s\')", attrRid))); attrRid.setTextContent(String.format("${%s(\'%s\', curChartUid)}", ChartResFunction.functionName, originalRid)); } private boolean appendSiblingToTheParent(Node currentNode, String parentNodeName, AsttpPos posOfSibling, Node magicNode) { Node p = currentNode; for (;;) { Node t = p.getParentNode(); if (t == null) return false; p = t; if (p.getNodeName().equals(parentNodeName)) break; } if (p.getParentNode() == null) return false; switch (posOfSibling) { case BEFORE: p.getParentNode().insertBefore(magicNode, p); break; case AFTER: p.getParentNode().insertBefore(magicNode, p.getNextSibling()); break; } return true; } private Node getMagicNode(Document doc, String content) { Element magicNode = doc.createElement("KMagicNode"); magicNode.appendChild(doc.createCDATASection(content)); return magicNode; } }