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