/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if OSL_DEBUG_LEVEL > 0 #include #endif /** XML ID handling. There is an abstract base class XmlIdRegistry, with 2 subclasses XmlIdRegistryDocument for "normal" documents, and XmlIdRegistryClipboard for clipboard documents. These classes are responsible for managing XML IDs for all elements of the model. Only the implementation of the Metadatable base class needs to know the registries, so they are not in the header. The handling of XML IDs differs between clipboard and non-clipboard documents in several aspects. Most importantly, non-clipboard documents can have several elements associated with one XML ID. This is necessary because of the weird undo implementation: deleting a text node moves the deleted node to the undo array, but executing undo will then create a copy of that node in the document array. These 2 nodes must have the same XML ID, because we cannot know whether the user will do a redo next, or something else. Because we need to have a mechanism for several objects per XML ID anyway, we use that also to enable some usability features: The document registry has a list of Metadatables per XML ID. This list is sorted by priority, i.e., the first element has highest priority. When inserting copies, care must be taken that they are inserted at the right position: either before or after the source. This is done by Metadatable::RegisterAsCopyOf. When a text node is split, then both resulting text nodes are inserted into the list. If the user then deletes one text node, the other one will have the XML ID. Also, when a Metadatable is copied to the clipboard and then pasted, the copy is inserted into the list. If the user then deletes the source, the XML ID is not lost. The goal is that it should be hard to lose an XML ID by accident, which is especially important as long as we do not have an UI that displays them. There are two subclasses of Metadatable:
  • MetadatableClipboard: for copies in the clipboard
  • MetadatableUndo: for undo, because a Metadatable may be destroyed on delete and a new one created on undo.
These serve only to track the position in an XML ID list in a document registry, so that future actions can insert objects at the right position. Unfortunately, inserting dummy objects seems to be necessary:
  • it is not sufficient to just remember the saved id, because then the relative priorities might change when executing the undo
  • it is not sufficient to record the position as an integer, because if we delete a text node and then undo, the node will be copied(!), and we will have one more node in the list.
  • it is not sufficient to record the pointer of the previous/next Metadatable, because if we delete a text node, undo, and then do something to clear the redo array, the original text node is destroyed, and is replaced by the copy created by undo
