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