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