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 
39 #include <sax/tools/converter.hxx>
40 
41 #include <xmloff/xmltoken.hxx>
42 #include <xmloff/xmlnamespace.hxx>
43 #include <xmloff/xmlexp.hxx>
44 #include <xmloff/xmluconv.hxx>
46 #include <tools/date.hxx>
47 #include <tools/datetime.hxx>
48 
49 
50 using namespace ::com::sun::star;
51 using namespace ::xmloff::token;
52 
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;
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* pChangesList = aFind->second.get();
133 
134  // export only if changes are found
135  if (pChangesList->empty())
136  return;
137 
138  // changes container element
141  true, true);
142 
143  // iterate over changes list
144  for (auto const& change : *pChangesList)
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  {
162  aChangeMap[rText].reset( pList );
163  pCurrentChangesList = pList;
164  }
165  else
166  pCurrentChangesList = aIter->second.get();
167  }
168  else
169  {
170  // don't record changes
171  SetCurrentXText();
172  }
173 }
174 
176 {
177  pCurrentChangesList = nullptr;
178 }
179 
180 
182 {
183  // get redlines (aka tracked changes) from the model
184  Reference<XRedlinesSupplier> xSupplier(rExport.GetModel(), uno::UNO_QUERY);
185  if (!xSupplier.is())
186  return;
187 
188  Reference<XEnumerationAccess> aEnumAccess = xSupplier->getRedlines();
189 
190  // redline protection key
191  Reference<XPropertySet> aDocPropertySet( rExport.GetModel(),
192  uno::UNO_QUERY );
193  // redlining enabled?
194  bool bEnabled = *o3tl::doAccess<bool>(aDocPropertySet->getPropertyValue(
195  "RecordChanges" ));
196 
197  // only export if we have redlines or attributes
198  if ( !(aEnumAccess->hasElements() || bEnabled) )
199  return;
200 
201 
202  // export only if we have changes, but tracking is not enabled
203  if ( !bEnabled != !aEnumAccess->hasElements() )
204  {
207  bEnabled ? XML_TRUE : XML_FALSE );
208  }
209 
210  // changes container element
213  true, true);
214 
215  // get enumeration and iterate over elements
216  Reference<XEnumeration> aEnum = aEnumAccess->createEnumeration();
217  while (aEnum->hasMoreElements())
218  {
219  Any aAny = aEnum->nextElement();
220  Reference<XPropertySet> xPropSet;
221  aAny >>= xPropSet;
222 
223  DBG_ASSERT(xPropSet.is(),
224  "can't get XPropertySet; skipping Redline");
225  if (xPropSet.is())
226  {
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))
231  {
232  // and finally, export change
233  ExportChangedRegion(xPropSet);
234  }
235  }
236  // else: no XPropertySet -> no export
237  }
238  // else: no redlines -> no export
239  // else: no XRedlineSupplier -> no export
240 }
241 
243  const Reference<XPropertySet> & rPropSet)
244 {
245  // record change (if changes should be recorded)
246  if (nullptr != pCurrentChangesList)
247  {
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");
251 
252  if ( *o3tl::doAccess<bool>(aIsStart) ||
253  *o3tl::doAccess<bool>(aIsCollapsed) )
254  pCurrentChangesList->push_back(rPropSet);
255  }
256 
257  // get XText for export of redline auto styles
258  Any aAny = rPropSet->getPropertyValue("RedlineText");
259  Reference<XText> xText;
260  aAny >>= xText;
261  if (xText.is())
262  {
263  // export the auto styles
264  rExport.GetTextParagraphExport()->collectTextAutoStyles(xText);
265  }
266 }
267 
269 {
270  // get redlines (aka tracked changes) from the model
271  Reference<XRedlinesSupplier> xSupplier(rExport.GetModel(), uno::UNO_QUERY);
272  if (!xSupplier.is())
273  return;
274 
275  Reference<XEnumerationAccess> aEnumAccess = xSupplier->getRedlines();
276 
277  // only export if we actually have redlines
278  if (!aEnumAccess->hasElements())
279  return;
280 
281  // get enumeration and iterate over elements
282  Reference<XEnumeration> aEnum = aEnumAccess->createEnumeration();
283  while (aEnum->hasMoreElements())
284  {
285  Any aAny = aEnum->nextElement();
286  Reference<XPropertySet> xPropSet;
287  aAny >>= xPropSet;
288 
289  DBG_ASSERT(xPropSet.is(),
290  "can't get XPropertySet; skipping Redline");
291  if (xPropSet.is())
292  {
293 
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))
298  {
299  ExportChangeAutoStyle(xPropSet);
300  }
301  }
302  }
303 }
304 
306  const Reference<XPropertySet> & rPropSet)
307 {
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);
312  if (bCollapsed)
313  {
314  eElement = XML_CHANGE;
315  }
316  else
317  {
318  aAny = rPropSet->getPropertyValue("IsStart");
319  const bool bStart = *o3tl::doAccess<bool>(aAny);
320  eElement = bStart ? XML_CHANGE_START : XML_CHANGE_END;
321  }
322 
323  if (XML_TOKEN_INVALID != eElement)
324  {
325  // we always need the ID
327  GetRedlineID(rPropSet));
328 
329  // export the element (no whitespace because we're in the text body)
331  eElement, false, false);
332  }
333 }
334 
335 
337  const Reference<XPropertySet> & rPropSet)
338 {
339  // Redline-ID
341 
342  // merge-last-paragraph
343  Any aAny = rPropSet->getPropertyValue("MergeLastPara");
344  if( ! *o3tl::doAccess<bool>(aAny) )
346  XML_FALSE);
347 
348  // export change region element
350  XML_CHANGED_REGION, true, true);
351 
352 
353  // scope for (first) change element
354  {
355  aAny = rPropSet->getPropertyValue("RedlineType");
356  OUString sType;
357  aAny >>= sType;
359  ConvertTypeName(sType), true, true);
360 
361  ExportChangeInfo(rPropSet);
362 
363  // get XText from the redline and export (if the XText exists)
364  aAny = rPropSet->getPropertyValue("RedlineText");
365  Reference<XText> xText;
366  aAny >>= xText;
367  if (xText.is())
368  {
369  rExport.GetTextParagraphExport()->exportText(xText);
370  // default parameters: bProgress, bExportParagraph ???
371  }
372  // else: no text interface -> content is inline and will
373  // be exported there
374  }
375 
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;
381 
382  // if we actually got a hierarchical change, make element and
383  // process change info
384  if (aSuccessorData.hasElements())
385  {
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(
392  true, true);
393 
394  ExportChangeInfo(aSuccessorData);
395  }
396  // else: no hierarchical change
397 }
398 
399 
401  std::u16string_view sApiName)
402 {
403  if (sApiName == u"Delete")
404  {
405  return sDeletion;
406  }
407  else if (sApiName == u"Insert")
408  {
409  return sInsertion;
410  }
411  else if (sApiName == u"Format")
412  {
413  return sFormatChange;
414  }
415  else
416  {
417  OSL_FAIL("unknown redline type");
418  static const OUString sUnknownChange("UnknownChange");
419  return sUnknownChange;
420  }
421 }
422 
423 
426  const Reference<XPropertySet> & rPropSet)
427 {
428  Any aAny = rPropSet->getPropertyValue("RedlineIdentifier");
429  OUString sTmp;
430  aAny >>= sTmp;
431 
432  return "ct" + sTmp;
433 }
434 
435 
437  const Reference<XPropertySet> & rPropSet)
438 {
439  bool bRemovePersonalInfo = SvtSecurityOptions::IsOptionSet(
441 
443  XML_CHANGE_INFO, true, true);
444 
445  Any aAny = rPropSet->getPropertyValue("RedlineAuthor");
446  OUString sTmp;
447  aAny >>= sTmp;
448  if (!sTmp.isEmpty())
449  {
451  XML_CREATOR, true,
452  false );
453  rExport.Characters(bRemovePersonalInfo
454  ? "Author" + OUString::number(rExport.GetInfoID(sTmp))
455  : sTmp );
456  }
457 
458  aAny = rPropSet->getPropertyValue("RedlineDateTime");
459  util::DateTime aDateTime;
460  aAny >>= aDateTime;
461  {
462  OUStringBuffer sBuf;
463  ::sax::Converter::convertDateTime(sBuf, bRemovePersonalInfo
464  ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
465  : aDateTime, nullptr);
467  XML_DATE, true,
468  false );
469  rExport.Characters(sBuf.makeStringAndClear());
470  }
471 
472  // comment as <text:p> sequence
473  aAny = rPropSet->getPropertyValue("RedlineComment");
474  aAny >>= sTmp;
475  WriteComment( sTmp );
476 }
477 
479  const Sequence<PropertyValue> & rPropertyValues)
480 {
481  OUString sComment;
482  bool bRemovePersonalInfo = SvtSecurityOptions::IsOptionSet(
484 
485  for(const PropertyValue& rVal : rPropertyValues)
486  {
487  if( rVal.Name == "RedlineAuthor" )
488  {
489  OUString sTmp;
490  rVal.Value >>= sTmp;
491  if (!sTmp.isEmpty())
492  {
494  ? "Author" + OUString::number(rExport.GetInfoID(sTmp))
495  : sTmp);
496  }
497  }
498  else if( rVal.Name == "RedlineComment" )
499  {
500  rVal.Value >>= sComment;
501  }
502  else if( rVal.Name == "RedlineDateTime" )
503  {
504  util::DateTime aDateTime;
505  rVal.Value >>= aDateTime;
506  OUStringBuffer sBuf;
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());
511  }
512  else if( rVal.Name == "RedlineType" )
513  {
514  // check if this is an insertion; cf. comment at calling location
515  OUString sTmp;
516  rVal.Value >>= sTmp;
517  DBG_ASSERT(sTmp == "Insert",
518  "hierarchical change must be insertion");
519  }
520  // else: unknown value -> ignore
521  }
522 
523  // finally write element
525  XML_CHANGE_INFO, true, true);
526 
527  WriteComment( sComment );
528 }
529 
531  const Reference<XPropertySet> & rPropSet,
532  bool bStart)
533 {
534  if( ! rPropSet.is() )
535  return;
536 
537  // get appropriate (start or end) property
538  Any aAny;
539  try
540  {
541  aAny = rPropSet->getPropertyValue(bStart ? OUString("StartRedline") : OUString("EndRedline"));
542  }
543  catch(const UnknownPropertyException&)
544  {
545  // If we don't have the property, there's nothing to do.
546  return;
547  }
548 
549  Sequence<PropertyValue> aValues;
550  aAny >>= aValues;
551 
552  // seek for redline properties
553  bool bIsCollapsed = false;
554  bool bIsStart = true;
555  OUString sId;
556  bool bIdOK = false; // have we seen an ID?
557  for(const auto& rValue : std::as_const(aValues))
558  {
559  if (rValue.Name == "RedlineIdentifier")
560  {
561  rValue.Value >>= sId;
562  bIdOK = true;
563  }
564  else if (rValue.Name == "IsCollapsed")
565  {
566  bIsCollapsed = *o3tl::doAccess<bool>(rValue.Value);
567  }
568  else if (rValue.Name == "IsStart")
569  {
570  bIsStart = *o3tl::doAccess<bool>(rValue.Value);
571  }
572  }
573 
574  if( !bIdOK )
575  return;
576 
577  SAL_WARN_IF( sId.isEmpty(), "xmloff", "Redlines must have IDs" );
578 
579  // TODO: use GetRedlineID or eliminate that function
581  "ct" + sId);
582 
583  // export the element
584  // (whitespace because we're not inside paragraphs)
585  SvXMLElementExport aChangeElem(
587  bIsCollapsed ? XML_CHANGE :
588  ( bIsStart ? XML_CHANGE_START : XML_CHANGE_END ),
589  true, true);
590 }
591 
593  const Reference<XTextContent> & rContent,
594  bool bStart)
595 {
596  Reference<XPropertySet> xPropSet(rContent, uno::UNO_QUERY);
597  if (xPropSet.is())
598  {
599  ExportStartOrEndRedline(xPropSet, bStart);
600  }
601  else
602  {
603  OSL_FAIL("XPropertySet expected");
604  }
605 }
606 
608  const Reference<XTextSection> & rSection,
609  bool bStart)
610 {
611  Reference<XPropertySet> xPropSet(rSection, uno::UNO_QUERY);
612  if (xPropSet.is())
613  {
614  ExportStartOrEndRedline(xPropSet, bStart);
615  }
616  else
617  {
618  OSL_FAIL("XPropertySet expected");
619  }
620 }
621 
622 void XMLRedlineExport::WriteComment(std::u16string_view rComment)
623 {
624  if (rComment.empty())
625  return;
626 
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))
632  {
633  SvXMLElementExport aParagraph(
634  rExport, XML_NAMESPACE_TEXT, XML_P, true, false);
635  rExport.Characters(OUString(aSubString));
636  }
637 }
638 
639 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
ChangesMapType aChangeMap
bool IsOptionSet(EOption eOption)
constexpr sal_uInt16 XML_NAMESPACE_OFFICE
void ExportStartOrEndRedline(const css::uno::Reference< css::beans::XPropertySet > &rPropSet, bool bStart)
export redline marks which start or end at start nodes, i.e.
size_t GetInfoID(const OUString sPersonalInfo) const
Definition: xmlexp.hxx:397
rtl::Reference< XMLTextParagraphExport > const & GetTextParagraphExport()
Definition: xmlexp.hxx:566
const OUString sFormatChange
void WriteComment(std::u16string_view rComment)
write a comment string as sequence of elements
void SAL_DLLPRIVATE AddAttributeIdLegacy(sal_uInt16 const nLegacyPrefix, OUString const &rValue)
add xml:id and legacy namespace id
Definition: xmlexp.cxx:2333
const css::uno::Reference< css::frame::XModel > & GetModel() const
Definition: xmlexp.hxx:420
OUString const & ConvertTypeName(std::u16string_view sApiName)
convert the change type from API to XML names
void ExportChangeInline(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the change mark contained in the text body
void AddAttribute(sal_uInt16 nPrefix, const char *pName, const OUString &rValue)
Definition: xmlexp.cxx:938
void ExportChangesList(bool bAutoStyles)
export the list of changes (complete list minus recorded changed)
ChangesVectorType * pCurrentChangesList
map of recorded changes
void ExportChangesListAutoStyles()
export the auto styles needed by the changes list
void ExportChangesListElements()
export the changes list ()
OptionalString sType
#define DBG_ASSERT(sCon, aError)
bool getNextToken(std::u16string_view &rToken)
Definition: xmluconv.cxx:516
constexpr sal_uInt16 XML_NAMESPACE_TEXT
static OUString GetRedlineID(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
Get ID string!
void ExportChange(const css::uno::Reference< css::beans::XPropertySet > &rPropSet, bool bAutoStyle)
export a change
float u
static void convertDateTime(OUStringBuffer &rBuffer, const css::util::DateTime &rDateTime, sal_Int16 const *pTimeZoneOffset, bool bAddTimeIf0AM=false)
const OUString sDeletion
constexpr sal_uInt16 XML_NAMESPACE_DC
void ExportChangeAutoStyle(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the auto styles used in this change
::std::vector< css::uno::Reference< css::beans::XPropertySet > > ChangesVectorType
const OUString sInsertion
#define SAL_WARN_IF(condition, area, stream)
const OUString & GetXMLToken(enum XMLTokenEnum eToken)
return the OUString representation for eToken
Definition: xmltoken.cxx:3452
Handling of tokens in XML:
void Characters(const OUString &rChars)
Definition: xmlexp.cxx:2185
XMLTokenEnum
The enumeration of all XML tokens.
Definition: xmltoken.hxx:49
void SetCurrentXText()
Do not record changes.
void ExportChangedRegion(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the changed-region element
XMLRedlineExport(SvXMLExport &rExp)
SvXMLExport & rExport
void ExportChangeInfo(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export a change-info element (from a PropertySet)
OUString sId