/*************************************************************************** * Copyright (c) 2011 Jürgen Riegel * * Copyright (c) 2011 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include "PreCompiled.h" #ifndef _PreComp_ #include #endif #include #include #include #include #include "Transactions.h" #include "Document.h" #include "DocumentObject.h" #include "Property.h" FC_LOG_LEVEL_INIT("App", true, true) using namespace App; using namespace std; TYPESYSTEM_SOURCE(App::Transaction, Base::Persistence) //************************************************************************** // Construction/Destruction Transaction::Transaction(int id) { if (!id) { id = getNewID(); } transID = id; } /** * A destructor. * A more elaborate description of the destructor. */ Transaction::~Transaction() { auto& index = _Objects.get<0>(); for (const auto& It : index) { if (It.second->status == TransactionObject::New) { // If an object has been removed from the document the transaction // status is 'New'. The 'pcNameInDocument' member serves as criterion // to check whether the object is part of the document or not. // Note, it's possible that the transaction status is 'New' while the // object is (again) part of the document. This usually happens when // a previous removal is undone. // Thus, if the object has been removed, i.e. the status is 'New' and // is still not part of the document the object must be destroyed not // to cause a memory leak. This usually is the case when the removal // of an object is not undone or when an addition is undone. if (!It.first->isAttachedToDocument()) { if (It.first->isDerivedFrom()) { // #0003323: Crash when clearing transaction list // It can happen that when clearing the transaction list several objects // are destroyed with dependencies which can lead to dangling pointers. // When setting the 'Destroy' flag of an object the destructors of link // properties don't ry to remove backlinks, i.e. they don't try to access // possible dangling pointers. // An alternative solution is to call breakDependency inside // Document::_removeObject. Make this change in v0.18. const DocumentObject* obj = static_cast(It.first); const_cast(obj)->setStatus(ObjectStatus::Destroy, true); } delete It.first; } } delete It.second; } } static std::atomic _TransactionID; int Transaction::getNewID() { int id = ++_TransactionID; if (id) { return id; } // wrap around? really? return ++_TransactionID; } int Transaction::getLastID() { return _TransactionID; } unsigned int Transaction::getMemSize() const { return 0; } void Transaction::Save(Base::Writer& /*writer*/) const { assert(0); } void Transaction::Restore(Base::XMLReader& /*reader*/) { assert(0); } int Transaction::getID() const { return transID; } bool Transaction::isEmpty() const { return _Objects.empty(); } bool Transaction::hasObject(const TransactionalObject* Obj) const { return !!_Objects.get<1>().count(Obj); } void Transaction::addOrRemoveProperty(TransactionalObject* Obj, const Property* pcProp, bool add) { auto& index = _Objects.get<1>(); auto pos = index.find(Obj); TransactionObject* To; if (pos != index.end()) { To = pos->second; } else { To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); To->status = TransactionObject::Chn; index.emplace(Obj, To); } To->addOrRemoveProperty(pcProp, add); } //************************************************************************** // separator for other implementation aspects void Transaction::apply(Document& Doc, bool forward) { std::string errMsg; try { auto& index = _Objects.get<0>(); for (auto& info : index) { info.second->applyDel(Doc, const_cast(info.first)); } for (auto& info : index) { info.second->applyNew(Doc, const_cast(info.first)); } for (auto& info : index) { info.second->applyChn(Doc, const_cast(info.first), forward); } } catch (Base::Exception& e) { e.ReportException(); errMsg = e.what(); } catch (std::exception& e) { errMsg = e.what(); } catch (...) { errMsg = "Unknown exception"; } if (!errMsg.empty()) { FC_ERR("Exception on " << (forward ? "redo" : "undo") << " '" << Name << "':" << errMsg); } } void Transaction::addObjectNew(TransactionalObject* Obj) { auto& index = _Objects.get<1>(); auto pos = index.find(Obj); if (pos != index.end()) { if (pos->second->status == TransactionObject::Del) { // first remove the item from the container before deleting it auto second = pos->second; auto first = pos->first; index.erase(pos); delete second; delete first; } else { pos->second->status = TransactionObject::New; pos->second->_NameInDocument = Obj->detachFromDocument(); // move item at the end to make sure the order of removal is kept auto& seq = _Objects.get<0>(); seq.relocate(seq.end(), _Objects.project<0>(pos)); } } else { TransactionObject* To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); To->status = TransactionObject::New; To->_NameInDocument = Obj->detachFromDocument(); index.emplace(Obj, To); } } void Transaction::addObjectDel(const TransactionalObject* Obj) { auto& index = _Objects.get<1>(); auto pos = index.find(Obj); // is it created in this transaction ? if (pos != index.end() && pos->second->status == TransactionObject::New) { // remove completely from transaction delete pos->second; index.erase(pos); } else if (pos != index.end() && pos->second->status == TransactionObject::Chn) { pos->second->status = TransactionObject::Del; } else { TransactionObject* To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); To->status = TransactionObject::Del; index.emplace(Obj, To); } } void Transaction::addObjectChange(const TransactionalObject* Obj, const Property* Prop) { auto& index = _Objects.get<1>(); auto pos = index.find(Obj); TransactionObject* To; if (pos != index.end()) { To = pos->second; } else { To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); To->status = TransactionObject::Chn; index.emplace(Obj, To); } To->setProperty(Prop); } //************************************************************************** //************************************************************************** // TransactionObject //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TYPESYSTEM_SOURCE_ABSTRACT(App::TransactionObject, Base::Persistence) //************************************************************************** // Construction/Destruction /** * A constructor. * A more elaborate description of the constructor. */ TransactionObject::TransactionObject() = default; /** * A destructor. * A more elaborate description of the destructor. */ TransactionObject::~TransactionObject() { for (auto& v : _PropChangeMap) { delete v.second.property; } } void TransactionObject::applyDel(Document& /*Doc*/, TransactionalObject* /*pcObj*/) {} void TransactionObject::applyNew(Document& /*Doc*/, TransactionalObject* /*pcObj*/) {} void TransactionObject::applyChn(Document& /*Doc*/, TransactionalObject* pcObj, bool /* Forward */) { if (status == New || status == Chn) { // Property change order is not preserved, as it is recursive in nature for (auto& v : _PropChangeMap) { auto& data = v.second; auto prop = const_cast(data.propertyOrig); if (!data.property) { // here means we are undoing/redoing and property add operation pcObj->removeDynamicProperty(v.second.name.c_str()); continue; } // getPropertyName() is specially coded to be safe even if prop has // been destroies. We must prepare for the case where user removed // a dynamic property but does not recordered as transaction. auto name = pcObj->getPropertyName(prop); if (!name || (!data.name.empty() && data.name != name) || data.propertyType != prop->getTypeId()) { // Here means the original property is not found, probably removed if (data.name.empty()) { // not a dynamic property, nothing to do continue; } // It is possible for the dynamic property to be removed and // restored. But since restoring property is actually creating // a new property, the property key inside redo stack will not // match. So we search by name first. prop = pcObj->getDynamicPropertyByName(data.name.c_str()); if (!prop) { // Still not found, re-create the property prop = pcObj->addDynamicProperty(data.propertyType.getName(), data.name.c_str(), data.group.c_str(), data.doc.c_str(), data.attr, data.readonly, data.hidden); if (!prop) { continue; } prop->setStatusValue(data.property->getStatus()); } } // Many properties do not bother implement Copy() and accepts // derived types just fine in Paste(). So we do not enforce type // matching here. But instead, strengthen type checking in all // Paste() implementation. // // if(data.propertyType != prop->getTypeId()) { // FC_WARN("Cannot " << (Forward?"redo":"undo") // << " change of property " << prop->getName() // << " because of type change: " // << data.propertyType.getName() // << " -> " << prop->getTypeId().getName()); // continue; // } try { prop->Paste(*data.property); } catch (Base::Exception& e) { e.ReportException(); FC_ERR("exception while restoring " << prop->getFullName() << ": " << e.what()); } catch (std::exception& e) { FC_ERR("exception while restoring " << prop->getFullName() << ": " << e.what()); } catch (...) { } } } } void TransactionObject::setProperty(const Property* pcProp) { auto& data = _PropChangeMap[pcProp->getID()]; if (!data.property && data.name.empty()) { static_cast(data) = pcProp->getContainer()->getDynamicPropertyData(pcProp); data.propertyOrig = pcProp; data.property = pcProp->Copy(); data.propertyType = pcProp->getTypeId(); data.property->setStatusValue(pcProp->getStatus()); } } void TransactionObject::addOrRemoveProperty(const Property* pcProp, bool add) { (void)add; if (!pcProp || !pcProp->getContainer()) { return; } auto& data = _PropChangeMap[pcProp->getID()]; if (!data.name.empty()) { if (!add && !data.property) { // this means add and remove the same property inside a single // transaction, so they cancel each other out. _PropChangeMap.erase(pcProp->getID()); } return; } if (data.property) { delete data.property; data.property = nullptr; } data.propertyOrig = pcProp; static_cast(data) = pcProp->getContainer()->getDynamicPropertyData(pcProp); if (add) { data.property = nullptr; } else { data.property = pcProp->Copy(); data.propertyType = pcProp->getTypeId(); data.property->setStatusValue(pcProp->getStatus()); } } unsigned int TransactionObject::getMemSize() const { return 0; } void TransactionObject::Save(Base::Writer& /*writer*/) const { assert(0); } void TransactionObject::Restore(Base::XMLReader& /*reader*/) { assert(0); } //************************************************************************** //************************************************************************** // TransactionDocumentObject //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TYPESYSTEM_SOURCE_ABSTRACT(App::TransactionDocumentObject, App::TransactionObject) //************************************************************************** // Construction/Destruction /** * A constructor. * A more elaborate description of the constructor. */ TransactionDocumentObject::TransactionDocumentObject() = default; /** * A destructor. * A more elaborate description of the destructor. */ TransactionDocumentObject::~TransactionDocumentObject() = default; void TransactionDocumentObject::applyDel(Document& Doc, TransactionalObject* pcObj) { if (status == Del) { DocumentObject* obj = static_cast(pcObj); #ifndef USE_OLD_DAG // Make sure the backlinks of all linked objects are updated. As the links of the removed // object are never set to [] they also do not remove the backlink. But as they are // not in the document anymore we need to remove them anyway to ensure a correct graph auto list = obj->getOutList(); for (auto link : list) { link->_removeBackLink(obj); } #endif // simply filling in the saved object Doc._removeObject(obj); } } void TransactionDocumentObject::applyNew(Document& Doc, TransactionalObject* pcObj) { if (status == New) { DocumentObject* obj = static_cast(pcObj); Doc._addObject(obj, _NameInDocument.c_str()); #ifndef USE_OLD_DAG // make sure the backlinks of all linked objects are updated auto list = obj->getOutList(); for (auto link : list) { link->_addBackLink(obj); } #endif } } //************************************************************************** //************************************************************************** // TransactionFactory //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ App::TransactionFactory* App::TransactionFactory::self = nullptr; TransactionFactory& TransactionFactory::instance() { if (!self) { self = new TransactionFactory; } return *self; } void TransactionFactory::destruct() { delete self; self = nullptr; } void TransactionFactory::addProducer(const Base::Type& type, Base::AbstractProducer* producer) { producers[type] = producer; } /** * Creates a transaction object for the given type id. */ TransactionObject* TransactionFactory::createTransaction(const Base::Type& type) const { std::map::const_iterator it; for (it = producers.begin(); it != producers.end(); ++it) { if (type.isDerivedFrom(it->first)) { return static_cast(it->second->Produce()); } } assert(0); return nullptr; }