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/xmlnmspe.hxx>
43 #include <xmloff/xmlexp.hxx>
44 #include <xmloff/xmluconv.hxx>
45 
46 
47 using namespace ::com::sun::star;
48 using namespace ::xmloff::token;
49 
50 using ::com::sun::star::beans::PropertyValue;
51 using ::com::sun::star::beans::XPropertySet;
52 using ::com::sun::star::beans::UnknownPropertyException;
53 using ::com::sun::star::document::XRedlinesSupplier;
54 using ::com::sun::star::container::XEnumerationAccess;
55 using ::com::sun::star::container::XEnumeration;
56 using ::com::sun::star::text::XText;
57 using ::com::sun::star::text::XTextContent;
58 using ::com::sun::star::text::XTextSection;
59 using ::com::sun::star::uno::Any;
60 using ::com::sun::star::uno::Reference;
61 using ::com::sun::star::uno::Sequence;
62 
63 
65 : sDeletion(GetXMLToken(XML_DELETION))
66 , sFormatChange(GetXMLToken(XML_FORMAT_CHANGE))
67 , sInsertion(GetXMLToken(XML_INSERTION))
68 , rExport(rExp)
69 , pCurrentChangesList(nullptr)
70 {
71 }
72 
73 
75 {
76 }
77 
78 
80  const Reference<XPropertySet> & rPropSet,
81  bool bAutoStyle)
82 {
83  if (bAutoStyle)
84  {
85  // For the headers/footers, we have to collect the autostyles
86  // here. For the general case, however, it's better to collect
87  // the autostyles by iterating over the global redline
88  // list. So that's what we do: Here, we collect autostyles
89  // only if we have no current list of changes. For the
90  // main-document case, the autostyles are collected in
91  // ExportChangesListAutoStyles().
92  if (pCurrentChangesList != nullptr)
93  ExportChangeAutoStyle(rPropSet);
94  }
95  else
96  {
97  ExportChangeInline(rPropSet);
98  }
99 }
100 
101 
103 {
104  if (bAutoStyles)
105  {
107  }
108  else
109  {
111  }
112 }
113 
114 
116  const Reference<XText> & rText,
117  bool bAutoStyles)
118 {
119  // in the header/footer case, auto styles are collected from the
120  // inline change elements.
121  if (bAutoStyles)
122  return;
123 
124  // look for changes list for this XText
125  ChangesMapType::iterator aFind = aChangeMap.find(rText);
126  if (aFind == aChangeMap.end())
127  return;
128 
129  ChangesVectorType* pChangesList = aFind->second.get();
130 
131  // export only if changes are found
132  if (pChangesList->empty())
133  return;
134 
135  // changes container element
138  true, true);
139 
140  // iterate over changes list
141  for (auto const& change : *pChangesList)
142  {
143  ExportChangedRegion(change);
144  }
145  // else: changes list empty -> ignore
146  // else: no changes list found -> empty
147 }
148 
150  const Reference<XText> & rText)
151 {
152  if (rText.is())
153  {
154  // look for appropriate list in map; use the found one, or create new
155  ChangesMapType::iterator aIter = aChangeMap.find(rText);
156  if (aIter == aChangeMap.end())
157  {
159  aChangeMap[rText].reset( pList );
160  pCurrentChangesList = pList;
161  }
162  else
163  pCurrentChangesList = aIter->second.get();
164  }
165  else
166  {
167  // don't record changes
168  SetCurrentXText();
169  }
170 }
171 
173 {
174  pCurrentChangesList = nullptr;
175 }
176 
177 
179 {
180  // get redlines (aka tracked changes) from the model
181  Reference<XRedlinesSupplier> xSupplier(rExport.GetModel(), uno::UNO_QUERY);
182  if (!xSupplier.is())
183  return;
184 
185  Reference<XEnumerationAccess> aEnumAccess = xSupplier->getRedlines();
186 
187  // redline protection key
188  Reference<XPropertySet> aDocPropertySet( rExport.GetModel(),
189  uno::UNO_QUERY );
190  // redlining enabled?
191  bool bEnabled = *o3tl::doAccess<bool>(aDocPropertySet->getPropertyValue(
192  "RecordChanges" ));
193 
194  // only export if we have redlines or attributes
195  if ( !(aEnumAccess->hasElements() || bEnabled) )
196  return;
197 
198 
199  // export only if we have changes, but tracking is not enabled
200  if ( !bEnabled != !aEnumAccess->hasElements() )
201  {
204  bEnabled ? XML_TRUE : XML_FALSE );
205  }
206 
207  // changes container element
210  true, true);
211 
212  // get enumeration and iterate over elements
213  Reference<XEnumeration> aEnum = aEnumAccess->createEnumeration();
214  while (aEnum->hasMoreElements())
215  {
216  Any aAny = aEnum->nextElement();
217  Reference<XPropertySet> xPropSet;
218  aAny >>= xPropSet;
219 
220  DBG_ASSERT(xPropSet.is(),
221  "can't get XPropertySet; skipping Redline");
222  if (xPropSet.is())
223  {
224  // export only if not in header or footer
225  // (those must be exported with their XText)
226  aAny = xPropSet->getPropertyValue("IsInHeaderFooter");
227  if (! *o3tl::doAccess<bool>(aAny))
228  {
229  // and finally, export change
230  ExportChangedRegion(xPropSet);
231  }
232  }
233  // else: no XPropertySet -> no export
234  }
235  // else: no redlines -> no export
236  // else: no XRedlineSupplier -> no export
237 }
238 
240  const Reference<XPropertySet> & rPropSet)
241 {
242  // record change (if changes should be recorded)
243  if (nullptr != pCurrentChangesList)
244  {
245  // put redline in list if it's collapsed or the redline start
246  Any aIsStart = rPropSet->getPropertyValue("IsStart");
247  Any aIsCollapsed = rPropSet->getPropertyValue("IsCollapsed");
248 
249  if ( *o3tl::doAccess<bool>(aIsStart) ||
250  *o3tl::doAccess<bool>(aIsCollapsed) )
251  pCurrentChangesList->push_back(rPropSet);
252  }
253 
254  // get XText for export of redline auto styles
255  Any aAny = rPropSet->getPropertyValue("RedlineText");
256  Reference<XText> xText;
257  aAny >>= xText;
258  if (xText.is())
259  {
260  // export the auto styles
261  rExport.GetTextParagraphExport()->collectTextAutoStyles(xText);
262  }
263 }
264 
266 {
267  // get redlines (aka tracked changes) from the model
268  Reference<XRedlinesSupplier> xSupplier(rExport.GetModel(), uno::UNO_QUERY);
269  if (!xSupplier.is())
270  return;
271 
272  Reference<XEnumerationAccess> aEnumAccess = xSupplier->getRedlines();
273 
274  // only export if we actually have redlines
275  if (!aEnumAccess->hasElements())
276  return;
277 
278  // get enumeration and iterate over elements
279  Reference<XEnumeration> aEnum = aEnumAccess->createEnumeration();
280  while (aEnum->hasMoreElements())
281  {
282  Any aAny = aEnum->nextElement();
283  Reference<XPropertySet> xPropSet;
284  aAny >>= xPropSet;
285 
286  DBG_ASSERT(xPropSet.is(),
287  "can't get XPropertySet; skipping Redline");
288  if (xPropSet.is())
289  {
290 
291  // export only if not in header or footer
292  // (those must be exported with their XText)
293  aAny = xPropSet->getPropertyValue("IsInHeaderFooter");
294  if (! *o3tl::doAccess<bool>(aAny))
295  {
296  ExportChangeAutoStyle(xPropSet);
297  }
298  }
299  }
300 }
301 
303  const Reference<XPropertySet> & rPropSet)
304 {
305  // determine element name (depending on collapsed, start/end)
306  enum XMLTokenEnum eElement = XML_TOKEN_INVALID;
307  Any aAny = rPropSet->getPropertyValue("IsCollapsed");
308  bool bCollapsed = *o3tl::doAccess<bool>(aAny);
309  if (bCollapsed)
310  {
311  eElement = XML_CHANGE;
312  }
313  else
314  {
315  aAny = rPropSet->getPropertyValue("IsStart");
316  const bool bStart = *o3tl::doAccess<bool>(aAny);
317  eElement = bStart ? XML_CHANGE_START : XML_CHANGE_END;
318  }
319 
320  if (XML_TOKEN_INVALID != eElement)
321  {
322  // we always need the ID
324  GetRedlineID(rPropSet));
325 
326  // export the element (no whitespace because we're in the text body)
328  eElement, false, false);
329  }
330 }
331 
332 
334  const Reference<XPropertySet> & rPropSet)
335 {
336  // Redline-ID
338 
339  // merge-last-paragraph
340  Any aAny = rPropSet->getPropertyValue("MergeLastPara");
341  if( ! *o3tl::doAccess<bool>(aAny) )
343  XML_FALSE);
344 
345  // export change region element
347  XML_CHANGED_REGION, true, true);
348 
349 
350  // scope for (first) change element
351  {
352  aAny = rPropSet->getPropertyValue("RedlineType");
353  OUString sType;
354  aAny >>= sType;
356  ConvertTypeName(sType), true, true);
357 
358  ExportChangeInfo(rPropSet);
359 
360  // get XText from the redline and export (if the XText exists)
361  aAny = rPropSet->getPropertyValue("RedlineText");
362  Reference<XText> xText;
363  aAny >>= xText;
364  if (xText.is())
365  {
366  rExport.GetTextParagraphExport()->exportText(xText);
367  // default parameters: bProgress, bExportParagraph ???
368  }
369  // else: no text interface -> content is inline and will
370  // be exported there
371  }
372 
373  // changed change? Hierarchical changes can only be two levels
374  // deep. Here we check for the second level.
375  aAny = rPropSet->getPropertyValue("RedlineSuccessorData");
376  Sequence<PropertyValue> aSuccessorData;
377  aAny >>= aSuccessorData;
378 
379  // if we actually got a hierarchical change, make element and
380  // process change info
381  if (aSuccessorData.hasElements())
382  {
383  // The only change that can be "undone" is an insertion -
384  // after all, you can't re-insert a deletion, but you can
385  // delete an insertion. This assumption is asserted in
386  // ExportChangeInfo(Sequence<PropertyValue>&).
387  SvXMLElementExport aSecondChangeElem(
389  true, true);
390 
391  ExportChangeInfo(aSuccessorData);
392  }
393  // else: no hierarchical change
394 }
395 
396 
398  const OUString& sApiName)
399 {
400  if (sApiName == "Delete")
401  {
402  return sDeletion;
403  }
404  else if (sApiName == "Insert")
405  {
406  return sInsertion;
407  }
408  else if (sApiName == "Format")
409  {
410  return sFormatChange;
411  }
412  else
413  {
414  OSL_FAIL("unknown redline type");
415  static const OUString sUnknownChange("UnknownChange");
416  return sUnknownChange;
417  }
418 }
419 
420 
423  const Reference<XPropertySet> & rPropSet)
424 {
425  Any aAny = rPropSet->getPropertyValue("RedlineIdentifier");
426  OUString sTmp;
427  aAny >>= sTmp;
428 
429  return "ct" + sTmp;
430 }
431 
432 
434  const Reference<XPropertySet> & rPropSet)
435 {
436 
438  XML_CHANGE_INFO, true, true);
439 
440  Any aAny = rPropSet->getPropertyValue("RedlineAuthor");
441  OUString sTmp;
442  aAny >>= sTmp;
443  if (!sTmp.isEmpty())
444  {
446  XML_CREATOR, true,
447  false );
448  rExport.Characters(sTmp);
449  }
450 
451  aAny = rPropSet->getPropertyValue("RedlineDateTime");
452  util::DateTime aDateTime;
453  aAny >>= aDateTime;
454  {
455  OUStringBuffer sBuf;
456  ::sax::Converter::convertDateTime(sBuf, aDateTime, nullptr);
458  XML_DATE, true,
459  false );
460  rExport.Characters(sBuf.makeStringAndClear());
461  }
462 
463  // comment as <text:p> sequence
464  aAny = rPropSet->getPropertyValue("RedlineComment");
465  aAny >>= sTmp;
466  WriteComment( sTmp );
467 }
468 
470  const Sequence<PropertyValue> & rPropertyValues)
471 {
472  OUString sComment;
473 
474  for(const PropertyValue& rVal : rPropertyValues)
475  {
476  if( rVal.Name == "RedlineAuthor" )
477  {
478  OUString sTmp;
479  rVal.Value >>= sTmp;
480  if (!sTmp.isEmpty())
481  {
483  }
484  }
485  else if( rVal.Name == "RedlineComment" )
486  {
487  rVal.Value >>= sComment;
488  }
489  else if( rVal.Name == "RedlineDateTime" )
490  {
491  util::DateTime aDateTime;
492  rVal.Value >>= aDateTime;
493  OUStringBuffer sBuf;
494  ::sax::Converter::convertDateTime(sBuf, aDateTime, nullptr);
496  sBuf.makeStringAndClear());
497  }
498  else if( rVal.Name == "RedlineType" )
499  {
500  // check if this is an insertion; cf. comment at calling location
501  OUString sTmp;
502  rVal.Value >>= sTmp;
503  DBG_ASSERT(sTmp == "Insert",
504  "hierarchical change must be insertion");
505  }
506  // else: unknown value -> ignore
507  }
508 
509  // finally write element
511  XML_CHANGE_INFO, true, true);
512 
513  WriteComment( sComment );
514 }
515 
517  const Reference<XPropertySet> & rPropSet,
518  bool bStart)
519 {
520  if( ! rPropSet.is() )
521  return;
522 
523  // get appropriate (start or end) property
524  Any aAny;
525  try
526  {
527  aAny = rPropSet->getPropertyValue(bStart ? OUString("StartRedline") : OUString("EndRedline"));
528  }
529  catch(const UnknownPropertyException&)
530  {
531  // If we don't have the property, there's nothing to do.
532  return;
533  }
534 
535  Sequence<PropertyValue> aValues;
536  aAny >>= aValues;
537 
538  // seek for redline properties
539  bool bIsCollapsed = false;
540  bool bIsStart = true;
541  OUString sId;
542  bool bIdOK = false; // have we seen an ID?
543  for(const auto& rValue : std::as_const(aValues))
544  {
545  if (rValue.Name == "RedlineIdentifier")
546  {
547  rValue.Value >>= sId;
548  bIdOK = true;
549  }
550  else if (rValue.Name == "IsCollapsed")
551  {
552  bIsCollapsed = *o3tl::doAccess<bool>(rValue.Value);
553  }
554  else if (rValue.Name == "IsStart")
555  {
556  bIsStart = *o3tl::doAccess<bool>(rValue.Value);
557  }
558  }
559 
560  if( !bIdOK )
561  return;
562 
563  SAL_WARN_IF( sId.isEmpty(), "xmloff", "Redlines must have IDs" );
564 
565  // TODO: use GetRedlineID or eliminate that function
567  "ct" + sId);
568 
569  // export the element
570  // (whitespace because we're not inside paragraphs)
571  SvXMLElementExport aChangeElem(
573  bIsCollapsed ? XML_CHANGE :
574  ( bIsStart ? XML_CHANGE_START : XML_CHANGE_END ),
575  true, true);
576 }
577 
579  const Reference<XTextContent> & rContent,
580  bool bStart)
581 {
582  Reference<XPropertySet> xPropSet(rContent, uno::UNO_QUERY);
583  if (xPropSet.is())
584  {
585  ExportStartOrEndRedline(xPropSet, bStart);
586  }
587  else
588  {
589  OSL_FAIL("XPropertySet expected");
590  }
591 }
592 
594  const Reference<XTextSection> & rSection,
595  bool bStart)
596 {
597  Reference<XPropertySet> xPropSet(rSection, uno::UNO_QUERY);
598  if (xPropSet.is())
599  {
600  ExportStartOrEndRedline(xPropSet, bStart);
601  }
602  else
603  {
604  OSL_FAIL("XPropertySet expected");
605  }
606 }
607 
608 void XMLRedlineExport::WriteComment(const OUString& rComment)
609 {
610  if (rComment.isEmpty())
611  return;
612 
613  // iterate over all string-pieces separated by return (0x0a) and
614  // put each inside a paragraph element.
615  SvXMLTokenEnumerator aEnumerator(rComment, char(0x0a));
616  OUString aSubString;
617  while (aEnumerator.getNextToken(aSubString))
618  {
619  SvXMLElementExport aParagraph(
620  rExport, XML_NAMESPACE_TEXT, XML_P, true, false);
621  rExport.Characters(aSubString);
622  }
623 }
624 
625 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
ChangesMapType aChangeMap
void ExportStartOrEndRedline(const css::uno::Reference< css::beans::XPropertySet > &rPropSet, bool bStart)
export redline marks which start or end at start nodes, i.e.
rtl::Reference< XMLTextParagraphExport > const & GetTextParagraphExport()
Definition: xmlexp.hxx:560
const OUString sFormatChange
constexpr sal_uInt16 XML_NAMESPACE_TEXT
Definition: xmlnmspe.hxx:31
constexpr sal_uInt16 XML_NAMESPACE_DC
Definition: xmlnmspe.hxx:36
bool getNextToken(OUString &rToken)
Definition: xmluconv.cxx:449
OUString const & ConvertTypeName(const OUString &sApiName)
convert the change type from API to XML names
void SAL_DLLPRIVATE AddAttributeIdLegacy(sal_uInt16 const nLegacyPrefix, OUString const &rValue)
add xml:id and legacy namespace id
Definition: xmlexp.cxx:2307
css::uno::Any const & rValue
Definition: ImageStyle.hxx:38
const css::uno::Reference< css::frame::XModel > & GetModel() const
Definition: xmlexp.hxx:414
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:909
void ExportChangesList(bool bAutoStyles)
export the list of changes (complete list minus recorded changed)
XMLTokenEnum
The enumeration of all XML tokens.
Definition: xmltoken.hxx:49
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)
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
static void convertDateTime(OUStringBuffer &rBuffer, const css::util::DateTime &rDateTime, sal_Int16 const *pTimeZoneOffset, bool bAddTimeIf0AM=false)
const OUString sDeletion
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
void WriteComment(const OUString &rComment)
write a comment string as sequence of elements
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:3372
Handling of tokens in XML:
void Characters(const OUString &rChars)
Definition: xmlexp.cxx:2158
void SetCurrentXText()
Do not record changes.
void ExportChangedRegion(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export the changed-region element
OUString sId
XMLRedlineExport(SvXMLExport &rExp)
SvXMLExport & rExport
css::uno::Any const SvXMLExport & rExport
Definition: ImageStyle.hxx:38
void ExportChangeInfo(const css::uno::Reference< css::beans::XPropertySet > &rPropSet)
export a change-info element (from a PropertySet)
constexpr sal_uInt16 XML_NAMESPACE_OFFICE
Definition: xmlnmspe.hxx:29