cutelyst  4.3.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
controller.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2013-2022 Daniel Nicoletti <dantti12@gmail.com>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 #include "action.h"
6 #include "application.h"
7 #include "common.h"
8 #include "context_p.h"
9 #include "controller_p.h"
10 #include "dispatcher.h"
11 
12 #include <QMetaClassInfo>
13 #include <QRegularExpression>
14 
15 using namespace Cutelyst;
16 
241  : QObject(parent)
242  , d_ptr(new ControllerPrivate(this))
243 {
244 }
245 
246 Controller::~Controller()
247 {
248  Q_D(Controller);
249  qDeleteAll(d->actionList);
250  delete d_ptr;
251 }
252 
253 QString Controller::ns() const noexcept
254 {
255  Q_D(const Controller);
256  return d->pathPrefix;
257 }
258 
260 {
261  Q_D(const Controller);
262  auto it = d->actions.constFind(name);
263  if (it != d->actions.constEnd()) {
264  return it->action;
265  }
266  return d->dispatcher->getAction(name.toString(), d->pathPrefix);
267 }
268 
270 {
271  Q_D(const Controller);
272  return d->actionList;
273 }
274 
275 bool Controller::operator==(const char *className)
276 {
277  return !qstrcmp(metaObject()->className(), className);
278 }
279 
281 {
282  Q_UNUSED(app)
283  return true;
284 }
285 
287 {
288  Q_UNUSED(app)
289  return true;
290 }
291 
292 ControllerPrivate::ControllerPrivate(Controller *parent)
293  : q_ptr(parent)
294 {
295 }
296 
297 void ControllerPrivate::init(Application *app, Dispatcher *_dispatcher)
298 {
299  Q_Q(Controller);
300 
301  q->setObjectName(QString::fromLatin1(q->metaObject()->className()));
302 
303  dispatcher = _dispatcher;
304  application = app;
305 
306  // Application must always be our parent
307  q->setParent(app);
308 
309  const QMetaObject *meta = q->metaObject();
310  const QString className = QString::fromLatin1(meta->className());
311  q->setObjectName(className);
312 
313  bool namespaceFound = false;
314  for (int i = meta->classInfoCount() - 1; i >= 0; --i) {
315  if (qstrcmp(meta->classInfo(i).name(), "Namespace") == 0) {
316  pathPrefix = QString::fromLatin1(meta->classInfo(i).value());
317  while (pathPrefix.startsWith(u'/')) {
318  pathPrefix.remove(0, 1);
319  }
320  namespaceFound = true;
321  break;
322  }
323  }
324 
325  if (!namespaceFound) {
326  QString controlerNS;
327  bool lastWasUpper = true;
328 
329  for (int i = 0; i < className.length(); ++i) {
330  const QChar c = className.at(i);
331  if (c.isLower() || c.isDigit()) {
332  controlerNS.append(c);
333  lastWasUpper = false;
334  } else if (c == u'_') {
335  controlerNS.append(c);
336  lastWasUpper = true;
337  } else {
338  if (!lastWasUpper) {
339  controlerNS.append(u'/');
340  }
341  if (c != u':') {
342  controlerNS.append(c.toLower());
343  }
344  lastWasUpper = true;
345  }
346  }
347  pathPrefix = controlerNS;
348  }
349 
350  registerActionMethods(meta, q, app);
351 }
352 
353 void ControllerPrivate::setupFinished()
354 {
355  Q_Q(Controller);
356 
357  const ActionList beginList = dispatcher->getActions(QStringLiteral("Begin"), pathPrefix);
358  if (!beginList.isEmpty()) {
359  beginAutoList.append(beginList.last());
360  }
361 
362  beginAutoList.append(dispatcher->getActions(QStringLiteral("Auto"), pathPrefix));
363 
364  const ActionList endList = dispatcher->getActions(QStringLiteral("End"), pathPrefix);
365  if (!endList.isEmpty()) {
366  end = endList.last();
367  }
368 
369  const auto actions = actionList;
370  for (Action *action : actions) {
371  action->dispatcherReady(dispatcher, q);
372  }
373 
374  q->preFork(qobject_cast<Application *>(q->parent()));
375 }
376 
378 {
379  Q_D(Controller);
380 
381  bool ret = true;
382 
383  int &actionRefCount = c->d_ptr->actionRefCount;
384 
385  // Dispatch to Begin and Auto
386  const auto beginAutoList = d->beginAutoList;
387  for (Action *action : beginAutoList) {
388  if (actionRefCount) {
389  c->d_ptr->pendingAsync.enqueue(action);
390  } else if (!action->dispatch(c)) {
391  ret = false;
392  break;
393  }
394  }
395 
396  // Dispatch to Action
397  if (ret) {
398  if (actionRefCount) {
399  c->d_ptr->pendingAsync.enqueue(c->action());
400  } else {
401  ret = c->action()->dispatch(c);
402  }
403  }
404 
405  // Dispatch to End
406  if (d->end) {
407  if (actionRefCount) {
408  c->d_ptr->pendingAsync.enqueue(d->end);
409  } else if (!d->end->dispatch(c)) {
410  ret = false;
411  }
412  }
413 
414  if (actionRefCount) {
415  c->d_ptr->engineRequest->status |= EngineRequest::Async;
416  }
417 
418  return ret;
419 }
420 
421 Action *ControllerPrivate::actionClass(const QVariantHash &args)
422 {
423  const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
424  const QString actionClass = attributes.value(QStringLiteral("ActionClass"));
425 
426  QObject *object = instantiateClass(actionClass, "Cutelyst::Action");
427  if (object) {
428  Action *action = qobject_cast<Action *>(object);
429  if (action) {
430  return action;
431  }
432  qCWarning(CUTELYST_CONTROLLER) << "ActionClass" << actionClass << "is not an ActionClass";
433  delete object;
434  }
435 
436  return new Action;
437 }
438 
439 Action *ControllerPrivate::createAction(const QVariantHash &args,
440  const QMetaMethod &method,
441  Controller *controller,
442  Application *app)
443 {
444  Action *action = actionClass(args);
445  if (!action) {
446  return nullptr;
447  }
448 
449  QStack<Component *> roles = gatherActionRoles(args);
450  for (int i = 0; i < roles.size(); ++i) {
451  Component *code = roles.at(i);
452  code->init(app, args);
453  code->setParent(action);
454  }
455  action->applyRoles(roles);
456  action->setMethod(method);
457  action->setController(controller);
458  action->setName(args.value(QStringLiteral("name")).toString());
459  action->setReverse(args.value(QStringLiteral("reverse")).toString());
460  action->setupAction(args, app);
461 
462  return action;
463 }
464 
465 void ControllerPrivate::registerActionMethods(const QMetaObject *meta,
466  Controller *controller,
467  Application *app)
468 {
469  // Setup actions
470  for (int i = 0; i < meta->methodCount(); ++i) {
471  const QMetaMethod method = meta->method(i);
472  const QByteArray name = method.name();
473 
474  // We register actions that are either a Q_SLOT
475  // or a Q_INVOKABLE function which has the first
476  // parameter type equal to Context*
477  if (method.isValid() &&
478  (method.methodType() == QMetaMethod::Method ||
479  method.methodType() == QMetaMethod::Slot) &&
480  (method.parameterCount() &&
481  method.parameterType(0) == qMetaTypeId<Cutelyst::Context *>())) {
482 
483  // Build up the list of attributes for the class info
484  QByteArray attributeArray;
485  for (int i2 = meta->classInfoCount() - 1; i2 >= 0; --i2) {
486  QMetaClassInfo classInfo = meta->classInfo(i2);
487  if (name == classInfo.name()) {
488  attributeArray.append(classInfo.value());
489  }
490  }
491  ParamsMultiMap attrs = parseAttributes(method, attributeArray, name);
492 
493  QString reverse;
494  if (controller->ns().isEmpty()) {
495  reverse = QString::fromLatin1(name);
496  } else {
497  reverse = controller->ns() + QLatin1Char('/') + QString::fromLatin1(name);
498  }
499 
500  Action *action =
501  createAction({{QStringLiteral("name"), QVariant::fromValue(name)},
502  {QStringLiteral("reverse"), QVariant::fromValue(reverse)},
503  {QStringLiteral("namespace"), QVariant::fromValue(controller->ns())},
504  {QStringLiteral("attributes"), QVariant::fromValue(attrs)}},
505  method,
506  controller,
507  app);
508 
509  actions.insert(action->reverse(), {action->reverse(), action});
510  actionList.append(action);
511  }
512  }
513 }
514 
515 ParamsMultiMap ControllerPrivate::parseAttributes(const QMetaMethod &method,
516  const QByteArray &str,
517  const QByteArray &name)
518 {
519  ParamsMultiMap ret;
520  std::vector<std::pair<QString, QString>> attributes;
521  // This is probably not the best parser ever
522  // but it handles cases like:
523  // :Args:Local('fo"')o'):ActionClass('foo')
524  // into
525  // (("Args",""), ("Local","'fo"')o'"), ("ActionClass","'foo'"))
526 
527  int size = str.size();
528  int pos = 0;
529  while (pos < size) {
530  QString key;
531  QString value;
532 
533  // find the start of a key
534  if (str.at(pos) == ':') {
535  int keyStart = ++pos;
536  int keyLength = 0;
537  while (pos < size) {
538  if (str.at(pos) == '(') {
539  // attribute has value
540  int valueStart = ++pos;
541  while (pos < size) {
542  if (str.at(pos) == ')') {
543  // found the possible end of the value
544  int valueEnd = pos;
545  if (++pos < size && str.at(pos) == ':') {
546  // found the start of a key so this is
547  // really the end of a value
548  value =
549  QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
550  break;
551  } else if (pos >= size) {
552  // found the end of the string
553  // save the remainig as the value
554  value =
555  QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
556  break;
557  }
558  // string was not '):' or ')$'
559  continue;
560  }
561  ++pos;
562  }
563 
564  break;
565  } else if (str.at(pos) == ':') {
566  // Attribute has no value
567  break;
568  }
569  ++keyLength;
570  ++pos;
571  }
572 
573  // stopre the key
574  key = QString::fromLatin1(str.mid(keyStart, keyLength));
575 
576  // remove quotes
577  if (!value.isEmpty()) {
578  if ((value.startsWith(u'\'') && value.endsWith(u'\'')) ||
579  (value.startsWith(u'"') && value.endsWith(u'"'))) {
580  value.remove(0, 1);
581  value.remove(value.size() - 1, 1);
582  }
583  }
584 
585  // store the key/value pair found
586  attributes.emplace_back(std::make_pair(key, value));
587  continue;
588  }
589  ++pos;
590  }
591 
592  const static auto digitRE = QRegularExpression(u"\\D"_qs);
593 
594  // Add the attributes to the map in the reverse order so
595  // that values() return them in the right order
596  for (const auto &pair : attributes) {
597  QString key = pair.first;
598  QString value = pair.second;
599  if (key.compare(u"Global") == 0) {
600  key = QStringLiteral("Path");
601  value = parsePathAttr(QLatin1Char('/') + QString::fromLatin1(name));
602  } else if (key.compare(u"Local") == 0) {
603  key = QStringLiteral("Path");
604  value = parsePathAttr(QString::fromLatin1(name));
605  } else if (key.compare(u"Path") == 0) {
606  value = parsePathAttr(value);
607  } else if (key.compare(u"Args") == 0) {
608  QString args = value;
609  if (!args.isEmpty()) {
610  value = args.remove(digitRE);
611  }
612  } else if (key.compare(u"CaptureArgs") == 0) {
613  QString captureArgs = value;
614  value = captureArgs.remove(digitRE);
615  } else if (key.compare(u"Chained") == 0) {
616  value = parseChainedAttr(value);
617  }
618 
619  ret.insert(key, value);
620  }
621 
622  // Handle special AutoArgs and AutoCaptureArgs case
623  if (!ret.contains(QStringLiteral("Args")) && !ret.contains(QStringLiteral("CaptureArgs")) &&
624  (ret.contains(QStringLiteral("AutoArgs")) ||
625  ret.contains(QStringLiteral("AutoCaptureArgs")))) {
626  if (ret.contains(QStringLiteral("AutoArgs")) &&
627  ret.contains(QStringLiteral("AutoCaptureArgs"))) {
628  qFatal("Action '%s' has both AutoArgs and AutoCaptureArgs, which is not allowed",
629  name.constData());
630  } else {
631  QString parameterName;
632  if (ret.contains(QStringLiteral("AutoArgs"))) {
633  ret.remove(QStringLiteral("AutoArgs"));
634  parameterName = QStringLiteral("Args");
635  } else {
636  ret.remove(QStringLiteral("AutoCaptureArgs"));
637  parameterName = QStringLiteral("CaptureArgs");
638  }
639 
640  // If the signature is not QStringList we count them
641  if (!(method.parameterCount() == 2 &&
642  method.parameterType(1) == QMetaType::QStringList)) {
643  int parameterCount = 0;
644  for (int i2 = 1; i2 < method.parameterCount(); ++i2) {
645  int typeId = method.parameterType(i2);
646  if (typeId == QMetaType::QString) {
647  ++parameterCount;
648  }
649  }
650  ret.replace(parameterName, QString::number(parameterCount));
651  }
652  }
653  }
654 
655  // If the method is private add a Private attribute
656  if (!ret.contains(QStringLiteral("Private")) && method.access() == QMetaMethod::Private) {
657  ret.replace(QStringLiteral("Private"), QString());
658  }
659 
660  return ret;
661 }
662 
663 QStack<Component *> ControllerPrivate::gatherActionRoles(const QVariantHash &args)
664 {
665  QStack<Component *> roles;
666  const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
667  auto doesIt = attributes.constFind(QStringLiteral("Does"));
668  while (doesIt != attributes.constEnd() && doesIt.key().compare(u"Does") == 0) {
669  QObject *object =
670  instantiateClass(doesIt.value(), QByteArrayLiteral("Cutelyst::Component"));
671  if (object) {
672  roles.push(qobject_cast<Component *>(object));
673  }
674  ++doesIt;
675  }
676  return roles;
677 }
678 
679 QString ControllerPrivate::parsePathAttr(const QString &value)
680 {
681  QString ret = pathPrefix;
682  if (value.startsWith(u'/')) {
683  ret = value;
684  } else if (!value.isEmpty()) {
685  ret = pathPrefix + u'/' + value;
686  }
687  return ret;
688 }
689 
690 QString ControllerPrivate::parseChainedAttr(const QString &attr)
691 {
692  QString ret = QStringLiteral("/");
693  if (attr.isEmpty()) {
694  return ret;
695  }
696 
697  if (attr.compare(u".") == 0) {
698  ret.append(pathPrefix);
699  } else if (!attr.startsWith(u'/')) {
700  if (!pathPrefix.isEmpty()) {
701  ret.append(pathPrefix + u'/' + attr);
702  } else {
703  // special case namespace '' (root)
704  ret.append(attr);
705  }
706  } else {
707  ret = attr;
708  }
709 
710  return ret;
711 }
712 
713 QObject *ControllerPrivate::instantiateClass(const QString &name, const QByteArray &super)
714 {
715  QString instanceName = name;
716  if (!instanceName.isEmpty()) {
717  instanceName.remove(QRegularExpression(QStringLiteral("\\W")));
718 
719  QMetaType id = QMetaType::fromName(instanceName.toLatin1().data());
720  if (!id.isValid()) {
721  if (!instanceName.endsWith(QLatin1Char('*'))) {
722  instanceName.append(QLatin1Char('*'));
723  }
724 
725  id = QMetaType::fromName(instanceName.toLatin1().data());
726  if (!id.isValid() && !instanceName.startsWith(u"Cutelyst::")) {
727  instanceName = QLatin1String("Cutelyst::") + instanceName;
728  id = QMetaType::fromName(instanceName.toLatin1().data());
729  }
730  }
731 
732  if (id.isValid()) {
733  const QMetaObject *metaObj = id.metaObject();
734  if (metaObj) {
735  if (!superIsClassName(metaObj->superClass(), super)) {
736  qCWarning(CUTELYST_CONTROLLER)
737  << "Class name" << instanceName << "is not a derived class of" << super;
738  }
739 
740  QObject *object = metaObj->newInstance();
741  if (!object) {
742  qCWarning(CUTELYST_CONTROLLER)
743  << "Could create a new instance of" << instanceName
744  << "make sure it's default constructor is "
745  "marked with the Q_INVOKABLE macro";
746  }
747 
748  return object;
749  }
750  } else {
751  Component *component = application->createComponentPlugin(name);
752  if (component) {
753  return component;
754  }
755 
756  component = application->createComponentPlugin(instanceName);
757  if (component) {
758  return component;
759  }
760  }
761 
762  if (!id.isValid()) {
763  qCCritical(CUTELYST_CONTROLLER,
764  "Could not create component '%s', you can register it with "
765  "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
766  qPrintable(instanceName),
767  qPrintable(instanceName));
768  }
769  }
770 
771  return nullptr;
772 }
773 
774 bool ControllerPrivate::superIsClassName(const QMetaObject *super, const QByteArray &className)
775 {
776  if (super) {
777  if (super->className() == className) {
778  return true;
779  }
780  return superIsClassName(super->superClass(), className);
781  }
782  return false;
783 }
784 
785 #include "moc_controller.cpp"
void setName(const QString &name)
Definition: component.cpp:39
QString & append(QChar ch)
bool dispatch(Context *c)
Definition: action.h:89
QMultiMap::const_iterator constFind(const Key &key) const const
QByteArray name() const const
QMultiMap::iterator replace(const Key &key, const T &value)
char at(qsizetype i) const const
void setReverse(const QString &reverse)
Definition: component.cpp:51
const QMetaObject * superClass() const const
void push(const T &t)
bool isDigit() const const
QList::const_reference at(qsizetype i) const const
virtual const QMetaObject * metaObject() const const
virtual bool preFork(Application *app)
Definition: controller.cpp:280
QMetaType fromName(QByteArrayView typeName)
const char * name() const const
QString & remove(qsizetype position, qsizetype n)
QString reverse() const noexcept
Definition: component.cpp:45
QMetaMethod::Access access() const const
The Cutelyst Component base class.
Definition: component.h:30
qsizetype size() const const
This class represents a Cutelyst Action.
Definition: action.h:35
T value(qsizetype i) const const
The Cutelyst Context.
Definition: context.h:42
Action action
Definition: context.h:48
QString number(int n, int base)
Cutelyst Controller base class.
Definition: controller.h:55
void append(QList::parameter_type value)
QMultiMap::size_type remove(const Key &key)
bool isEmpty() const const
bool isEmpty() const const
int methodCount() const const
const char * constData() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
bool isValid() const const
int classInfoCount() const const
int parameterCount() const const
const char * value() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
virtual bool init(Application *application, const QVariantHash &args)
Definition: component.cpp:57
QMultiMap::iterator insert(const Key &key, const T &value)
bool _DISPATCH(Context *c)
Definition: controller.cpp:377
The Cutelyst namespace holds all public Cutelyst API.
void applyRoles(const QStack< Component *> &roles)
Definition: component.cpp:133
QByteArray mid(qsizetype pos, qsizetype len) const const
QMetaClassInfo classInfo(int index) const const
int parameterType(int index) const const
QByteArray & append(char ch)
void setParent(QObject *parent)
const char * className() const const
QVariant fromValue(const T &value)
QChar toLower() const const
void setController(Controller *controller)
Definition: action.cpp:40
virtual bool postFork(Application *app)
Definition: controller.cpp:286
bool contains(const Key &key) const const
QMetaMethod::MethodType methodType() const const
Key key(const T &value, const Key &defaultKey) const const
QString fromLatin1(QByteArrayView str)
QByteArray toLatin1() const const
QString toString() const const
void setupAction(const QVariantHash &args, Application *app)
Definition: action.cpp:46
const QChar at(qsizetype position) const const
T & last()
QString ns() const noexcept
Definition: controller.cpp:253
qsizetype length() const const
char * data()
bool operator==(const char *className)
Definition: controller.cpp:275
The Cutelyst application.
Definition: application.h:72
QString first(qsizetype n) const const
void setMethod(const QMetaMethod &method)
Definition: action.cpp:27
Action * actionFor(QStringView name) const
Definition: controller.cpp:259
qsizetype size() const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
The Cutelyst Dispatcher.
Definition: dispatcher.h:28
QObject * newInstance(QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9) const const
QMetaMethod method(int index) const const
ActionList actions() const noexcept
Definition: controller.cpp:269
bool isLower() const const
T value(const Key &key, const T &defaultValue) const const
Controller(QObject *parent=nullptr)
Definition: controller.cpp:240