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