cutelyst  4.4.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<Cutelyst::Action *>(object);
429  if (action) {
430  return action;
431  }
432  qCWarning(CUTELYST_CONTROLLER) << "ActionClass" << actionClass << "is not an ActionClass"
433  << object->metaObject()->superClass()->className();
434  delete object;
435  }
436 
437  return new Action;
438 }
439 
440 Action *ControllerPrivate::createAction(const QVariantHash &args,
441  const QMetaMethod &method,
442  Controller *controller,
443  Application *app)
444 {
445  Action *action = actionClass(args);
446  if (!action) {
447  return nullptr;
448  }
449 
450  QStack<Component *> roles = gatherActionRoles(args);
451  for (int i = 0; i < roles.size(); ++i) {
452  Component *code = roles.at(i);
453  code->init(app, args);
454  code->setParent(action);
455  }
456  action->applyRoles(roles);
457  action->setMethod(method);
458  action->setController(controller);
459  action->setName(args.value(QStringLiteral("name")).toString());
460  action->setReverse(args.value(QStringLiteral("reverse")).toString());
461  action->setupAction(args, app);
462 
463  return action;
464 }
465 
466 void ControllerPrivate::registerActionMethods(const QMetaObject *meta,
467  Controller *controller,
468  Application *app)
469 {
470  // Setup actions
471  for (int i = 0; i < meta->methodCount(); ++i) {
472  const QMetaMethod method = meta->method(i);
473  const QByteArray name = method.name();
474 
475  // We register actions that are either a Q_SLOT
476  // or a Q_INVOKABLE function which has the first
477  // parameter type equal to Context*
478  if (method.isValid() &&
479  (method.methodType() == QMetaMethod::Method ||
480  method.methodType() == QMetaMethod::Slot) &&
481  (method.parameterCount() &&
482  method.parameterType(0) == qMetaTypeId<Cutelyst::Context *>())) {
483 
484  // Build up the list of attributes for the class info
485  QByteArray attributeArray;
486  for (int i2 = meta->classInfoCount() - 1; i2 >= 0; --i2) {
487  QMetaClassInfo classInfo = meta->classInfo(i2);
488  if (name == classInfo.name()) {
489  attributeArray.append(classInfo.value());
490  }
491  }
492  ParamsMultiMap attrs = parseAttributes(method, attributeArray, name);
493 
494  QString reverse;
495  if (controller->ns().isEmpty()) {
496  reverse = QString::fromLatin1(name);
497  } else {
498  reverse = controller->ns() + QLatin1Char('/') + QString::fromLatin1(name);
499  }
500 
501  Action *action =
502  createAction({{QStringLiteral("name"), QVariant::fromValue(name)},
503  {QStringLiteral("reverse"), QVariant::fromValue(reverse)},
504  {QStringLiteral("namespace"), QVariant::fromValue(controller->ns())},
505  {QStringLiteral("attributes"), 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  QString key;
532  QString value;
533 
534  // find the start of a key
535  if (str.at(pos) == ':') {
536  int keyStart = ++pos;
537  int keyLength = 0;
538  while (pos < size) {
539  if (str.at(pos) == '(') {
540  // attribute has value
541  int valueStart = ++pos;
542  while (pos < size) {
543  if (str.at(pos) == ')') {
544  // found the possible end of the value
545  int valueEnd = 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 remainig 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 
565  break;
566  } else if (str.at(pos) == ':') {
567  // Attribute has no value
568  break;
569  }
570  ++keyLength;
571  ++pos;
572  }
573 
574  // stopre the key
575  key = QString::fromLatin1(str.mid(keyStart, keyLength));
576 
577  // remove quotes
578  if (!value.isEmpty()) {
579  if ((value.startsWith(u'\'') && value.endsWith(u'\'')) ||
580  (value.startsWith(u'"') && value.endsWith(u'"'))) {
581  value.remove(0, 1);
582  value.remove(value.size() - 1, 1);
583  }
584  }
585 
586  // store the key/value pair found
587  attributes.emplace_back(std::make_pair(key, value));
588  continue;
589  }
590  ++pos;
591  }
592 
593  const static auto digitRE = QRegularExpression(u"\\D"_qs);
594 
595  // Add the attributes to the map in the reverse order so
596  // that values() return them in the right order
597  for (const auto &pair : attributes) {
598  QString key = pair.first;
599  QString value = pair.second;
600  if (key.compare(u"Global") == 0) {
601  key = QStringLiteral("Path");
602  value = parsePathAttr(QLatin1Char('/') + QString::fromLatin1(name));
603  } else if (key.compare(u"Local") == 0) {
604  key = QStringLiteral("Path");
605  value = parsePathAttr(QString::fromLatin1(name));
606  } else if (key.compare(u"Path") == 0) {
607  value = parsePathAttr(value);
608  } else if (key.compare(u"Args") == 0) {
609  QString args = value;
610  if (!args.isEmpty()) {
611  value = args.remove(digitRE);
612  }
613  } else if (key.compare(u"CaptureArgs") == 0) {
614  QString captureArgs = value;
615  value = captureArgs.remove(digitRE);
616  } else if (key.compare(u"Chained") == 0) {
617  value = parseChainedAttr(value);
618  }
619 
620  ret.insert(key, value);
621  }
622 
623  // Handle special AutoArgs and AutoCaptureArgs case
624  if (!ret.contains(QStringLiteral("Args")) && !ret.contains(QStringLiteral("CaptureArgs")) &&
625  (ret.contains(QStringLiteral("AutoArgs")) ||
626  ret.contains(QStringLiteral("AutoCaptureArgs")))) {
627  if (ret.contains(QStringLiteral("AutoArgs")) &&
628  ret.contains(QStringLiteral("AutoCaptureArgs"))) {
629  qFatal("Action '%s' has both AutoArgs and AutoCaptureArgs, which is not allowed",
630  name.constData());
631  } else {
632  QString parameterName;
633  if (ret.contains(QStringLiteral("AutoArgs"))) {
634  ret.remove(QStringLiteral("AutoArgs"));
635  parameterName = QStringLiteral("Args");
636  } else {
637  ret.remove(QStringLiteral("AutoCaptureArgs"));
638  parameterName = QStringLiteral("CaptureArgs");
639  }
640 
641  // If the signature is not QStringList we count them
642  if (!(method.parameterCount() == 2 &&
643  method.parameterType(1) == QMetaType::QStringList)) {
644  int parameterCount = 0;
645  for (int i2 = 1; i2 < method.parameterCount(); ++i2) {
646  int typeId = method.parameterType(i2);
647  if (typeId == QMetaType::QString) {
648  ++parameterCount;
649  }
650  }
651  ret.replace(parameterName, QString::number(parameterCount));
652  }
653  }
654  }
655 
656  // If the method is private add a Private attribute
657  if (!ret.contains(QStringLiteral("Private")) && method.access() == QMetaMethod::Private) {
658  ret.replace(QStringLiteral("Private"), QString());
659  }
660 
661  return ret;
662 }
663 
664 QStack<Component *> ControllerPrivate::gatherActionRoles(const QVariantHash &args)
665 {
666  QStack<Component *> roles;
667  const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
668  auto doesIt = attributes.constFind(QStringLiteral("Does"));
669  while (doesIt != attributes.constEnd() && doesIt.key().compare(u"Does") == 0) {
670  QObject *object =
671  instantiateClass(doesIt.value(), QByteArrayLiteral("Cutelyst::Component"));
672  if (object) {
673  roles.push(qobject_cast<Component *>(object));
674  }
675  ++doesIt;
676  }
677  return roles;
678 }
679 
680 QString ControllerPrivate::parsePathAttr(const QString &value)
681 {
682  QString ret = pathPrefix;
683  if (value.startsWith(u'/')) {
684  ret = value;
685  } else if (!value.isEmpty()) {
686  ret = pathPrefix + u'/' + value;
687  }
688  return ret;
689 }
690 
691 QString ControllerPrivate::parseChainedAttr(const QString &attr)
692 {
693  QString ret = QStringLiteral("/");
694  if (attr.isEmpty()) {
695  return ret;
696  }
697 
698  if (attr.compare(u".") == 0) {
699  ret.append(pathPrefix);
700  } else if (!attr.startsWith(u'/')) {
701  if (!pathPrefix.isEmpty()) {
702  ret.append(pathPrefix + u'/' + attr);
703  } else {
704  // special case namespace '' (root)
705  ret.append(attr);
706  }
707  } else {
708  ret = attr;
709  }
710 
711  return ret;
712 }
713 
714 QObject *ControllerPrivate::instantiateClass(const QString &name, const QByteArray &super)
715 {
716  QString instanceName = name;
717  if (!instanceName.isEmpty()) {
718  const static QRegularExpression nonWordsRE(u"\\W"_qs);
719  instanceName.remove(nonWordsRE);
720 
721  QMetaType id = QMetaType::fromName(instanceName.toLatin1().data());
722  if (!id.isValid()) {
723  if (!instanceName.endsWith(QLatin1Char('*'))) {
724  instanceName.append(QLatin1Char('*'));
725  }
726 
727  id = QMetaType::fromName(instanceName.toLatin1().data());
728  if (!id.isValid() && !instanceName.startsWith(u"Cutelyst::")) {
729  instanceName = QLatin1String("Cutelyst::") + instanceName;
730  id = QMetaType::fromName(instanceName.toLatin1().data());
731  }
732  }
733 
734  if (id.isValid()) {
735  const QMetaObject *metaObj = id.metaObject();
736  if (metaObj) {
737  if (!superIsClassName(metaObj->superClass(), super)) {
738  qCWarning(CUTELYST_CONTROLLER)
739  << "Class name" << instanceName << "is not a derived class of" << super;
740  }
741 
742  QObject *object = metaObj->newInstance();
743  if (!object) {
744  qCWarning(CUTELYST_CONTROLLER)
745  << "Could create a new instance of" << instanceName
746  << "make sure it's default constructor is "
747  "marked with the Q_INVOKABLE macro";
748  }
749 
750  return object;
751  }
752  } else {
753  Component *component = application->createComponentPlugin(name);
754  if (component) {
755  return component;
756  }
757 
758  component = application->createComponentPlugin(instanceName);
759  if (component) {
760  return component;
761  }
762  }
763 
764  if (!id.isValid()) {
765  qCCritical(CUTELYST_CONTROLLER,
766  "Could not create component '%s', you can register it with "
767  "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
768  qPrintable(instanceName),
769  qPrintable(instanceName));
770  }
771  }
772 
773  return nullptr;
774 }
775 
776 bool ControllerPrivate::superIsClassName(const QMetaObject *super, const QByteArray &className)
777 {
778  if (super) {
779  if (super->className() == className) {
780  return true;
781  }
782  return superIsClassName(super->superClass(), className);
783  }
784  return false;
785 }
786 
787 #include "moc_controller.cpp"
This class represents a Cutelyst Action.
Definition: action.h:35
void setupAction(const QVariantHash &args, Application *app)
Definition: action.cpp:46
bool dispatch(Context *c)
Definition: action.h:88
void setMethod(const QMetaMethod &method)
Definition: action.cpp:27
void setController(Controller *controller)
Definition: action.cpp:40
The Cutelyst application.
Definition: application.h:66
The Cutelyst Component base class.
Definition: component.h:30
void setReverse(const QString &reverse)
Definition: component.cpp:51
virtual bool init(Application *application, const QVariantHash &args)
Definition: component.cpp:57
void applyRoles(const QStack< Component * > &roles)
Definition: component.cpp:133
QString reverse() const noexcept
Definition: component.cpp:45
void setName(const QString &name)
Definition: component.cpp:39
The Cutelyst Context.
Definition: context.h:42
Action * action
Definition: context.h:47
Cutelyst Controller base class.
Definition: controller.h:56
virtual bool preFork(Application *app)
Definition: controller.cpp:280
virtual bool postFork(Application *app)
Definition: controller.cpp:286
bool operator==(const char *className)
Definition: controller.cpp:275
QString ns() const noexcept
Definition: controller.cpp:253
ActionList actions() const noexcept
Definition: controller.cpp:269
bool _DISPATCH(Context *c)
Definition: controller.cpp:377
Controller(QObject *parent=nullptr)
Definition: controller.cpp:240
Action * actionFor(QStringView name) const
Definition: controller.cpp:259
The Cutelyst Dispatcher.
Definition: dispatcher.h:29
The Cutelyst namespace holds all public Cutelyst API.
QByteArray & append(QByteArrayView data)
char at(qsizetype i) const const
const char * constData() const const
char * data()
QByteArray mid(qsizetype pos, qsizetype len) const const
qsizetype size() const const
bool isDigit(char32_t ucs4)
bool isLower(char32_t ucs4)
char32_t toLower(char32_t ucs4)
void append(QList::parameter_type value)
QList::const_reference at(qsizetype i) const const
bool isEmpty() const const
T & last()
qsizetype size() const const
T value(qsizetype i) const const
const char * name() const const
const char * value() const const
QMetaMethod::Access access() const const
bool isValid() const const
QMetaMethod::MethodType methodType() const const
QByteArray name() const const
int parameterCount() const const
int parameterType(int index) const const
QMetaClassInfo classInfo(int index) const const
int classInfoCount() const const
const char * className() const const
QMetaMethod method(int index) const const
int methodCount() const const
QObject * newInstance(Args &&... arguments) const const
const QMetaObject * superClass() const const
QMetaType fromName(QByteArrayView typeName)
QMultiMap::const_iterator constFind(const Key &key) const const
bool contains(const Key &key) const const
QMultiMap::iterator insert(QMultiMap::const_iterator pos, const Key &key, const T &value)
Key key(const T &value, const Key &defaultKey) const const
QMultiMap::size_type remove(const Key &key)
QMultiMap::iterator replace(const Key &key, const T &value)
T value(const Key &key, const T &defaultValue) const const
virtual const QMetaObject * metaObject() const const
void setParent(QObject *parent)
void push(const T &t)
QString & append(QChar ch)
const QChar at(qsizetype position) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString first(qsizetype n) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
qsizetype length() const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QString toString() const const
QVariant fromValue(T &&value)