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 
13 using namespace Cutelyst;
14 
16  : DispatchType(parent)
17  , d_ptr(new DispatchTypeChainedPrivate)
18 {
19 }
20 
21 DispatchTypeChained::~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 {
194  Q_D(DispatchTypeChained);
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 
274  QString parent;
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 
358 BestActionMatch 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 
464 bool 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 
490 QString 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 
501 QString 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"
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:68
Action * expandAction(const Context *c, Action *action) const final
QString & append(QChar ch)
virtual bool registerAction(Action *action) override
registerAction
QString & prepend(QChar ch)
qsizetype count() const const
qsizetype size() const const
Holds a chain of Cutelyst Actions.
Definition: actionchain.h:23
QString join(QChar separator) const const
void setupMatchedAction(Context *c, Action *action) const
void setMatch(const QString &match)
Definition: request.cpp:145
qsizetype size() const const
This class represents a Cutelyst Action.
Definition: action.h:34
The Cutelyst Context.
Definition: context.h:38
QString number(double n, char format, int precision)
DispatchTypeChained(QObject *parent=nullptr)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString name() const
Definition: component.cpp:33
void setAttributes(const ParamsMultiMap &attributes)
Definition: action.cpp:80
int toInt(bool *ok, int base) const const
bool isEmpty() const const
bool isEmpty() const const
virtual qint8 numberOfCaptures() const noexcept
Definition: action.cpp:130
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
T & first()
virtual MatchType match(Context *c, const QString &path, const QStringList &args) const override
QString reverse() const
Definition: component.cpp:45
void setCaptures(const QStringList &captures)
Definition: request.cpp:169
void push_back(QChar ch)
iterator end()
virtual QString uriForAction(Action *action, const QStringList &captures) const override
void setArguments(const QStringList &arguments)
Definition: request.cpp:157
void prepend(parameter_type value)
bool contains(const Key &key) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
void append(QList< T > &&value)
QList< T > mid(qsizetype pos, qsizetype length) const const
virtual QByteArray list() const override
list the registered actions To be implemented by subclasses
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:74
QObject * parent() const const
iterator begin()
T value(const Key &key, const T &defaultValue) const const
QList< T > values() const const