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 
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"
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:280
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: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(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: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
bool isDigit(char32_t ucs4)
void setupAction(const QVariantHash &args, Application *app)
Definition: action.cpp:46
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: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(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:269
bool isLower(char32_t ucs4)
T value(const Key &key, const T &defaultValue) const const
Controller(QObject *parent=nullptr)
Definition: controller.cpp:240