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 }