cutelyst 5.0.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 // 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 // find the start of a key
532 if (str.at(pos) == ':') {
533 QString key;
534 QString value;
535 int keyStart = ++pos;
536 int keyLength = 0;
537 while (pos < size) {
538 if (str.at(pos) == '(') {
539 // attribute has value
540 int valueStart = ++pos;
541 while (pos < size) {
542 if (str.at(pos) == ')') {
543 // found the possible end of the value
544 int valueEnd = pos;
545 ++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 remaining 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 break;
565 } else if (str.at(pos) == ':') {
566 // Attribute has no value
567 break;
568 }
569 ++keyLength;
570 ++pos;
571 }
572
573 // store the key
574 key = QString::fromLatin1(str.mid(keyStart, keyLength));
575
576 // remove quotes
577 if (!value.isEmpty()) {
578 if ((value.startsWith(u'\'') && value.endsWith(u'\'')) ||
579 (value.startsWith(u'"') && value.endsWith(u'"'))) {
580 value.remove(0, 1);
581 value.remove(value.size() - 1, 1);
582 }
583 }
584
585 // store the key/value pair found
586 attributes.emplace_back(key, value);
587 continue;
588 }
589 ++pos;
590 }
591
592 const static auto digitRE = QRegularExpression(u"\\D"_s);
593
594 // Add the attributes to the map in the reverse order
595 std::ranges::for_each(attributes | std::views::reverse, [&](const auto &pair) {
596 QString key = pair.first;
597 QString value = pair.second;
598 if (key.compare(u"Global") == 0) {
599 key = u"Path"_s;
600 value = parsePathAttr(u'/' + QString::fromLatin1(name));
601 } else if (key.compare(u"Local") == 0) {
602 key = u"Path"_s;
603 value = parsePathAttr(QString::fromLatin1(name));
604 } else if (key.compare(u"Path") == 0) {
605 value = parsePathAttr(value);
606 } else if (key.compare(u"Args") == 0) {
607 QString args = value;
608 if (!args.isEmpty()) {
609 value = args.remove(digitRE);
610 }
611 } else if (key.compare(u"CaptureArgs") == 0) {
612 QString captureArgs = value;
613 value = captureArgs.remove(digitRE);
614 } else if (key.compare(u"Chained") == 0) {
615 value = parseChainedAttr(value);
616 }
617
618 ret.insert(key, value);
619 });
620
621 // Handle special AutoArgs and AutoCaptureArgs case
622 if (!ret.contains(u"Args"_s) && !ret.contains(u"CaptureArgs"_s) &&
623 (ret.contains(u"AutoArgs"_s) || ret.contains(u"AutoCaptureArgs"_s))) {
624 if (ret.contains(u"AutoArgs"_s) && ret.contains(u"AutoCaptureArgs"_s)) {
625 qFatal("Action '%s' has both AutoArgs and AutoCaptureArgs, which is not allowed",
626 name.constData());
627 } else {
628 QString parameterName;
629 if (ret.contains(u"AutoArgs"_s)) {
630 ret.remove(u"AutoArgs"_s);
631 parameterName = u"Args"_s;
632 } else {
633 ret.remove(u"AutoCaptureArgs"_s);
634 parameterName = u"CaptureArgs"_s;
635 }
636
637 // If the signature is not QStringList we count them
638 if (!(method.parameterCount() == 2 &&
640 int parameterCount = 0;
641 for (int i2 = 1; i2 < method.parameterCount(); ++i2) {
642 int typeId = method.parameterType(i2);
643 if (typeId == QMetaType::QString) {
644 ++parameterCount;
645 }
646 }
647 ret.replace(parameterName, QString::number(parameterCount));
648 }
649 }
650 }
651
652 // If the method is private add a Private attribute
653 if (!ret.contains(u"Private"_s) && method.access() == QMetaMethod::Private) {
654 ret.insert(u"Private"_s, {});
655 }
656
657 return ret;
658}
659
660QStack<Component *> ControllerPrivate::gatherActionRoles(const QVariantHash &args)
661{
663 const auto attributes = args.value(u"attributes"_s).value<ParamsMultiMap>();
664 auto doesIt = attributes.constFind(u"Does"_s);
665 while (doesIt != attributes.constEnd() && doesIt.key().compare(u"Does") == 0) {
666 QObject *object =
667 instantiateClass(doesIt.value(), QByteArrayLiteral("Cutelyst::Component"));
668 if (object) {
669 roles.push(qobject_cast<Component *>(object));
670 }
671 ++doesIt;
672 }
673 return roles;
674}
675
676QString ControllerPrivate::parsePathAttr(const QString &value)
677{
678 QString ret = pathPrefix;
679 if (value.startsWith(u'/')) {
680 ret = value;
681 } else if (!value.isEmpty()) {
682 ret = pathPrefix + u'/' + value;
683 }
684 return ret;
685}
686
687QString ControllerPrivate::parseChainedAttr(const QString &attr)
688{
689 QString ret = u"/"_s;
690 if (attr.isEmpty()) {
691 return ret;
692 }
693
694 if (attr.compare(u".") == 0) {
695 ret.append(pathPrefix);
696 } else if (!attr.startsWith(u'/')) {
697 if (!pathPrefix.isEmpty()) {
698 ret.append(pathPrefix + u'/' + attr);
699 } else {
700 // special case namespace '' (root)
701 ret.append(attr);
702 }
703 } else {
704 ret = attr;
705 }
706
707 return ret;
708}
709
710QObject *ControllerPrivate::instantiateClass(const QString &name, const QByteArray &super)
711{
712 QString instanceName = name;
713 if (!instanceName.isEmpty()) {
714 const static QRegularExpression nonWordsRE(u"\\W"_s);
715 instanceName.remove(nonWordsRE);
716
717 QMetaType id = QMetaType::fromName(instanceName.toLatin1().data());
718 if (!id.isValid()) {
719 if (!instanceName.endsWith(u'*')) {
720 instanceName.append(u'*');
721 }
722
723 id = QMetaType::fromName(instanceName.toLatin1().data());
724 if (!id.isValid() && !instanceName.startsWith(u"Cutelyst::")) {
725 instanceName = u"Cutelyst::" + instanceName;
726 id = QMetaType::fromName(instanceName.toLatin1().data());
727 }
728 }
729
730 if (id.isValid()) {
731 const QMetaObject *metaObj = id.metaObject();
732 if (metaObj) {
733 if (!superIsClassName(metaObj->superClass(), super)) {
734 qCWarning(CUTELYST_CONTROLLER)
735 << "Class name" << instanceName << "is not a derived class of" << super;
736 }
737
738 QObject *object = metaObj->newInstance();
739 if (!object) {
740 qCWarning(CUTELYST_CONTROLLER)
741 << "Could create a new instance of" << instanceName
742 << "make sure it's default constructor is "
743 "marked with the Q_INVOKABLE macro";
744 }
745
746 return object;
747 }
748 } else {
749 Component *component = application->createComponentPlugin(name);
750 if (component) {
751 return component;
752 }
753
754 component = application->createComponentPlugin(instanceName);
755 if (component) {
756 return component;
757 }
758 }
759
760 if (!id.isValid()) {
761 qCCritical(CUTELYST_CONTROLLER,
762 "Could not create component '%s', you can register it with "
763 "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
764 qPrintable(instanceName),
765 qPrintable(instanceName));
766 }
767 }
768
769 return nullptr;
770}
771
772bool ControllerPrivate::superIsClassName(const QMetaObject *super, const QByteArray &className)
773{
774 if (super) {
775 if (super->className() == className) {
776 return true;
777 }
778 return superIsClassName(super->superClass(), className);
779 }
780 return false;
781}
782
783#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 toString() const const
QVariant fromValue(T &&value)