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