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