/* * sxesession.cpp - Sxe Session * Copyright (C) 2006 Joonas Govenius * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "sxesession.h" #include "sxemanager.h" #include "QTimer" #include "QUuid" // The maxlength of a chdata that gets put in one edit enum {MAXCHDATA = 1024}; using namespace XMPP; //---------------------------------------------------------------------------- // SxeSession //---------------------------------------------------------------------------- SxeSession::SxeSession(SxeManager *manager, const Jid &target, const QString &session, const Jid &ownJid, bool groupChat, bool serverSupport, const QList &features) : QObject(manager) , session_(session) , target_(target) , ownJid_(ownJid) , groupChat_(groupChat) , serverSupport_(serverSupport) , queueing_(false) , importing_(false) , features_(features) , uuidMaxPostfix_(0) { setUUIDPrefix(); } SxeSession::~SxeSession() { qDebug("destruct SxeSession"); qDeleteAll(recordByNodeId_); recordByNodeId_.clear(); emit sessionEnded(this); } void SxeSession::initializeDocument(const QDomDocument &doc) { bool origImporting = importing_; importing_ = true; // reset the document doc_ = QDomDocument(); foreach(SxeRecord* meta, recordByNodeId_.values()) meta->deleteLater(); // recordByNode_.clear(); recordByNodeId_.clear(); queuedIncomingEdits_.clear(); queuedOutgoingEdits_.clear(); // import prolog doc_.setContent(parseProlog(doc)); // import other nodes // create all nodes recursively from root QDomNodeList children = doc.childNodes(); for(int i = 0; i < children.size(); i++) { // skip the XML declaration because it isn't a processing instruction if(!(children.at(i).isProcessingInstruction() && children.at(i).toProcessingInstruction().target().toLower() == "xml")) generateNewNode(children.at(i), QString(), i); } importing_ = origImporting; } void SxeSession::processIncomingSxeElement(const QDomElement &sxe, const QString &id) { if(id.isEmpty() && !importing_) { qDebug("Trying to process an SXE element without an associated id!"); return; } if(processSxe(sxe, id)) emit documentUpdated(true); } bool SxeSession::processSxe(const QDomElement &sxe, const QString &id) { // Don't accept duplicates if(!id.isEmpty() && usedSxeIds_.contains(id)) { qDebug() << QString("Tried to process a duplicate %1 (received: %2).").arg(sxe.attribute("id")).arg(usedSxeIds_.size()).toLatin1(); return false; } if(!id.isEmpty()) usedSxeIds_ += id; // store incoming edits when queueing if(queueing_) { // Make sure the element is not already in the queue. foreach(IncomingEdit i, queuedIncomingEdits_) if(i.xml == sxe) return false; IncomingEdit incoming; incoming.id = id; incoming.xml = sxe.cloneNode(true).toElement(); queuedIncomingEdits_.append(incoming); return false; } // create an SxeEdit for each child of the QList edits; for(QDomNode n = sxe.firstChild(); !n.isNull(); n = n.nextSibling()) { if(n.nodeName() == "new") edits.append(new SxeNewEdit(n.toElement())); else if(n.nodeName() == "set") edits.append(new SxeRecordEdit(n.toElement())); else if(n.nodeName() == "remove") edits.append(new SxeRemoveEdit(n.toElement())); } if (edits.size() == 0) return false; // process all the edits foreach(SxeEdit* e, edits) { SxeRecord* meta; if(e->type() == SxeEdit::New) meta = createRecord(e->rid()); else meta = record(e->rid()); if(meta) meta->apply(doc_, e); } return true; } const QDomDocument& SxeSession::document() const { return doc_; } bool SxeSession::groupChat() const { return groupChat_; } bool SxeSession::serverSupport() const { return serverSupport_; } const Jid SxeSession::target() const { return target_; } const QString SxeSession::session() const { return session_; } const Jid SxeSession::ownJid() const { return ownJid_; } const QList SxeSession::features() const { return features_; } QList SxeSession::startQueueing() { // do nothing if already queueing if(queueing_) return QList(); queueing_ = true; // Return all the effective Edits to the session so far (snapshot) // make sure that they are added in the right order (parents first) QString rootid; QList nonDocElementEdits; QMultiHash ridByParent; // first collect all nodes into a hash by their parent foreach(SxeRecord* m, recordByNodeId_.values()) { if(!m->parent().isEmpty()) { ridByParent.insert(m->parent(), m->rid()); } else if(!m->node().isElement()) { nonDocElementEdits += m->edits(); } else rootid = m->rid(); } // starting from the root, add all edits to a list recursively QList edits; if(!rootid.isNull()) arrangeEdits(ridByParent, edits, rootid); return nonDocElementEdits + edits; } void SxeSession::arrangeEdits(QHash &ridByParent, QList &output, const QString &iterator) { // add the edits to this node if(recordByNodeId_.contains(iterator)) output += recordByNodeId_.value(iterator)->edits(); // process all the children QString child; while(!(child = ridByParent.take(iterator)).isNull()) { arrangeEdits(ridByParent, output, child); } } void SxeSession::stopQueueing() { // do nothing if not queueing if(!queueing_) return; queueing_ = false; // Process queued elements flush(); if(!queuedIncomingEdits_.isEmpty()) { while(!queuedIncomingEdits_.isEmpty()) { IncomingEdit queued = queuedIncomingEdits_.takeFirst(); processSxe(queued.xml, queued.id); } emit documentUpdated(true); } } void SxeSession::startImporting(const QDomDocument &doc) { importing_ = true; // reset the document initializeDocument(doc); // start queueing outgoing edits startQueueing(); } void SxeSession::stopImporting() { stopQueueing(); importing_ = false; } void SxeSession::endSession() { deleteLater(); } const QDomNode SxeSession::insertNodeBefore(const QDomNode &node, const QDomNode &parent, const QDomNode &referenceNode) { if(referenceNode.isNull() || referenceNode.previousSibling().isNull()) { // insert as the first node SxeRecord* firstMeta = record(parent.firstChild()); double primaryWeight; if(firstMeta) primaryWeight = firstMeta->primaryWeight() - 1; else primaryWeight = 0; // find out the rid of the parent node QString parentId; SxeRecord* parentMeta = record(parent); if(parentMeta) parentId = parentMeta->rid(); else { qDebug("Trying to insert a node to parent without an id"); return QDomNode(); } return insertNode(node, parentId, primaryWeight); } else return insertNodeAfter(node, parent, referenceNode.previousSibling()); } const QDomNode SxeSession::insertNodeAfter(const QDomNode &node, const QDomNode &parent, const QDomNode &referenceNode) { if(node.isNull()) return QDomNode(); // process each child of a document fragment separately if(node.isDocumentFragment()) { // insert the first node relative to the specified referenceNode QDomNode reference = referenceNode; QDomNodeList children = node.childNodes(); for(int i = 0; i < children.size(); i++) { QDomNode newNode = children.at(i); insertNodeAfter(newNode, parent, reference); // and the rest relative to the previous sibling reference = newNode; } return QDomNode(); } // find out the rid of the parent node QString parentId; SxeRecord* parentMeta = record(parent); if(parentMeta) parentId = parentMeta->rid(); else { qDebug("Trying to insert a node to parent without an id"); return QDomNode(); } // find out the appropriate weight for the node double primaryWeight; SxeRecord* referenceMeta = record(referenceNode); SxeRecord* nextReferenceMeta = record(referenceNode.nextSibling()); if(parent.childNodes().count() == 0) primaryWeight = 0; else if(referenceMeta && nextReferenceMeta && referenceNode.parentNode() == parent) { // get the average of the weights of the reference and it's next sibling primaryWeight = (referenceMeta->primaryWeight() + nextReferenceMeta->primaryWeight()) / 2; } else { // insert as the last node referenceMeta = record(parent.lastChild()); if(referenceMeta) primaryWeight = referenceMeta->primaryWeight() + 1; else primaryWeight = 0; } return insertNode(node, parentId, primaryWeight); } const QDomNode SxeSession::insertNode(const QDomNode &node, const QString &parentId, double primaryWeight) { QDomNode newNode; SxeRecord* meta = record(node); if(meta) { // move an existing node // figure out what's to be changed QHash changes; if(meta->parent() != parentId) changes.insert(SxeRecordEdit::Parent, parentId); if(meta->primaryWeight() != primaryWeight) changes.insert(SxeRecordEdit::PrimaryWeight, QString::number(primaryWeight)); if(changes.size() > 0) { // create the edit SxeRecordEdit* edit = new SxeRecordEdit(meta->rid(), meta->version() + 1, changes); // apply it meta->apply(doc_, edit); // send the edit to others queueOutgoingEdit(edit); emit documentUpdated(false); } return node; } else { QList edits; // create a new node QDomNode result = generateNewNode(node, parentId, primaryWeight); emit documentUpdated(false); return result; } } void SxeSession::removeNode(const QDomNode &node) { if(node.isNull()) return; // create SxeRemoveEdits for all child nodes generateRemoves(node); flush(); emit documentUpdated(false); } void SxeSession::setAttribute(const QDomNode &node, const QString &attribute, const QString &value, int from, int n) { if(!node.isElement() || attribute.isEmpty()) return; if(value.isNull()) { if(from < 0) { // Interpret passing QString() as value as wishing to remove the attribute if(node.toElement().hasAttribute(attribute)) removeNode(node.toElement().attributeNode(attribute)); } return; } if(node.toElement().hasAttribute(attribute)) { setNodeValue(node.attributes().namedItem(attribute), value, from, n); } else { if(from >= 0) { qDebug("from > 0 although attribute doesn't exist yet."); return; } QDomAttr domattr = document_.createAttribute(attribute); domattr.setValue(value); insertNodeAfter(domattr, node, QDomNode()); } } void SxeSession::setNodeValue(const QDomNode &node, const QString &value, int from, int n) { SxeRecord* meta = record(node); if(!meta) { qDebug() << "Trying to set value of " << node.nodeName() << " (a non-existent node) to \"" << value << "\""; return; } if(!(node.isAttr() || node.isText())) { qDebug() << "Trying to set value of a non-attr/text node " << node.nodeName(); return; } // Check whether anythings changing: QString newValue; if(from >= 0 && n >= 0) { if((from + n) > node.nodeValue().length()) { qDebug() << QString("from (%1) + n (%2) > (length of existing node value) (%3).").arg(from).arg(n).arg(node.nodeValue().length()); return; } newValue = node.nodeValue().replace(from, n, value); } else newValue = value; if(newValue == node.nodeValue()) return; // Create the appropriate RecordEdit QHash changes; changes.insert(SxeRecordEdit::Chdata, value); if(from >= 0 && n >= 0) { changes.insert(SxeRecordEdit::ReplaceFrom, QString("%1").arg(from)); changes.insert(SxeRecordEdit::ReplaceN, QString("%1").arg(n)); } // create the edit SxeRecordEdit* edit = new SxeRecordEdit(meta->rid(), meta->version() + 1, changes); // apply it meta->apply(doc_, edit); // send the edit to others queueOutgoingEdit(edit); emit documentUpdated(false); } void SxeSession::flush() { if(queuedOutgoingEdits_.isEmpty()) return; // create the sxe element QDomDocument *doc = ((SxeManager*)parent())->client()->doc(); QDomElement sxe = doc->createElementNS(SXENS, "sxe"); sxe.setAttribute("session", session_); // append all queued edits while(!queuedOutgoingEdits_.isEmpty()) { sxe.appendChild(queuedOutgoingEdits_.takeFirst()); } // pass the bundle to SxeManager emit newSxeElement(sxe, target(), groupChat_); } QDomNode SxeSession::generateNewNode(const QDomNode node, const QString &parent, double primaryWeight) { if(!record(node)) { // generate the appropriate edit(s) for the node QString rid = generateUUIDForSession(); // create the SxeRecord SxeRecord* meta = createRecord(rid); if(!meta) return QDomNode(); if((node.isAttr() || node.isText()) && node.nodeValue().length() > MAXCHDATA) { // Generate a "stub" of the new node QDomNode clone = node.cloneNode(); QString full = clone.nodeValue(); clone.setNodeValue(""); QDomNode newNode = generateNewNode(clone, parent, primaryWeight); flush(); // append the value for(int i = 0; i < full.length(); i += MAXCHDATA) { setNodeValue(newNode, full.mid(i, MAXCHDATA), i, 0); flush(); } } else { SxeEdit* edit = new SxeNewEdit(rid, node, parent, primaryWeight, false); meta->apply(doc_, edit); queueOutgoingEdit(edit); } // process all the attributes and child nodes recursively if(node.isElement()) { // attributes QDomNamedNodeMap attributes = node.attributes(); for(int i = 0; i < attributes.count(); i++) generateNewNode(attributes.item(i), rid, i); // child nodes int i = 0; for(QDomNode n = node.firstChild(); !n.isNull(); n = n.nextSibling()) generateNewNode(n, rid, i++); } return meta->node(); } return QDomNode(); } void SxeSession::generateRemoves(const QDomNode &node) { SxeRecord* meta = record(node); if(meta) { // process all the attributes and child nodes recursively if(node.isElement()) { // attributes QDomNamedNodeMap attributes = node.attributes(); for(int i = 0; i < attributes.count(); i++) generateRemoves(attributes.item(i)); // child nodes QDomNodeList children = node.childNodes(); for(int i = 0; i < children.count(); i++) generateRemoves(children.at(i)); } // generate the appropriate edit for the node SxeRemoveEdit* edit = new SxeRemoveEdit(meta->rid()); queueOutgoingEdit(edit); meta->apply(doc_, edit); } } void SxeSession::reposition(const QDomNode &node, bool remote) { Q_UNUSED(remote); SxeRecord* meta = record(node); if(!meta) { qDebug("Trying to reposition a node without record."); return; } // inserting nodes to the document node is a special case if(meta->parent().isEmpty()) { if(node.isElement() && !(doc_.documentElement().isNull() || doc_.documentElement() == node)) { qDebug("Trying to add a root node when one already exists."); removeNode(node); flush(); return; } doc_.appendChild(node); return; } // find the parent node SxeRecord* parentMeta = record(meta->parent()); QDomNode parentNode; if(!parentMeta || (parentNode = parentMeta->node()).isNull()) { qDebug("non-existent parent. Deleting node."); removeNode(node); flush(); return; } // simply insert if an attribute if(node.isAttr()) { QDomElement parentElement = parentNode.toElement(); if(parentElement.isNull()) { qDebug("Trying to insert an attribute to a non-element."); return; } // unless an attribute with the same name already exists if(parentElement.hasAttribute(node.nodeName()) && node != parentElement.attributeNode(node.nodeName())) { // qDebug() << QString("Removing an attribute node '%1' because one already exists").arg(node.nodeName()); // delete the node with smaller secondary weight if(removeSmaller(meta, record(parentElement.attributeNode(node.nodeName())))) return; } parentElement.setAttributeNode(node.toAttr()); return; } // get the list of siblings QDomNodeList children = parentNode.childNodes(); // default to appending QDomNode before; bool insertLast = true; if(children.length() > 0) { // find the child with the smallest weight greater than the weight of the node itself // if any, insert the node before that node for(int i=0; i < children.length(); i++) { if(children.item(i) != node) { SxeRecord* siblingMeta = record(children.item(i)); if(siblingMeta && *meta < *siblingMeta) { // qDebug() << QString("%1 (pw: %2) is less than %3 (pw: %4)").arg(meta->name()).arg(meta->primaryWeight()).arg(siblingMeta->name()).arg(siblingMeta->primaryWeight()).toLatin1(); before = children.item(i); insertLast = false; break; } } } } if(insertLast) { // qDebug() << QString("Repositioning '%1' (pw: %2) as last.").arg(node.nodeName()).arg(meta->primaryWeight()).toLatin1(); parentNode.appendChild(node); } else { // qDebug() << QString("Repositioning '%1' (pw: %2) before '%3' (pw: %4).").arg(node.nodeName()).arg(meta->primaryWeight()).arg(before.nodeName()).arg(record(before)->primaryWeight()).toLatin1(); parentNode.insertBefore(node, before); } } // void SxeSession::addToLookup(const QDomNode &node, bool, const QString &rid) { // recordByNode_[node] // = recordByNodeId_[rid]; // } void SxeSession::handleNodeToBeAdded(const QDomNode &node, bool remote) { emit nodeToBeAdded(node, remote); reposition(node, remote); emit nodeAdded(node, remote); } void SxeSession::handleNodeToBeMoved(const QDomNode &node, bool remote) { emit nodeToBeMoved(node, remote); reposition(node, remote); emit nodeMoved(node, remote); } void SxeSession::handleNodeToBeRemoved(const QDomNode &node, bool remote) { emit nodeToBeRemoved(node, remote); removeRecord(node); } void SxeSession::removeRecord(const QDomNode &node) { QMutableHashIterator i(recordByNodeId_); while(i.hasNext()) { if(node == i.next().value()->node()) { i.remove(); return; } } } bool SxeSession::removeSmaller(SxeRecord* meta1, SxeRecord* meta2) { if(!meta1) return true; if(!meta2) return false; if(meta1->hasSmallerSecondaryWeight(*meta2)) { removeNode(meta1->node()); flush(); return true; } else { removeNode(meta2->node()); flush(); return false; } } void SxeSession::addUsedSxeId(QString id) { usedSxeIds_ += id; } QList SxeSession::usedSxeIds() { return usedSxeIds_; } void SxeSession::queueOutgoingEdit(SxeEdit* edit) { if(!importing_) { QDomElement el = edit->xml(doc_); queuedOutgoingEdits_.append(((SxeManager*)parent())->client()->doc()->importNode(el, true)); } } SxeRecord* SxeSession::createRecord(const QString &id) { if(recordByNodeId_.contains(id)) { qDebug() << QString("record by id '%1' already exists.").arg(id).toLatin1(); return NULL; } SxeRecord* m = new SxeRecord(id); recordByNodeId_[id] = m; // once the node is actually created, add it to the lookup table // connect(m, SIGNAL(nodeAdded(QDomNode, bool, QString)), SLOT(addToLookup(const QDomNode &, bool, const QString &))); // remove the node in case of a conflicting edit connect(m, SIGNAL(nodeRemovalRequired(QDomNode)), SLOT(removeNode(QDomNode))); // reposition and emit public signals as needed when record is changed connect(m, SIGNAL(nodeToBeAdded(QDomNode, bool, QString)), SLOT(handleNodeToBeAdded(const QDomNode &, bool))); connect(m, SIGNAL(nodeToBeMoved(QDomNode, bool)), SLOT(handleNodeToBeMoved(const QDomNode &, bool))); connect(m, SIGNAL(nodeToBeRemoved(QDomNode, bool)), SLOT(handleNodeToBeRemoved(const QDomNode &, bool))); connect(m, SIGNAL(chdataToBeChanged(QDomNode, bool)), SIGNAL(chdataToBeChanged(const QDomNode &, bool))); connect(m, SIGNAL(chdataChanged(QDomNode, bool)), SIGNAL(chdataChanged(const QDomNode &, bool))); // connect(m, SIGNAL(nameChanged(QDomNode, bool)), SIGNAL(nameChanged(const QDomNode &, bool))); return m; } SxeRecord* SxeSession::record(const QString &id) { return recordByNodeId_.value(id); } SxeRecord* SxeSession::record(const QDomNode &node) const { if(node.isNull()) return NULL; // go through all the SxeRecord's foreach(SxeRecord* meta, recordByNodeId_.values()) { // qDebug() << QString("id: %1").arg(meta->rid()).toLatin1(); if(node == meta->node()) return meta; } return NULL; } void SxeSession::setUUIDPrefix(const QString uuidPrefix) { if(!uuidPrefix.isNull()) uuidPrefix_ = uuidPrefix; else uuidPrefix_ = generateUUID(); } QString SxeSession::generateUUIDForSession() { return QString("%1.%2").arg(uuidPrefix_).arg(++uuidMaxPostfix_, 0, 36); // 36 is the max allowed base } QString SxeSession::generateUUID() { QString fullstring = QUuid::createUuid().toString(); // return the string between "{" and "}" int start = fullstring.indexOf("{") + 1; return fullstring.mid(start, fullstring.lastIndexOf("}") - start); } QString SxeSession::parseProlog(const QDomDocument &doc) { QString prolog; QTextStream stream(&prolog); // check for the XML declaration if(doc.childNodes().at(0).isProcessingInstruction() && doc.childNodes().at(0).toProcessingInstruction().target().toLower() == "xml") doc.childNodes().at(0).save(stream, 1); if(!doc.doctype().isNull()) doc.doctype().save(stream, 1); return prolog; }