ODTDocumentBuilder.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.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.odftoolkit.odfdom.dom.element.text.TextAElement;
import org.odftoolkit.simple.TextDocument;
import org.odftoolkit.simple.meta.Meta;
import org.odftoolkit.simple.table.Row;
import org.odftoolkit.simple.table.Table;
import org.odftoolkit.simple.text.Paragraph;
import org.odftoolkit.simple.text.Section;
import org.odftoolkit.simple.text.list.ListItem;
import org.silverpeas.components.kmelia.model.KmeliaPublication;
import org.silverpeas.components.kmelia.service.KmeliaService;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.comment.model.Comment;
import org.silverpeas.core.contribution.attachment.AttachmentServiceProvider;
import org.silverpeas.core.contribution.attachment.model.SimpleDocument;
import org.silverpeas.core.contribution.content.form.DataRecord;
import org.silverpeas.core.contribution.content.form.Form;
import org.silverpeas.core.contribution.content.form.PagesContext;
import org.silverpeas.core.contribution.content.form.RecordSet;
import org.silverpeas.core.contribution.content.form.RenderingContext;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygContentTransformer;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
import org.silverpeas.core.contribution.converter.DocumentFormatConverterProvider;
import org.silverpeas.core.contribution.converter.HTMLConverter;
import org.silverpeas.core.contribution.publication.model.PublicationDetail;
import org.silverpeas.core.contribution.template.publication.PublicationTemplate;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateManager;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.core.pdc.pdc.model.ClassifyPosition;
import org.silverpeas.core.pdc.pdc.model.ClassifyValue;
import org.silverpeas.core.pdc.pdc.model.Value;
import org.silverpeas.core.util.Charsets;
import org.silverpeas.kernel.bundle.LocalizationBundle;
import org.silverpeas.kernel.bundle.ResourceLocator;
import org.silverpeas.kernel.bundle.SettingBundle;
import org.silverpeas.core.util.UnitUtil;
import org.silverpeas.core.util.file.FileRepositoryManager;
import java.io.File;
import java.util.Calendar;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import static org.silverpeas.components.kmelia.export.DocumentTemplateParts.*;
import static org.silverpeas.components.kmelia.export.ODTDocumentTextTranslator.aTranslatorWith;
import static org.silverpeas.components.kmelia.export.ODTDocumentsMerging.atSection;
import static org.silverpeas.components.kmelia.export.ODTDocumentsMerging.decorates;
import static org.silverpeas.core.contribution.converter.DocumentFormat.inFormat;
import static org.silverpeas.core.contribution.converter.DocumentFormat.odt;
import static org.silverpeas.core.util.DateUtil.formatDate;
import static org.silverpeas.core.util.DateUtil.getOutputDate;
import static org.silverpeas.kernel.util.StringUtil.isDefined;
import static org.silverpeas.kernel.util.StringUtil.isInteger;
/**
* A builder of an ODT document based on a given template and from a specified Kmelia publication.
*/
public class ODTDocumentBuilder {
private static final String DOCUMENT_TEMPLATE = "kmelia.export.template";
private static final SettingBundle settings =
ResourceLocator.getSettingBundle("org.silverpeas.kmelia.settings.kmeliaSettings");
private User user;
private String language = "";
private String topicIdToConsider;
private LocalizationBundle messages;
/**
* Gets an instance of a builder of ODT documents.
* @return an ODTDocumentBuilder instance.
*/
public static ODTDocumentBuilder anODTDocumentBuilder() {
return new ODTDocumentBuilder();
}
/**
* Informs this builder the build is for the specified user. If not set, then the builds will be
* performed for the publication creator. Only information the user is authorized to access will
* be rendered into the ODT documents.
* @param user the user for which the build of the documents should be done.
* @return itself.
*/
public ODTDocumentBuilder forUser(final User user) {
this.user = user;
return this;
}
/**
* Informs this builder the prefered language to use for the content of the documents to build.
* If
* the publication doesn't have a content in the specified language, then it is the default
* publication's text that will be taken (whatever the language in which it is written).
* @param language the language in which the text should be displayed in the built documents.
* @return itself.
*/
public ODTDocumentBuilder inLanguage(String language) {
this.language = (language == null ? "" : language);
return this;
}
/**
* Informs explicitly the topic to consider when building a document from publications. This
* topic
* can be provided by the caller itself as it was already computed for the publications to export
* and according to the rights of the user on a such topic. If this topic isn't provided
* explicitly, then it is computed directly from the publication and according to the rights of
* the user on the topics the publication belongs to.
* @param topicId the topic to explicitly consider.
* @return itself.
*/
public ODTDocumentBuilder inTopic(final String topicId) {
this.topicIdToConsider = topicId;
return this;
}
/**
* A convenient method to improve the readability in the call of the method
* buildFromPublication(). It can be uses as:
* <code>File odt = builder.buildFrom(mypublication, anODTAt("/tmp/foo.odt"));</code>
* @param documentPath the path of the document to build.
* @return the document path as passed as parameter.
*/
public static String anODTAt(String documentPath) {
return documentPath;
}
/**
* Builds an ODT document at the specified path and from the specified Kmelia publication. If an
* error occurs while building the document, a runtime exception DocumentBuildException is
* thrown.
* @param publication the publication from which an ODT document is built.
* @param documentPath the path of the ODT document to build.
* @return the file corresponding to the ODT document built from the publication.
*/
public File buildFrom(final KmeliaPublication publication, String documentPath) {
boolean isUserSet = true;
try {
String odtFilePath = documentPath;
String extension = FilenameUtils.getExtension(odtFilePath);
if (!isDefined(extension) || !"odt".equalsIgnoreCase(extension)) {
odtFilePath += ".odt";
}
TextDocument odtDocument = loadTemplate();
if (getUser() == null) {
forUser(publication.getCreator());
isUserSet = false;
}
translate(odtDocument);
fill(odtDocument, with(publication));
File odtFile = new File(odtFilePath);
odtDocument.save(odtFile);
return odtFile;
} catch (Exception ex) {
throw new DocumentBuildException(ex.getMessage(), ex);
} finally {
if (!isUserSet) {
forUser(null);
}
}
}
private static KmeliaPublication with(final KmeliaPublication publication) {
return publication;
}
private static TextDocument in(final TextDocument document) {
return document;
}
/**
* Loads the template to use in the build of ODT documents.
* @return an ODT document corresponding to the loaded template.
* @throws Exception if the template loading fails.
*/
private TextDocument loadTemplate() throws Exception {
String exportTemplateDir = FileRepositoryManager.getExportTemplateRepository();
String templateDoc = settings.getString(DOCUMENT_TEMPLATE);
return TextDocument.loadDocument(new File(exportTemplateDir + templateDoc));
}
private void translate(final TextDocument odtDocument) {
ODTDocumentTextTranslator translator = aTranslatorWith(getMessagesBundle());
translator.translate(odtDocument);
}
private void fill(final TextDocument odtDocument, final KmeliaPublication publication)
throws Exception {
buildInfoSection(in(odtDocument), with(publication));
buildContentSection(in(odtDocument), with(publication));
buildAttachmentsSection(in(odtDocument), with(publication));
buildSeeAlsoSection(in(odtDocument), with(publication));
buildPdCSection(in(odtDocument), with(publication));
buildCommentSection(in(odtDocument), with(publication));
}
private void buildInfoSection(final TextDocument odtDocument,
final KmeliaPublication publication) {
PublicationDetail detail = publication.getDetail();
Meta metadata = odtDocument.getOfficeMetadata();
metadata.setCreator(detail.getCreatorName());
metadata.setCreationDate(Calendar.getInstance());
metadata.setSubject(detail.getDescription(getLanguage()));
metadata.setDcdate(Calendar.getInstance());
metadata.setTitle(detail.getName(getLanguage()));
metadata.setUserDefinedDataValue(FIELD_CREATION_DATE,
getOutputDate(detail.getCreationDate(), getLanguage()));
metadata.setUserDefinedDataValue(FIELD_MODIFICATION_DATE,
getOutputDate(detail.getLastUpdateDate(), getLanguage()));
metadata.setUserDefinedDataValue(FIELD_AUTHOR, publication.getCreator().getDisplayedName());
metadata.setUserDefinedDataValue(FIELD_LAST_MODIFIER, publication.getLastUpdater().
getDisplayedName());
metadata.setUserDefinedDataValue(FIELD_URL, publication.getURL());
metadata.setUserDefinedDataValue(FIELD_VERSION, detail.getVersion());
}
private void buildCommentSection(final TextDocument odtDocument,
final KmeliaPublication publication) {
List<Comment> comments = publication.getComments();
if (comments.isEmpty()) {
Section commentsSection = odtDocument.getSectionByName(SECTION_COMMENTS);
commentsSection.remove();
} else {
Table commentsTable = odtDocument.getTableByName(LIST_OF_COMMENTS);
int i = 1;
for (Comment comment : comments) {
Row row = commentsTable.getRowByIndex(i++);
row.getCellByIndex(0).setStringValue(comment.getCreator().getDisplayedName());
row.getCellByIndex(1).setStringValue(comment.getMessage());
row.getCellByIndex(2).setStringValue(formatDate(comment.getCreationDate()));
row.getCellByIndex(3).setStringValue(formatDate(comment.getLastUpdateDate()));
}
}
}
private void buildContentSection(final TextDocument odtDocument,
final KmeliaPublication publication) throws Exception {
String htmlContent = WysiwygController.load(publication.getComponentInstanceId(),
publication.getId(), getLanguage());
if (isDefined(htmlContent)) {
buildWithHTMLText(htmlContent, in(odtDocument));
} else {
buildWithXMLText(publication, in(odtDocument));
}
}
private void buildWithHTMLText(String htmlText, final TextDocument odtDocument) throws Exception {
Section content = odtDocument.getSectionByName(SECTION_CONTENT);
if (isDefined(htmlText)) {
String text = WysiwygContentTransformer.on(htmlText).resolveVariablesDirective().transform();
String html = "<html><body>" + text + "</body></html>";
Paragraph p = content.getParagraphByIndex(1, false);
if (p != null) {
content.removeParagraph(p);
}
File htmlFile = null;
File odtConvertedHtmlFile = null;
try {
htmlFile = new File(
FileRepositoryManager.getTemporaryPath() + UUID.randomUUID().toString() + ".html");
FileUtils.writeStringToFile(htmlFile, html, Charsets.UTF_8);
HTMLConverter converter = DocumentFormatConverterProvider.getHTMLConverter();
odtConvertedHtmlFile = converter.convert(htmlFile, inFormat(odt));
TextDocument htmlContent = TextDocument.loadDocument(odtConvertedHtmlFile);
decorates(odtDocument).merge(htmlContent, atSection(SECTION_CONTENT));
} finally {
if (htmlFile != null) {
FileUtils.deleteQuietly(htmlFile);
}
if (odtConvertedHtmlFile != null) {
FileUtils.deleteQuietly(odtConvertedHtmlFile);
}
}
} else {
content.remove();
}
}
private void buildWithXMLText(final KmeliaPublication publication, final TextDocument odtDocument)
throws Exception {
boolean removeSection = true;
String templateId = publication.getDetail().getInfoId();
if (!isInteger(templateId)) {
PublicationTemplate template = PublicationTemplateManager.getInstance().
getPublicationTemplate(publication.getComponentInstanceId() + ":" + templateId);
Form viewForm = template.getViewForm();
RecordSet recordSet = template.getRecordSet();
DataRecord dataRecord = recordSet.getRecord(publication.getId(), getLanguage());
if (dataRecord == null) {
dataRecord = recordSet.getEmptyRecord();
dataRecord.setId(publication.getId());
}
PagesContext context = new PagesContext();
context.setRenderingContext(RenderingContext.EXPORT);
context.setLanguage(getLanguage());
context.setComponentId(publication.getComponentInstanceId());
context.setObjectId(publication.getId());
context.setBorderPrinted(false);
context.setContentLanguage(getLanguage());
context.setUserId(getUser().getId());
context.setNodeId(getTopicIdOf(publication));
String htmlText = viewForm.toString(context, dataRecord);
if (isDefined(htmlText)) {
//Suppress script tag
htmlText = Pattern.compile("<script[^>]*>.*?</script>",
Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.DOTALL)
.matcher(htmlText)
.replaceAll("");
buildWithHTMLText(htmlText, in(odtDocument));
removeSection = false;
}
}
if (removeSection) {
Section contentSection = odtDocument.getSectionByName(SECTION_CONTENT);
contentSection.remove();
}
}
private void buildAttachmentsSection(final TextDocument odtDocument,
final KmeliaPublication publication) {
List<SimpleDocument> attachments = AttachmentServiceProvider.getAttachmentService()
.listDocumentsByForeignKey(publication.getIdentifier().toReference(), getLanguage());
boolean hasNoAttachmentToDisplay = true;
Table attachmentsTable = odtDocument.getTableByName(LIST_OF_ATTACHMENTS);
int i = 1;
for (SimpleDocument document : attachments) {
SimpleDocument attachment = document.getLastPublicVersion();
if (attachment != null) {
hasNoAttachmentToDisplay = false;
SimpleDocumentHolder attachmentHolder = SimpleDocumentHolder.hold(attachment);
Row row = attachmentsTable.getRowByIndex(i++);
row.getCellByIndex(0).setStringValue(attachment.getFilename());
row.getCellByIndex(1).setStringValue(attachment.getTitle());
row.getCellByIndex(2).setStringValue(attachment.getDescription());
row.getCellByIndex(3).setStringValue(UnitUtil.formatMemSize(attachment.getSize()));
row.getCellByIndex(4).setStringValue(attachmentHolder.getVersionNumber());
row.getCellByIndex(5).setStringValue(attachmentHolder.getLastModification(getLanguage()));
row.getCellByIndex(6).setStringValue(attachmentHolder.getAuthorFullName());
}
}
if (hasNoAttachmentToDisplay) {
Section attachmentsSection = odtDocument.getSectionByName(SECTION_ATTACHMENTS);
attachmentsSection.remove();
}
}
private void buildSeeAlsoSection(final TextDocument odtDocument,
final KmeliaPublication publication) {
try {
Section seeAlso = odtDocument.getSectionByName(SECTION_SEEALSO);
List<KmeliaPublication> linkedPublications =
getKmeliaService().getLinkedPublications(publication, getUser().getId());
if (linkedPublications.isEmpty()) {
seeAlso.remove();
} else {
Paragraph p = seeAlso.getParagraphByIndex(1, false);
if (p != null) {
seeAlso.removeParagraph(p);
}
for (KmeliaPublication aLinkedPublication : linkedPublications) {
PublicationDetail publicationDetail = aLinkedPublication.getDetail();
if (publicationDetail.isValid() &&
!publicationDetail.getId().equals(publication.getId())) {
TextAElement hyperlink = new TextAElement(odtDocument.getContentDom());
hyperlink.setXlinkHrefAttribute(aLinkedPublication.getURL());
hyperlink.setXlinkTypeAttribute("simple");
hyperlink.setTextContent(publicationDetail.getName(getLanguage()));
org.odftoolkit.simple.text.list.List ul = odtDocument.addList();
ListItem li = ul.addItem("");
li.getOdfElement().getFirstChild().appendChild(hyperlink);
seeAlso.getOdfElement().appendChild(ul.getOdfElement().cloneNode(true));
ul.remove();
seeAlso.addParagraph(aLinkedPublication.getLastUpdater().getDisplayedName() + " - " +
getOutputDate(publicationDetail.getLastUpdateDate(), getLanguage()));
seeAlso.addParagraph(publicationDetail.getDescription(getLanguage()));
}
}
}
} catch (Exception ex) {
throw new DocumentBuildException(ex.getMessage(), ex);
}
}
private void buildPdCSection(TextDocument odtDocument, KmeliaPublication publication) {
Section classification = odtDocument.getSectionByName(SECTION_CLASSIFICATION);
List<ClassifyPosition> positions = publication.getPDCPositions();
int pathMaxLength = 5;
int pathNodeMinNb = 2;
String pathSeparator = "/";
int rank = 1;
if (positions.isEmpty()) {
classification.remove();
} else {
Paragraph p = classification.getParagraphByIndex(1, false);
if (p != null) {
classification.removeParagraph(p);
}
for (ClassifyPosition aPosition : positions) {
rank = buildPosition(odtDocument, classification, pathMaxLength, pathNodeMinNb,
pathSeparator, rank, aPosition);
}
}
}
private int buildPosition(final TextDocument odtDocument, final Section classification,
final int pathMaxLength, final int pathNodeMinNb, final String pathSeparator, int rank,
final ClassifyPosition aPosition) {
classification.addParagraph("Position " + rank++);
org.odftoolkit.simple.text.list.List ul = odtDocument.addList();
for (ClassifyValue positionValue : aPosition.getValues()) {
buildPositionValue(pathMaxLength, pathNodeMinNb, pathSeparator, ul, positionValue);
}
classification.getOdfElement().appendChild(ul.getOdfElement().cloneNode(true));
ul.remove();
return rank;
}
private void buildPositionValue(final int pathMaxLength, final int pathNodeMinNb,
final String pathSeparator, final org.odftoolkit.simple.text.list.List ul,
final ClassifyValue positionValue) {
StringBuilder pathToRender = new StringBuilder();
List<Value> pathNodes = positionValue.getFullPath();
int pathLength = pathNodes.size();
if (pathLength > pathMaxLength) {
for (int i = 0; i < pathNodeMinNb; i++) {
pathToRender.append(pathNodes.get(i).getName(getLanguage())).append(pathSeparator);
}
pathToRender.append("...").append(pathSeparator);
for (int i = pathNodeMinNb; i > 0; i--) {
pathToRender.append(pathNodes.get(pathLength - i).getName(getLanguage()))
.append(pathSeparator);
}
} else {
for (Value pathNode : pathNodes) {
pathToRender.append(pathNode.getName(getLanguage())).append(pathSeparator);
}
}
if (!pathSeparator.equals(pathToRender.toString()) && pathToRender.length() > 0) {
ul.addItem(pathToRender.substring(0, pathToRender.length() - pathSeparator.length()));
}
}
private String getTopicIdOf(final KmeliaPublication publication) {
String theTopicId = this.topicIdToConsider;
if (theTopicId == null) {
NodePK pk = getKmeliaService().
getBestLocationOfPublicationForUser(publication.getPk(), getUser().getId());
theTopicId = pk.getId();
}
return theTopicId;
}
private String getLanguage() {
return this.language;
}
private User getUser() {
return this.user;
}
private LocalizationBundle getMessagesBundle() {
if (this.messages == null) {
this.messages =
ResourceLocator.getLocalizationBundle("org.silverpeas.kmelia.multilang.kmeliaExport", getLanguage());
}
return this.messages;
}
/**
* Gets the Kmelia service.
* @return an instance of KmeliaService.
*/
protected KmeliaService getKmeliaService() {
return KmeliaService.get();
}
}