cutelyst  4.6.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
QMultiMap::const_iterator constFind(const Key &key) const const
QMultiMap::iterator insert(QMultiMap::const_iterator pos, const Key &key, const T &value)
QByteArray name() const const
QMultiMap::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
void push(const T &t)
QList::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)
const char * name() const const
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: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
void append(QList::parameter_type value)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
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 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
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
QVariant fromValue(const T &value)
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
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
bool isDigit(char32_t ucs4)
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
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:270
bool isLower(char32_t ucs4)
T value(const Key &key, const T &defaultValue) const const
Controller(QObject *parent=nullptr)
Definition: controller.cpp:241