1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include "XMLRedlineExport.hxx"
21 #include <o3tl/any.hxx>
22 #include <tools/debug.hxx>
23 #include <rtl/ustring.hxx>
24 #include <rtl/ustrbuf.hxx>
25 #include <sal/log.hxx>
26 #include <osl/diagnose.h>
27 #include <com/sun/star/frame/XModel.hpp>
28 #include <com/sun/star/beans/XPropertySet.hpp>
29 #include <com/sun/star/beans/UnknownPropertyException.hpp>
30 #include <com/sun/star/container/XEnumerationAccess.hpp>
32 #include <com/sun/star/container/XEnumeration.hpp>
33 #include <com/sun/star/document/XRedlinesSupplier.hpp>
34 #include <com/sun/star/text/XText.hpp>
35 #include <com/sun/star/text/XTextContent.hpp>
36 #include <com/sun/star/text/XTextSection.hpp>
37 #include <com/sun/star/util/DateTime.hpp>
39 #include <sax/tools/converter.hxx>
41 #include <xmloff/xmltoken.hxx>
42 #include <xmloff/xmlnamespace.hxx>
43 #include <xmloff/xmlexp.hxx>
44 #include <xmloff/xmluconv.hxx>
45 #include <unotools/securityoptions.hxx>
46 #include <tools/date.hxx>
47 #include <tools/datetime.hxx>
50 using namespace ::com::sun::star
;
51 using namespace ::xmloff::token
;
53 using ::com::sun::star::beans::PropertyValue
;
54 using ::com::sun::star::beans::XPropertySet
;
55 using ::com::sun::star::beans::UnknownPropertyException
;
56 using ::com::sun::star::document::XRedlinesSupplier
;
57 using ::com::sun::star::container::XEnumerationAccess
;
58 using ::com::sun::star::container::XEnumeration
;
59 using ::com::sun::star::text::XText
;
60 using ::com::sun::star::text::XTextContent
;
61 using ::com::sun::star::text::XTextSection
;
62 using ::com::sun::star::uno::Any
;
63 using ::com::sun::star::uno::Reference
;
64 using ::com::sun::star::uno::Sequence
;
67 XMLRedlineExport::XMLRedlineExport(SvXMLExport
& rExp
)
68 : sDeletion(GetXMLToken(XML_DELETION
))
69 , sFormatChange(GetXMLToken(XML_FORMAT_CHANGE
))
70 , sInsertion(GetXMLToken(XML_INSERTION
))
72 , pCurrentChangesList(nullptr)
77 XMLRedlineExport::~XMLRedlineExport()
82 void XMLRedlineExport::ExportChange(
83 const Reference
<XPropertySet
> & rPropSet
,
88 // For the headers/footers, we have to collect the autostyles
89 // here. For the general case, however, it's better to collect
90 // the autostyles by iterating over the global redline
91 // list. So that's what we do: Here, we collect autostyles
92 // only if we have no current list of changes. For the
93 // main-document case, the autostyles are collected in
94 // ExportChangesListAutoStyles().
95 if (pCurrentChangesList
!= nullptr)
96 ExportChangeAutoStyle(rPropSet
);
100 ExportChangeInline(rPropSet
);
105 void XMLRedlineExport::ExportChangesList(bool bAutoStyles
)
109 ExportChangesListAutoStyles();
113 ExportChangesListElements();
118 void XMLRedlineExport::ExportChangesList(
119 const Reference
<XText
> & rText
,
122 // in the header/footer case, auto styles are collected from the
123 // inline change elements.
127 // look for changes list for this XText
128 ChangesMapType::iterator aFind
= aChangeMap
.find(rText
);
129 if (aFind
== aChangeMap
.end())
132 ChangesVectorType
* pChangesList
= aFind
->second
.get();
134 // export only if changes are found
135 if (pChangesList
->empty())
138 // changes container element
139 SvXMLElementExport
aChanges(rExport
, XML_NAMESPACE_TEXT
,
143 // iterate over changes list
144 for (auto const& change
: *pChangesList
)
146 ExportChangedRegion(change
);
148 // else: changes list empty -> ignore
149 // else: no changes list found -> empty
152 void XMLRedlineExport::SetCurrentXText(
153 const Reference
<XText
> & rText
)
157 // look for appropriate list in map; use the found one, or create new
158 ChangesMapType::iterator aIter
= aChangeMap
.find(rText
);
159 if (aIter
== aChangeMap
.end())
161 ChangesVectorType
* pList
= new ChangesVectorType
;
162 aChangeMap
[rText
].reset( pList
);
163 pCurrentChangesList
= pList
;
166 pCurrentChangesList
= aIter
->second
.get();
170 // don't record changes
175 void XMLRedlineExport::SetCurrentXText()
177 pCurrentChangesList
= nullptr;
181 void XMLRedlineExport::ExportChangesListElements()
183 // get redlines (aka tracked changes) from the model
184 Reference
<XRedlinesSupplier
> xSupplier(rExport
.GetModel(), uno::UNO_QUERY
);
188 Reference
<XEnumerationAccess
> aEnumAccess
= xSupplier
->getRedlines();
190 // redline protection key
191 Reference
<XPropertySet
> aDocPropertySet( rExport
.GetModel(),
193 // redlining enabled?
194 bool bEnabled
= *o3tl::doAccess
<bool>(aDocPropertySet
->getPropertyValue(
197 // only export if we have redlines or attributes
198 if ( !(aEnumAccess
->hasElements() || bEnabled
) )
202 // export only if we have changes, but tracking is not enabled
203 if ( !bEnabled
!= !aEnumAccess
->hasElements() )
205 rExport
.AddAttribute(
206 XML_NAMESPACE_TEXT
, XML_TRACK_CHANGES
,
207 bEnabled
? XML_TRUE
: XML_FALSE
);
210 // changes container element
211 SvXMLElementExport
aChanges(rExport
, XML_NAMESPACE_TEXT
,
215 // get enumeration and iterate over elements
216 Reference
<XEnumeration
> aEnum
= aEnumAccess
->createEnumeration();
217 while (aEnum
->hasMoreElements())
219 Any aAny
= aEnum
->nextElement();
220 Reference
<XPropertySet
> xPropSet
;
223 DBG_ASSERT(xPropSet
.is(),
224 "can't get XPropertySet; skipping Redline");
227 // export only if not in header or footer
228 // (those must be exported with their XText)
229 aAny
= xPropSet
->getPropertyValue("IsInHeaderFooter");
230 if (! *o3tl::doAccess
<bool>(aAny
))
232 // and finally, export change
233 ExportChangedRegion(xPropSet
);
236 // else: no XPropertySet -> no export
238 // else: no redlines -> no export
239 // else: no XRedlineSupplier -> no export
242 void XMLRedlineExport::ExportChangeAutoStyle(
243 const Reference
<XPropertySet
> & rPropSet
)
245 // record change (if changes should be recorded)
246 if (nullptr != pCurrentChangesList
)
248 // put redline in list if it's collapsed or the redline start
249 Any aIsStart
= rPropSet
->getPropertyValue("IsStart");
250 Any aIsCollapsed
= rPropSet
->getPropertyValue("IsCollapsed");
252 if ( *o3tl::doAccess
<bool>(aIsStart
) ||
253 *o3tl::doAccess
<bool>(aIsCollapsed
) )
254 pCurrentChangesList
->push_back(rPropSet
);
257 // get XText for export of redline auto styles
258 Any aAny
= rPropSet
->getPropertyValue("RedlineText");
259 Reference
<XText
> xText
;
263 // export the auto styles
264 rExport
.GetTextParagraphExport()->collectTextAutoStyles(xText
);
268 void XMLRedlineExport::ExportChangesListAutoStyles()
270 // get redlines (aka tracked changes) from the model
271 Reference
<XRedlinesSupplier
> xSupplier(rExport
.GetModel(), uno::UNO_QUERY
);
275 Reference
<XEnumerationAccess
> aEnumAccess
= xSupplier
->getRedlines();
277 // only export if we actually have redlines
278 if (!aEnumAccess
->hasElements())
281 // get enumeration and iterate over elements
282 Reference
<XEnumeration
> aEnum
= aEnumAccess
->createEnumeration();
283 while (aEnum
->hasMoreElements())
285 Any aAny
= aEnum
->nextElement();
286 Reference
<XPropertySet
> xPropSet
;
289 DBG_ASSERT(xPropSet
.is(),
290 "can't get XPropertySet; skipping Redline");
294 // export only if not in header or footer
295 // (those must be exported with their XText)
296 aAny
= xPropSet
->getPropertyValue("IsInHeaderFooter");
297 if (! *o3tl::doAccess
<bool>(aAny
))
299 ExportChangeAutoStyle(xPropSet
);
305 void XMLRedlineExport::ExportChangeInline(
306 const Reference
<XPropertySet
> & rPropSet
)
308 // determine element name (depending on collapsed, start/end)
309 enum XMLTokenEnum eElement
= XML_TOKEN_INVALID
;
310 Any aAny
= rPropSet
->getPropertyValue("IsCollapsed");
311 bool bCollapsed
= *o3tl::doAccess
<bool>(aAny
);
314 eElement
= XML_CHANGE
;
318 aAny
= rPropSet
->getPropertyValue("IsStart");
319 const bool bStart
= *o3tl::doAccess
<bool>(aAny
);
320 eElement
= bStart
? XML_CHANGE_START
: XML_CHANGE_END
;
323 if (XML_TOKEN_INVALID
!= eElement
)
325 // we always need the ID
326 rExport
.AddAttribute(XML_NAMESPACE_TEXT
, XML_CHANGE_ID
,
327 GetRedlineID(rPropSet
));
329 // export the element (no whitespace because we're in the text body)
330 SvXMLElementExport
aChangeElem(rExport
, XML_NAMESPACE_TEXT
,
331 eElement
, false, false);
336 void XMLRedlineExport::ExportChangedRegion(
337 const Reference
<XPropertySet
> & rPropSet
)
340 rExport
.AddAttributeIdLegacy(XML_NAMESPACE_TEXT
, GetRedlineID(rPropSet
));
342 // merge-last-paragraph
343 Any aAny
= rPropSet
->getPropertyValue("MergeLastPara");
344 if( ! *o3tl::doAccess
<bool>(aAny
) )
345 rExport
.AddAttribute(XML_NAMESPACE_TEXT
, XML_MERGE_LAST_PARAGRAPH
,
348 // export change region element
349 SvXMLElementExport
aChangedRegion(rExport
, XML_NAMESPACE_TEXT
,
350 XML_CHANGED_REGION
, true, true);
353 // scope for (first) change element
355 aAny
= rPropSet
->getPropertyValue("RedlineType");
358 SvXMLElementExport
aChange(rExport
, XML_NAMESPACE_TEXT
,
359 ConvertTypeName(sType
), true, true);
361 ExportChangeInfo(rPropSet
);
363 // get XText from the redline and export (if the XText exists)
364 aAny
= rPropSet
->getPropertyValue("RedlineText");
365 Reference
<XText
> xText
;
369 rExport
.GetTextParagraphExport()->exportText(xText
);
370 // default parameters: bProgress, bExportParagraph ???
372 // else: no text interface -> content is inline and will
376 // changed change? Hierarchical changes can only be two levels
377 // deep. Here we check for the second level.
378 aAny
= rPropSet
->getPropertyValue("RedlineSuccessorData");
379 Sequence
<PropertyValue
> aSuccessorData
;
380 aAny
>>= aSuccessorData
;
382 // if we actually got a hierarchical change, make element and
383 // process change info
384 if (aSuccessorData
.hasElements())
386 // The only change that can be "undone" is an insertion -
387 // after all, you can't re-insert a deletion, but you can
388 // delete an insertion. This assumption is asserted in
389 // ExportChangeInfo(Sequence<PropertyValue>&).
390 SvXMLElementExport
aSecondChangeElem(
391 rExport
, XML_NAMESPACE_TEXT
, XML_INSERTION
,
394 ExportChangeInfo(aSuccessorData
);
396 // else: no hierarchical change
400 OUString
const & XMLRedlineExport::ConvertTypeName(
401 std::u16string_view sApiName
)
403 if (sApiName
== u
"Delete")
407 else if (sApiName
== u
"Insert")
411 else if (sApiName
== u
"Format")
413 return sFormatChange
;
417 OSL_FAIL("unknown redline type");
418 static const OUString
sUnknownChange("UnknownChange");
419 return sUnknownChange
;
424 /** Create a Redline-ID */
425 OUString
XMLRedlineExport::GetRedlineID(
426 const Reference
<XPropertySet
> & rPropSet
)
428 Any aAny
= rPropSet
->getPropertyValue("RedlineIdentifier");
436 void XMLRedlineExport::ExportChangeInfo(
437 const Reference
<XPropertySet
> & rPropSet
)
439 bool bRemovePersonalInfo
= SvtSecurityOptions::IsOptionSet(
440 SvtSecurityOptions::EOption::DocWarnRemovePersonalInfo
);
442 SvXMLElementExport
aChangeInfo(rExport
, XML_NAMESPACE_OFFICE
,
443 XML_CHANGE_INFO
, true, true);
445 Any aAny
= rPropSet
->getPropertyValue("RedlineAuthor");
450 SvXMLElementExport
aCreatorElem( rExport
, XML_NAMESPACE_DC
,
453 rExport
.Characters(bRemovePersonalInfo
454 ? "Author" + OUString::number(rExport
.GetInfoID(sTmp
))
458 aAny
= rPropSet
->getPropertyValue("RedlineDateTime");
459 util::DateTime aDateTime
;
463 ::sax::Converter::convertDateTime(sBuf
, bRemovePersonalInfo
464 ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
465 : aDateTime
, nullptr);
466 SvXMLElementExport
aDateElem( rExport
, XML_NAMESPACE_DC
,
469 rExport
.Characters(sBuf
.makeStringAndClear());
472 // comment as <text:p> sequence
473 aAny
= rPropSet
->getPropertyValue("RedlineComment");
475 WriteComment( sTmp
);
478 void XMLRedlineExport::ExportChangeInfo(
479 const Sequence
<PropertyValue
> & rPropertyValues
)
482 bool bRemovePersonalInfo
= SvtSecurityOptions::IsOptionSet(
483 SvtSecurityOptions::EOption::DocWarnRemovePersonalInfo
);
485 for(const PropertyValue
& rVal
: rPropertyValues
)
487 if( rVal
.Name
== "RedlineAuthor" )
493 rExport
.AddAttribute(XML_NAMESPACE_OFFICE
, XML_CHG_AUTHOR
, bRemovePersonalInfo
494 ? "Author" + OUString::number(rExport
.GetInfoID(sTmp
))
498 else if( rVal
.Name
== "RedlineComment" )
500 rVal
.Value
>>= sComment
;
502 else if( rVal
.Name
== "RedlineDateTime" )
504 util::DateTime aDateTime
;
505 rVal
.Value
>>= aDateTime
;
507 ::sax::Converter::convertDateTime(sBuf
, bRemovePersonalInfo
508 ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
509 : aDateTime
, nullptr);
510 rExport
.AddAttribute(XML_NAMESPACE_OFFICE
, XML_CHG_DATE_TIME
, sBuf
.makeStringAndClear());
512 else if( rVal
.Name
== "RedlineType" )
514 // check if this is an insertion; cf. comment at calling location
517 DBG_ASSERT(sTmp
== "Insert",
518 "hierarchical change must be insertion");
520 // else: unknown value -> ignore
523 // finally write element
524 SvXMLElementExport
aChangeInfo(rExport
, XML_NAMESPACE_OFFICE
,
525 XML_CHANGE_INFO
, true, true);
527 WriteComment( sComment
);
530 void XMLRedlineExport::ExportStartOrEndRedline(
531 const Reference
<XPropertySet
> & rPropSet
,
534 if( ! rPropSet
.is() )
537 // get appropriate (start or end) property
541 aAny
= rPropSet
->getPropertyValue(bStart
? OUString("StartRedline") : OUString("EndRedline"));
543 catch(const UnknownPropertyException
&)
545 // If we don't have the property, there's nothing to do.
549 Sequence
<PropertyValue
> aValues
;
552 // seek for redline properties
553 bool bIsCollapsed
= false;
554 bool bIsStart
= true;
556 bool bIdOK
= false; // have we seen an ID?
557 for(const auto& rValue
: std::as_const(aValues
))
559 if (rValue
.Name
== "RedlineIdentifier")
561 rValue
.Value
>>= sId
;
564 else if (rValue
.Name
== "IsCollapsed")
566 bIsCollapsed
= *o3tl::doAccess
<bool>(rValue
.Value
);
568 else if (rValue
.Name
== "IsStart")
570 bIsStart
= *o3tl::doAccess
<bool>(rValue
.Value
);
577 SAL_WARN_IF( sId
.isEmpty(), "xmloff", "Redlines must have IDs" );
579 // TODO: use GetRedlineID or eliminate that function
580 rExport
.AddAttribute(XML_NAMESPACE_TEXT
, XML_CHANGE_ID
,
583 // export the element
584 // (whitespace because we're not inside paragraphs)
585 SvXMLElementExport
aChangeElem(
586 rExport
, XML_NAMESPACE_TEXT
,
587 bIsCollapsed
? XML_CHANGE
:
588 ( bIsStart
? XML_CHANGE_START
: XML_CHANGE_END
),
592 void XMLRedlineExport::ExportStartOrEndRedline(
593 const Reference
<XTextContent
> & rContent
,
596 Reference
<XPropertySet
> xPropSet(rContent
, uno::UNO_QUERY
);
599 ExportStartOrEndRedline(xPropSet
, bStart
);
603 OSL_FAIL("XPropertySet expected");
607 void XMLRedlineExport::ExportStartOrEndRedline(
608 const Reference
<XTextSection
> & rSection
,
611 Reference
<XPropertySet
> xPropSet(rSection
, uno::UNO_QUERY
);
614 ExportStartOrEndRedline(xPropSet
, bStart
);
618 OSL_FAIL("XPropertySet expected");
622 void XMLRedlineExport::WriteComment(std::u16string_view rComment
)
624 if (rComment
.empty())
627 // iterate over all string-pieces separated by return (0x0a) and
628 // put each inside a paragraph element.
629 SvXMLTokenEnumerator
aEnumerator(rComment
, char(0x0a));
630 std::u16string_view aSubString
;
631 while (aEnumerator
.getNextToken(aSubString
))
633 SvXMLElementExport
aParagraph(
634 rExport
, XML_NAMESPACE_TEXT
, XML_P
, true, false);
635 rExport
.Characters(OUString(aSubString
));
639 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */