cutelyst 3.9.1
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
19 , d_ptr(new ControllerPrivate(this))
20{
21}
22
23Controller::~Controller()
24{
25 Q_D(Controller);
26 qDeleteAll(d->actionList);
27 delete d_ptr;
28}
29
31{
32 Q_D(const Controller);
33 return d->pathPrefix;
34}
35
37{
38 Q_D(const Controller);
39 auto it = d->actions.constFind(name);
40 if (it != d->actions.constEnd()) {
41 return it->action;
42 }
43 return d->dispatcher->getAction(name, d->pathPrefix);
44}
45
47{
48 Q_D(const Controller);
49 auto it = d->actions.constFind(name);
50 if (it != d->actions.constEnd()) {
51 return it->action;
52 }
53 return d->dispatcher->getAction(name.toString(), d->pathPrefix);
54}
55
57{
58 Q_D(const Controller);
59 return d->actionList;
60}
61
62bool Controller::operator==(const char *className)
63{
64 return !qstrcmp(metaObject()->className(), className);
65}
66
67bool Controller::preFork(Application *app)
68{
69 Q_UNUSED(app)
70 return true;
71}
72
73bool Controller::postFork(Application *app)
74{
75 Q_UNUSED(app)
76 return true;
77}
78
79ControllerPrivate::ControllerPrivate(Controller *parent)
80 : q_ptr(parent)
81{
82}
83
84void ControllerPrivate::init(Application *app, Dispatcher *_dispatcher)
85{
86 Q_Q(Controller);
87
88 dispatcher = _dispatcher;
89 application = app;
90
91 // Application must always be our parent
92 q->setParent(app);
93
94 const QMetaObject *meta = q->metaObject();
95 const QString className = QString::fromLatin1(meta->className());
96 q->setObjectName(className);
97
98 bool namespaceFound = false;
99 for (int i = meta->classInfoCount() - 1; i >= 0; --i) {
100 if (qstrcmp(meta->classInfo(i).name(), "Namespace") == 0) {
101 pathPrefix = QString::fromLatin1(meta->classInfo(i).value());
102 while (pathPrefix.startsWith(u'/')) {
103 pathPrefix.remove(0, 1);
104 }
105 namespaceFound = true;
106 break;
107 }
108 }
109
110 if (!namespaceFound) {
111 QString controlerNS;
112 bool lastWasUpper = true;
113
114 for (int i = 0; i < className.length(); ++i) {
115 const QChar c = className.at(i);
116 if (c.isLower() || c.isDigit()) {
117 controlerNS.append(c);
118 lastWasUpper = false;
119 } else if (c == u'_') {
120 controlerNS.append(c);
121 lastWasUpper = true;
122 } else {
123 if (!lastWasUpper) {
124 controlerNS.append(u'/');
125 }
126 if (c != u':') {
127 controlerNS.append(c.toLower());
128 }
129 lastWasUpper = true;
130 }
131 }
132 pathPrefix = controlerNS;
133 }
134
135 registerActionMethods(meta, q, app);
136}
137
138void ControllerPrivate::setupFinished()
139{
140 Q_Q(Controller);
141
142 const ActionList beginList = dispatcher->getActions(QStringLiteral("Begin"), pathPrefix);
143 if (!beginList.isEmpty()) {
144 beginAutoList.append(beginList.last());
145 }
146
147 beginAutoList.append(dispatcher->getActions(QStringLiteral("Auto"), pathPrefix));
148
149 const ActionList endList = dispatcher->getActions(QStringLiteral("End"), pathPrefix);
150 if (!endList.isEmpty()) {
151 end = endList.last();
152 }
153
154 const auto actions = actionList;
155 for (Action *action : actions) {
156 action->dispatcherReady(dispatcher, q);
157 }
158
159 q->preFork(qobject_cast<Application *>(q->parent()));
160}
161
163{
164 Q_D(Controller);
165
166 bool ret = true;
167
168 int &actionRefCount = c->d_ptr->actionRefCount;
169
170 // Dispatch to Begin and Auto
171 const auto beginAutoList = d->beginAutoList;
172 for (Action *action : beginAutoList) {
173 if (actionRefCount) {
174 c->d_ptr->pendingAsync.enqueue(action);
175 } else if (!action->dispatch(c)) {
176 ret = false;
177 break;
178 }
179 }
180
181 // Dispatch to Action
182 if (ret) {
183 if (actionRefCount) {
184 c->d_ptr->pendingAsync.enqueue(c->action());
185 } else {
186 ret = c->action()->dispatch(c);
187 }
188 }
189
190 // Dispatch to End
191 if (d->end) {
192 if (actionRefCount) {
193 c->d_ptr->pendingAsync.enqueue(d->end);
194 } else if (!d->end->dispatch(c)) {
195 ret = false;
196 }
197 }
198
199 if (actionRefCount) {
200 c->d_ptr->engineRequest->status |= EngineRequest::Async;
201 }
202
203 return ret;
204}
205
206Action *ControllerPrivate::actionClass(const QVariantHash &args)
207{
208 const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
209 const QString actionClass = attributes.value(QStringLiteral("ActionClass"));
210
211 QObject *object = instantiateClass(actionClass, "Cutelyst::Action");
212 if (object) {
213 Action *action = qobject_cast<Action *>(object);
214 if (action) {
215 return action;
216 }
217 qCWarning(CUTELYST_CONTROLLER) << "ActionClass" << actionClass << "is not an ActionClass";
218 delete object;
219 }
220
221 return new Action;
222}
223
224Action *ControllerPrivate::createAction(const QVariantHash &args,
225 const QMetaMethod &method,
226 Controller *controller,
227 Application *app)
228{
229 Action *action = actionClass(args);
230 if (!action) {
231 return nullptr;
232 }
233
234 QStack<Component *> roles = gatherActionRoles(args);
235 for (int i = 0; i < roles.size(); ++i) {
236 Component *code = roles.at(i);
237 code->init(app, args);
238 code->setParent(action);
239 }
240 action->applyRoles(roles);
241 action->setMethod(method);
242 action->setController(controller);
243 action->setName(args.value(QStringLiteral("name")).toString());
244 action->setReverse(args.value(QStringLiteral("reverse")).toString());
245 action->setupAction(args, app);
246
247 return action;
248}
249
250void ControllerPrivate::registerActionMethods(const QMetaObject *meta,
251 Controller *controller,
252 Application *app)
253{
254 // Setup actions
255 for (int i = 0; i < meta->methodCount(); ++i) {
256 const QMetaMethod method = meta->method(i);
257 const QByteArray name = method.name();
258
259 // We register actions that are either a Q_SLOT
260 // or a Q_INVOKABLE function which has the first
261 // parameter type equal to Context*
262 if (method.isValid() &&
263 (method.methodType() == QMetaMethod::Method ||
264 method.methodType() == QMetaMethod::Slot) &&
265 (method.parameterCount() &&
266 method.parameterType(0) == qMetaTypeId<Cutelyst::Context *>())) {
267
268 // Build up the list of attributes for the class info
269 QByteArray attributeArray;
270 for (int i2 = meta->classInfoCount() - 1; i2 >= 0; --i2) {
271 QMetaClassInfo classInfo = meta->classInfo(i2);
272 if (name == classInfo.name()) {
273 attributeArray.append(classInfo.value());
274 }
275 }
276 ParamsMultiMap attrs = parseAttributes(method, attributeArray, name);
277
278 QString reverse;
279 if (controller->ns().isEmpty()) {
280 reverse = QString::fromLatin1(name);
281 } else {
282 reverse = controller->ns() + QLatin1Char('/') + QString::fromLatin1(name);
283 }
284
285 Action *action =
286 createAction({{QStringLiteral("name"), QVariant::fromValue(name)},
287 {QStringLiteral("reverse"), QVariant::fromValue(reverse)},
288 {QStringLiteral("namespace"), QVariant::fromValue(controller->ns())},
289 {QStringLiteral("attributes"), QVariant::fromValue(attrs)}},
290 method,
291 controller,
292 app);
293
294 actions.insert(action->reverse(), {action->reverse(), action});
295 actionList.append(action);
296 }
297 }
298}
299
300ParamsMultiMap ControllerPrivate::parseAttributes(const QMetaMethod &method,
301 const QByteArray &str,
302 const QByteArray &name)
303{
304 ParamsMultiMap ret;
305 std::vector<std::pair<QString, QString>> attributes;
306 // This is probably not the best parser ever
307 // but it handles cases like:
308 // :Args:Local('fo"')o'):ActionClass('foo')
309 // into
310 // (("Args",""), ("Local","'fo"')o'"), ("ActionClass","'foo'"))
311
312 int size = str.size();
313 int pos = 0;
314 while (pos < size) {
315 QString key;
316 QString value;
317
318 // find the start of a key
319 if (str.at(pos) == ':') {
320 int keyStart = ++pos;
321 int keyLength = 0;
322 while (pos < size) {
323 if (str.at(pos) == '(') {
324 // attribute has value
325 int valueStart = ++pos;
326 while (pos < size) {
327 if (str.at(pos) == ')') {
328 // found the possible end of the value
329 int valueEnd = pos;
330 if (++pos < size && str.at(pos) == ':') {
331 // found the start of a key so this is
332 // really the end of a value
333 value =
334 QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
335 break;
336 } else if (pos >= size) {
337 // found the end of the string
338 // save the remainig as the value
339 value =
340 QString::fromLatin1(str.mid(valueStart, valueEnd - valueStart));
341 break;
342 }
343 // string was not '):' or ')$'
344 continue;
345 }
346 ++pos;
347 }
348
349 break;
350 } else if (str.at(pos) == ':') {
351 // Attribute has no value
352 break;
353 }
354 ++keyLength;
355 ++pos;
356 }
357
358 // stopre the key
359 key = QString::fromLatin1(str.mid(keyStart, keyLength));
360
361 // remove quotes
362 if (!value.isEmpty()) {
363 if ((value.startsWith(u'\'') && value.endsWith(u'\'')) ||
364 (value.startsWith(u'"') && value.endsWith(u'"'))) {
365 value.remove(0, 1);
366 value.remove(value.size() - 1, 1);
367 }
368 }
369
370 // store the key/value pair found
371 attributes.emplace_back(std::make_pair(key, value));
372 continue;
373 }
374 ++pos;
375 }
376
377 // Add the attributes to the map in the reverse order so
378 // that values() return them in the right order
379 for (const auto &pair : attributes) {
380 QString key = pair.first;
381 QString value = pair.second;
382 if (key.compare(u"Global") == 0) {
383 key = QStringLiteral("Path");
384 value = parsePathAttr(QLatin1Char('/') + QString::fromLatin1(name));
385 } else if (key.compare(u"Local") == 0) {
386 key = QStringLiteral("Path");
387 value = parsePathAttr(QString::fromLatin1(name));
388 } else if (key.compare(u"Path") == 0) {
389 value = parsePathAttr(value);
390 } else if (key.compare(u"Args") == 0) {
391 QString args = value;
392 if (!args.isEmpty()) {
393 value = args.remove(QRegularExpression(QStringLiteral("\\D")));
394 }
395 } else if (key.compare(u"CaptureArgs") == 0) {
396 QString captureArgs = value;
397 value = captureArgs.remove(QRegularExpression(QStringLiteral("\\D")));
398 } else if (key.compare(u"Chained") == 0) {
399 value = parseChainedAttr(value);
400 }
401
402 ret.insert(key, value);
403 }
404
405 // Handle special AutoArgs and AutoCaptureArgs case
406 if (!ret.contains(QStringLiteral("Args")) && !ret.contains(QStringLiteral("CaptureArgs")) &&
407 (ret.contains(QStringLiteral("AutoArgs")) ||
408 ret.contains(QStringLiteral("AutoCaptureArgs")))) {
409 if (ret.contains(QStringLiteral("AutoArgs")) &&
410 ret.contains(QStringLiteral("AutoCaptureArgs"))) {
411 qFatal("Action '%s' has both AutoArgs and AutoCaptureArgs, which is not allowed",
412 name.constData());
413 } else {
414 QString parameterName;
415 if (ret.contains(QStringLiteral("AutoArgs"))) {
416 ret.remove(QStringLiteral("AutoArgs"));
417 parameterName = QStringLiteral("Args");
418 } else {
419 ret.remove(QStringLiteral("AutoCaptureArgs"));
420 parameterName = QStringLiteral("CaptureArgs");
421 }
422
423 // If the signature is not QStringList we count them
424 if (!(method.parameterCount() == 2 &&
426 int parameterCount = 0;
427 for (int i2 = 1; i2 < method.parameterCount(); ++i2) {
428 int typeId = method.parameterType(i2);
429 if (typeId == QMetaType::QString) {
430 ++parameterCount;
431 }
432 }
433 ret.replace(parameterName, QString::number(parameterCount));
434 }
435 }
436 }
437
438 // If the method is private add a Private attribute
439 if (!ret.contains(QStringLiteral("Private")) && method.access() == QMetaMethod::Private) {
440 ret.replace(QStringLiteral("Private"), QString());
441 }
442
443 return ret;
444}
445
446QStack<Component *> ControllerPrivate::gatherActionRoles(const QVariantHash &args)
447{
448 QStack<Component *> roles;
449 const auto attributes = args.value(QStringLiteral("attributes")).value<ParamsMultiMap>();
450 auto doesIt = attributes.constFind(QStringLiteral("Does"));
451 while (doesIt != attributes.constEnd() && doesIt.key().compare(u"Does") == 0) {
452 QObject *object =
453 instantiateClass(doesIt.value(), QByteArrayLiteral("Cutelyst::Component"));
454 if (object) {
455 roles.push(qobject_cast<Component *>(object));
456 }
457 ++doesIt;
458 }
459 return roles;
460}
461
462QString ControllerPrivate::parsePathAttr(const QString &value)
463{
464 QString ret = pathPrefix;
465 if (value.startsWith(u'/')) {
466 ret = value;
467 } else if (!value.isEmpty()) {
468 ret = pathPrefix + u'/' + value;
469 }
470 return ret;
471}
472
473QString ControllerPrivate::parseChainedAttr(const QString &attr)
474{
475 QString ret = QStringLiteral("/");
476 if (attr.isEmpty()) {
477 return ret;
478 }
479
480 if (attr.compare(u".") == 0) {
481 ret.append(pathPrefix);
482 } else if (!attr.startsWith(u'/')) {
483 if (!pathPrefix.isEmpty()) {
484 ret.append(pathPrefix + u'/' + attr);
485 } else {
486 // special case namespace '' (root)
487 ret.append(attr);
488 }
489 } else {
490 ret = attr;
491 }
492
493 return ret;
494}
495
496QObject *ControllerPrivate::instantiateClass(const QString &name, const QByteArray &super)
497{
498 QString instanceName = name;
499 if (!instanceName.isEmpty()) {
500 instanceName.remove(QRegularExpression(QStringLiteral("\\W")));
501
502#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
503 QMetaType id = QMetaType::fromName(instanceName.toLatin1().data());
504 if (!id.isValid()) {
505 if (!instanceName.endsWith(QLatin1Char('*'))) {
506 instanceName.append(QLatin1Char('*'));
507 }
508
509 id = QMetaType::fromName(instanceName.toLatin1().data());
510 if (!id.isValid() && !instanceName.startsWith(u"Cutelyst::")) {
511 instanceName = QLatin1String("Cutelyst::") + instanceName;
512 id = QMetaType::fromName(instanceName.toLatin1().data());
513 }
514 }
515
516 if (id.isValid()) {
517 const QMetaObject *metaObj = id.metaObject();
518 if (metaObj) {
519 if (!superIsClassName(metaObj->superClass(), super)) {
520 qCWarning(CUTELYST_CONTROLLER)
521 << "Class name" << instanceName << "is not a derived class of" << super;
522 }
523
524 QObject *object = metaObj->newInstance();
525 if (!object) {
526 qCWarning(CUTELYST_CONTROLLER)
527 << "Could create a new instance of" << instanceName
528 << "make sure it's default constructor is "
529 "marked with the Q_INVOKABLE macro";
530 }
531
532 return object;
533 }
534 } else {
535 Component *component = application->createComponentPlugin(name);
536 if (component) {
537 return component;
538 }
539
540 component = application->createComponentPlugin(instanceName);
541 if (component) {
542 return component;
543 }
544 }
545
546 if (!id.isValid()) {
547 qCCritical(CUTELYST_CONTROLLER,
548 "Could not create component '%s', you can register it with "
549 "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
550 qPrintable(instanceName),
551 qPrintable(instanceName));
552 }
553 }
554#else
555 int id = QMetaType::type(instanceName.toLatin1().data());
556 if (!id) {
557 if (!instanceName.endsWith(QLatin1Char('*'))) {
558 instanceName.append(QLatin1Char('*'));
559 }
560
561 id = QMetaType::type(instanceName.toLatin1().data());
562 if (!id && !instanceName.startsWith(u"Cutelyst::")) {
563 instanceName = QLatin1String("Cutelyst::") + instanceName;
564 id = QMetaType::type(instanceName.toLatin1().data());
565 }
566 }
567
568 if (id) {
569 const QMetaObject *metaObj = QMetaType::metaObjectForType(id);
570 if (metaObj) {
571 if (!superIsClassName(metaObj->superClass(), super)) {
572 qCWarning(CUTELYST_CONTROLLER)
573 << "Class name" << instanceName << "is not a derived class of" << super;
574 }
575
576 QObject *object = metaObj->newInstance();
577 if (!object) {
578 qCWarning(CUTELYST_CONTROLLER)
579 << "Could create a new instance of" << instanceName
580 << "make sure it's default constructor is "
581 "marked with the Q_INVOKABLE macro";
582 }
583
584 return object;
585 }
586 } else {
587 Component *component = application->createComponentPlugin(name);
588 if (component) {
589 return component;
590 }
591
592 component = application->createComponentPlugin(instanceName);
593 if (component) {
594 return component;
595 }
596 }
597
598 if (!id) {
599 qCCritical(CUTELYST_CONTROLLER,
600 "Could not create component '%s', you can register it with "
601 "qRegisterMetaType<%s>(); or set a proper CUTELYST_PLUGINS_DIR",
602 qPrintable(instanceName),
603 qPrintable(instanceName));
604 }
605 }
606#endif
607 return nullptr;
608}
609
610bool ControllerPrivate::superIsClassName(const QMetaObject *super, const QByteArray &className)
611{
612 if (super) {
613 if (super->className() == className) {
614 return true;
615 }
616 return superIsClassName(super->superClass(), className);
617 }
618 return false;
619}
620
621#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:81
void setMethod(const QMetaMethod &method)
Definition action.cpp:27
void setController(Controller *controller)
Definition action.cpp:40
The Cutelyst Application.
Definition application.h:43
The Cutelyst Component base class.
Definition component.h:26
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)
void setName(const QString &name)
Definition component.cpp:39
QString reverse() const
Definition component.cpp:45
The Cutelyst Context.
Definition context.h:39
Cutelyst Controller base class
Definition controller.h:88
virtual bool preFork(Application *app)
Action * actionFor(const QString &name) const
ActionList actions() const
virtual bool postFork(Application *app)
bool operator==(const char *className)
QString ns() const
bool _DISPATCH(Context *c)
Controller(QObject *parent=nullptr)
The Cutelyst Dispatcher.
Definition dispatcher.h:28
The Cutelyst namespace holds all public Cutelyst API.
Definition Mainpage.dox:8
QMultiMap< QString, QString > ParamsMultiMap
QVector< Action * > ActionList
Definition action.h:154
QByteArray & append(char ch)
char at(int i) const const
const char * constData() const const
char * data()
QByteArray mid(int pos, int len) const const
int size() const const
bool isDigit() const const
bool isLower() const const
QChar toLower() const const
const char * name() const const
const char * value() const const
Access access() const const
bool isValid() const const
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
const QMetaObject * metaObjectForType(int type)
int type(const char *typeName)
bool contains(const Key &key, const T &value) const const
typename QMap< Key, T >::iterator insert(const Key &key, const T &value)
int remove(const Key &key, const T &value)
typename QMap< Key, T >::iterator replace(const Key &key, const T &value)
QObject(QObject *parent)
virtual const QMetaObject * metaObject() const const
QObject * parent() const const
void setParent(QObject *parent)
void push(const T &t)
QString & append(QChar ch)
const QChar at(int position) const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
int length() const const
QString number(int n, int base)
QString & remove(int position, int n)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QString toString() const const
QVariant fromValue(const T &value)
const T & at(int i) const const
bool isEmpty() const const
T & last()
int size() const const
T value(int i) const const