1 module hunt.framework.routing.RouteConfigManager;
2 
3 import hunt.framework.routing.ActionRouteItem;
4 import hunt.framework.routing.RouteItem;
5 import hunt.framework.routing.ResourceRouteItem;
6 import hunt.framework.routing.RouteGroup;
7 
8 import hunt.framework.config.ApplicationConfig;
9 import hunt.framework.Init;
10 import hunt.logging.ConsoleLogger;
11 import hunt.http.routing.RouterManager;
12 
13 import std.algorithm;
14 import std.array;
15 import std.conv;
16 import std.file;
17 import std.path;
18 import std.range;
19 import std.regex;
20 import std.string;
21 
22 
23 /** 
24  * 
25  */
26 class RouteConfigManager {
27 
28     private ApplicationConfig _appConfig;
29     private RouteItem[][string] _allRouteItems;
30     private RouteGroup[] _allRouteGroups;
31     private string _basePath;
32 
33     this(ApplicationConfig appConfig) {
34         _appConfig = appConfig;
35         _basePath = DEFAULT_CONFIG_PATH;
36         loadGroupRoutes();
37         loadDefaultRoutes();
38     }
39 
40     string basePath() {
41         return _basePath;
42     }
43 
44     RouteConfigManager basePath(string value) {
45         _basePath = value;
46         return this;
47     }
48 
49     private void addGroupRoute(RouteGroup group, RouteItem[] routes) {
50         _allRouteItems[group.name] = routes;
51         group.appendRoutes(routes);
52         _allRouteGroups ~= group;
53 
54         RouteGroupType groupType = RouteGroupType.Host;
55         if (group.type == "path") {
56             groupType = RouteGroupType.Path;
57         }
58     }
59 
60     RouteItem get(string actionId) {
61         return getRoute(RouteGroup.DEFAULT, actionId);
62     }
63 
64     RouteItem get(string group, string actionId) {
65         return getRoute(group, actionId);
66     }
67 
68     RouteItem[][RouteGroup] allRoutes() {
69         RouteItem[][RouteGroup] r;
70         foreach (string key, RouteItem[] value; _allRouteItems) {
71             foreach (RouteGroup g; _allRouteGroups) {
72                 if (g.name == key) {
73                     r[g] ~= value;
74                     break;
75                 }
76             }
77         }
78         return r;
79     }
80 
81     ActionRouteItem getRoute(string group, string actionId) {
82         auto itemPtr = group in _allRouteItems;
83         if (itemPtr is null)
84             return null;
85 
86         foreach (RouteItem item; *itemPtr) {
87             ActionRouteItem actionItem = cast(ActionRouteItem) item;
88             if (actionItem is null)
89                 continue;
90             if (actionItem.actionId == actionId)
91                 return actionItem;
92         }
93 
94         return null;
95     }
96 
97     ActionRouteItem getRoute(string groupName, string method, string path) {
98         version(HUNT_FM_DEBUG) {
99             tracef("matching: groupName=%s, method=%s, path=%s", groupName, method, path);
100         }
101 
102         if(path.empty) {
103             warning("path is empty");
104             return null;
105         }
106 
107         if(path != "/")
108             path = path.stripRight("/");
109 
110         //
111         // auto itemPtr = group.name in _allRouteItems;
112         auto itemPtr = groupName in _allRouteItems;
113         if (itemPtr is null)
114             return null;
115 
116         foreach (RouteItem item; *itemPtr) {
117             ActionRouteItem actionItem = cast(ActionRouteItem) item;
118             if (actionItem is null)
119                 continue;
120 
121             // TODO: Tasks pending completion -@zhangxueping at 2020-03-13T16:23:18+08:00
122             // handle /user/{id<[0-9]+>} 
123             if(actionItem.path != path) continue;
124             // tracef("actionItem: %s", actionItem);
125             string[] methods = actionItem.methods;
126             if (!methods.empty && !methods.canFind(method))
127                 continue;
128 
129             return actionItem;
130         }
131 
132         return null;
133     }
134 
135     RouteGroup group(string name = RouteGroup.DEFAULT) {
136         auto item = _allRouteGroups.find!(g => g.name == name).takeOne;
137         if (item.empty) {
138             errorf("Can't find the route group: %s", name);
139             return null;
140         }
141         return item.front;
142     }
143 
144     private void loadGroupRoutes() {
145         RouteGroupConfig[] routeGroups = _appConfig.route.groups;
146         if (routeGroups.empty) {
147             version(HUNT_DEBUG) warning("No route group defined.");
148             return;
149         }
150 
151         version (HUNT_DEBUG) {
152             info(routeGroups);
153         }
154 
155         foreach (RouteGroupConfig v; routeGroups) {
156             RouteGroup groupInfo = new RouteGroup();
157             groupInfo.name = strip(v.name);
158             groupInfo.type = strip(v.type);
159             groupInfo.value = strip(v.value);
160 
161             version (HUNT_FM_DEBUG)
162                 infof("route group: %s", groupInfo);
163 
164             string routeConfigFile = groupInfo.name ~ DEFAULT_ROUTE_CONFIG_EXT;
165             routeConfigFile = buildPath(_basePath, routeConfigFile);
166 
167             if (!exists(routeConfigFile)) {
168                 warningf("Config file does not exist: %s", routeConfigFile);
169             } else {
170                 RouteItem[] routes = load(routeConfigFile);
171 
172                 if (routes.length > 0) {
173                     addGroupRoute(groupInfo, routes);
174                 } else {
175                     version (HUNT_DEBUG)
176                         warningf("No routes defined for group %s", groupInfo.name);
177                 }
178             }
179         }
180     }
181 
182     private void loadDefaultRoutes() {
183         // load default routes
184         string routeConfigFile = buildPath(_basePath, DEFAULT_ROUTE_CONFIG);
185         if (!exists(routeConfigFile)) {
186             warningf("The config file for route does not exist: %s", routeConfigFile);
187         } else {
188             RouteItem[] routes = load(routeConfigFile);
189             _allRouteItems[RouteGroup.DEFAULT] = routes;
190 
191             RouteGroup defaultGroup = new RouteGroup();
192             defaultGroup.name = RouteGroup.DEFAULT;
193             defaultGroup.type = RouteGroup.DEFAULT;
194             defaultGroup.value = RouteGroup.DEFAULT;
195             defaultGroup.appendRoutes(routes);
196 
197             _allRouteGroups ~= defaultGroup;
198         }
199     }
200 
201     void withMiddleware(T)() if(is(T : MiddlewareInterface)) {
202         group().withMiddleware!T();
203     }
204     
205     void withMiddleware(string name) {
206         try {
207             group().withMiddleware(name);
208         } catch(Exception ex) {
209             warning(ex.msg);
210         }
211     }    
212     
213     void withoutMiddleware(T)() if(is(T : MiddlewareInterface)) {
214         group().withoutMiddleware!T();
215     }
216     
217     void withoutMiddleware(string name) {
218         try {
219             group().withoutMiddleware(name);
220         } catch(Exception ex) {
221             warning(ex.msg);
222         }
223     } 
224 
225     string createUrl(string actionId, string[string] params = null, string groupName = RouteGroup.DEFAULT) {
226 
227         if (groupName.empty)
228             groupName = RouteGroup.DEFAULT;
229 
230         // find Route
231         // RouteConfigManager routeConfig = serviceContainer().resolve!(RouteConfigManager);
232         RouteGroup routeGroup = group(groupName);
233         if (routeGroup is null)
234             return null;
235 
236         RouteItem route = getRoute(groupName, actionId);
237         if (route is null) {
238             return null;
239         }
240 
241         string url;
242         if (route.isRegex) {
243             if (params is null) {
244                 warningf("Need route params for (%s).", actionId);
245                 return null;
246             }
247 
248             if (!route.paramKeys.empty) {
249                 url = route.urlTemplate;
250                 foreach (i, key; route.paramKeys) {
251                     string value = params.get(key, null);
252 
253                     if (value is null) {
254                         logWarningf("this route template need param (%s).", key);
255                         return null;
256                     }
257 
258                     params.remove(key);
259                     url = url.replaceFirst("{" ~ key ~ "}", value);
260                 }
261             }
262         } else {
263             url = route.pattern;
264         }
265 
266         string groupValue = routeGroup.value;
267         if (routeGroup.type == RouteGroup.HOST || routeGroup.type == RouteGroup.DOMAIN) {
268             url = (_appConfig.https.enabled ? "https://" : "http://") ~ groupValue ~ url;
269         } else {
270             string baseUrl = strip(_appConfig.application.baseUrl, "", "/");
271             string tempUrl = (groupValue.empty || groupValue == RouteGroup.DEFAULT) ? baseUrl : (baseUrl ~ "/" ~ groupValue);
272             url = tempUrl ~ url;
273         }
274 
275         return url ~ (params.length > 0 ? ("?" ~ buildUriQueryString(params)) : "");
276     }
277 
278     static string buildUriQueryString(string[string] params) {
279         if (params.length == 0) {
280             return "";
281         }
282 
283         string r;
284         foreach (k, v; params) {
285             r ~= (r ? "&" : "") ~ k ~ "=" ~ v;
286         }
287 
288         return r;
289     }
290 
291     static RouteItem[] load(string filename) {
292         import std.stdio;
293 
294         RouteItem[] items;
295         auto f = File(filename);
296 
297         scope (exit) {
298             f.close();
299         }
300 
301         foreach (line; f.byLineCopy) {
302             RouteItem item = parseOne(cast(string) line);
303             if (item is null)
304                 continue;
305 
306             if (item.path.length > 0) {
307                 items ~= item;
308             }
309         }
310 
311         return items;
312     }
313 
314     static RouteItem parseOne(string line) {
315         line = strip(line);
316 
317         // not availabale line return null
318         if (line.length == 0 || line[0] == '#') {
319             return null;
320         }
321 
322         // match example: 
323         // GET, POST    /users    module.controller.action | staticDir:wwwroot:true
324         auto matched = line.match(
325                 regex(`([^/]+)\s+(/[\S]*?)\s+((staticDir[\:][\w|\/|\\|\:|\.]+)|([\w\.]+))`));
326 
327         if (!matched) {
328             if (!line.empty()) {
329                 warningf("Unmatched line: %s", line);
330             }
331             return null;
332         }
333 
334         //
335         RouteItem item;
336         string part3 = matched.captures[3].to!string.strip;
337 
338         // 
339         if (part3.startsWith(DEFAULT_RESOURCES_ROUTE_LEADER)) {
340             ResourceRouteItem routeItem = new ResourceRouteItem();
341             string remaining = part3.chompPrefix(DEFAULT_RESOURCES_ROUTE_LEADER);
342             string[] subParts = remaining.split(":");
343 
344             version(HUNT_HTTP_DEBUG) {
345                 tracef("Resource route: %s", subParts);
346             }
347 
348             if(subParts.length > 1) {
349                 routeItem.resourcePath = subParts[0].strip();
350                 string s = subParts[1].strip();
351                 try {
352                     routeItem.canListing = to!bool(s);
353                 } catch(Throwable t) {
354                     version(HUNT_DEBUG) warning(t);
355                 }
356             } else {
357                 routeItem.resourcePath = remaining.strip();
358             }
359 
360             item = routeItem;
361         } else {
362             ActionRouteItem routeItem = new ActionRouteItem();
363             // actionId
364             string actionId = part3;
365             string[] mcaArray = split(actionId, ".");
366 
367             if (mcaArray.length > 3 || mcaArray.length < 2) {
368                 logWarningf("this route config actionId length is: %d (%s)", mcaArray.length, actionId);
369                 return null;
370             }
371 
372             if (mcaArray.length == 2) {
373                 routeItem.controller = mcaArray[0];
374                 routeItem.action = mcaArray[1];
375             } else {
376                 routeItem.moduleName = mcaArray[0];
377                 routeItem.controller = mcaArray[1];
378                 routeItem.action = mcaArray[2];
379             }
380             item = routeItem;
381         }
382 
383         // methods
384         string methods = matched.captures[1].to!string.strip;
385         methods = methods.toUpper();
386 
387         if (methods.length > 2) {
388             if (methods[0] == '[' && methods[$ - 1] == ']')
389                 methods = methods[1 .. $ - 2];
390         }
391 
392         if (methods == "*" || methods == "ALL") {
393             item.methods = null;
394         } else {
395             item.methods = split(methods, ",");
396         }
397 
398         // path
399         string path = matched.captures[2].to!string.strip;
400         item.path = path;
401         item.pattern = mendPath(path);
402 
403         // warningf("old: %s, new: %s", path, item.pattern);
404 
405         // regex path
406         auto matches = path.matchAll(regex(`\{(\w+)(<([^>]+)>)?\}`));
407         if (matches) {
408             string[int] paramKeys;
409             int paramCount = 0;
410             string pattern = path;
411             string urlTemplate = path;
412 
413             foreach (m; matches) {
414                 paramKeys[paramCount] = m[1];
415                 string reg = m[3].length ? m[3] : "\\w+";
416                 pattern = pattern.replaceFirst(m[0], "(" ~ reg ~ ")");
417                 urlTemplate = urlTemplate.replaceFirst(m[0], "{" ~ m[1] ~ "}");
418                 paramCount++;
419             }
420 
421             item.isRegex = true;
422             item.pattern = pattern;
423             item.paramKeys = paramKeys;
424             item.urlTemplate = urlTemplate;
425         }
426 
427         return item;
428     }
429 
430     static string mendPath(string path) {
431         if (path.empty || path == "/")
432             return "/";
433 
434         if (path[0] != '/') {
435             path = "/" ~ path;
436         }
437 
438         if (path[$ - 1] != '/')
439             path ~= "/";
440 
441         return path;
442     }
443 }
444 
445 /**
446  * Examples:
447  *  # without component
448  *  app.controller.attachment.attachmentcontroller.upload
449  * 
450  *  # with component
451  *  app.component.attachment.controller.attachment.attachmentcontroller.upload
452  */
453 string makeRouteHandlerKey(ActionRouteItem route, RouteGroup group = null) {
454     string moduleName = route.moduleName;
455     string controller = route.controller;
456 
457     string groupName = "";
458     if (group !is null && group.name != RouteGroup.DEFAULT)
459         groupName = group.name ~ ".";
460 
461     string key = format("app.%scontroller.%s%s.%scontroller.%s", moduleName.empty()
462             ? "" : "component." ~ moduleName ~ ".", groupName, controller,
463             controller, route.action);
464     return key.toLower();
465 }