Ok this is the major rework of quassel we've all been waiting for. For the actual...
[quassel.git] / src / common / synchronizer.cpp
diff --git a/src/common/synchronizer.cpp b/src/common/synchronizer.cpp
new file mode 100644 (file)
index 0000000..8c29400
--- /dev/null
@@ -0,0 +1,343 @@
+/***************************************************************************
+ *   Copyright (C) 2005-07 by The Quassel Team                             *
+ *   devel@quassel-irc.org                                                 *
+ *                                                                         *
+ *   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 program; if not, write to the                         *
+ *   Free Software Foundation, Inc.,                                       *
+ *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
+ ***************************************************************************/
+#include "synchronizer.h"
+
+#include <QString>
+#include <QRegExp>
+#include <QMetaProperty>
+#include <QMetaMethod>
+
+#include "util.h"
+#include "signalproxy.h"
+
+#include <QByteArray>
+#include <QDebug>
+
+// ====================
+//  Public:
+// ====================
+Synchronizer::Synchronizer(QObject *parent, SignalProxy *signalproxy)
+  : QObject(parent),
+    _initialized(false),
+    _signalproxy(signalproxy)
+{
+  attach();
+  if(!getMethodByName("objectNameSet()").isEmpty())
+    connect(parent,SIGNAL(objectNameSet()), this, SLOT(parentChangedName()));
+}
+
+bool Synchronizer::initialized() const {
+  return _initialized;
+}
+
+SignalProxy *Synchronizer::proxy() const {
+  return _signalproxy;
+}
+
+QVariantMap Synchronizer::initData() const {
+  QVariantMap properties;
+  
+  // we collect data from properties
+  foreach(QMetaProperty property, parentProperties()) {
+    QString name = QString(property.name());
+    QVariant value = property.read(parent());
+    properties[name] = value;
+    // qDebug() << ">>> SYNC:" << name << value;
+  }
+
+  // ...as well as methods, which have names starting with "init"
+  foreach(QMetaMethod method, parentSlots()) {
+    QString methodname = QString(method.signature()).section("(", 0, 0);
+    if(methodname.startsWith("initSet") ||
+       !methodname.startsWith("init"))
+      continue;
+
+    QVariant value = QVariant(QVariant::nameToType(method.typeName()));
+    QGenericReturnArgument genericvalue = QGenericReturnArgument(method.typeName(), &value);
+    QMetaObject::invokeMethod(parent(), methodname.toAscii(), genericvalue);
+
+    properties[methodBaseName(method)] = value;
+    // qDebug() << ">>> SYNC:" << methodBaseName(method) << value;
+  }
+
+  // properties["Payload"] = QByteArray(10000000, 'a');  // for testing purposes
+  return properties;
+}
+
+void Synchronizer::setInitData(const QVariantMap &properties) {
+  if(initialized())
+    return;
+
+  const QMetaObject *metaobject = parent()->metaObject();  
+  QMapIterator<QString, QVariant> iterator(properties);
+  while(iterator.hasNext()) {
+    iterator.next();
+    QString name = iterator.key();
+    int propertyIndex = metaobject->indexOfProperty(name.toAscii());
+    if(propertyIndex == -1) {
+      setInitValue(name, iterator.value());
+    } else {
+      if((metaobject->property(propertyIndex)).isWritable())
+       parent()->setProperty(name.toAscii(), iterator.value());
+      else
+       setInitValue(name, iterator.value());   
+    }
+    // qDebug() << "<<< SYNC:" << name << iterator.value();
+  }
+
+  _initialized = true;
+  emit initDone();
+}
+
+// ====================
+//  Public Slots
+// ====================
+void Synchronizer::synchronizeClients() const {
+  emit sendInitData(initData());
+}
+
+void Synchronizer::recvInitData(QVariantMap properties) {
+  proxy()->detachObject(this);
+  setInitData(properties);
+}
+
+void Synchronizer::parentChangedName() {
+  proxy()->detachObject(parent());
+  proxy()->detachObject(this);
+  attach();
+  if(proxy()->proxyType() == SignalProxy::Client && initialized())
+    proxy()->detachObject(this);
+}
+
+// ====================
+//  Private
+// ====================
+QString Synchronizer::signalPrefix() const {
+  return QString(parent()->metaObject()->className()) + "_" + QString(parent()->objectName()) + "_";
+}
+
+QString Synchronizer::initSignal() const {
+  return QString(metaObject()->className())
+    + "_" + QString(parent()->metaObject()->className())
+    + "_" + QString(parent()->objectName())
+    + "_" + QString(SIGNAL(sendInitData(QVariantMap)));
+}
+
+QString Synchronizer::requestSyncSignal() const {
+  return QString(metaObject()->className())
+    + "_" + QString(parent()->metaObject()->className())
+    + "_" + QString(parent()->objectName())
+    + "_" + QString(SIGNAL(requestSync()));
+}
+
+QString Synchronizer::methodBaseName(const QMetaMethod &method) const {
+  QString methodname = QString(method.signature()).section("(", 0, 0);
+
+  // determine where we have to chop:
+  if(method.methodType() == QMetaMethod::Slot) {
+    // we take evertyhing from the first uppercase char if it's slot
+    methodname = methodname.mid(methodname.indexOf(QRegExp("[A-Z]")));
+  } else {
+    // and if it's a signal we discard everything from the last uppercase char
+    methodname = methodname.left(methodname.lastIndexOf(QRegExp("[A-Z]")));
+  }
+
+  methodname[0] = methodname[0].toUpper();
+
+  return methodname;
+}
+
+bool Synchronizer::methodsMatch(const QMetaMethod &signal, const QMetaMethod &slot) const {
+  // if we don't even have the same basename it's a sure NO
+  if(methodBaseName(signal) != methodBaseName(slot))
+    return false;
+
+  const QMetaObject *metaobject = parent()->metaObject();
+
+  // are the signatures compatible?
+  if(! metaobject->checkConnectArgs(signal.signature(), slot.signature()))
+    return false;
+
+  // we take an educated guess if the signals and slots match
+  QString signalsuffix = QString(signal.signature()).section("(", 0, 0);
+  signalsuffix = signalsuffix.mid(signalsuffix.lastIndexOf(QRegExp("[A-Z]"))).toLower();
+    
+  QString slotprefix = QString(slot.signature()).section("(", 0, 0);
+  slotprefix = slotprefix.left(slotprefix.indexOf(QRegExp("[A-Z]"))).toLower();
+
+  uint sizediff;
+  if(signalsuffix.size() < slotprefix.size())
+    sizediff = slotprefix.size() - signalsuffix.size();
+  else
+    sizediff = signalsuffix.size() - slotprefix.size();
+
+  int ratio = editingDistance(slotprefix, signalsuffix) - sizediff;
+
+  return (ratio < 2);
+}
+
+QList<QMetaProperty> Synchronizer::parentProperties() const {
+  QList<QMetaProperty> _properties;
+
+  const QMetaObject *metaobject = parent()->metaObject();
+  for(int i = metaobject->propertyOffset(); i < metaobject->propertyCount(); i++) {
+    _properties << metaobject->property(i);
+  }
+  
+  return _properties;
+}
+
+QList<QMetaMethod> Synchronizer::parentSlots() const {
+  QList<QMetaMethod> _slots;
+
+  const QMetaObject *metaobject = parent()->metaObject();
+  for(int i = metaobject->methodOffset(); i < metaobject->methodCount(); i++) {
+    QMetaMethod method = metaobject->method(i);
+    if(method.methodType() == QMetaMethod::Slot)
+      _slots << method;
+  }
+
+  return _slots;
+}
+
+
+QList<QMetaMethod> Synchronizer::parentSignals() const {
+  QList<QMetaMethod> _signals;
+
+  const QMetaObject *metaobject = parent()->metaObject();
+  for(int i = metaobject->methodOffset(); i < metaobject->methodCount(); i++) {
+    QMetaMethod method = metaobject->method(i);
+    if(method.methodType() == QMetaMethod::Signal)
+      _signals << method;
+  }
+  
+  return _signals;
+}
+
+QList<QMetaMethod> Synchronizer::getMethodByName(const QString &methodname) {
+  QList<QMetaMethod> _methods;
+  
+  const QMetaObject* metaobject = parent()->metaObject();
+  for(int i = metaobject->methodOffset(); i < metaobject->methodCount(); i++) {
+    if(QString(metaobject->method(i).signature()).startsWith(methodname))
+      _methods << metaobject->method(i);
+  }
+
+  return _methods;
+}
+
+void Synchronizer::attach() {
+  if(proxy()->proxyType() == SignalProxy::Server)
+    attachAsMaster();
+  else
+    attachAsSlave();
+}
+void Synchronizer::attachAsSlave() {
+  QList<QMetaMethod> signals_ = parentSignals();
+  
+  foreach(QMetaMethod slot, parentSlots()) {
+    if(signals_.empty())
+      break;
+    
+    for(int i = 0; i < signals_.count(); i++) {
+      QMetaMethod signal = signals_[i];
+      if(!methodsMatch(signal, slot))
+       continue;
+
+      // we could simply put a "1" in front of the normalized signature
+      // but to guarantee future compatibility we construct a dummy signal
+      // and replace the known the fake signature by ours...
+      QString dummySlot = QString(SIGNAL(dummy()));
+      QString proxySignal = signalPrefix() + QString(signal.signature());
+      QString slotsignature = dummySlot.replace("dummy()", QString(slot.signature()));
+
+      // qDebug() << "attachSlot:" << proxySignal << slotsignature;
+      proxy()->attachSlot(proxySignal.toAscii(), parent(), slotsignature.toAscii());
+      signals_.removeAt(i);
+      break;
+    }
+  }
+
+  // and then we connect ourself, so we can receive init data
+  // qDebug() << "attachSlot:" << initSignal() << "recvInitData(QVariantMap)";
+  // qDebug() << "attachSignal:" << "requestSync()" << requestSyncSignal();
+  proxy()->attachSlot(initSignal().toAscii(), this, SLOT(recvInitData(QVariantMap)));
+  proxy()->attachSignal(this, SIGNAL(requestSync()), requestSyncSignal().toAscii());
+
+  if(!getMethodByName("setInitialized()").isEmpty())
+    connect(this, SIGNAL(initDone()), parent(), SLOT(setInitialized()));
+
+  emit requestSync();
+}
+
+void Synchronizer::attachAsMaster() {
+  QList<QMetaMethod> slots_ = parentSlots();
+  
+  foreach(QMetaMethod signal, parentSignals()) {
+    if(slots_.isEmpty())
+      break;
+
+    // we don't attach all signals, just the ones that have a maching counterpart
+    for(int i = 0; i < slots_.count(); i++) {
+      QMetaMethod slot = slots_[i];
+      if(!methodsMatch(signal, slot))
+       continue;
+      
+      // we could simply put a "2" in front of the normalized signature
+      // but to guarantee future compatibility we construct a dummy signal
+      // and replace the known the fake signature by ours...
+      QString dummySignal = QString(SIGNAL(dummy()));
+      QString proxySignal = signalPrefix() + QString(signal.signature());
+      QString signalsignature = dummySignal.replace("dummy()", QString(signal.signature()));
+    
+      // qDebug() << "attachSignal:" << signalsignature << proxySignal;
+      proxy()->attachSignal(parent(), signalsignature.toAscii(), proxySignal.toAscii());
+      slots_.removeAt(i);
+      break;
+    }
+  }
+  
+  // and then we connect ourself, so we can initialize slaves
+  // qDebug() << "attachSignal:" << "sendInitData(QVariantMap)" << initSignal();
+  // qDebug() << "attachSlot:" << "synchronizeClients()" << requestSyncSignal();
+  proxy()->attachSignal(this, SIGNAL(sendInitData(QVariantMap)), initSignal().toAscii());
+  proxy()->attachSlot(requestSyncSignal().toAscii(), this, SLOT(synchronizeClients()));
+}
+
+bool Synchronizer::setInitValue(const QString &property, const QVariant &value) {
+  QString handlername = QString("initSet") + property;
+  handlername[7] = handlername[7].toUpper();
+
+  //determine param type
+  QByteArray paramtype;
+  foreach(QMetaMethod method, getMethodByName(handlername)) {
+    if(method.parameterTypes().size() == 1) {
+      paramtype = method.parameterTypes()[0];
+      break;
+    }
+  }
+
+  if(paramtype.isNull())
+    return false;
+
+  QGenericArgument param = QGenericArgument(paramtype, &value);
+  return QMetaObject::invokeMethod(parent(), handlername.toAscii(), param);
+}