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
15using namespace Cutelyst;
16
241 : QObject(parent)
242 , d_ptr(new ControllerPrivate(this))
243{
244}
245
246Controller::~Controller()
247{
248 Q_D(Controller);
249 qDeleteAll(d->actionList);
250 delete d_ptr;
251}
252
253QString Controller::ns() const noexcept
254{
255 Q_D(const Controller);
256 return d->pathPrefix;
257}
258
259Action *Controller::actionFor(QStringView name) const
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
275bool 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
292ControllerPrivate::ControllerPrivate(Controller *parent)
293 : q_ptr(parent)
294{
295}
296
297void 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
353void 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
421Action *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
440Action *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
466void 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
516ParamsMultiMap 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
664QStack<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
680QString 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
691QString 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
714QObject *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
776bool 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
QString className() const noexcept
Definition: action.cpp:86
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
QString name() const noexcept
Definition: component.cpp:33
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
QMultiMap< QString, QString > ParamsMultiMap
The Cutelyst namespace holds all public Cutelyst API.
QVector< Action * > ActionList
Definition: action.h:161