LibreOffice Module xmloff (master) 1
XMLRedlineExport.cxx
Go to the documentation of this file.
1/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3 * This file is part of the LibreOffice project.
4 *
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/.
8 *
9 * This file incorporates work covered by the following license notice:
10 *
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 .
18 */
19
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>
31
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>
38
40
41#include <xmloff/xmltoken.hxx>
43#include <xmloff/xmlexp.hxx>
44#include <xmloff/xmluconv.hxx>
46#include <tools/date.hxx>
47#include <tools/datetime.hxx>
48
49
50using namespace ::com::sun::star;
51using namespace ::xmloff::token;
52
53using ::com::sun::star::beans::PropertyValue;
54using ::com::sun::star::beans::XPropertySet;
55using ::com::sun::star::beans::UnknownPropertyException;
56using ::com::sun::star::document::XRedlinesSupplier;
57using ::com::sun::star::container::XEnumerationAccess;
58using ::com::sun::star::container::XEnumeration;
59using ::com::sun::star::text::XText;
60using ::com::sun::star::text::XTextContent;
61using ::com::sun::star::text::XTextSection;
62using ::com::sun::star::uno::Any;
63using ::com::sun::star::uno::Reference;
64using ::com::sun::star::uno::Sequence;
65
66
68: sDeletion(GetXMLToken(XML_DELETION))
69, sFormatChange(GetXMLToken(XML_FORMAT_CHANGE))
70, sInsertion(GetXMLToken(XML_INSERTION))
71, rExport(rExp)
72, pCurrentChangesList(nullptr)
73{
74}
75
76
78{
79}
80
81
83 const Reference<XPropertySet> & rPropSet,
84 bool bAutoStyle)
85{
86 if (bAutoStyle)
87 {
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);
97 }
98 else
99 {
100 ExportChangeInline(rPropSet);
101 }
102}
103
104
106{
107 if (bAutoStyles)
108 {
110 }
111 else
112 {
114 }
115}
116
117
119 const Reference<XText> & rText,
120 bool bAutoStyles)
121{
122 // in the header/footer case, auto styles are collected from the
123 // inline change elements.
124 if (bAutoStyles)
125 return;
126
127 // look for changes list for this XText
128 ChangesMapType::iterator aFind = aChangeMap.find(rText);
129 if (aFind == aChangeMap.end())
130 return;
131
132 ChangesVectorType& rChangesList = aFind->second;
133
134 // export only if changes are found
135 if (rChangesList.empty())
136 return;
137
138 // changes container element
141 true, true);
142
143 // iterate over changes list
144 for (auto const& change : rChangesList)
145 {
146 ExportChangedRegion(change);
147 }
148 // else: changes list empty -> ignore
149 // else: no changes list found -> empty
150}
151
153 const Reference<XText> & rText)
154{
155 if (rText.is())
156 {
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())
160 {
161 auto rv = aChangeMap.emplace(std::piecewise_construct, std::forward_as_tuple(rText), std::forward_as_tuple());
162 pCurrentChangesList = &rv.first->second;
163 }
164 else
165 pCurrentChangesList = &aIter->second;
166 }
167 else
168 {
169 // don't record changes
171 }
172}
173
175{
176 pCurrentChangesList = nullptr;
177}
178
179
181{
182 // get redlines (aka tracked changes) from the model
183 Reference<XRedlinesSupplier> xSupplier(rExport.GetModel(), uno::UNO_QUERY);
184 if (!xSupplier.is())
185 return;
186
187 Reference<XEnumerationAccess> aEnumAccess = xSupplier->getRedlines();
188
189 // redline protection key
190 Reference<XPropertySet> aDocPropertySet( rExport.GetModel(),
191 uno::UNO_QUERY );
192 // redlining enabled?
193 bool bEnabled = *o3tl::doAccess<bool>(aDocPropertySet->getPropertyValue(
194 "RecordChanges" ));
195
196 // only export if we have redlines or attributes
197 if ( !(aEnumAccess->hasElements() || bEnabled) )
198 return;
199
200
201 // export only if we have changes, but tracking is not enabled
202 if ( !bEnabled != !aEnumAccess->hasElements() )
203 {
206 bEnabled ? XML_TRUE : XML_FALSE );
207 }
208
209 // changes container element
212 true, true);
213
214 // get enumeration and iterate over elements
215 Reference<XEnumeration> aEnum = aEnumAccess->createEnumeration();
216 while (aEnum->hasMoreElements())
217 {
218 Any aAny = aEnum->nextElement();
219 Reference<XPropertySet> xPropSet;
220 aAny >>= xPropSet;
221
222 DBG_ASSERT(xPropSet.is(),
223 "can't get XPropertySet; skipping Redline");
224 if (xPropSet.is())
225 {
226 // export only if not in header or footer
227 // (those must be exported with their XText)
228 aAny = xPropSet->getPropertyValue("IsInHeaderFooter");
229 if (! *o3tl::doAccess<bool>(aAny))
230 {
231 // and finally, export change
232 ExportChangedRegion(xPropSet);
233 }
234 }
235 // else: no XPropertySet -> no export
236 }
237 // else: no redlines -> no export
238 // else: no XRedlineSupplier -> no export
239}
240
242 const Reference<XPropertySet> & rPropSet)
243{
244 // record change (if changes should be recorded)
245 if (nullptr != pCurrentChangesList)
246 {
247 // put redline in list if it's collapsed or the redline start
248 Any aIsStart = rPropSet->getPropertyValue("IsStart");
249 Any aIsCollapsed = rPropSet->getPropertyValue("IsCollapsed");
250
251 if ( *o3tl::doAccess<bool>(aIsStart) ||
252 *o3tl::doAccess<bool>(aIsCollapsed) )
253 pCurrentChangesList->push_back(rPropSet);
254 }
255
256 // get XText for export of redline auto styles
257 Any aAny = rPropSet->getPropertyValue("RedlineText");
258 Reference<XText> xText;
259 aAny >>= xText;
260 if (xText.is())
261 {
262 // export the auto styles
263 rExport.GetTextParagraphExport()->collectTextAutoStyles(xText);
264 }
265}
266
268{
269 // get redlines (aka tracked changes) from the model
270 Reference<XRedlinesSupplier> xSupplier(rExport.GetModel(), uno::UNO_QUERY);
271 if (!xSupplier.is())
272 return;
273
274 Reference<XEnumerationAccess> aEnumAccess = xSupplier->getRedlines();
275
276 // only export if we actually have redlines
277 if (!aEnumAccess->hasElements())
278 return;
279
280 // get enumeration and iterate over elements
281 Reference<XEnumeration> aEnum = aEnumAccess->createEnumeration();
282 while (aEnum->hasMoreElements())
283 {
284 Any aAny = aEnum->nextElement();
285 Reference<XPropertySet> xPropSet;
286 aAny >>= xPropSet;
287
288 DBG_ASSERT(xPropSet.is(),
289 "can't get XPropertySet; skipping Redline");
290 if (xPropSet.is())
291 {
292
293 // export only if not in header or footer
294 // (those must be exported with their XText)
295 aAny = xPropSet->getPropertyValue("IsInHeaderFooter");
296 if (! *o3tl::doAccess<bool>(aAny))
297 {
298 ExportChangeAutoStyle(xPropSet);
299 }
300 }
301 }
302}
303
305 const Reference<XPropertySet> & rPropSet)
306{
307 // determine element name (depending on collapsed, start/end)
308 enum XMLTokenEnum eElement = XML_TOKEN_INVALID;
309 Any aAny = rPropSet->getPropertyValue("IsCollapsed");
310 bool bCollapsed = *o3tl::doAccess<bool>(aAny);
311 if (bCollapsed)
312 {
313 eElement = XML_CHANGE;
314 }
315 else
316 {
317 aAny = rPropSet->getPropertyValue("IsStart");
318 const bool bStart = *o3tl::doAccess<bool>(aAny);
319 eElement = bStart ? XML_CHANGE_START : XML_CHANGE_END;
320 }
321
322 if (XML_TOKEN_INVALID != eElement)
323 {
324 // we always need the ID
326 GetRedlineID(rPropSet));
327
328 // export the element (no whitespace because we're in the text body)
330 eElement, false, false);
331 }
332}
333
334
336 const Reference<XPropertySet> & rPropSet)
337{
338 // Redline-ID
340
341 // merge-last-paragraph
342 Any aAny = rPropSet->getPropertyValue("MergeLastPara");
343 if( ! *o3tl::doAccess<bool>(aAny) )
345 XML_FALSE);
346
347 // export change region element
349 XML_CHANGED_REGION, true, true);
350
351
352 // scope for (first) change element
353 {
354 aAny = rPropSet->getPropertyValue("RedlineType");
355 OUString sType;
356 aAny >>= sType;
358 ConvertTypeName(sType), true, true);
359
360 ExportChangeInfo(rPropSet);
361
362 // get XText from the redline and export (if the XText exists)
363 aAny = rPropSet->getPropertyValue("RedlineText");
364 Reference<XText> xText;
365 aAny >>= xText;
366 if (xText.is())
367 {
368 rExport.GetTextParagraphExport()->exportText(xText);
369 // default parameters: bProgress, bExportParagraph ???
370 }
371 // else: no text interface -> content is inline and will
372 // be exported there
373 }
374
375 // changed change? Hierarchical changes can only be two levels
376 // deep. Here we check for the second level.
377 aAny = rPropSet->getPropertyValue("RedlineSuccessorData");
378 Sequence<PropertyValue> aSuccessorData;
379 aAny >>= aSuccessorData;
380
381 // if we actually got a hierarchical change, make element and
382 // process change info
383 if (aSuccessorData.hasElements())
384 {
385 // The only change that can be "undone" is an insertion -
386 // after all, you can't re-insert a deletion, but you can
387 // delete an insertion. This assumption is asserted in
388 // ExportChangeInfo(Sequence<PropertyValue>&).
389 SvXMLElementExport aSecondChangeElem(
391 true, true);
392
393 ExportChangeInfo(aSuccessorData);
394 }
395 // else: no hierarchical change
396}
397
398
400 std::u16string_view sApiName)
401{
402 if (sApiName == u"Delete")
403 {
404 return sDeletion;
405 }
406 else if (sApiName == u"Insert")
407 {
408 return sInsertion;
409 }
410 else if (sApiName == u"Format")
411 {
412 return sFormatChange;
413 }
414 else
415 {
416 OSL_FAIL("unknown redline type");
417 static const OUString sUnknownChange("UnknownChange");
418 return sUnknownChange;
419 }
420}
421
422
425 const Reference<XPropertySet> & rPropSet)
426{
427 Any aAny = rPropSet->getPropertyValue("RedlineIdentifier");
428 OUString sTmp;
429 aAny >>= sTmp;
430
431 return "ct" + sTmp;
432}
433
434
436 const Reference<XPropertySet> & rPropSet)
437{
438 bool bRemovePersonalInfo = SvtSecurityOptions::IsOptionSet(
440
442 XML_CHANGE_INFO, true, true);
443
444 Any aAny = rPropSet->getPropertyValue("RedlineAuthor");
445 OUString sTmp;
446 aAny >>= sTmp;
447 if (!sTmp.isEmpty())
448 {
450 XML_CREATOR, true,
451 false );
452 rExport.Characters(bRemovePersonalInfo
453 ? "Author" + OUString::number(rExport.GetInfoID(sTmp))
454 : sTmp );
455 }
456
457 aAny = rPropSet->getPropertyValue("RedlineDateTime");
458 util::DateTime aDateTime;
459 aAny >>= aDateTime;
460 {
461 OUStringBuffer sBuf;
462 ::sax::Converter::convertDateTime(sBuf, bRemovePersonalInfo
463 ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
464 : aDateTime, nullptr);
466 XML_DATE, true,
467 false );
468 rExport.Characters(sBuf.makeStringAndClear());
469 }
470
471 // comment as <text:p> sequence
472 aAny = rPropSet->getPropertyValue("RedlineComment");
473 aAny >>= sTmp;
474 WriteComment( sTmp );
475}
476
477// write RedlineSuccessorData
479 const Sequence<PropertyValue> & rPropertyValues)
480{
481 OUString sComment;
482 bool bRemovePersonalInfo = SvtSecurityOptions::IsOptionSet(
484
486 XML_CHANGE_INFO, true, true);
487
488 for(const PropertyValue& rVal : rPropertyValues)
489 {
490 if( rVal.Name == "RedlineAuthor" )
491 {
492 OUString sTmp;
493 rVal.Value >>= sTmp;
494 if (!sTmp.isEmpty())
495 {
497 XML_CREATOR, true,
498 false );
499 rExport.Characters(bRemovePersonalInfo
500 ? "Author" + OUString::number(rExport.GetInfoID(sTmp))
501 : sTmp );
502 }
503 }
504 else if( rVal.Name == "RedlineComment" )
505 {
506 rVal.Value >>= sComment;
507 }
508 else if( rVal.Name == "RedlineDateTime" )
509 {
510 util::DateTime aDateTime;
511 rVal.Value >>= aDateTime;
512 OUStringBuffer sBuf;
513 ::sax::Converter::convertDateTime(sBuf, bRemovePersonalInfo
514 ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
515 : aDateTime, nullptr);
517 XML_DATE, true,
518 false );
519 rExport.Characters(sBuf.makeStringAndClear());
520 }
521 else if( rVal.Name == "RedlineType" )
522 {
523 // check if this is an insertion; cf. comment at calling location
524 OUString sTmp;
525 rVal.Value >>= sTmp;
526 DBG_ASSERT(sTmp == "Insert",
527 "hierarchical change must be insertion");
528 }
529 // else: unknown value -> ignore
530 }
531
532 // finally write comment paragraphs
533 WriteComment( sComment );
534}
535
537 const Reference<XPropertySet> & rPropSet,
538 bool bStart)
539{
540 if( ! rPropSet.is() )
541 return;
542
543 // get appropriate (start or end) property
544 Any aAny;
545 try
546 {
547 aAny = rPropSet->getPropertyValue(bStart ? OUString("StartRedline") : OUString("EndRedline"));
548 }
549 catch(const UnknownPropertyException&)
550 {
551 // If we don't have the property, there's nothing to do.
552 return;
553 }
554
555 Sequence<PropertyValue> aValues;
556 aAny >>= aValues;
557
558 // seek for redline properties
559 bool bIsCollapsed = false;
560 bool bIsStart = true;
561 OUString sId;
562 bool bIdOK = false; // have we seen an ID?
563 for(const auto& rValue : std::as_const(aValues))
564 {
565 if (rValue.Name == "RedlineIdentifier")
566 {
567 rValue.Value >>= sId;
568 bIdOK = true;
569 }
570 else if (rValue.Name == "IsCollapsed")
571 {
572 bIsCollapsed = *o3tl::doAccess<bool>(rValue.Value);
573 }
574 else if (rValue.Name == "IsStart")
575 {
576 bIsStart = *o3tl::doAccess<bool>(rValue.Value);
577 }
578 }
579
580 if( !bIdOK )
581 return;
582
583 SAL_WARN_IF( sId.isEmpty(), "xmloff", "Redlines must have IDs" );
584
585 // TODO: use GetRedlineID or eliminate that function
587 "ct" + sId);
588
589 // export the element
590 // (whitespace because we're not inside paragraphs)
591 SvXMLElementExport aChangeElem(
593 bIsCollapsed ? XML_CHANGE :
594 ( bIsStart ? XML_CHANGE_START : XML_CHANGE_END ),
595 true, true);
596}
597
599 const Reference<XTextContent> & rContent,
600 bool bStart)
601{
602 Reference<XPropertySet> xPropSet(rContent, uno::UNO_QUERY);
603 if (xPropSet.is())
604 {
605 ExportStartOrEndRedline(xPropSet, bStart);
606 }
607 else
608 {
609 OSL_FAIL("XPropertySet expected");
610 }
611}
612
614 const Reference<XTextSection> & rSection,
615 bool bStart)
616{
617 Reference<XPropertySet> xPropSet(rSection, uno::UNO_QUERY);
618 if (xPropSet.is())
619 {
620 ExportStartOrEndRedline(xPropSet, bStart);
621 }
622 else
623 {
624 OSL_FAIL("XPropertySet expected");
625 }
626}
627
628void XMLRedlineExport::WriteComment(std::u16string_view rComment)
629{
630 if (rComment.empty())
631 return;
632
633 // iterate over all string-pieces separated by return (0x0a) and
634 // put each inside a paragraph element.
635 SvXMLTokenEnumerator aEnumerator(rComment, char(0x0a));
636 std::u16string_view aSubString;
637 while (aEnumerator.getNextToken(aSubString))
638 {
639 SvXMLElementExport aParagraph(
640 rExport, XML_NAMESPACE_TEXT, XML_P, true, false);
641 rExport.Characters(OUString(aSubString));
642 }
643}
644
645/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
OptionalString sType
::std::vector< css::uno::Reference< css::beans::XPropertySet > > ChangesVectorType
rtl::Reference< XMLTextParagraphExport > const & GetTextParagraphExport()
Definition: xmlexp.hxx:557
void AddAttribute(sal_uInt16 nPrefix, const OUString &rName, const OUString &rValue)
Definition: xmlexp.cxx:907
void Characters(const OUString &rChars)
Definition: xmlexp.cxx:2126
const css::uno::Reference< css::frame::XModel > & GetModel() const
Definition: xmlexp.hxx:411
void SAL_DLLPRIVATE AddAttributeIdLegacy(sal_uInt16 const nLegacyPrefix, OUString const &rValue)
add xml:id and legacy namespace id
Definition: xmlexp.cxx:2274
size_t GetInfoID(const OUString sPersonalInfo) const
Definition: xmlexp.hxx:388
bool getNextToken(std::u16string_view &rToken)
Definition: xmluconv.cxx:529
void ExportChangeInline(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the change mark contained in the text body
void ExportChangeAutoStyle(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the auto styles used in this change
ChangesVectorType * pCurrentChangesList
map of recorded changes
void ExportChangesListElements()
export the changes list (<text:tracked-changes>)
SvXMLExport & rExport
void ExportChangesListAutoStyles()
export the auto styles needed by the changes list
OUString const & ConvertTypeName(std::u16string_view sApiName)
convert the change type from API to XML names
void WriteComment(std::u16string_view rComment)
write a comment string as sequence of <text:p> elements
void ExportStartOrEndRedline(const css::uno::Reference< css::beans::XPropertySet > &rPropSet, bool bStart)
export redline marks which start or end at start nodes, i.e.
static OUString GetRedlineID(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
Get ID string!
const OUString sDeletion
void ExportChangedRegion(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the changed-region element
void ExportChange(const css::uno::Reference< css::beans::XPropertySet > &rPropSet, bool bAutoStyle)
export a change
const OUString sFormatChange
void ExportChangeInfo(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export a change-info element (from a PropertySet)
void SetCurrentXText()
Do not record changes.
void ExportChangesList(bool bAutoStyles)
export the list of changes (complete list minus recorded changed)
ChangesMapType aChangeMap
XMLRedlineExport(SvXMLExport &rExp)
const OUString sInsertion
static void convertDateTime(OUStringBuffer &rBuffer, const css::util::DateTime &rDateTime, sal_Int16 const *pTimeZoneOffset, bool bAddTimeIf0AM=false)
#define DBG_ASSERT(sCon, aError)
float u
#define SAL_WARN_IF(condition, area, stream)
bool IsOptionSet(EOption eOption)
Handling of tokens in XML:
XMLTokenEnum
The enumeration of all XML tokens.
Definition: xmltoken.hxx:50
@ XML_MERGE_LAST_PARAGRAPH
Definition: xmltoken.hxx:2281
const OUString & GetXMLToken(enum XMLTokenEnum eToken)
return the OUString representation for eToken
Definition: xmltoken.cxx:3541
OUString sId
constexpr sal_uInt16 XML_NAMESPACE_DC
constexpr sal_uInt16 XML_NAMESPACE_TEXT
constexpr sal_uInt16 XML_NAMESPACE_OFFICE