If content from a non-clipboard document is copied into a clipboard document, a dummy MetadatableClipboard is inserted into the non-clipboard document registry in order to track the position of the source element. When the clipboard content is pasted back into the source document, this dummy object is used to associate the pasted element with that same XML ID. If a Metadatable is deleted or merged, Metadatable::CreateUndo is called, and returns a MetadatableUndo instance, which can be used to undo the action by passing it to Metadatable::RestoreMetadata. */ using namespace ::com::sun::star; using ::sfx2::isValidXmlId; namespace sfx2 { constexpr OUString s_content = u"content.xml"_ustr; constexpr OUString s_styles = u"styles.xml"_ustr; static bool isContentFile(std::u16string_view i_rPath) { return i_rPath == s_content; } static bool isStylesFile (std::u16string_view i_rPath) { return i_rPath == s_styles; } // XML ID handling --------------------------------------------------- /** handles registration of XMetadatable. This class is responsible for guaranteeing that XMetadatable objects always have XML IDs that are unique within a stream. This is an abstract base class; see subclasses XmlIdRegistryDocument and XmlIdRegistryClipboard. @see SwDoc::GetXmlIdRegistry @see SwDocShell::GetXmlIdRegistry */ class XmlIdRegistry : public sfx2::IXmlIdRegistry { public: XmlIdRegistry(); /** get the ODF element with the given metadata reference. */ virtual css::uno::Reference< css::rdf::XMetadatable > GetElementByMetadataReference( const css::beans::StringPair & i_rReference) const override; /** register an ODF element at a newly generated, unique metadata reference.

Find a fresh XML ID, and register it for the element. The generated ID does not occur in any stream of the document.

*/ virtual void RegisterMetadatableAndCreateID(Metadatable& i_xObject) = 0; /** try to register an ODF element at a given XML ID, or update its registration to a different XML ID.

If the given new metadata reference is not already occupied in the document, unregister the element at its old metadata reference if it has one, and register the new metadata reference for the element. Note that this method only ensures that XML IDs are unique per stream, so using the same XML ID in both content.xml and styles.xml is allowed.

@returns true iff the element has successfully been registered */ virtual bool TryRegisterMetadatable(Metadatable& i_xObject, OUString const& i_rStreamName, OUString const& i_rIdref) = 0; /** unregister an ODF element.

Unregister the element at its metadata reference. Does not remove the metadata reference from the element.

@see RemoveXmlIdForElement */ virtual void UnregisterMetadatable(Metadatable const&) = 0; /** get the metadata reference for the given element. */ css::beans::StringPair GetXmlIdForElement(Metadatable const&) const; /** remove the metadata reference for the given element. */ virtual void RemoveXmlIdForElement(Metadatable const&) = 0; protected: virtual bool LookupXmlId(const Metadatable& i_xObject, OUString & o_rStream, OUString & o_rIdref) const = 0; virtual Metadatable* LookupElement(const OUString & i_rStreamName, const OUString & i_rIdref) const = 0; }; // XmlIdRegistryDocument --------------------------------------------- namespace { /** non-clipboard documents */ class XmlIdRegistryDocument : public XmlIdRegistry { public: XmlIdRegistryDocument(); virtual ~XmlIdRegistryDocument() override; virtual void RegisterMetadatableAndCreateID(Metadatable& i_xObject) override; virtual bool TryRegisterMetadatable(Metadatable& i_xObject, OUString const& i_rStreamName, OUString const& i_rIdref) override; virtual void UnregisterMetadatable(Metadatable const&) override; virtual void RemoveXmlIdForElement(Metadatable const&) override; /** register i_rCopy as a copy of i_rSource, with precedence iff i_bCopyPrecedesSource is true */ void RegisterCopy(Metadatable const& i_rSource, Metadatable & i_rCopy, const bool i_bCopyPrecedesSource); /** create a Undo Metadatable for i_rObject. */ static std::shared_ptr CreateUndo( Metadatable const& i_rObject); /** merge i_rMerged and i_rOther into i_rMerged. */ void JoinMetadatables(Metadatable & i_rMerged, Metadatable const& i_rOther); // unfortunately public, Metadatable::RegisterAsCopyOf needs this virtual bool LookupXmlId(const Metadatable& i_xObject, OUString & o_rStream, OUString & o_rIdref) const override; private: virtual Metadatable* LookupElement(const OUString & i_rStreamName, const OUString & i_rIdref) const override; struct XmlIdRegistry_Impl; ::std::unique_ptr m_pImpl; }; } // MetadatableUndo --------------------------------------------------- /** the horrible Undo Metadatable: is inserted into lists to track position */ class MetadatableUndo : public Metadatable { /// as determined by the stream of the source in original document const bool m_isInContent; public: explicit MetadatableUndo(const bool i_isInContent) : m_isInContent(i_isInContent) { } virtual ::sfx2::XmlIdRegistry& GetRegistry() override { // N.B. for Undo, m_pReg is initialized by registering this as copy in // CreateUndo; it is never cleared OSL_ENSURE(m_pReg, "no m_pReg in MetadatableUndo ?"); return *m_pReg; } virtual bool IsInClipboard() const override { return false; } virtual bool IsInUndo() const override { return true; } virtual bool IsInContent() const override { return m_isInContent; } virtual css::uno::Reference< css::rdf::XMetadatable > MakeUnoObject() override { OSL_FAIL("MetadatableUndo::MakeUnoObject"); throw; } }; // MetadatableClipboard ---------------------------------------------- /** the horrible Clipboard Metadatable: inserted into lists to track position */ class MetadatableClipboard : public Metadatable { /// as determined by the stream of the source in original document const bool m_isInContent; public: explicit MetadatableClipboard(const bool i_isInContent) : m_isInContent(i_isInContent) { } virtual ::sfx2::XmlIdRegistry& GetRegistry() override { // N.B. for Clipboard, m_pReg is initialized by registering this as copy in // RegisterAsCopyOf; it is only cleared by OriginNoLongerInBusinessAnymore assert(m_pReg && "no m_pReg in MetadatableClipboard ?"); return *m_pReg; } virtual bool IsInClipboard() const override { return true; } virtual bool IsInUndo() const override { return false; } virtual bool IsInContent() const override { return m_isInContent; } virtual css::uno::Reference< css::rdf::XMetadatable > MakeUnoObject() override { OSL_FAIL("MetadatableClipboard::MakeUnoObject"); throw; } void OriginNoLongerInBusinessAnymore() { m_pReg = nullptr; } }; // XmlIdRegistryClipboard -------------------------------------------- namespace { class XmlIdRegistryClipboard : public XmlIdRegistry { public: XmlIdRegistryClipboard(); virtual void RegisterMetadatableAndCreateID(Metadatable& i_xObject) override; virtual bool TryRegisterMetadatable(Metadatable& i_xObject, OUString const& i_rStreamName, OUString const& i_rIdref) override; virtual void UnregisterMetadatable(Metadatable const&) override; virtual void RemoveXmlIdForElement(Metadatable const&) override; /** register i_rCopy as a copy of i_rSource */ MetadatableClipboard & RegisterCopyClipboard(Metadatable & i_rCopy, beans::StringPair const & i_rReference, const bool i_isLatent); /** get the Metadatable that links i_rObject to its origin registry */ MetadatableClipboard const* SourceLink(Metadatable const& i_rObject); private: virtual bool LookupXmlId(const Metadatable& i_xObject, OUString & o_rStream, OUString & o_rIdref) const override; virtual Metadatable* LookupElement(const OUString & i_rStreamName, const OUString & i_rIdref) const override; /** create a Clipboard Metadatable for i_rObject. */ static std::shared_ptr CreateClipboard( const bool i_isInContent); struct XmlIdRegistry_Impl; ::std::unique_ptr m_pImpl; }; } // XmlIdRegistry ::sfx2::IXmlIdRegistry * createXmlIdRegistry(const bool i_DocIsClipboard) { return i_DocIsClipboard ? static_cast( new XmlIdRegistryClipboard ) : static_cast( new XmlIdRegistryDocument ); } XmlIdRegistry::XmlIdRegistry() { } css::uno::Reference< css::rdf::XMetadatable > XmlIdRegistry::GetElementByMetadataReference( const beans::StringPair & i_rReference) const { Metadatable* pObject( LookupElement(i_rReference.First, i_rReference.Second) ); return pObject ? pObject->MakeUnoObject() : nullptr; } beans::StringPair XmlIdRegistry::GetXmlIdForElement(const Metadatable& i_rObject) const { OUString path; OUString idref; if (LookupXmlId(i_rObject, path, idref)) { if (LookupElement(path, idref) == &i_rObject) { return beans::StringPair(path, idref); } } return beans::StringPair(); } /// generate unique xml:id template< typename T > static OUString create_id(const std::unordered_map< OUString, T > & i_rXmlIdMap) { static bool bHack = (getenv("LIBO_ONEWAY_STABLE_ODF_EXPORT") != nullptr); static const char prefix[] = "id"; // prefix for generated xml:id typename std::unordered_map< OUString, T > ::const_iterator iter; OUString id; if (bHack) { static sal_Int64 nIdCounter = SAL_CONST_INT64(4000000000); do { id = prefix + OUString::number(nIdCounter++); iter = i_rXmlIdMap.find(id); } while (iter != i_rXmlIdMap.end()); } else { do { unsigned int const n(comphelper::rng::uniform_uint_distribution(0, std::numeric_limits::max())); id = prefix + OUString::number(n); iter = i_rXmlIdMap.find(id); } while (iter != i_rXmlIdMap.end()); } return id; } // Document XML ID Registry (_Impl) /// element list typedef ::std::vector< Metadatable* > XmlIdVector_t; /// Idref -> (content.xml element list, styles.xml element list) typedef std::unordered_map< OUString, ::std::pair< XmlIdVector_t, XmlIdVector_t > > XmlIdMap_t; namespace { /// pointer hash template template struct PtrHash { size_t operator() (T const * i_pT) const { return reinterpret_cast(i_pT); } }; } /// element -> (stream name, idref) typedef std::unordered_map< const Metadatable*, ::std::pair< OUString, OUString>, PtrHash > XmlIdReverseMap_t; struct XmlIdRegistryDocument::XmlIdRegistry_Impl { XmlIdRegistry_Impl() {} bool TryInsertMetadatable(Metadatable& i_xObject, std::u16string_view i_rStream, const OUString & i_rIdref); bool LookupXmlId(const Metadatable& i_xObject, OUString & o_rStream, OUString & o_rIdref) const; Metadatable* LookupElement(std::u16string_view i_rStreamName, const OUString & i_rIdref) const; const XmlIdVector_t * LookupElementVector( std::u16string_view i_rStreamName, const OUString & i_rIdref) const; XmlIdVector_t * LookupElementVector( std::u16string_view i_rStreamName, const OUString & i_rIdref) { return const_cast( const_cast(this) ->LookupElementVector(i_rStreamName, i_rIdref)); } XmlIdMap_t m_XmlIdMap; XmlIdReverseMap_t m_XmlIdReverseMap; }; static void rmIter(XmlIdMap_t & i_rXmlIdMap, XmlIdMap_t::iterator const& i_rIter, std::u16string_view i_rStream, Metadatable const& i_rObject) { if (i_rIter != i_rXmlIdMap.end()) { XmlIdVector_t & rVector( isContentFile(i_rStream) ? i_rIter->second.first : i_rIter->second.second ); std::erase(rVector, &const_cast(i_rObject)); if (i_rIter->second.first.empty() && i_rIter->second.second.empty()) { i_rXmlIdMap.erase(i_rIter); } } } const XmlIdVector_t * XmlIdRegistryDocument::XmlIdRegistry_Impl::LookupElementVector( std::u16string_view i_rStreamName, const OUString & i_rIdref) const { const XmlIdMap_t::const_iterator iter( m_XmlIdMap.find(i_rIdref) ); if (iter != m_XmlIdMap.end()) { OSL_ENSURE(!iter->second.first.empty() || !iter->second.second.empty(), "null entry in m_XmlIdMap"); return (isContentFile(i_rStreamName)) ? &iter->second.first : &iter->second.second; } else { return nullptr; } } Metadatable* XmlIdRegistryDocument::XmlIdRegistry_Impl::LookupElement( std::u16string_view i_rStreamName, const OUString & i_rIdref) const { if (!isValidXmlId(i_rStreamName, i_rIdref)) { throw lang::IllegalArgumentException("illegal XmlId", nullptr, 0); } const XmlIdVector_t * pList( LookupElementVector(i_rStreamName, i_rIdref) ); if (pList) { const XmlIdVector_t::const_iterator iter( ::std::find_if(pList->begin(), pList->end(), [](Metadatable* item)->bool { return !(item->IsInUndo() || item->IsInClipboard()); } ) ) ; if (iter != pList->end()) { return *iter; } } return nullptr; } bool XmlIdRegistryDocument::XmlIdRegistry_Impl::LookupXmlId( const Metadatable& i_rObject, OUString & o_rStream, OUString & o_rIdref) const { const XmlIdReverseMap_t::const_iterator iter( m_XmlIdReverseMap.find(&i_rObject) ); if (iter != m_XmlIdReverseMap.end()) { OSL_ENSURE(!iter->second.first.isEmpty(), "null stream in m_XmlIdReverseMap"); OSL_ENSURE(!iter->second.second.isEmpty(), "null id in m_XmlIdReverseMap"); o_rStream = iter->second.first; o_rIdref = iter->second.second; return true; } else { return false; } } bool XmlIdRegistryDocument::XmlIdRegistry_Impl::TryInsertMetadatable( Metadatable & i_rObject, std::u16string_view i_rStreamName, const OUString & i_rIdref) { const bool bContent( isContentFile(i_rStreamName) ); OSL_ENSURE(isContentFile(i_rStreamName) || isStylesFile(i_rStreamName), "invalid stream"); XmlIdVector_t * pList( LookupElementVector(i_rStreamName, i_rIdref) ); if (pList) { if (pList->empty()) { pList->push_back( &i_rObject ); return true; } else { // this is only called from TryRegister now, so check // if all elements in the list are deleted (in undo) or // placeholders, then "steal" the id from them if ( std::none_of(pList->begin(), pList->end(), [](Metadatable* item)->bool { return !(item->IsInUndo() || item->IsInClipboard()); } ) ) { pList->insert(pList->begin(), &i_rObject ); return true; } else { return false; } } } else { m_XmlIdMap.insert(::std::make_pair(i_rIdref, bContent ? ::std::make_pair( XmlIdVector_t( 1, &i_rObject ), XmlIdVector_t() ) : ::std::make_pair( XmlIdVector_t(), XmlIdVector_t( 1, &i_rObject ) ))); return true; } } // Document XML ID Registry XmlIdRegistryDocument::XmlIdRegistryDocument() : m_pImpl( new XmlIdRegistry_Impl ) { } static void removeLink(Metadatable* i_pObject) { OSL_ENSURE(i_pObject, "null in list ???"); if (!i_pObject) return; if (i_pObject->IsInClipboard()) { MetadatableClipboard* pLink( dynamic_cast( i_pObject ) ); OSL_ENSURE(pLink, "IsInClipboard, but no MetadatableClipboard ?"); if (pLink) { pLink->OriginNoLongerInBusinessAnymore(); } } } XmlIdRegistryDocument::~XmlIdRegistryDocument() { // notify all list elements that are actually in the clipboard for (const auto& aXmlId : m_pImpl->m_XmlIdMap) { for (auto aLink : aXmlId.second.first) removeLink(aLink); for (auto aLink : aXmlId.second.second) removeLink(aLink); } } bool XmlIdRegistryDocument::LookupXmlId( const Metadatable& i_rObject, OUString & o_rStream, OUString & o_rIdref) const { return m_pImpl->LookupXmlId(i_rObject, o_rStream, o_rIdref); } Metadatable* XmlIdRegistryDocument::LookupElement( const OUString & i_rStreamName, const OUString & i_rIdref) const { return m_pImpl->LookupElement(i_rStreamName, i_rIdref); } bool XmlIdRegistryDocument::TryRegisterMetadatable(Metadatable & i_rObject, OUString const& i_rStreamName, OUString const& i_rIdref) { SAL_INFO("sfx", "TryRegisterMetadatable: " << &i_rObject << " (" << i_rStreamName << "#" << i_rIdref << ")"); OSL_ENSURE(!dynamic_cast(&i_rObject), "TryRegisterMetadatable called for MetadatableUndo?"); OSL_ENSURE(!dynamic_cast(&i_rObject), "TryRegisterMetadatable called for MetadatableClipboard?"); if (!isValidXmlId(i_rStreamName, i_rIdref)) { throw lang::IllegalArgumentException("illegal XmlId", nullptr, 0); } if (i_rObject.IsInContent() ? !isContentFile(i_rStreamName) : !isStylesFile(i_rStreamName)) { throw lang::IllegalArgumentException("illegal XmlId: wrong stream", nullptr, 0); } OUString old_path; OUString old_idref; m_pImpl->LookupXmlId(i_rObject, old_path, old_idref); if (old_path == i_rStreamName && old_idref == i_rIdref) { return (m_pImpl->LookupElement(old_path, old_idref) == &i_rObject); } XmlIdMap_t::iterator old_id( m_pImpl->m_XmlIdMap.end() ); if (!old_idref.isEmpty()) { old_id = m_pImpl->m_XmlIdMap.find(old_idref); OSL_ENSURE(old_id != m_pImpl->m_XmlIdMap.end(), "old id not found"); } if (m_pImpl->TryInsertMetadatable(i_rObject, i_rStreamName, i_rIdref)) { rmIter(m_pImpl->m_XmlIdMap, old_id, old_path, i_rObject); m_pImpl->m_XmlIdReverseMap[&i_rObject] = ::std::make_pair(i_rStreamName, i_rIdref); return true; } else { return false; } } void XmlIdRegistryDocument::RegisterMetadatableAndCreateID(Metadatable & i_rObject) { SAL_INFO("sfx", "RegisterMetadatableAndCreateID: " << &i_rObject); OSL_ENSURE(!dynamic_cast(&i_rObject), "RegisterMetadatableAndCreateID called for MetadatableUndo?"); OSL_ENSURE(!dynamic_cast(&i_rObject), "RegisterMetadatableAndCreateID called for MetadatableClipboard?"); const bool isInContent( i_rObject.IsInContent() ); const OUString stream( isInContent ? s_content : s_styles ); // check if we have a latent xmlid, and if yes, remove it OUString old_path; OUString old_idref; m_pImpl->LookupXmlId(i_rObject, old_path, old_idref); XmlIdMap_t::iterator old_id( m_pImpl->m_XmlIdMap.end() ); if (!old_idref.isEmpty()) { old_id = m_pImpl->m_XmlIdMap.find(old_idref); OSL_ENSURE(old_id != m_pImpl->m_XmlIdMap.end(), "old id not found"); if (m_pImpl->LookupElement(old_path, old_idref) == &i_rObject) { return; } else { // remove latent xmlid rmIter(m_pImpl->m_XmlIdMap, old_id, old_path, i_rObject); } } // create id const OUString id( create_id(m_pImpl->m_XmlIdMap) ); OSL_ENSURE(m_pImpl->m_XmlIdMap.find(id) == m_pImpl->m_XmlIdMap.end(), "created id is in use"); m_pImpl->m_XmlIdMap.insert(::std::make_pair(id, isInContent ? ::std::make_pair( XmlIdVector_t( 1, &i_rObject ), XmlIdVector_t() ) : ::std::make_pair( XmlIdVector_t(), XmlIdVector_t( 1, &i_rObject ) ))); m_pImpl->m_XmlIdReverseMap[&i_rObject] = ::std::make_pair(stream, id); } void XmlIdRegistryDocument::UnregisterMetadatable(const Metadatable& i_rObject) { SAL_INFO("sfx", "UnregisterMetadatable: " << &i_rObject); OUString path; OUString idref; if (!m_pImpl->LookupXmlId(i_rObject, path, idref)) { OSL_FAIL("unregister: no xml id?"); return; } const XmlIdMap_t::iterator iter( m_pImpl->m_XmlIdMap.find(idref) ); if (iter != m_pImpl->m_XmlIdMap.end()) { rmIter(m_pImpl->m_XmlIdMap, iter, path, i_rObject); } } void XmlIdRegistryDocument::RemoveXmlIdForElement(const Metadatable& i_rObject) { SAL_INFO("sfx", "RemoveXmlIdForElement: " << &i_rObject); const XmlIdReverseMap_t::iterator iter( m_pImpl->m_XmlIdReverseMap.find(&i_rObject) ); if (iter != m_pImpl->m_XmlIdReverseMap.end()) { OSL_ENSURE(!iter->second.second.isEmpty(), "null id in m_XmlIdReverseMap"); m_pImpl->m_XmlIdReverseMap.erase(iter); } } void XmlIdRegistryDocument::RegisterCopy(Metadatable const& i_rSource, Metadatable & i_rCopy, const bool i_bCopyPrecedesSource) { SAL_INFO("sfx", "RegisterCopy: " << &i_rSource << " -> " << &i_rCopy << " (" << i_bCopyPrecedesSource << ")"); // potential sources: clipboard, undo array, splitNode // assumption: stream change can only happen via clipboard, and is handled // by Metadatable::RegisterAsCopyOf OSL_ENSURE(i_rSource.IsInUndo() || i_rCopy.IsInUndo() || (i_rSource.IsInContent() == i_rCopy.IsInContent()), "RegisterCopy: not in same stream?"); OUString path; OUString idref; if (!m_pImpl->LookupXmlId( i_rSource, path, idref )) { OSL_FAIL("no xml id?"); return; } XmlIdVector_t * pList ( m_pImpl->LookupElementVector(path, idref) ); OSL_ENSURE( ::std::find( pList->begin(), pList->end(), &i_rCopy ) == pList->end(), "copy already registered???"); XmlIdVector_t::iterator srcpos( ::std::find( pList->begin(), pList->end(), &i_rSource ) ); OSL_ENSURE(srcpos != pList->end(), "source not in list???"); if (srcpos == pList->end()) { return; } if (i_bCopyPrecedesSource) { pList->insert( srcpos, &i_rCopy ); } else { // for undo push_back does not work! must insert right after source pList->insert( ++srcpos, &i_rCopy ); } m_pImpl->m_XmlIdReverseMap.insert(::std::make_pair(&i_rCopy, ::std::make_pair(path, idref))); } std::shared_ptr XmlIdRegistryDocument::CreateUndo(Metadatable const& i_rObject) { SAL_INFO("sfx", "CreateUndo: " << &i_rObject); return std::make_shared( i_rObject.IsInContent() ); } /* i_rMerged is both a source and the target node of the merge i_rOther is the other source, and will be deleted after the merge dimensions: none|latent|actual empty|nonempty i_rMerged(1) i_rOther(2) result *|empty *|empty => 1|2 (arbitrary) *|empty *|nonempty => 2 *|nonempty *|empty => 1 none|nonempty none|nonempty => none none|nonempty latent|nonempty => 2 latent|nonempty none|nonempty => 1 latent|nonempty latent|nonempty => 1|2 *|nonempty actual|nonempty => 2 actual|nonempty *|nonempty => 1 actual|nonempty actual|nonempty => 1|2 */ void XmlIdRegistryDocument::JoinMetadatables( Metadatable & i_rMerged, Metadatable const & i_rOther) { SAL_INFO("sfx", "JoinMetadatables: " << &i_rMerged << " <- " << &i_rOther); bool mergedOwnsRef; OUString path; OUString idref; if (m_pImpl->LookupXmlId(i_rMerged, path, idref)) { mergedOwnsRef = (m_pImpl->LookupElement(path, idref) == &i_rMerged); } else { OSL_FAIL("JoinMetadatables: no xmlid?"); return; } if (!mergedOwnsRef) { i_rMerged.RemoveMetadataReference(); i_rMerged.RegisterAsCopyOf(i_rOther, true); return; } // other cases: merged has actual ref and is nonempty, // other has latent/actual ref and is nonempty: other loses => nothing to do } // Clipboard XML ID Registry (_Impl) namespace { struct RMapEntry { RMapEntry() {} RMapEntry(OUString i_aStream, OUString i_aXmlId, std::shared_ptr i_pLink = std::shared_ptr()) : m_Stream(std::move(i_aStream)), m_XmlId(std::move(i_aXmlId)), m_xLink(std::move(i_pLink)) {} OUString m_Stream; OUString m_XmlId; // this would have been an auto_ptr, if only that would have compiled... std::shared_ptr m_xLink; }; } /// element -> (stream name, idref, source) typedef std::unordered_map< const Metadatable*, struct RMapEntry, PtrHash > ClipboardXmlIdReverseMap_t; /// Idref -> (content.xml element, styles.xml element) typedef std::unordered_map< OUString, ::std::pair< Metadatable*, Metadatable* > > ClipboardXmlIdMap_t; struct XmlIdRegistryClipboard::XmlIdRegistry_Impl { XmlIdRegistry_Impl() {} bool TryInsertMetadatable(Metadatable& i_xObject, std::u16string_view i_rStream, const OUString & i_rIdref); bool LookupXmlId(const Metadatable& i_xObject, OUString & o_rStream, OUString & o_rIdref, MetadatableClipboard const* &o_rpLink) const; Metadatable* LookupElement(std::u16string_view i_rStreamName, const OUString & i_rIdref) const; Metadatable* const* LookupEntry(std::u16string_view i_rStreamName, const OUString & i_rIdref) const; ClipboardXmlIdMap_t m_XmlIdMap; ClipboardXmlIdReverseMap_t m_XmlIdReverseMap; }; static void rmIter(ClipboardXmlIdMap_t & i_rXmlIdMap, ClipboardXmlIdMap_t::iterator const& i_rIter, std::u16string_view i_rStream, Metadatable const& i_rObject) { if (i_rIter == i_rXmlIdMap.end()) return; Metadatable *& rMeta = isContentFile(i_rStream) ? i_rIter->second.first : i_rIter->second.second; if (rMeta == &i_rObject) { rMeta = nullptr; } if (!i_rIter->second.first && !i_rIter->second.second) { i_rXmlIdMap.erase(i_rIter); } } Metadatable* const* XmlIdRegistryClipboard::XmlIdRegistry_Impl::LookupEntry( std::u16string_view i_rStreamName, const OUString & i_rIdref) const { if (!isValidXmlId(i_rStreamName, i_rIdref)) { throw lang::IllegalArgumentException("illegal XmlId", nullptr, 0); } const ClipboardXmlIdMap_t::const_iterator iter( m_XmlIdMap.find(i_rIdref) ); if (iter != m_XmlIdMap.end()) { OSL_ENSURE(iter->second.first || iter->second.second, "null entry in m_XmlIdMap"); return (isContentFile(i_rStreamName)) ? &iter->second.first : &iter->second.second; } else { return nullptr; } } Metadatable* XmlIdRegistryClipboard::XmlIdRegistry_Impl::LookupElement( std::u16string_view i_rStreamName, const OUString & i_rIdref) const { Metadatable * const * ppEntry = LookupEntry(i_rStreamName, i_rIdref); return ppEntry ? *ppEntry : nullptr; } bool XmlIdRegistryClipboard::XmlIdRegistry_Impl::LookupXmlId( const Metadatable& i_rObject, OUString & o_rStream, OUString & o_rIdref, MetadatableClipboard const* &o_rpLink) const { const ClipboardXmlIdReverseMap_t::const_iterator iter( m_XmlIdReverseMap.find(&i_rObject) ); if (iter != m_XmlIdReverseMap.end()) { OSL_ENSURE(!iter->second.m_Stream.isEmpty(), "null stream in m_XmlIdReverseMap"); OSL_ENSURE(!iter->second.m_XmlId.isEmpty(), "null id in m_XmlIdReverseMap"); o_rStream = iter->second.m_Stream; o_rIdref = iter->second.m_XmlId; o_rpLink = iter->second.m_xLink.get(); return true; } else { return false; } } bool XmlIdRegistryClipboard::XmlIdRegistry_Impl::TryInsertMetadatable( Metadatable & i_rObject, std::u16string_view i_rStreamName, const OUString & i_rIdref) { bool bContent( isContentFile(i_rStreamName) ); OSL_ENSURE(isContentFile(i_rStreamName) || isStylesFile(i_rStreamName), "invalid stream"); Metadatable ** ppEntry = const_cast(LookupEntry(i_rStreamName, i_rIdref)); if (ppEntry) { if (*ppEntry) { return false; } else { *ppEntry = &i_rObject; return true; } } else { m_XmlIdMap.insert(::std::make_pair(i_rIdref, bContent ? ::std::make_pair( &i_rObject, static_cast(nullptr) ) : ::std::make_pair( static_cast(nullptr), &i_rObject ))); return true; } } // Clipboard XML ID Registry XmlIdRegistryClipboard::XmlIdRegistryClipboard() : m_pImpl( new XmlIdRegistry_Impl ) { } bool XmlIdRegistryClipboard::LookupXmlId( const Metadatable& i_rObject, OUString & o_rStream, OUString & o_rIdref) const { const MetadatableClipboard * pLink; return m_pImpl->LookupXmlId(i_rObject, o_rStream, o_rIdref, pLink); } Metadatable* XmlIdRegistryClipboard::LookupElement( const OUString & i_rStreamName, const OUString & i_rIdref) const { return m_pImpl->LookupElement(i_rStreamName, i_rIdref); } bool XmlIdRegistryClipboard::TryRegisterMetadatable(Metadatable & i_rObject, OUString const& i_rStreamName, OUString const& i_rIdref) { SAL_INFO("sfx", "TryRegisterMetadatable: " << &i_rObject << " (" << i_rStreamName << "#" << i_rIdref <<")"); OSL_ENSURE(!dynamic_cast(&i_rObject), "TryRegisterMetadatable called for MetadatableUndo?"); OSL_ENSURE(!dynamic_cast(&i_rObject), "TryRegisterMetadatable called for MetadatableClipboard?"); if (!isValidXmlId(i_rStreamName, i_rIdref)) { throw lang::IllegalArgumentException("illegal XmlId", nullptr, 0); } if (i_rObject.IsInContent() ? !isContentFile(i_rStreamName) : !isStylesFile(i_rStreamName)) { throw lang::IllegalArgumentException("illegal XmlId: wrong stream", nullptr, 0); } OUString old_path; OUString old_idref; const MetadatableClipboard * pLink; m_pImpl->LookupXmlId(i_rObject, old_path, old_idref, pLink); if (old_path == i_rStreamName && old_idref == i_rIdref) { return (m_pImpl->LookupElement(old_path, old_idref) == &i_rObject); } ClipboardXmlIdMap_t::iterator old_id( m_pImpl->m_XmlIdMap.end() ); if (!old_idref.isEmpty()) { old_id = m_pImpl->m_XmlIdMap.find(old_idref); OSL_ENSURE(old_id != m_pImpl->m_XmlIdMap.end(), "old id not found"); } if (m_pImpl->TryInsertMetadatable(i_rObject, i_rStreamName, i_rIdref)) { rmIter(m_pImpl->m_XmlIdMap, old_id, old_path, i_rObject); m_pImpl->m_XmlIdReverseMap[&i_rObject] = RMapEntry(i_rStreamName, i_rIdref); return true; } else { return false; } } void XmlIdRegistryClipboard::RegisterMetadatableAndCreateID(Metadatable & i_rObject) { SAL_INFO("sfx", "RegisterMetadatableAndCreateID: " << &i_rObject); OSL_ENSURE(!dynamic_cast(&i_rObject), "RegisterMetadatableAndCreateID called for MetadatableUndo?"); OSL_ENSURE(!dynamic_cast(&i_rObject), "RegisterMetadatableAndCreateID called for MetadatableClipboard?"); bool isInContent( i_rObject.IsInContent() ); OUString stream( isInContent ? s_content : s_styles ); OUString old_path; OUString old_idref; LookupXmlId(i_rObject, old_path, old_idref); if (!old_idref.isEmpty() && (m_pImpl->LookupElement(old_path, old_idref) == &i_rObject)) { return; } // create id const OUString id( create_id(m_pImpl->m_XmlIdMap) ); OSL_ENSURE(m_pImpl->m_XmlIdMap.find(id) == m_pImpl->m_XmlIdMap.end(), "created id is in use"); m_pImpl->m_XmlIdMap.insert(::std::make_pair(id, isInContent ? ::std::make_pair( &i_rObject, static_cast(nullptr) ) : ::std::make_pair( static_cast(nullptr), &i_rObject ))); // N.B.: if i_rObject had a latent XmlId, then we implicitly delete the // MetadatableClipboard and thus the latent XmlId here m_pImpl->m_XmlIdReverseMap[&i_rObject] = RMapEntry(stream, id); } void XmlIdRegistryClipboard::UnregisterMetadatable(const Metadatable& i_rObject) { SAL_INFO("sfx", "UnregisterMetadatable: " << &i_rObject); OUString path; OUString idref; const MetadatableClipboard * pLink; if (!m_pImpl->LookupXmlId(i_rObject, path, idref, pLink)) { OSL_FAIL("unregister: no xml id?"); return; } const ClipboardXmlIdMap_t::iterator iter( m_pImpl->m_XmlIdMap.find(idref) ); if (iter != m_pImpl->m_XmlIdMap.end()) { rmIter(m_pImpl->m_XmlIdMap, iter, path, i_rObject); } } void XmlIdRegistryClipboard::RemoveXmlIdForElement(const Metadatable& i_rObject) { SAL_INFO("sfx", "RemoveXmlIdForElement: " << &i_rObject); ClipboardXmlIdReverseMap_t::iterator iter( m_pImpl->m_XmlIdReverseMap.find(&i_rObject) ); if (iter != m_pImpl->m_XmlIdReverseMap.end()) { OSL_ENSURE(!iter->second.m_XmlId.isEmpty(), "null id in m_XmlIdReverseMap"); m_pImpl->m_XmlIdReverseMap.erase(iter); } } std::shared_ptr XmlIdRegistryClipboard::CreateClipboard(const bool i_isInContent) { SAL_INFO("sfx", "CreateClipboard:"); return std::make_shared( i_isInContent ); } MetadatableClipboard & XmlIdRegistryClipboard::RegisterCopyClipboard(Metadatable & i_rCopy, beans::StringPair const & i_rReference, const bool i_isLatent) { SAL_INFO("sfx", "RegisterCopyClipboard: " << &i_rCopy << " -> (" << i_rReference.First << "#" << i_rReference.Second << ") (" << i_isLatent << ")"); // N.B.: when copying to the clipboard, the selection is always inserted // into the body, even if the source is a header/footer! // so we do not check whether the stream is right in this function if (!isValidXmlId(i_rReference.First, i_rReference.Second)) { throw lang::IllegalArgumentException("illegal XmlId", nullptr, 0); } if (!i_isLatent) { // this should succeed assuming clipboard has a single source document const bool success( m_pImpl->TryInsertMetadatable(i_rCopy, i_rReference.First, i_rReference.Second) ); OSL_ENSURE(success, "RegisterCopyClipboard: TryInsert failed?"); } const std::shared_ptr xLink( CreateClipboard( isContentFile(i_rReference.First)) ); m_pImpl->m_XmlIdReverseMap.insert(::std::make_pair(&i_rCopy, RMapEntry(i_rReference.First, i_rReference.Second, xLink))); return *xLink; } MetadatableClipboard const* XmlIdRegistryClipboard::SourceLink(Metadatable const& i_rObject) { OUString path; OUString idref; const MetadatableClipboard * pLink( nullptr ); m_pImpl->LookupXmlId(i_rObject, path, idref, pLink); return pLink; } // Metadatable mixin Metadatable::~Metadatable() { RemoveMetadataReference(); } void Metadatable::RemoveMetadataReference() { try { if (m_pReg) { m_pReg->UnregisterMetadatable( *this ); m_pReg->RemoveXmlIdForElement( *this ); m_pReg = nullptr; } } catch (const uno::Exception &) { TOOLS_WARN_EXCEPTION( "sfx.doc", "Metadatable::RemoveMetadataReference"); } } // css::rdf::XMetadatable: beans::StringPair Metadatable::GetMetadataReference() const { if (m_pReg) { return m_pReg->GetXmlIdForElement(*this); } return beans::StringPair(); } void Metadatable::SetMetadataReference( const css::beans::StringPair & i_rReference) { if (i_rReference.Second.isEmpty()) { RemoveMetadataReference(); } else { OUString streamName( i_rReference.First ); if (streamName.isEmpty()) { // handle empty stream name as auto-detect. // necessary for importing flat file format. streamName = IsInContent() ? s_content : s_styles; } XmlIdRegistry & rReg( dynamic_cast( GetRegistry() ) ); if (!rReg.TryRegisterMetadatable(*this, streamName, i_rReference.Second)) { throw lang::IllegalArgumentException( "Metadatable::SetMetadataReference: argument is invalid", /*this*/nullptr, 0); } m_pReg = &rReg; } } void Metadatable::EnsureMetadataReference() { XmlIdRegistry& rReg( m_pReg ? *m_pReg : dynamic_cast( GetRegistry() ) ); rReg.RegisterMetadatableAndCreateID( *this ); m_pReg = &rReg; } static const ::sfx2::IXmlIdRegistry& GetRegistryConst(Metadatable const& i_rObject) { return const_cast< Metadatable& >( i_rObject ).GetRegistry(); } void Metadatable::RegisterAsCopyOf(Metadatable const & i_rSource, const bool i_bCopyPrecedesSource) { OSL_ENSURE(typeid(*this) == typeid(i_rSource) || typeid(i_rSource) == typeid(MetadatableUndo) || typeid(*this) == typeid(MetadatableUndo) || typeid(i_rSource) == typeid(MetadatableClipboard) || typeid(*this) == typeid(MetadatableClipboard), "RegisterAsCopyOf element with different class?"); OSL_ENSURE(!m_pReg, "RegisterAsCopyOf called on element with XmlId?"); if (m_pReg) { RemoveMetadataReference(); } try { if (i_rSource.m_pReg) { XmlIdRegistry & rReg( dynamic_cast( GetRegistry() ) ); if (i_rSource.m_pReg == &rReg) { OSL_ENSURE(!IsInClipboard(), "RegisterAsCopy: both in clipboard?"); if (!IsInClipboard()) { XmlIdRegistryDocument & rRegDoc( dynamic_cast( rReg ) ); rRegDoc.RegisterCopy(i_rSource, *this, i_bCopyPrecedesSource); m_pReg = &rRegDoc; } return; } // source is in different document XmlIdRegistryDocument * pRegDoc( dynamic_cast(&rReg) ); XmlIdRegistryClipboard * pRegClp( dynamic_cast(&rReg) ); if (pRegClp) { beans::StringPair SourceRef( i_rSource.m_pReg->GetXmlIdForElement(i_rSource) ); bool isLatent( SourceRef.Second.isEmpty() ); XmlIdRegistryDocument * pSourceRegDoc( dynamic_cast(i_rSource.m_pReg) ); OSL_ENSURE(pSourceRegDoc, "RegisterAsCopyOf: 2 clipboards?"); if (!pSourceRegDoc) return; // this is a copy _to_ the clipboard if (isLatent) { pSourceRegDoc->LookupXmlId(i_rSource, SourceRef.First, SourceRef.Second); } Metadatable & rLink( pRegClp->RegisterCopyClipboard(*this, SourceRef, isLatent)); m_pReg = pRegClp; // register as copy in the non-clipboard registry pSourceRegDoc->RegisterCopy(i_rSource, rLink, false); // i_bCopyPrecedesSource); rLink.m_pReg = pSourceRegDoc; } else if (pRegDoc) { XmlIdRegistryClipboard * pSourceRegClp( dynamic_cast(i_rSource.m_pReg) ); OSL_ENSURE(pSourceRegClp, "RegisterAsCopyOf: 2 non-clipboards?"); if (!pSourceRegClp) return; const MetadatableClipboard * pLink( pSourceRegClp->SourceLink(i_rSource) ); // may happen if src got its id via UNO call if (!pLink) return; // only register copy if clipboard content is from this SwDoc! if (&GetRegistryConst(*pLink) == pRegDoc) { // this is a copy _from_ the clipboard; check if the // element is still in the same stream // N.B.: we check the stream of pLink, not of i_rSource! bool srcInContent( pLink->IsInContent() ); bool tgtInContent( IsInContent() ); if (srcInContent == tgtInContent) { pRegDoc->RegisterCopy(*pLink, *this, true); // i_bCopyPrecedesSource); m_pReg = pRegDoc; } // otherwise: stream change! do not register! } } else { OSL_FAIL("neither RegDoc nor RegClp cannot happen"); } } } catch (const uno::Exception &) { TOOLS_WARN_EXCEPTION( "sfx.doc", "Metadatable::RegisterAsCopyOf"); } } std::shared_ptr Metadatable::CreateUndo() const { OSL_ENSURE(!IsInUndo(), "CreateUndo called for object in undo?"); OSL_ENSURE(!IsInClipboard(), "CreateUndo called for object in clipboard?"); try { if (!IsInClipboard() && !IsInUndo() && m_pReg) { XmlIdRegistryDocument * pRegDoc( dynamic_cast( m_pReg ) ); assert(pRegDoc); std::shared_ptr xUndo( sfx2::XmlIdRegistryDocument::CreateUndo(*this) ); pRegDoc->RegisterCopy(*this, *xUndo, false); xUndo->m_pReg = pRegDoc; return xUndo; } } catch (const uno::Exception &) { TOOLS_WARN_EXCEPTION( "sfx.doc", "Metadatable::CreateUndo"); } return std::shared_ptr(); } std::shared_ptr Metadatable::CreateUndoForDelete() { std::shared_ptr const xUndo( CreateUndo() ); RemoveMetadataReference(); return xUndo; } void Metadatable::RestoreMetadata( std::shared_ptr const& i_pUndo) { OSL_ENSURE(!IsInUndo(), "RestoreMetadata called for object in undo?"); OSL_ENSURE(!IsInClipboard(), "RestoreMetadata called for object in clipboard?"); if (IsInClipboard() || IsInUndo()) return; RemoveMetadataReference(); if (i_pUndo) { RegisterAsCopyOf(*i_pUndo, true); } } void Metadatable::JoinMetadatable(Metadatable const & i_rOther, const bool i_isMergedEmpty, const bool i_isOtherEmpty) { OSL_ENSURE(!IsInUndo(), "JoinMetadatables called for object in undo?"); OSL_ENSURE(!IsInClipboard(), "JoinMetadatables called for object in clipboard?"); if (IsInClipboard() || IsInUndo()) return; if (i_isOtherEmpty && !i_isMergedEmpty) { // other is empty, thus loses => nothing to do return; } if (i_isMergedEmpty && !i_isOtherEmpty) { RemoveMetadataReference(); RegisterAsCopyOf(i_rOther, true); return; } if (!i_rOther.m_pReg) { // other doesn't have xmlid, thus loses => nothing to do return; } if (!m_pReg) { RegisterAsCopyOf(i_rOther, true); // assumption: i_rOther will be deleted, so don't unregister it here return; } try { XmlIdRegistryDocument * pRegDoc( dynamic_cast( m_pReg ) ); OSL_ENSURE(pRegDoc, "JoinMetadatable: no pRegDoc?"); if (pRegDoc) { pRegDoc->JoinMetadatables(*this, i_rOther); } } catch (const uno::Exception &) { TOOLS_WARN_EXCEPTION( "sfx.doc", "Metadatable::JoinMetadatable"); } } // XMetadatable mixin // css::rdf::XNode: OUString SAL_CALL MetadatableMixin::getStringValue() { return getNamespace() + getLocalName(); } // css::rdf::XURI: OUString SAL_CALL MetadatableMixin::getLocalName() { SolarMutexGuard aGuard; beans::StringPair mdref( getMetadataReference() ); if (mdref.Second.isEmpty()) { ensureMetadataReference(); // N.B.: side effect! mdref = getMetadataReference(); } return mdref.First + "#" + mdref.Second; } OUString SAL_CALL MetadatableMixin::getNamespace() { SolarMutexGuard aGuard; const uno::Reference< frame::XModel > xModel( GetModel() ); const uno::Reference< rdf::XURI > xDMA( xModel, uno::UNO_QUERY_THROW ); return xDMA->getStringValue(); } // css::rdf::XMetadatable: beans::StringPair SAL_CALL MetadatableMixin::getMetadataReference() { SolarMutexGuard aGuard; Metadatable *const pObject( GetCoreObject() ); if (!pObject) { throw uno::RuntimeException( "MetadatableMixin: cannot get core object; not inserted?", *this); } return pObject->GetMetadataReference(); } void SAL_CALL MetadatableMixin::setMetadataReference( const beans::StringPair & i_rReference) { SolarMutexGuard aGuard; Metadatable *const pObject( GetCoreObject() ); if (!pObject) { throw uno::RuntimeException( "MetadatableMixin: cannot get core object; not inserted?", *this); } return pObject->SetMetadataReference(i_rReference); } void SAL_CALL MetadatableMixin::ensureMetadataReference() { SolarMutexGuard aGuard; Metadatable *const pObject( GetCoreObject() ); if (!pObject) { throw uno::RuntimeException( "MetadatableMixin: cannot get core object; not inserted?", *this); } return pObject->EnsureMetadataReference(); } } // namespace sfx2 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */