cutelyst 4.3.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
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<Action *>(object);
429 if (action) {
430 return action;
431 }
432 qCWarning(CUTELYST_CONTROLLER) << "ActionClass" << actionClass << "is not an ActionClass";
433 delete object;
434 }
435
436 return new Action;
437}
438
439Action *ControllerPrivate::createAction(const QVariantHash &args,
440 const QMetaMethod &method,
441 Controller *controller,
442 Application *app)
443{
444 Action *action = actionClass(args);
445 if (!action) {
446 return nullptr;
447 }
448
449 QStack<Component *> roles = gatherActionRoles(args);
450 for (int i = 0; i < roles.size(); ++i) {
451 Component *code = roles.at(i);
452 code->init(app, args);
453 code->setParent(action);
454 }
455 action->applyRoles(roles);
456 action->setMethod(method);
457 action->setController(controller);
458 action->setName(args.value(QStringLiteral("name")).toString());
459 action->setReverse(args.value(QStringLiteral("reverse")).toString());
460 action->setupAction(args, app);
461
462 return action;
463}
464
465void ControllerPrivate::registerActionMethods(const QMetaObject *meta,
466 Controller *controller,
467 Application *app)
468{
469 // Setup actions
470 for (int i = 0; i < meta->methodCount(); ++i) {
471 const QMetaMethod method = meta->method(i);
472 const QByteArray name = method.name();
473
474 // We register actions that are either a Q_SLOT
475 // or a Q_INVOKABLE function which has the first
476 // parameter type equal to Context*
477 if (method.isValid() &&
478 (method.methodType() == QMetaMethod::Method ||
479 method.methodType() == QMetaMethod::Slot) &&
480 (method.parameterCount() &&
481 method.parameterType(0) == qMetaTypeId<Cutelyst::Context *>())) {
482
483 // Build up the list of attributes for the class info
484 QByteArray attributeArray;
485 for (int i2 = meta->classInfoCount() - 1; i2 >= 0; --i2) {
486 QMetaClassInfo classInfo = meta->classInfo(i2);
487 if (name == classInfo.name()) {
488 attributeArray.append(classInfo.value());
489 }
490 }
491 ParamsMultiMap attrs = parseAttributes(method, attributeArray, name);
492
493 QString reverse;
494 if (controller->ns().isEmpty()) {
495 reverse = QString::fromLatin1(name);
496 } else {
497 reverse = controller->ns() + QLatin1Char('/') + QString::fromLatin1(name);
498 }
499
500 Action *action =
501 createAction({{QStringLiteral("name"), QVariant::fromValue(name)},
502 {QStringLiteral("reverse"), QVariant::fromValue(reverse)},
503 {QStringLiteral("namespace"), QVariant::fromValue(controller->ns())},
504 {QStringLiteral("attributes"), QVariant::fromValue(attrs)}},
505 method,
506 controller,
507 app);
508
509 actions.insert(action->reverse(), {action->reverse(), action});
510 actionList.append(action);
511 }
512 }
513}
514
515ParamsMultiMap ControllerPrivate::parseAttributes(const QMetaMethod &method,
516 const QByteArray &str,
517 const QByteArray &name)
518{
519 ParamsMultiMap ret;
520 std::vector<std::pair<QString, QString>> attributes;
521 // This is probably not the best parser ever
522 // but it handles cases like:
523 // :Args:Local('fo"')o'):ActionClass('foo')
524 // into
525 // (("Args",""), ("Local","'fo"')o'"), ("ActionClass","'foo'"))
526
527 int size = str.size();
528 int pos = 0;
529 while (pos < size) {
530 QString key;
531 QString value;
532
533 // find the start of a key
534 if (str.at(pos) == ':') {
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 if (++pos < size && str.at(pos) == ':') {
546 // found the start of a key so this is
547 // really the end of a value
548 value =
549 QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
550 break;
551 } else if (pos >= size) {
552 // found the end of the string
553 // save the remainig as the value
554 value =
555 QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
556 break;
557 }
558 // string was not '):' or ')$'
559 continue;
560 }
561 ++pos;
562 }
563
564 break;
565 } else if (str.at(pos) == ':') {
566 // Attribute has no value
567 break;
568 }
569 ++keyLength;
570 ++pos;
571 }
572
573 // stopre 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(std::make_pair(key, value));
587 continue;
588 }
589 ++pos;
590 }
591
592 const static auto digitRE = QRegularExpression(u"\\D"_qs);
593
594 // Add the attributes to the map in the reverse order so
595 // that values() return them in the right order
596 for (const auto &pair : attributes) {
597 QString key = pair.first;
598 QString value = pair.second;
599 if (key.compare(u"Global") == 0) {
600 key = QStringLiteral("Path");
601 value = parsePathAttr(QLatin1Char('/') + QString::fromLatin1(name));
602 } else if (key.compare(u"Local") == 0) {
603 key = QStringLiteral("Path");
604 value = parsePathAttr(QString::fromLatin1(name));
605 } else if (key.compare(u"Path") == 0) {
606 value = parsePathAttr(value);
607 } else if (key.compare(u"Args") == 0) {
608 QString args = value;
609 if (!args.isEmpty()) {
610 value = args.remove(digitRE);
611 }
612 } else if (key.compare(u"CaptureArgs") == 0) {
613 QString captureArgs = value;
614 value = captureArgs.remove(digitRE);
615 } else if (key.compare(u"Chained") == 0) {
616 value = parseChainedAttr(value);
617 }
618
619 ret.insert(key, value);
620 }
621
622 // Handle special AutoArgs and AutoCaptureArgs case
623 if (!ret.contains(QStringLiteral("Args")) && !ret.contains(QStringLiteral("CaptureArgs")) &&
624 (ret.contains(QStringLiteral("AutoArgs")) ||
625 ret.contains(QStringLiteral("AutoCaptureArgs")))) {
626 if (ret.contains(QStringLiteral("AutoArgs")) &&
627 ret.contains(QStringLiteral("AutoCaptureArgs"))) {
628 qFatal("Action '%s' has both AutoArgs and AutoCaptureArgs, which is not allowed",
629 name.constData());
630 } else {
631 QString parameterName;
632 if (ret.contains(QStringLiteral("AutoArgs"))) {
633 ret.remove(QStringLiteral("AutoArgs"));
634 parameterName = QStringLiteral("Args");
635 } else {
636 ret.remove(QStringLiteral("AutoCaptureArgs"));
637 parameterName = QStringLiteral("CaptureArgs");
638 }
639
640 // If the signature is not QStringList we count them
641 if (!(method.parameterCount() == 2 &&
643 int parameterCount = 0;
644 for (int i2 = 1; i2 < method.parameterCount(); ++i2) {
645 int typeId = method.parameterType(i2);
646 if (typeId == QMetaType::QString) {
647 ++parameterCount;
648 }
649 }
650 ret.replace(parameterName, QString::number(parameterCount));
651 }
652 }
653 }
654
655 // If the method is private add a Private attribute
656 if (!ret.contains(QStringLiteral("Private")) && method.access() == QMetaMethod::Private) {
657 ret.replace(QStringLiteral("Private"), QString());
658 }
659
660 return ret;
661}
662
663QStack<Component *> ControllerPrivate::gatherActionRoles(const QVariantHash &args)
664{
666 const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
667 auto doesIt = attributes.constFind(QStringLiteral("Does"));
668 while (doesIt != attributes.constEnd() && doesIt.key().compare(u"Does") == 0) {
669 QObject *object =
670 instantiateClass(doesIt.value(), QByteArrayLiteral("Cutelyst::Component"));
671 if (object) {
672 roles.push(qobject_cast<Component *>(object));
673 }
674 ++doesIt;
675 }
676 return roles;
677}
678
679QString ControllerPrivate::parsePathAttr(const QString &value)
680{
681 QString ret = pathPrefix;
682 if (value.startsWith(u'/')) {
683 ret = value;
684 } else if (!value.isEmpty()) {
685 ret = pathPrefix + u'/' + value;
686 }
687 return ret;
688}
689
690QString ControllerPrivate::parseChainedAttr(const QString &attr)
691{
692 QString ret = QStringLiteral("/");
693 if (attr.isEmpty()) {
694 return ret;
695 }
696
697 if (attr.compare(u".") == 0) {
698 ret.append(pathPrefix);
699 } else if (!attr.startsWith(u'/')) {
700 if (!pathPrefix.isEmpty()) {
701 ret.append(pathPrefix + u'/' + attr);
702 } else {
703 // special case namespace '' (root)
704 ret.append(attr);
705 }
706 } else {
707 ret = attr;
708 }
709
710 return ret;
711}
712
713QObject *ControllerPrivate::instantiateClass(const QString &name, const QByteArray &super)
714{
715 QString instanceName = name;
716 if (!instanceName.isEmpty()) {
717 instanceName.remove(QRegularExpression(QStringLiteral("\\W")));
718
719 QMetaType id = QMetaType::fromName(instanceName.toLatin1().data());
720 if (!id.isValid()) {
721 if (!instanceName.endsWith(QLatin1Char('*'))) {
722 instanceName.append(QLatin1Char('*'));
723 }
724
725 id = QMetaType::fromName(instanceName.toLatin1().data());
726 if (!id.isValid() && !instanceName.startsWith(u"Cutelyst::")) {
727 instanceName = QLatin1String("Cutelyst::") + instanceName;
728 id = QMetaType::fromName(instanceName.toLatin1().data());
729 }
730 }
731
732 if (id.isValid()) {
733 const QMetaObject *metaObj = id.metaObject();
734 if (metaObj) {
735 if (!superIsClassName(metaObj->superClass(), super)) {
736 qCWarning(CUTELYST_CONTROLLER)
737 << "Class name" << instanceName << "is not a derived class of" << super;
738 }
739
740 QObject *object = metaObj->newInstance();
741 if (!object) {
742 qCWarning(CUTELYST_CONTROLLER)
743 << "Could create a new instance of" << instanceName
744 << "make sure it's default constructor is "
745 "marked with the Q_INVOKABLE macro";
746 }
747
748 return object;
749 }
750 } else {
751 Component *component = application->createComponentPlugin(name);
752 if (component) {
753 return component;
754 }
755
756 component = application->createComponentPlugin(instanceName);
757 if (component) {
758 return component;
759 }
760 }
761
762 if (!id.isValid()) {
763 qCCritical(CUTELYST_CONTROLLER,
764 "Could not create component '%s', you can register it with "
765 "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
766 qPrintable(instanceName),
767 qPrintable(instanceName));
768 }
769 }
770
771 return nullptr;
772}
773
774bool ControllerPrivate::superIsClassName(const QMetaObject *super, const QByteArray &className)
775{
776 if (super) {
777 if (super->className() == className) {
778 return true;
779 }
780 return superIsClassName(super->superClass(), className);
781 }
782 return false;
783}
784
785#include "moc_controller.cpp"
This class represents a Cutelyst Action.
Definition action.h:36
void setupAction(const QVariantHash &args, Application *app)
Definition action.cpp:46
bool dispatch(Context *c)
Definition action.h:89
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)
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(QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9) 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(const T &value)