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