Cutelyst  2.5.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 
39 QByteArray DispatchTypeChained::list() const
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 
49  QVector<QStringList> paths;
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 
61  QString parent;
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 
98  QVector<QStringList> rows;
99  for (Action *p : parents) {
100  QString name = QLatin1Char('/') + p->reverse();
101 
102  QString extra = DispatchTypeChainedPrivate::listExtraHttpMethods(p);
103  if (!extra.isEmpty()) {
104  name.prepend(extra + 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  QTextStream out(&buffer, QIODevice::WriteOnly);
151 
152  if (!paths.isEmpty()) {
153  out << Utils::buildTable(paths, { QLatin1String("Path Spec"), QLatin1String("Private") },
154  QLatin1String("Loaded Chained actions:"));
155  }
156 
157  if (!unattachedTable.isEmpty()) {
158  out << Utils::buildTable(unattachedTable, { QLatin1String("Private"), QLatin1String("Missing parent") },
159  QLatin1String("Unattached Chained actions:"));
160  }
161 
162  return buffer;
163 }
164 
165 DispatchType::MatchType DispatchTypeChained::match(Context *c, const QString &path, const QStringList &args) const
166 {
167  if (!args.isEmpty()) {
168  return NoMatch;
169  }
170 
171  Q_D(const DispatchTypeChained);
172 
173  const BestActionMatch ret = d->recurseMatch(args.size(), QStringLiteral("/"), path.split(QLatin1Char('/')));
174  const ActionList chain = ret.actions;
175  if (ret.isNull || chain.isEmpty()) {
176  return NoMatch;
177  }
178 
179  QStringList decodedArgs;
180  const QStringList parts = ret.parts;
181  for (const QString &arg : parts) {
182  QString aux = arg;
183  decodedArgs.append(Utils::decodePercentEncoding(&aux));
184  }
185 
186  ActionChain *action = new ActionChain(chain, c);
187  Request *request = c->request();
188  request->setArguments(decodedArgs);
189  request->setCaptures(ret.captures);
190  request->setMatch(QLatin1Char('/') + action->reverse());
191  setupMatchedAction(c, action);
192 
193  return ExactMatch;
194 }
195 
197 {
198  Q_D(DispatchTypeChained);
199 
200  auto attributes = action->attributes();
201  const QStringList chainedList = attributes.values(QLatin1String("Chained"));
202  if (chainedList.isEmpty()) {
203  return false;
204  }
205 
206  if (chainedList.size() > 1) {
207  qCCritical(CUTELYST_DISPATCHER_CHAINED)
208  << "Multiple Chained attributes not supported registering" << action->reverse();
209  return false;
210  }
211 
212  const QString chainedTo = chainedList.first();
213  if (chainedTo == QLatin1Char('/') + action->name()) {
214  qCCritical(CUTELYST_DISPATCHER_CHAINED)
215  << "Actions cannot chain to themselves registering /" << action->name();
216  return false;
217  }
218 
219  const QStringList pathPart = attributes.values(QLatin1String("PathPart"));
220 
221  QString part = action->name();
222 
223  if (pathPart.size() == 1 && !pathPart[0].isEmpty()) {
224  part = pathPart[0];
225  } else if (pathPart.size() > 1) {
226  qCCritical(CUTELYST_DISPATCHER_CHAINED)
227  << "Multiple PathPart attributes not supported registering"
228  << action->reverse();
229  return false;
230  }
231 
232  if (part.startsWith(QLatin1Char('/'))) {
233  qCCritical(CUTELYST_DISPATCHER_CHAINED)
234  << "Absolute parameters to PathPart not allowed registering"
235  << action->reverse();
236  return false;
237  }
238 
239  attributes.insert(QStringLiteral("PathPart"), part);
240  action->setAttributes(attributes);
241 
242  auto &childrenOf = d->childrenOf[chainedTo][part];
243  childrenOf.insert(childrenOf.begin(), action);
244 
245  d->actions[QLatin1Char('/') + action->reverse()] = action;
246 
247  if (!d->checkArgsAttr(action, QLatin1String("Args")) ||
248  !d->checkArgsAttr(action, QLatin1String("CaptureArgs"))) {
249  return false;
250  }
251 
252  if (attributes.contains(QLatin1String("Args")) && attributes.contains(QLatin1String("CaptureArgs"))) {
253  qCCritical(CUTELYST_DISPATCHER_CHAINED)
254  << "Combining Args and CaptureArgs attributes not supported registering"
255  << action->reverse();
256  return false;
257  }
258 
259  if (!attributes.contains(QLatin1String("CaptureArgs"))) {
260  d->endPoints.push_back(action);
261  }
262 
263  return true;
264 }
265 
266 QString DispatchTypeChained::uriForAction(Action *action, const QStringList &captures) const
267 {
268  Q_D(const DispatchTypeChained);
269 
270  QString ret;
271  const QMap<QString, QString> attributes = action->attributes();
272  if (!(attributes.contains(QStringLiteral("Chained")) &&
273  !attributes.contains(QStringLiteral("CaptureArgs")))) {
274  qCWarning(CUTELYST_DISPATCHER_CHAINED) << "uriForAction: action is not an end point" << action;
275  return ret;
276  }
277 
278  QString parent;
279  QStringList localCaptures = captures;
280  QStringList parts;
281  Action *curr = action;
282  while (curr) {
283  const QMap<QString, QString> attributes = curr->attributes();
284  if (attributes.contains(QStringLiteral("CaptureArgs"))) {
285  if (localCaptures.size() < curr->numberOfCaptures()) {
286  // Not enough captures
287  qCWarning(CUTELYST_DISPATCHER_CHAINED) << "uriForAction: not enough captures" << curr->numberOfCaptures() << captures.size();
288  return ret;
289  }
290 
291  parts = localCaptures.mid(localCaptures.size() - curr->numberOfCaptures()) + parts;
292  localCaptures = localCaptures.mid(0, localCaptures.size() - curr->numberOfCaptures());
293  }
294 
295  const QString pp = attributes.value(QStringLiteral("PathPart"));
296  if (!pp.isEmpty()) {
297  parts.prepend(pp);
298  }
299 
300  parent = attributes.value(QStringLiteral("Chained"));
301  curr = d->actions.value(parent);
302  }
303 
304  if (parent != QLatin1String("/")) {
305  // fail for dangling action
306  qCWarning(CUTELYST_DISPATCHER_CHAINED) << "uriForAction: dangling action" << parent;
307  return ret;
308  }
309 
310  if (!localCaptures.isEmpty()) {
311  // fail for too many captures
312  qCWarning(CUTELYST_DISPATCHER_CHAINED) << "uriForAction: too many captures" << localCaptures;
313  return ret;
314  }
315 
316  ret = QLatin1Char('/') + parts.join(QLatin1Char('/'));
317  return ret;
318 }
319 
321 {
322  Q_D(const DispatchTypeChained);
323 
324  // Do not expand action if action already is an ActionChain
325  if (qobject_cast<ActionChain*>(action)) {
326  return action;
327  }
328 
329  // The action must be chained to something
330  if (!action->attributes().contains(QStringLiteral("Chained"))) {
331  return nullptr;
332  }
333 
334  ActionList chain;
335  Action *curr = action;
336 
337  while (curr) {
338  chain.prepend(curr);
339  const QString parent = curr->attribute(QStringLiteral("Chained"));
340  curr = d->actions.value(parent);
341  }
342 
343  return new ActionChain(chain, const_cast<Context*>(c));
344 }
345 
347 {
348  Q_D(const DispatchTypeChained);
349 
350  if (d->actions.isEmpty()) {
351  return false;
352  }
353 
354  // Optimize end points
355 
356  return true;
357 }
358 
359 BestActionMatch DispatchTypeChainedPrivate::recurseMatch(int reqArgsSize, const QString &parent, 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 QMap<QString, QString> 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 = recurseMatch(reqArgsSize, QLatin1Char('/') + action->reverse(), localParts);
409 
410  // No best action currently
411  // OR The action has less parts
412  // OR The action has equal parts but less captured data (ergo more defined)
413  ActionList actions = ret.actions;
414  const QStringList actionCaptures = ret.captures;
415  const QStringList actionParts = ret.parts;
416  int bestActionParts = bestAction.parts.size();
417 
418  if (!actions.isEmpty() &&
419  (bestAction.isNull ||
420  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 = attributes.value(QStringLiteral("PathPart")).count(QLatin1Char('/')) + 1;
426  bestAction.actions = actions;
427  bestAction.captures = captures + actionCaptures;
428  bestAction.parts = actionParts;
429  bestAction.n_pathParts = pathparts + ret.n_pathParts;
430  bestAction.isNull = false;
431  }
432  } else {
433  if (!action->match(reqArgsSize + parts.size())) {
434  continue;
435  }
436 
437  const QString argsAttr = attributes.value(QStringLiteral("Args"));
438  const int pathparts = attributes.value(QStringLiteral("PathPart")).count(QLatin1Char('/')) + 1;
439  // No best action currently
440  // OR This one matches with fewer parts left than the current best action,
441  // And therefore is a better match
442  // OR No parts and this expects 0
443  // The current best action might also be Args(0),
444  // but we couldn't chose between then anyway so we'll take the last seen
445 
446  if (bestAction.isNull ||
447  parts.size() < bestAction.parts.size() ||
448  (parts.isEmpty() && !argsAttr.isEmpty() && action->numberOfArgs() == 0)) {
449  bestAction.actions = { action };
450  bestAction.captures = QStringList();
451  bestAction.parts = parts;
452  bestAction.n_pathParts = pathparts;
453  bestAction.isNull = false;
454  }
455  }
456  }
457  }
458 
459  return bestAction;
460 }
461 
462 bool DispatchTypeChainedPrivate::checkArgsAttr(Action *action, const QString &name) const
463 {
464  const QMap<QString, QString> attributes = action->attributes();
465  if (!attributes.contains(name)) {
466  return true;
467  }
468 
469  const QStringList values = attributes.values(name);
470  if (values.size() > 1) {
471  qCCritical(CUTELYST_DISPATCHER_CHAINED)
472  << "Multiple"
473  << name
474  << "attributes not supported registering"
475  << action->reverse();
476  return false;
477  }
478 
479  QString args = values[0];
480  bool ok;
481  if (!args.isEmpty() && args.toInt(&ok) < 0 && !ok) {
482  qCCritical(CUTELYST_DISPATCHER_CHAINED)
483  << "Invalid"
484  << name << "(" << args << ") for action"
485  << action->reverse()
486  << "(use '" << name << "' or '" << name << "(<number>)')";
487  return false;
488  }
489 
490  return true;
491 }
492 
493 QString DispatchTypeChainedPrivate::listExtraHttpMethods(Action *action)
494 {
495  QString ret;
496  const auto attributes = action->attributes();
497  if (attributes.contains(QLatin1String("HTTP_METHODS"))) {
498  const QStringList extra = attributes.values(QLatin1String("HTTP_METHODS"));
499  ret = extra.join(QLatin1String(", "));
500  }
501  return ret;
502 }
503 
504 QString DispatchTypeChainedPrivate::listExtraConsumes(Action *action)
505 {
506  QString ret;
507  const auto attributes = action->attributes();
508  if (attributes.contains(QLatin1String("CONSUMES"))) {
509  const QStringList extra = attributes.values(QLatin1String("CONSUMES"));
510  ret = extra.join(QLatin1String(", "));
511  }
512  return ret;
513 }
514 
515 #include "moc_dispatchtypechained.cpp"
Action * expandAction(const Context *c, Action *action) const final
QVector< Action * > ActionList
Definition: action.h:162
virtual bool registerAction(Action *action) override
registerAction
QMap< QString, QString > attributes() const
Definition: action.cpp:81
virtual qint8 numberOfCaptures() const
Definition: action.cpp:143
Holds a chain of Cutelyst Actions.
Definition: actionchain.h:36
void setupMatchedAction(Context *c, Action *action) const
void setAttributes(const QMap< QString, QString > &attributes)
Definition: action.cpp:93
void setMatch(const QString &match)
Definition: request.cpp:155
This class represents a Cutelyst Action.
Definition: action.h:47
The Cutelyst Context.
Definition: context.h:50
DispatchTypeChained(QObject *parent=nullptr)
QString name() const
Definition: component.cpp:39
QString attribute(const QString &name, const QString &defaultValue=QString()) const
Definition: action.cpp:87
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
virtual MatchType match(Context *c, const QString &path, const QStringList &args) const override
QString reverse() const
Definition: component.cpp:51
void setCaptures(const QStringList &captures)
Definition: request.cpp:179
virtual QString uriForAction(Action *action, const QStringList &captures) const override
void setArguments(const QStringList &arguments)
Definition: request.cpp:167
virtual QByteArray list() const override
list the registered actions To be implemented by subclasses