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
typename QMap< Key, T >::const_iterator constFind(const Key &key, const T &value) const const
QByteArray name() const const
void append(const T &value)
typename QMap< Key, T >::iterator replace(const Key &key, const T &value)
char at(int i) const const
const QMetaObject * metaObjectForType(int type)
void setReverse(const QString &reverse)
Definition: component.cpp:51
const QMetaObject * superClass() const const
void push(const T &t)
bool isDigit() const const
T & last()
virtual const QMetaObject * metaObject() const const
virtual bool preFork(Application *app)
Definition: controller.cpp:67
Action * actionFor(const QString &name) const
Definition: controller.cpp:36
const char * name() const const
QString & remove(int position, int n)
int type(const char *typeName)
QMetaMethod::Access access() const const
The Cutelyst Component base class.
Definition: component.h:25
T value(int i) const const
This class represents a Cutelyst Action.
Definition: action.h:34
The Cutelyst Context.
Definition: context.h:38
QString number(int n, int base)
Cutelyst Controller base class
Definition: controller.h:87
int remove(const Key &key, const T &value)
bool isEmpty() const const
int methodCount() const const
const char * constData() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
bool isValid() const const
int classInfoCount() const const
int parameterCount() const const
const char * value() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
virtual bool init(Application *application, const QVariantHash &args)
Definition: component.cpp:57
typename QMap< Key, T >::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(int pos, int len) const const
QMetaClassInfo classInfo(int index) const const
int parameterType(int index) const const
QByteArray & append(char ch)
void setParent(QObject *parent)
const char * className() const const
QVariant fromValue(const T &value)
QChar toLower() const const
void setController(Controller *controller)
Definition: action.cpp:40
virtual bool postFork(Application *app)
Definition: controller.cpp:73
const Key key(const T &value, const Key &defaultKey) const const
bool contains(const Key &key, const T &value) const const
QMetaMethod::MethodType methodType() const const
const T & at(int i) const const
QByteArray toLatin1() const const
QString toString() const const
bool isEmpty() const const
void setupAction(const QVariantHash &args, Application *app)
Definition: action.cpp:46
const QChar at(int position) const const
int length() const const
char * data()
QString fromLatin1(const char *str, int size)
bool operator==(const char *className)
Definition: controller.cpp:62
The Cutelyst Application.
Definition: application.h:42
void setMethod(const QMetaMethod &method)
Definition: action.cpp:27
int size() const const
int size() const const
ActionList actions() const
Definition: controller.cpp:56
int compare(const QString &other, Qt::CaseSensitivity cs) const const
The Cutelyst Dispatcher.
Definition: dispatcher.h:27
QObject * newInstance(QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9) const const
QMetaMethod method(int index) const const
bool isLower() const const
QString ns() const
Definition: controller.cpp:30
const T value(const Key &key, const T &defaultValue) const const
Controller(QObject *parent=nullptr)
Definition: controller.cpp:17