cutelyst 3.9.1
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
dispatchtypechained.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2015-2022 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "actionchain.h"
6#include "common.h"
7#include "context.h"
8#include "dispatchtypechained_p.h"
9#include "utils.h"
10
11#include <QtCore/QUrl>
12
13using namespace Cutelyst;
14
17 , d_ptr(new DispatchTypeChainedPrivate)
18{
19}
20
21DispatchTypeChained::~DispatchTypeChained()
22{
23 delete d_ptr;
24}
25
27{
28 Q_D(const DispatchTypeChained);
29
30 QByteArray buffer;
31 Actions endPoints = d->endPoints;
32 std::sort(endPoints.begin(), endPoints.end(), [](Action *a, Action *b) -> bool {
33 return a->reverse() < b->reverse();
34 });
35
37 QVector<QStringList> unattachedTable;
38 for (Action *endPoint : endPoints) {
39 QStringList parts;
40 if (endPoint->numberOfArgs() == -1) {
41 parts.append(QLatin1String("..."));
42 } else {
43 for (int i = 0; i < endPoint->numberOfArgs(); ++i) {
44 parts.append(QLatin1String("*"));
45 }
46 }
47
49 QString extra = DispatchTypeChainedPrivate::listExtraHttpMethods(endPoint);
50 QString consumes = DispatchTypeChainedPrivate::listExtraConsumes(endPoint);
51 ActionList parents;
52 Action *current = endPoint;
53 while (current) {
54 for (int i = 0; i < current->numberOfCaptures(); ++i) {
55 parts.prepend(QLatin1String("*"));
56 }
57
58 const auto attributes = current->attributes();
59 const QStringList pathParts = attributes.values(QLatin1String("PathPart"));
60 for (const QString &part : pathParts) {
61 if (!part.isEmpty()) {
62 parts.prepend(part);
63 }
64 }
65
66 parent = attributes.value(QLatin1String("Chained"));
67 current = d->actions.value(parent);
68 if (current) {
69 parents.prepend(current);
70 }
71 }
72
73 if (parent.compare(u"/") != 0) {
74 QStringList row;
75 if (parents.isEmpty()) {
76 row.append(QLatin1Char('/') + endPoint->reverse());
77 } else {
78 row.append(QLatin1Char('/') + parents.first()->reverse());
79 }
80 row.append(parent);
81 unattachedTable.append(row);
82 continue;
83 }
84
86 for (Action *p : parents) {
87 QString name = QLatin1Char('/') + p->reverse();
88
89 QString extraHttpMethod = DispatchTypeChainedPrivate::listExtraHttpMethods(p);
90 if (!extraHttpMethod.isEmpty()) {
91 name.prepend(extraHttpMethod + QLatin1Char(' '));
92 }
93
94 const auto attributes = p->attributes();
95 auto it = attributes.constFind(QLatin1String("CaptureArgs"));
96 if (it != attributes.constEnd()) {
97 name.append(QLatin1String(" (") + it.value() + QLatin1Char(')'));
98 } else {
99 name.append(QLatin1String(" (0)"));
100 }
101
102 QString ct = DispatchTypeChainedPrivate::listExtraConsumes(p);
103 if (!ct.isEmpty()) {
104 name.append(QLatin1String(" :") + ct);
105 }
106
107 if (p != parents[0]) {
108 name = QLatin1String("-> ") + name;
109 }
110
111 rows.append({QString(), name});
112 }
113
114 QString line;
115 if (!rows.isEmpty()) {
116 line.append(QLatin1String("=> "));
117 }
118 if (!extra.isEmpty()) {
119 line.append(extra + QLatin1Char(' '));
120 }
121 line.append(QLatin1Char('/') + endPoint->reverse());
122 if (endPoint->numberOfArgs() == -1) {
123 line.append(QLatin1String(" (...)"));
124 } else {
125 line.append(QLatin1String(" (") + QString::number(endPoint->numberOfArgs()) +
126 QLatin1Char(')'));
127 }
128
129 if (!consumes.isEmpty()) {
130 line.append(QLatin1String(" :") + consumes);
131 }
132 rows.append({QString(), line});
133
134 rows[0][0] = QLatin1Char('/') + parts.join(QLatin1Char('/'));
135 paths.append(rows);
136 }
137
138#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
139 QTextStream out(&buffer, QTextStream::WriteOnly);
140#else
141 QTextStream out(&buffer, QIODevice::WriteOnly);
142#endif
143
144 if (!paths.isEmpty()) {
145 out << Utils::buildTable(paths,
146 {QLatin1String("Path Spec"), QLatin1String("Private")},
147 QLatin1String("Loaded Chained actions:"));
148 }
149
150 if (!unattachedTable.isEmpty()) {
151 out << Utils::buildTable(unattachedTable,
152 {QLatin1String("Private"), QLatin1String("Missing parent")},
153 QLatin1String("Unattached Chained actions:"));
154 }
155
156 return buffer;
157}
158
160 DispatchTypeChained::match(Context *c, const QString &path, const QStringList &args) const
161{
162 if (!args.isEmpty()) {
163 return NoMatch;
164 }
165
166 Q_D(const DispatchTypeChained);
167
168 const BestActionMatch ret =
169 d->recurseMatch(args.size(), QStringLiteral("/"), path.split(QLatin1Char('/')));
170 const ActionList chain = ret.actions;
171 if (ret.isNull || chain.isEmpty()) {
172 return NoMatch;
173 }
174
175 QStringList decodedArgs;
176 const QStringList parts = ret.parts;
177 for (const QString &arg : parts) {
178 QString aux = arg;
179 decodedArgs.append(Utils::decodePercentEncoding(&aux));
180 }
181
182 ActionChain *action = new ActionChain(chain, c);
183 Request *request = c->request();
184 request->setArguments(decodedArgs);
185 request->setCaptures(ret.captures);
186 request->setMatch(QLatin1Char('/') + action->reverse());
187 setupMatchedAction(c, action);
188
189 return ExactMatch;
190}
191
193{
195
196 auto attributes = action->attributes();
197 const QStringList chainedList = attributes.values(QLatin1String("Chained"));
198 if (chainedList.isEmpty()) {
199 return false;
200 }
201
202 if (chainedList.size() > 1) {
203 qCCritical(CUTELYST_DISPATCHER_CHAINED)
204 << "Multiple Chained attributes not supported registering" << action->reverse();
205 return false;
206 }
207
208 const QString chainedTo = chainedList.first();
209 if (chainedTo == u'/' + action->name()) {
210 qCCritical(CUTELYST_DISPATCHER_CHAINED)
211 << "Actions cannot chain to themselves registering /" << action->name();
212 return false;
213 }
214
215 const QStringList pathPart = attributes.values(QLatin1String("PathPart"));
216
217 QString part = action->name();
218
219 if (pathPart.size() == 1 && !pathPart[0].isEmpty()) {
220 part = pathPart[0];
221 } else if (pathPart.size() > 1) {
222 qCCritical(CUTELYST_DISPATCHER_CHAINED)
223 << "Multiple PathPart attributes not supported registering" << action->reverse();
224 return false;
225 }
226
227 if (part.startsWith(QLatin1Char('/'))) {
228 qCCritical(CUTELYST_DISPATCHER_CHAINED)
229 << "Absolute parameters to PathPart not allowed registering" << action->reverse();
230 return false;
231 }
232
233 attributes.replace(QStringLiteral("PathPart"), part);
234 action->setAttributes(attributes);
235
236 auto &childrenOf = d->childrenOf[chainedTo][part];
237 childrenOf.insert(childrenOf.begin(), action);
238
239 d->actions[QLatin1Char('/') + action->reverse()] = action;
240
241 if (!d->checkArgsAttr(action, QLatin1String("Args")) ||
242 !d->checkArgsAttr(action, QLatin1String("CaptureArgs"))) {
243 return false;
244 }
245
246 if (attributes.contains(QLatin1String("Args")) &&
247 attributes.contains(QLatin1String("CaptureArgs"))) {
248 qCCritical(CUTELYST_DISPATCHER_CHAINED)
249 << "Combining Args and CaptureArgs attributes not supported registering"
250 << action->reverse();
251 return false;
252 }
253
254 if (!attributes.contains(QLatin1String("CaptureArgs"))) {
255 d->endPoints.push_back(action);
256 }
257
258 return true;
259}
260
262{
263 Q_D(const DispatchTypeChained);
264
265 QString ret;
266 const ParamsMultiMap attributes = action->attributes();
267 if (!(attributes.contains(QStringLiteral("Chained")) &&
268 !attributes.contains(QStringLiteral("CaptureArgs")))) {
269 qCWarning(CUTELYST_DISPATCHER_CHAINED)
270 << "uriForAction: action is not an end point" << action;
271 return ret;
272 }
273
275 QStringList localCaptures = captures;
276 QStringList parts;
277 Action *curr = action;
278 while (curr) {
279 const ParamsMultiMap curr_attributes = curr->attributes();
280 if (curr_attributes.contains(QStringLiteral("CaptureArgs"))) {
281 if (localCaptures.size() < curr->numberOfCaptures()) {
282 // Not enough captures
283 qCWarning(CUTELYST_DISPATCHER_CHAINED)
284 << "uriForAction: not enough captures" << curr->numberOfCaptures()
285 << captures.size();
286 return ret;
287 }
288
289 parts = localCaptures.mid(localCaptures.size() - curr->numberOfCaptures()) + parts;
290 localCaptures = localCaptures.mid(0, localCaptures.size() - curr->numberOfCaptures());
291 }
292
293 const QString pp = curr_attributes.value(QStringLiteral("PathPart"));
294 if (!pp.isEmpty()) {
295 parts.prepend(pp);
296 }
297
298 parent = curr_attributes.value(QStringLiteral("Chained"));
299 curr = d->actions.value(parent);
300 }
301
302 if (parent.compare(u"/") != 0) {
303 // fail for dangling action
304 qCWarning(CUTELYST_DISPATCHER_CHAINED) << "uriForAction: dangling action" << parent;
305 return ret;
306 }
307
308 if (!localCaptures.isEmpty()) {
309 // fail for too many captures
310 qCWarning(CUTELYST_DISPATCHER_CHAINED)
311 << "uriForAction: too many captures" << localCaptures;
312 return ret;
313 }
314
315 ret = QLatin1Char('/') + parts.join(QLatin1Char('/'));
316 return ret;
317}
318
320{
321 Q_D(const DispatchTypeChained);
322
323 // Do not expand action if action already is an ActionChain
324 if (qobject_cast<ActionChain *>(action)) {
325 return action;
326 }
327
328 // The action must be chained to something
329 if (!action->attributes().contains(QStringLiteral("Chained"))) {
330 return nullptr;
331 }
332
333 ActionList chain;
334 Action *curr = action;
335
336 while (curr) {
337 chain.prepend(curr);
338 const QString parent = curr->attribute(QStringLiteral("Chained"));
339 curr = d->actions.value(parent);
340 }
341
342 return new ActionChain(chain, const_cast<Context *>(c));
343}
344
346{
347 Q_D(const DispatchTypeChained);
348
349 if (d->actions.isEmpty()) {
350 return false;
351 }
352
353 // Optimize end points
354
355 return true;
356}
357
358BestActionMatch DispatchTypeChainedPrivate::recurseMatch(int reqArgsSize,
359 const QString &parent,
360 const QStringList &pathParts) const
361{
362 BestActionMatch bestAction;
363 auto it = childrenOf.constFind(parent);
364 if (it == childrenOf.constEnd()) {
365 return bestAction;
366 }
367
368 const StringActionsMap &children = it.value();
369 QStringList keys = children.keys();
370 std::sort(keys.begin(), keys.end(), [](const QString &a, const QString &b) -> bool {
371 // action2 then action1 to try the longest part first
372 return b.size() < a.size();
373 });
374
375 for (const QString &tryPart : keys) {
376 QStringList parts = pathParts;
377 if (!tryPart.isEmpty()) {
378 // We want to count the number of parts a split would give
379 // and remove the number of parts from tryPart
380 int tryPartCount = tryPart.count(QLatin1Char('/')) + 1;
381 const QStringList possiblePart = parts.mid(0, tryPartCount);
382 if (tryPart != possiblePart.join(QLatin1Char('/'))) {
383 continue;
384 }
385 parts = parts.mid(tryPartCount);
386 }
387
388 const Actions tryActions = children.value(tryPart);
389 for (Action *action : tryActions) {
390 const ParamsMultiMap attributes = action->attributes();
391 if (attributes.contains(QStringLiteral("CaptureArgs"))) {
392 const int captureCount = action->numberOfCaptures();
393 // Short-circuit if not enough remaining parts
394 if (parts.size() < captureCount) {
395 continue;
396 }
397
398 // strip CaptureArgs into list
399 const QStringList captures = parts.mid(0, captureCount);
400
401 // check if the action may fit, depending on a given test by the app
402 if (!action->matchCaptures(captures.size())) {
403 continue;
404 }
405
406 const QStringList localParts = parts.mid(captureCount);
407
408 // try the remaining parts against children of this action
409 const BestActionMatch ret =
410 recurseMatch(reqArgsSize, QLatin1Char('/') + action->reverse(), localParts);
411
412 // No best action currently
413 // OR The action has less parts
414 // OR The action has equal parts but less captured data (ergo more defined)
415 ActionList actions = ret.actions;
416 const QStringList actionCaptures = ret.captures;
417 const QStringList actionParts = ret.parts;
418 int bestActionParts = bestAction.parts.size();
419
420 if (!actions.isEmpty() &&
421 (bestAction.isNull || actionParts.size() < bestActionParts ||
422 (actionParts.size() == bestActionParts &&
423 actionCaptures.size() < bestAction.captures.size() &&
424 ret.n_pathParts > bestAction.n_pathParts))) {
425 actions.prepend(action);
426 int pathparts =
427 attributes.value(QStringLiteral("PathPart")).count(QLatin1Char('/')) + 1;
428 bestAction.actions = actions;
429 bestAction.captures = captures + actionCaptures;
430 bestAction.parts = actionParts;
431 bestAction.n_pathParts = pathparts + ret.n_pathParts;
432 bestAction.isNull = false;
433 }
434 } else {
435 if (!action->match(reqArgsSize + parts.size())) {
436 continue;
437 }
438
439 const QString argsAttr = attributes.value(QStringLiteral("Args"));
440 const int pathparts =
441 attributes.value(QStringLiteral("PathPart")).count(QLatin1Char('/')) + 1;
442 // No best action currently
443 // OR This one matches with fewer parts left than the current best action,
444 // And therefore is a better match
445 // OR No parts and this expects 0
446 // The current best action might also be Args(0),
447 // but we couldn't chose between then anyway so we'll take the last seen
448
449 if (bestAction.isNull || parts.size() < bestAction.parts.size() ||
450 (parts.isEmpty() && !argsAttr.isEmpty() && action->numberOfArgs() == 0)) {
451 bestAction.actions = {action};
452 bestAction.captures = QStringList();
453 bestAction.parts = parts;
454 bestAction.n_pathParts = pathparts;
455 bestAction.isNull = false;
456 }
457 }
458 }
459 }
460
461 return bestAction;
462}
463
464bool DispatchTypeChainedPrivate::checkArgsAttr(Action *action, const QString &name) const
465{
466 const auto attributes = action->attributes();
467 if (!attributes.contains(name)) {
468 return true;
469 }
470
471 const QStringList values = attributes.values(name);
472 if (values.size() > 1) {
473 qCCritical(CUTELYST_DISPATCHER_CHAINED)
474 << "Multiple" << name << "attributes not supported registering" << action->reverse();
475 return false;
476 }
477
478 QString args = values[0];
479 bool ok;
480 if (!args.isEmpty() && args.toInt(&ok) < 0 && !ok) {
481 qCCritical(CUTELYST_DISPATCHER_CHAINED)
482 << "Invalid" << name << "(" << args << ") for action" << action->reverse() << "(use '"
483 << name << "' or '" << name << "(<number>)')";
484 return false;
485 }
486
487 return true;
488}
489
490QString DispatchTypeChainedPrivate::listExtraHttpMethods(Action *action)
491{
492 QString ret;
493 const auto attributes = action->attributes();
494 if (attributes.contains(QLatin1String("HTTP_METHODS"))) {
495 const QStringList extra = attributes.values(QLatin1String("HTTP_METHODS"));
496 ret = extra.join(QLatin1String(", "));
497 }
498 return ret;
499}
500
501QString DispatchTypeChainedPrivate::listExtraConsumes(Action *action)
502{
503 QString ret;
504 const auto attributes = action->attributes();
505 if (attributes.contains(QLatin1String("CONSUMES"))) {
506 const QStringList extra = attributes.values(QLatin1String("CONSUMES"));
507 ret = extra.join(QLatin1String(", "));
508 }
509 return ret;
510}
511
512#include "moc_dispatchtypechained.cpp"
Holds a chain of Cutelyst Actions.
Definition actionchain.h:24
This class represents a Cutelyst Action.
Definition action.h:35
void setAttributes(const ParamsMultiMap &attributes)
Definition action.cpp:80
virtual qint8 numberOfCaptures() const noexcept
Definition action.cpp:130
ParamsMultiMap attributes() const noexcept
Definition action.cpp:68
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition action.cpp:74
QString name() const
Definition component.cpp:33
QString reverse() const
Definition component.cpp:45
The Cutelyst Context.
Definition context.h:39
virtual QString uriForAction(Action *action, const QStringList &captures) const override
virtual QByteArray list() const override
list the registered actions To be implemented by subclasses
virtual MatchType match(Context *c, const QString &path, const QStringList &args) const override
virtual bool registerAction(Action *action) override
registerAction
Action * expandAction(const Context *c, Action *action) const final
DispatchTypeChained(QObject *parent=nullptr)
DispatchType(QObject *parent=nullptr)
void setupMatchedAction(Context *c, Action *action) const
void setCaptures(const QStringList &captures)
Definition request.cpp:169
void setArguments(const QStringList &arguments)
Definition request.cpp:157
void setMatch(const QString &match)
Definition request.cpp:145
The Cutelyst namespace holds all public Cutelyst API.
Definition Mainpage.dox:8
QMultiMap< QString, QString > ParamsMultiMap
QVector< Action * > ActionList
Definition action.h:154
void append(const T &value)
iterator begin()
iterator end()
T & first()
bool isEmpty() const const
QList< T > mid(int pos, int length) const const
void prepend(const T &value)
int size() const const
T value(int i) const const
bool contains(const Key &key, const T &value) const const
int count(const Key &key, const T &value) const const
QList< T > values(const Key &key) const const
QObject(QObject *parent)
QObject * parent() const const
T qobject_cast(QObject *object)
QStringList split(const QString &sep, SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
bool isEmpty() const const
QString number(int n, int base)
QString & prepend(QChar ch)
QString & replace(int position, int n, QChar after)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QString join(const QString &separator) const const
void append(const T &value)
T & first()
bool isEmpty() const const
void prepend(T &&value)