ODTDocumentsMerging.java
/*
* Copyright (C) 2000 - 2026 Silverpeas
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* As a special exception to the terms and conditions of version 3.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* Open Source Software ("FLOSS") applications as described in Silverpeas's
* FLOSS exception. You should have received a copy of the text describing
* the FLOSS exception, and it is also available here:
* "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.silverpeas.components.kmelia.export;
import org.odftoolkit.odfdom.dom.element.draw.DrawFrameElement;
import org.odftoolkit.odfdom.dom.element.draw.DrawImageElement;
import org.odftoolkit.odfdom.dom.element.style.StyleMasterPageElement;
import org.odftoolkit.odfdom.dom.style.OdfStyleFamily;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeAutomaticStyles;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeMasterStyles;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
import org.odftoolkit.odfdom.incubator.doc.style.OdfStyle;
import org.odftoolkit.odfdom.pkg.OdfPackage;
import org.odftoolkit.simple.TextDocument;
import org.odftoolkit.simple.draw.Frame;
import org.odftoolkit.simple.draw.Image;
import org.silverpeas.kernel.logging.SilverLogger;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.net.URI;
import java.util.Iterator;
import static org.silverpeas.kernel.util.StringUtil.isDefined;
/**
* A decorator of an OpenDocument text document. It decorates an ODT document by adding document
* merging capabilities to it.
*
* With the content of the document to merge, the merging operation imports only the text and
* paragraph styles; styles on tables, lists, and others elements aren't imported, so it is the
* styles defined in the document source that are applied on theses elements of the imported content.
*
* The merger is smart enough to rename the imported text and paragraph styles in order to avoid
* conflicts with eponymous styles defined in the document source, and to take new
* document layout definitions from the document to merge.
*/
class ODTDocumentsMerging extends TextDocument {
/**
* The prefix to concat with the names and with the display name of the styles to import.
*/
protected static final String MERGE_STYLE_PREFIX = "IMPORTED";
/**
* The prefix to concat with the display name of the styles to import.
*/
protected static final String MERGE_STYLE_DISPLAY_NAME_PREFIX = MERGE_STYLE_PREFIX + " ";
/**
* The prefix to concat with thr name of the styles to import.
*/
protected static final String MERGE_STYLE_NAME_PREFIX = MERGE_STYLE_PREFIX + "_";
/**
* Creates a merger for the specified ODT document.
* @param document the document to which merges can be done.
* @return an ODTDocumentsMerger instance.
*/
public static ODTDocumentsMerging decorates(final TextDocument document) {
return new ODTDocumentsMerging(document);
}
/**
* A convenient method to improve readability of the merge method call that waits for a section
* name as parameter.
* @param sectionName name of a text section in the wrapped document.
* @return the name of the section passed as parameter.
*/
public static String atSection(String sectionName) {
return sectionName;
}
/**
* Merges the specified document into self. The content is merged at the end of the
* decorated document.
* @param theDocument the document to merge.
* @return the document resulting of the merge.
*/
public TextDocument merge(final TextDocument theDocument) {
try {
importGlobalStylesOf(theDocument);
importContentStylesOf(theDocument);
Node myContent = getTextDocument().getContentDom().getFirstChild().getLastChild().
getFirstChild();
insertContentTextOf(theDocument, into(myContent));
return getTextDocument();
} catch (Exception ex) {
throw new DocumentMergeException(ex.getMessage(), ex);
}
}
/**
* Merges the specified document into the wrapped one. The content is merge at the specified
* text section.
* @param theDocument the document to merge.
* @param section the name of the section at which the imported document as to be inserted.
* @return the document resulting of the merge.
*/
public TextDocument merge(final TextDocument theDocument, String section) {
try {
importGlobalStylesOf(theDocument);
importContentStylesOf(theDocument);
Node theSection = getTextDocument().getSectionByName(section).getOdfElement();
insertContentTextOf(theDocument, into(theSection));
return getTextDocument();
} catch (Exception ex) {
throw new DocumentMergeException(ex.getMessage(), ex);
}
}
/**
* Imports some global styles of the specified document.
* The global styles are defined in the styles.xml files nested in the ODT document.
* Currently, only text and paragraph styles are fetched. Different master styles (id est document
* layout definitions) are also imported.
* @param document the document to import.
* @throws Exception if an error occurs while importing the global styles.
*/
protected void importGlobalStylesOf(final TextDocument document) throws Exception {
OdfOfficeStyles documentStyles = document.getDocumentStyles();
OdfOfficeStyles stylesNode = (OdfOfficeStyles) getTextDocument().getStylesDom().importNode(
documentStyles, true);
renameAndImportStyles(stylesNode.getStylesForFamily(OdfStyleFamily.Paragraph),
getTextDocument().getDocumentStyles());
renameAndImportStyles(stylesNode.getStylesForFamily(OdfStyleFamily.Text),
getTextDocument().getDocumentStyles());
// import new master styles (page layouts)
OdfOfficeMasterStyles documentMasterStyles = document.getOfficeMasterStyles();
OdfOfficeMasterStyles templateMasterStyles = getTextDocument().getOfficeMasterStyles();
Iterator<StyleMasterPageElement> masterPages = documentMasterStyles.getMasterPages();
while (masterPages.hasNext()) {
StyleMasterPageElement styleMasterPageElement = masterPages.next();
StyleMasterPageElement existingMasterPage =
templateMasterStyles.getMasterPage(styleMasterPageElement.getStyleNameAttribute());
if (existingMasterPage == null) {
Node masterPageNode = getTextDocument().getStylesDom().importNode(styleMasterPageElement,
true);
getTextDocument().getOfficeMasterStyles().appendChild(masterPageNode.cloneNode(true));
}
}
}
/**
* Imports the styles that are particular to the specified document.
* Such styles come from the customization of some global styles for the document and they are
* automatically generated by the ODT editor. They are defined within the XML element
* automatic-styles in the XML file content.xml nested in the ODT document.
* Currently, only text and paragraph styles are fetched.
* @param document the doccument to import.
* @throws Exception if an error occurs while importing the specific styles of the specified
* document.
*/
protected void importContentStylesOf(final TextDocument document) throws Exception {
OdfOfficeAutomaticStyles automaticStyles = document.getContentDom().getAutomaticStyles();
OdfOfficeAutomaticStyles automaticStylesNode = (OdfOfficeAutomaticStyles) getTextDocument().
getContentDom().importNode(automaticStyles, true);
renameAndImportStyles(automaticStylesNode.getStylesForFamily(OdfStyleFamily.Paragraph),
getTextDocument().getContentDom().getAutomaticStyles());
renameAndImportStyles(automaticStylesNode.getStylesForFamily(OdfStyleFamily.Text),
getTextDocument().getContentDom().getAutomaticStyles());
}
/**
* Imports the text content of the specified document and appends it at the specified location of
* this document.
*
* During the import of the text content, the external images referenced in the text of the
* specified document are embedded into this document.
* @param document the document to import.
* @param content a node in the XML structure of this document within which the text has to be
* imported.
* @throws Exception if an error occurs while importing the text of the document.
*/
protected void insertContentTextOf(final TextDocument document, final Node content) throws
Exception {
Node textContent = document.getContentDom().getElementsByTagName(
OpenDocumentTextElements.ELEMENT_OFFICE_TEXT).item(0);
copyXMLNode(textContent, content);
NodeList imageNodes = getTextDocument().getContentDom().getElementsByTagName(
OpenDocumentTextElements.ELEMENT_DRAW_IMAGE);
embedImages(imageNodes);
}
/**
* Renames the specified styles and then imports them in the specified location of this document.
*
* The styles are renamed by prefixing them with the terms as defined by the
* MERGE_STYLE_NAME_PREFIX constant.
* @param styles the styles to import.
* @param importNode the node in the XML structure of this document and within which styles are
* defined.
*/
protected void renameAndImportStyles(final Iterable<OdfStyle> styles, final Node importNode) {
for (OdfStyle style : styles) {
style.setStyleNameAttribute(MERGE_STYLE_NAME_PREFIX + style.getStyleNameAttribute());
String displayStyleName = style.getStyleDisplayNameAttribute();
if (isDefined(displayStyleName)) {
style.setStyleDisplayNameAttribute(MERGE_STYLE_DISPLAY_NAME_PREFIX + displayStyleName);
}
String parentStyleName = style.getStyleParentStyleNameAttribute();
if (isDefined(parentStyleName)) {
style.setStyleParentStyleNameAttribute(MERGE_STYLE_NAME_PREFIX + parentStyleName);
}
String nextStyleName = style.getStyleNextStyleNameAttribute();
if (isDefined(nextStyleName)) {
style.setStyleNextStyleNameAttribute(MERGE_STYLE_NAME_PREFIX + nextStyleName);
}
importNode.appendChild(style.cloneNode(true));
}
}
private void updateStylesIn(final Node node) {
NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
Node attribute = attributes.getNamedItem(OpenDocumentTextElements.ATTRIBUTE_STYLE_NAME);
if (attribute != null) {
String style = attribute.getNodeValue();
if (style.startsWith("P") || style.startsWith("Text")) {
attribute.setNodeValue("T3");
} else {
attribute.setNodeValue(MERGE_STYLE_NAME_PREFIX + style);
}
}
}
if (node.hasChildNodes()) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
updateStylesIn(children.item(i));
}
}
}
/**
* Copies the XML tree from the specified source node into the destination node of another XML
* tree.
* @param source the root node from which the XML tree should be copied.
* @param destination the XML node at which the source has to be copied.
* @throws Exception if an error occurs while copying a node to another one.
*/
private void copyXMLNode(final Node source, final Node destination) throws Exception {
Node importedTextContent = getTextDocument().getContentDom().importNode(source, true);
NodeList importedTextContentNodes = importedTextContent.getChildNodes();
for (int i = 0; i < importedTextContentNodes.getLength(); i++) {
Node aTextContent = importedTextContentNodes.item(i);
updateStylesIn(aTextContent);
destination.appendChild(aTextContent.cloneNode(true));
}
}
/**
* Embeds external images mapped with the specified XML nodes into this document.
* @param imageNodes a list of XML nodes mapping each of them an image.
*/
private void embedImages(final NodeList imageNodes) {
for (int i = 0; i < imageNodes.getLength(); i++) {
Node imageNode = imageNodes.item(i);
Node hrefNode = imageNode.getAttributes().getNamedItem(
OpenDocumentTextElements.ATTRIBUTE_LINK_REF);
if (hrefNode != null) {
URI imageURI = getAttachedImageURI(hrefNode.getNodeValue());
if (imageURI != null) {
Image image = Image.getInstanceof((DrawImageElement) imageNode);
Frame imageFrame = image.getFrame();
String height = imageFrame.getDrawFrameElement().getSvgHeightAttribute();
String width = imageFrame.getDrawFrameElement().getSvgWidthAttribute();
Image embeddedImage = Image.newImage(imageFrame, imageURI);
if (embeddedImage != null) {
DrawFrameElement drawFrame = embeddedImage.getFrame().getDrawFrameElement();
drawFrame.setSvgWidthAttribute(width);
drawFrame.setSvgHeightAttribute(height);
}
image.remove();
} else {
imageNode.getParentNode().removeChild(imageNode);
}
}
}
}
private URI getAttachedImageURI(String href) {
URI imageURI = null;
SilverpeasImageFinder imageFinder = SilverpeasImageFinder.getImageFinder();
try {
String imagePath = imageFinder.findImageReferencedBy(href);
imageURI = URI.create(imagePath);
} catch (Exception ex) {
SilverLogger.getLogger(this)
.error("Cannot find the image referenced by the href=''{0}''", new String[]{href}, ex);
}
return imageURI;
}
/**
* Constructs a new TextDocument instance decorated with merging features.
* As the specified text document is already opened, this one will refer the same data of the
* specified parameter.
* @see TextDocument#TextDocument(OdfPackage, String, TextDocument.OdfMediaType)
* @param document the ODT document from which the new ODTDocumentsMerging has to be constructed.
*/
private ODTDocumentsMerging(final TextDocument document) {
super(document.getPackage(), document.getDocumentPath(), TextDocument.OdfMediaType.TEXT);
}
private TextDocument getTextDocument() {
return this;
}
private static Node into(final Node node) {
return node;
}
/**
* This interface defines the qualified name of all of the XML elements that made up the text of
* an ODT document.
*/
protected static interface OpenDocumentTextElements {
/**
* The root XML element of the text content of an ODT document.
*/
final String ELEMENT_OFFICE_TEXT = "office:text";
/**
* The XML element of an image within a text content.
*/
final String ELEMENT_DRAW_IMAGE = "draw:image";
/**
* The attribute of an XML element refering an external inserted object.
*/
final String ATTRIBUTE_LINK_REF = "xlink:href";
/**
* The attribute an XML element indicating the name of a style. It can be the name of a style
* definition as well the name of the style to apply onto a text element of a text content.
*/
final String ATTRIBUTE_STYLE_NAME = "text:style-name";
}
}