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