cutelyst  4.9.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 using namespace Qt::Literals::StringLiterals;
17 
242  : QObject(parent)
243  , d_ptr(new ControllerPrivate(this))
244 {
245 }
246 
247 Controller::~Controller()
248 {
249  Q_D(Controller);
250  qDeleteAll(d->actionList);
251  delete d_ptr;
252 }
253 
254 QString Controller::ns() const noexcept
255 {
256  Q_D(const Controller);
257  return d->pathPrefix;
258 }
259 
261 {
262  Q_D(const Controller);
263  auto it = d->actions.constFind(name);
264  if (it != d->actions.constEnd()) {
265  return it->action;
266  }
267  return d->dispatcher->getAction(name.toString(), d->pathPrefix);
268 }
269 
271 {
272  Q_D(const Controller);
273  return d->actionList;
274 }
275 
276 bool Controller::operator==(const char *className)
277 {
278  return !qstrcmp(metaObject()->className(), className);
279 }
280 
282 {
283  Q_UNUSED(app)
284  return true;
285 }
286 
288 {
289  Q_UNUSED(app)
290  return true;
291 }
292 
293 ControllerPrivate::ControllerPrivate(Controller *parent)
294  : q_ptr(parent)
295 {
296 }
297 
298 void ControllerPrivate::init(Application *app, Dispatcher *_dispatcher)
299 {
300  Q_Q(Controller);
301 
302  q->setObjectName(QString::fromLatin1(q->metaObject()->className()));
303 
304  dispatcher = _dispatcher;
305  application = app;
306 
307  // Application must always be our parent
308  q->setParent(app);
309 
310  const QMetaObject *meta = q->metaObject();
311  const QString className = QString::fromLatin1(meta->className());
312  q->setObjectName(className);
313 
314  bool namespaceFound = false;
315  for (int i = meta->classInfoCount() - 1; i >= 0; --i) {
316  if (qstrcmp(meta->classInfo(i).name(), "Namespace") == 0) {
317  pathPrefix = QString::fromLatin1(meta->classInfo(i).value());
318  while (pathPrefix.startsWith(u'/')) {
319  pathPrefix.remove(0, 1);
320  }
321  namespaceFound = true;
322  break;
323  }
324  }
325 
326  if (!namespaceFound) {
327  QString controlerNS;
328  bool lastWasUpper = true;
329 
330  for (int i = 0; i < className.length(); ++i) {
331  const QChar c = className.at(i);
332  if (c.isLower() || c.isDigit()) {
333  controlerNS.append(c);
334  lastWasUpper = false;
335  } else if (c == u'_') {
336  controlerNS.append(c);
337  lastWasUpper = true;
338  } else {
339  if (!lastWasUpper) {
340  controlerNS.append(u'/');
341  }
342  if (c != u':') {
343  controlerNS.append(c.toLower());
344  }
345  lastWasUpper = true;
346  }
347  }
348  pathPrefix = controlerNS;
349  }
350 
351  registerActionMethods(meta, q, app);
352 }
353 
354 void ControllerPrivate::setupFinished()
355 {
356  Q_Q(Controller);
357 
358  const ActionList beginList = dispatcher->getActions(QStringLiteral("Begin"), pathPrefix);
359  if (!beginList.isEmpty()) {
360  beginAutoList.append(beginList.last());
361  }
362 
363  beginAutoList.append(dispatcher->getActions(QStringLiteral("Auto"), pathPrefix));
364 
365  const ActionList endList = dispatcher->getActions(QStringLiteral("End"), pathPrefix);
366  if (!endList.isEmpty()) {
367  end = endList.last();
368  }
369 
370  const auto actions = actionList;
371  for (Action *action : actions) {
372  action->dispatcherReady(dispatcher, q);
373  }
374 
375  q->preFork(qobject_cast<Application *>(q->parent()));
376 }
377 
379 {
380  Q_D(Controller);
381 
382  bool ret = true;
383 
384  int &actionRefCount = c->d_ptr->actionRefCount;
385 
386  // Dispatch to Begin and Auto
387  const auto beginAutoList = d->beginAutoList;
388  for (Action *action : beginAutoList) {
389  if (actionRefCount) {
390  c->d_ptr->pendingAsync.enqueue(action);
391  } else if (!action->dispatch(c)) {
392  ret = false;
393  break;
394  }
395  }
396 
397  // Dispatch to Action
398  if (ret) {
399  if (actionRefCount) {
400  c->d_ptr->pendingAsync.enqueue(c->action());
401  } else {
402  ret = c->action()->dispatch(c);
403  }
404  }
405 
406  // Dispatch to End
407  if (d->end) {
408  if (actionRefCount) {
409  c->d_ptr->pendingAsync.enqueue(d->end);
410  } else if (!d->end->dispatch(c)) {
411  ret = false;
412  }
413  }
414 
415  if (actionRefCount) {
416  c->d_ptr->engineRequest->status |= EngineRequest::Async;
417  }
418 
419  return ret;
420 }
421 
422 Action *ControllerPrivate::actionClass(const QVariantHash &args)
423 {
424  const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
425  const QString actionClass = attributes.value(QStringLiteral("ActionClass"));
426 
427  QObject *object = instantiateClass(actionClass, "Cutelyst::Action");
428  if (object) {
429  Action *action = qobject_cast<Cutelyst::Action *>(object);
430  if (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(QStringLiteral("name")).toString());
461  action->setReverse(args.value(QStringLiteral("reverse")).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() + QLatin1Char('/') + QString::fromLatin1(name);
500  }
501 
502  Action *action =
503  createAction({{QStringLiteral("name"), QVariant::fromValue(name)},
504  {QStringLiteral("reverse"), QVariant::fromValue(reverse)},
505  {QStringLiteral("namespace"), QVariant::fromValue(controller->ns())},
506  {QStringLiteral("attributes"), QVariant::fromValue(attrs)}},
507  method,
508  controller,
509  app);
510 
511  actions.insert(action->reverse(), {action->reverse(), action});
512  actionList.append(action);
513  }
514  }
515 }
516 
517 ParamsMultiMap ControllerPrivate::parseAttributes(const QMetaMethod &method,
518  const QByteArray &str,
519  const QByteArray &name)
520 {
521  ParamsMultiMap ret;
522  std::vector<std::pair<QString, QString>> attributes;
523  // This is probably not the best parser ever
524  // but it handles cases like:
525  // :Args:Local('fo"')o'):ActionClass('foo')
526  // into
527  // (("Args",""), ("Local","'fo"')o'"), ("ActionClass","'foo'"))
528 
529  int size = str.size();
530  int pos = 0;
531  while (pos < size) {
532  QString key;
533  QString value;
534 
535  // find the start of a key
536  if (str.at(pos) == ':') {
537  int keyStart = ++pos;
538  int keyLength = 0;
539  while (pos < size) {
540  if (str.at(pos) == '(') {
541  // attribute has value
542  int valueStart = ++pos;
543  while (pos < size) {
544  if (str.at(pos) == ')') {
545  // found the possible end of the value
546  int valueEnd = pos;
547  if (++pos < size && str.at(pos) == ':') {
548  // found the start of a key so this is
549  // really the end of a value
550  value =
551  QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
552  break;
553  } else if (pos >= size) {
554  // found the end of the string
555  // save the remainig as the value
556  value =
557  QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
558  break;
559  }
560  // string was not '):' or ')$'
561  continue;
562  }
563  ++pos;
564  }
565 
566  break;
567  } else if (str.at(pos) == ':') {
568  // Attribute has no value
569  break;
570  }
571  ++keyLength;
572  ++pos;
573  }
574 
575  // stopre the key
576  key = QString::fromLatin1(str.mid(keyStart, keyLength));
577 
578  // remove quotes
579  if (!value.isEmpty()) {
580  if ((value.startsWith(u'\'') && value.endsWith(u'\'')) ||
581  (value.startsWith(u'"') && value.endsWith(u'"'))) {
582  value.remove(0, 1);
583  value.remove(value.size() - 1, 1);
584  }
585  }
586 
587  // store the key/value pair found
588  attributes.emplace_back(std::make_pair(key, value));
589  continue;
590  }
591  ++pos;
592  }
593 
594  const static auto digitRE = QRegularExpression(u"\\D"_s);
595 
596  // Add the attributes to the map in the reverse order so
597  // that values() return them in the right order
598  for (const auto &pair : attributes) {
599  QString key = pair.first;
600  QString value = pair.second;
601  if (key.compare(u"Global") == 0) {
602  key = QStringLiteral("Path");
603  value = parsePathAttr(QLatin1Char('/') + QString::fromLatin1(name));
604  } else if (key.compare(u"Local") == 0) {
605  key = QStringLiteral("Path");
606  value = parsePathAttr(QString::fromLatin1(name));
607  } else if (key.compare(u"Path") == 0) {
608  value = parsePathAttr(value);
609  } else if (key.compare(u"Args") == 0) {
610  QString args = value;
611  if (!args.isEmpty()) {
612  value = args.remove(digitRE);
613  }
614  } else if (key.compare(u"CaptureArgs") == 0) {
615  QString captureArgs = value;
616  value = captureArgs.remove(digitRE);
617  } else if (key.compare(u"Chained") == 0) {
618  value = parseChainedAttr(value);
619  }
620 
621  ret.insert(key, value);
622  }
623 
624  // Handle special AutoArgs and AutoCaptureArgs case
625  if (!ret.contains(QStringLiteral("Args")) && !ret.contains(QStringLiteral("CaptureArgs")) &&
626  (ret.contains(QStringLiteral("AutoArgs")) ||
627  ret.contains(QStringLiteral("AutoCaptureArgs")))) {
628  if (ret.contains(QStringLiteral("AutoArgs")) &&
629  ret.contains(QStringLiteral("AutoCaptureArgs"))) {
630  qFatal("Action '%s' has both AutoArgs and AutoCaptureArgs, which is not allowed",
631  name.constData());
632  } else {
633  QString parameterName;
634  if (ret.contains(QStringLiteral("AutoArgs"))) {
635  ret.remove(QStringLiteral("AutoArgs"));
636  parameterName = QStringLiteral("Args");
637  } else {
638  ret.remove(QStringLiteral("AutoCaptureArgs"));
639  parameterName = QStringLiteral("CaptureArgs");
640  }
641 
642  // If the signature is not QStringList we count them
643  if (!(method.parameterCount() == 2 &&
644  method.parameterType(1) == QMetaType::QStringList)) {
645  int parameterCount = 0;
646  for (int i2 = 1; i2 < method.parameterCount(); ++i2) {
647  int typeId = method.parameterType(i2);
648  if (typeId == QMetaType::QString) {
649  ++parameterCount;
650  }
651  }
652  ret.replace(parameterName, QString::number(parameterCount));
653  }
654  }
655  }
656 
657  // If the method is private add a Private attribute
658  if (!ret.contains(QStringLiteral("Private")) && method.access() == QMetaMethod::Private) {
659  ret.replace(QStringLiteral("Private"), QString());
660  }
661 
662  return ret;
663 }
664 
665 QStack<Component *> ControllerPrivate::gatherActionRoles(const QVariantHash &args)
666 {
667  QStack<Component *> roles;
668  const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
669  auto doesIt = attributes.constFind(QStringLiteral("Does"));
670  while (doesIt != attributes.constEnd() && doesIt.key().compare(u"Does") == 0) {
671  QObject *object =
672  instantiateClass(doesIt.value(), QByteArrayLiteral("Cutelyst::Component"));
673  if (object) {
674  roles.push(qobject_cast<Component *>(object));
675  }
676  ++doesIt;
677  }
678  return roles;
679 }
680 
681 QString ControllerPrivate::parsePathAttr(const QString &value)
682 {
683  QString ret = pathPrefix;
684  if (value.startsWith(u'/')) {
685  ret = value;
686  } else if (!value.isEmpty()) {
687  ret = pathPrefix + u'/' + value;
688  }
689  return ret;
690 }
691 
692 QString ControllerPrivate::parseChainedAttr(const QString &attr)
693 {
694  QString ret = QStringLiteral("/");
695  if (attr.isEmpty()) {
696  return ret;
697  }
698 
699  if (attr.compare(u".") == 0) {
700  ret.append(pathPrefix);
701  } else if (!attr.startsWith(u'/')) {
702  if (!pathPrefix.isEmpty()) {
703  ret.append(pathPrefix + u'/' + attr);
704  } else {
705  // special case namespace '' (root)
706  ret.append(attr);
707  }
708  } else {
709  ret = attr;
710  }
711 
712  return ret;
713 }
714 
715 QObject *ControllerPrivate::instantiateClass(const QString &name, const QByteArray &super)
716 {
717  QString instanceName = name;
718  if (!instanceName.isEmpty()) {
719  const static QRegularExpression nonWordsRE(u"\\W"_s);
720  instanceName.remove(nonWordsRE);
721 
722  QMetaType id = QMetaType::fromName(instanceName.toLatin1().data());
723  if (!id.isValid()) {
724  if (!instanceName.endsWith(QLatin1Char('*'))) {
725  instanceName.append(QLatin1Char('*'));
726  }
727 
728  id = QMetaType::fromName(instanceName.toLatin1().data());
729  if (!id.isValid() && !instanceName.startsWith(u"Cutelyst::")) {
730  instanceName = QLatin1String("Cutelyst::") + instanceName;
731  id = QMetaType::fromName(instanceName.toLatin1().data());
732  }
733  }
734 
735  if (id.isValid()) {
736  const QMetaObject *metaObj = id.metaObject();
737  if (metaObj) {
738  if (!superIsClassName(metaObj->superClass(), super)) {
739  qCWarning(CUTELYST_CONTROLLER)
740  << "Class name" << instanceName << "is not a derived class of" << super;
741  }
742 
743  QObject *object = metaObj->newInstance();
744  if (!object) {
745  qCWarning(CUTELYST_CONTROLLER)
746  << "Could create a new instance of" << instanceName
747  << "make sure it's default constructor is "
748  "marked with the Q_INVOKABLE macro";
749  }
750 
751  return object;
752  }
753  } else {
754  Component *component = application->createComponentPlugin(name);
755  if (component) {
756  return component;
757  }
758 
759  component = application->createComponentPlugin(instanceName);
760  if (component) {
761  return component;
762  }
763  }
764 
765  if (!id.isValid()) {
766  qCCritical(CUTELYST_CONTROLLER,
767  "Could not create component '%s', you can register it with "
768  "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
769  qPrintable(instanceName),
770  qPrintable(instanceName));
771  }
772  }
773 
774  return nullptr;
775 }
776 
777 bool ControllerPrivate::superIsClassName(const QMetaObject *super, const QByteArray &className)
778 {
779  if (super) {
780  if (super->className() == className) {
781  return true;
782  }
783  return superIsClassName(super->superClass(), className);
784  }
785  return false;
786 }
787 
788 #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
const_iterator constFind(const Key &key) const const
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
virtual const QMetaObject * metaObject() const const
virtual bool preFork(Application *app)
Definition: controller.cpp:281
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
T value(qsizetype i) const const
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:378
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(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:287
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:254
qsizetype length() const const
char * data()
bool operator==(const char *className)
Definition: controller.cpp:276
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:260
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:270
bool isLower(char32_t ucs4)
T value(const Key &key, const T &defaultValue) const const
Controller(QObject *parent=nullptr)
Definition: controller.cpp:241