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 }