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
18 : QObject(parent)
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
68{
69 Q_UNUSED(app)
70 return true;
71}
72
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{
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
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 Key key(const T &value, const Key &defaultKey) const const
const T value(const Key &key, const T &defaultValue) 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
const QMetaObject * metaObjectForType(int type)
int type(const char *typeName)
typename QMap< Key, T >::const_iterator constFind(const Key &key, const T &value) const const
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)
virtual const QMetaObject * metaObject() 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)
void append(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