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 
15 using namespace Cutelyst;
16 
18  : QObject(parent)
19  , d_ptr(new ControllerPrivate(this))
20 {
21 }
22 
23 Controller::~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 
62 bool 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 
79 ControllerPrivate::ControllerPrivate(Controller *parent)
80  : q_ptr(parent)
81 {
82 }
83 
84 void 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 
138 void 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 
206 Action *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 
224 Action *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 
250 void 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 
300 ParamsMultiMap 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 &&
425  method.parameterType(1) == QMetaType::QStringList)) {
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 
446 QStack<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 
462 QString 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 
473 QString 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 
496 QObject *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 
610 bool 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"
void setName(const QString &name)
Definition: component.cpp:39
QString & append(QChar ch)
bool dispatch(Context *c)
Definition: action.h:81
const_iterator constFind(const Key &key) const const
QByteArray name() const const
int type(const ::QByteArray &typeName)
iterator replace(const Key &key, const T &value)
char at(qsizetype i) const const
char32_t toLower(char32_t ucs4)
void setReverse(const QString &reverse)
Definition: component.cpp:51
const QMetaObject * superClass() const const
QVariant fromValue(T &&value)
void push(const T &t)
const_reference at(qsizetype i) const const
virtual const QMetaObject * metaObject() const const
virtual bool preFork(Application *app)
Definition: controller.cpp:67
QMetaType fromName(QByteArrayView typeName)
QObject * newInstance(Args &&... arguments) const const
Action * actionFor(const QString &name) const
Definition: controller.cpp:36
const char * name() const const
Access access() const const
The Cutelyst Component base class.
Definition: component.h:25
qsizetype size() const const
This class represents a Cutelyst Action.
Definition: action.h:34
T value(qsizetype i) const const
The Cutelyst Context.
Definition: context.h:38
QString number(double n, char format, int precision)
Cutelyst Controller base class
Definition: controller.h:87
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
size_type remove(const Key &key)
bool isEmpty() const const
bool isEmpty() const const
int methodCount() const const
const char * constData() const const
bool isValid() const const
int classInfoCount() const const
int parameterCount() const const
const char * value() const const
virtual bool init(Application *application, const QVariantHash &args)
Definition: component.cpp:57
iterator insert(const Key &key, const T &value)
bool _DISPATCH(Context *c)
Definition: controller.cpp:162
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
void applyRoles(const QStack< Component *> &roles)
Definition: component.cpp:133
QString reverse() const
Definition: component.cpp:45
QByteArray mid(qsizetype pos, qsizetype len) const const
QMetaClassInfo classInfo(int index) const const
int parameterType(int index) const const
QByteArray & append(QByteArrayView data)
void setParent(QObject *parent)
const char * className() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
void setController(Controller *controller)
Definition: action.cpp:40
virtual bool postFork(Application *app)
Definition: controller.cpp:73
bool contains(const Key &key) const const
MethodType methodType() const const
Key key(const T &value, const Key &defaultKey) const const
QString fromLatin1(QByteArrayView str)
QByteArray toLatin1() const const
QString toString() const const
bool isDigit(char32_t ucs4)
void append(QList< T > &&value)
void setupAction(const QVariantHash &args, Application *app)
Definition: action.cpp:46
const QChar at(qsizetype position) const const
T & last()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype length() const const
char * data()
const QMetaObject * metaObjectForType(int type)
bool operator==(const char *className)
Definition: controller.cpp:62
The Cutelyst Application.
Definition: application.h:42
QString first(qsizetype n) const const
void setMethod(const QMetaMethod &method)
Definition: action.cpp:27
qsizetype size() const const
ActionList actions() const
Definition: controller.cpp:56
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
The Cutelyst Dispatcher.
Definition: dispatcher.h:27
QMetaMethod method(int index) const const
QString ns() const
Definition: controller.cpp:30
bool isLower(char32_t ucs4)
T value(const Key &key, const T &defaultValue) const const
Controller(QObject *parent=nullptr)
Definition: controller.cpp:17