├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── UPGRADE-0.7.md ├── UPGRADE-1.0.0.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── resthub │ │ └── web │ │ └── springmvc │ │ ├── router │ │ ├── HTTPRequestAdapter.java │ │ ├── Router.java │ │ ├── RouterConfigurationSupport.java │ │ ├── RouterHandlerMapping.java │ │ ├── exceptions │ │ │ ├── ActionNotFoundException.java │ │ │ ├── NoHandlerFoundException.java │ │ │ ├── NoRouteFoundException.java │ │ │ └── RouteFileParsingException.java │ │ ├── hateoas │ │ │ └── RouterLinkBuilder.java │ │ └── support │ │ │ ├── RouterHandler.java │ │ │ └── RouterHandlerResolver.java │ │ └── view │ │ ├── freemarker │ │ └── RouterModelAttribute.java │ │ ├── jsp │ │ └── URLRouteTag.java │ │ └── velocity │ │ └── RouteDirective.java └── resources │ └── url-routing.tld └── test ├── java └── org │ └── resthub │ └── web │ └── springmvc │ └── router │ ├── controllers │ ├── BindTestController.java │ └── MyTestController.java │ ├── javaconfig │ └── WebAppConfig.java │ ├── support │ └── TeapotHandlerInterceptor.java │ └── test │ ├── HandlersStepdefs.java │ ├── ReverseRoutingStepdefs.java │ └── RunCucumberTest.java └── resources ├── addroutes.conf ├── bindingTestContext.xml ├── bindingroutes.conf ├── log4j.properties ├── mappingroutes.conf ├── multiplefilesTestContext.xml ├── org └── resthub │ └── web │ └── springmvc │ └── router │ └── test │ ├── handler_adapter.feature │ ├── handler_mapping.feature │ ├── hateoas.feature │ ├── javaconfig.feature │ └── reverse_routing.feature ├── securityContext.xml ├── simpleTestContext.xml ├── wildcard-a.conf └── wildcard-b.conf /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings/ 4 | target/ 5 | .idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk7 4 | - oraclejdk7 5 | - oraclejdk8 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Brian Clozel 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SpringMVC Router 2 | ================ 3 | 4 | [![Build Status](https://secure.travis-ci.org/resthub/springmvc-router.png?branch=master)](http://travis-ci.org/resthub/springmvc-router) 5 | 6 | Developers mailing list: resthub-dev@googlegroups.com 7 | 8 | Route mapping with SpringMVC Router 9 | ----------------------------------- 10 | 11 | Spring MVC 4 [handles requests mapping](http://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/html/mvc.html) 12 | with `RequestMappingHandlerMapping` and `RequestMappingHandlerAdapter` beans (that's the "out-of-the-box" configuration that comes with your springmvc application). 13 | 14 | But you may want to use a request Router for your application: 15 | 16 | * Route Configuration is centralized in one place (you don't need to look into your controllers anymore) 17 | * URL Refactoring is easier 18 | * Many web frameworks use that system (Rails, PlayFramework and many others) 19 | * Handles routes priority 20 | 21 | 22 | Define your application routes like this! 23 | 24 | GET /user/? userController.listAll 25 | GET /user/{<[0-9]+>id} userController.showUser 26 | DELETE /user/{<[0-9]+>id} userController.deleteUser 27 | POST /user/add/? userController.createUser 28 | 29 | 30 | Configuring the SpringMVC Router for your project 31 | ------------------------------------------------- 32 | 33 | ### Add the dependency to your maven pom.xml 34 | 35 | Warning: **this project is currently tested on Spring 4.0.x**, should work on 3.2.x 36 | but is not compatible with Spring 3.0.x. 37 | 38 | Your project needs these dependencies 39 | (Hint: the [new "Bill of materials"](http://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/html/overview.html#overview-maven-bom)) 40 | is really handy: 41 | 42 | 43 | 44 | 45 | 46 | org.springframework 47 | spring-framework-bom 48 | 4.0.2.RELEASE 49 | pom 50 | import 51 | 52 | 53 | 54 | 55 | 56 | ... 57 | 58 | org.springframework 59 | spring-aop 60 | 61 | 62 | org.springframework 63 | spring-beans 64 | 65 | 66 | org.springframework 67 | spring-webmvc 68 | 69 | ... 70 | 71 | org.resthub 72 | springmvc-router 73 | 1.2.0 74 | 75 | ... 76 | 77 | 78 | If you want to use SNAPSHOTs, add oss.sonatype.org as a repository. 79 | 80 | 81 | 82 | sonatype.oss.snapshots 83 | Sonatype OSS Snapshot Repository 84 | http://oss.sonatype.org/content/repositories/snapshots 85 | 86 | false 87 | 88 | 89 | true 90 | 91 | 92 | 93 | 94 | 95 | ### Add the Router to your Spring MVC configuration 96 | 97 | In your *-servlet.xml file, add the following beans: 98 | 99 | 100 | 101 | 108 | 109 | 112 | 113 | 114 | 118 | 119 | 120 | 131 | 133 | 134 | 135 | routes.conf 136 | 140 | 141 | 142 | 143 | 149 | 150 | 151 | 152 | 153 | 154 | Or you can achieve the same thing with a Javaconfig class like this: 155 | 156 | @Configuration 157 | @ComponentScan(basePackages = "com.example.yourproject.controllers") 158 | // You should not use the @EnableWebMvc annotation 159 | public class WebAppConfig extends RouterConfigurationSupport { 160 | 161 | @Override 162 | public List listRouteFiles() { 163 | 164 | List routeFiles = new ArrayList(); 165 | routeFiles.add("routes.conf"); 166 | return routeFiles; 167 | } 168 | } 169 | 170 | ### Create your route configuration file 171 | 172 | 173 | The example above will load the configuration file using Spring ResourceLoader - so create a new file in your project `src/main/resources/routes.conf`. 174 | 175 | Routes configuration 176 | -------------------- 177 | 178 | The router maps HTTP request to a specific action (i.e. a public method of a Controller class handling requests). 179 | 180 | ### Get your first Controller ready! 181 | 182 | Controllers can use [Spring MVC annotations and conventions](http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/mvc.html) - only the `@RequestMapping` annotation is useless. 183 | 184 | 185 | @Controller 186 | public class HelloController { 187 | public void simpleAction() { 188 | 189 | } 190 | 191 | public @ResponseBody String sayHelloTo(@PathVariable(value = "name") String name) { 192 | return "Hello "+name+" !"; 193 | } 194 | } 195 | 196 | 197 | 198 | ### Edit your route configuration file 199 | 200 | **Warning: in the route configuration file, Controller names are case sensitive, and should always start with a lower case letter.** 201 | 202 | 203 | # this is a comment 204 | 205 | GET /simpleaction helloController.simpleAction 206 | GET /hello/{<[a-zA-Z]+>name} helloController.sayHelloTo 207 | 208 | 209 | For more details on routes syntax, [check out the PlayFramework documentation](http://www.playframework.org/documentation/1.2.4/routes). 210 | 211 | 212 | View Integration 213 | ---------------- 214 | 215 | Routing requests to actions is one thing. But refactoring routes can be a real pain if all your URLs are hard coded in your template views. Reverse routing is the solution. 216 | 217 | 218 | ### Reverse Routing 219 | 220 | Example route file: 221 | 222 | GET /user/? userController.listAll 223 | GET /user/{<[0-9]+>id} userController.showUser 224 | DELETE /user/{<[0-9]+>id} userController.deleteUser 225 | POST /user/add/? userController.createUser 226 | 227 | 228 | Reverse routing in your Java class: 229 | 230 | import org.resthub.web.springmvc.router.Router; 231 | 232 | public class MyClass { 233 | public void myMethod() { 234 | 235 | ActionDefinition action = Router.reverse("userController.listAll"); 236 | // logs "/user/" 237 | logger.info(action.url); 238 | 239 | HashMap args = new HashMap(); 240 | args.put("id",42L); 241 | ActionDefinition otherAction = Router.reverse("userController.showUser", args); 242 | // logs "/user/42" 243 | logger.info(otherAction.url); 244 | } 245 | } 246 | 247 | 248 | ### Integrating with Velocity 249 | 250 | First, add the RouteDirective to your Velocity Engine configuration: 251 | 252 | 256 | 258 | 259 | 260 | 261 | 262 | org.resthub.web.springmvc.view.velocity.RouteDirective 263 | 264 | 265 | 266 | 267 | Then use the #route directive within your .vm file: 268 | 269 | List all users 270 | Show user 42 271 | 272 | ### Integrating with FreeMarker 273 | 274 | In your Spring MVC context add the following: 275 | 276 | 277 | 278 | 279 | 280 | This will inject a model attribute called "route" to every model. The attribute name can be modified by setting the 281 | property "attributeName". 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | Then use the Router instance within your .ftl files: 290 | 291 | List all users 292 | 293 | <#assign params = {"id":42}/> 294 | Show user 42 295 | 296 | ### Integrating with JSP 297 | 298 | In your JSP, declare the taglib: 299 | 300 | <%@ taglib prefix="route" uri="/springmvc-router" %> 301 | 302 | Then use the ```reverse``` method to generate URLs: 303 | 304 | ">List all users 305 | 306 | Dynamic parameters can also be used: 307 | 308 | ">Show user 42 309 | 310 | 311 | Spring HATEOAS support 312 | ---------------------- 313 | 314 | SpringMVC Router has its own [LinkBuilder implementation](https://github.com/resthub/springmvc-router/blob/master/src/main/java/org/resthub/web/springmvc/router/hateoas/RouterLinkBuilder.java) to work with [Spring HATEOAS](https://github.com/SpringSource/spring-hateoas). 315 | 316 | 317 | Tools 318 | ----- 319 | 320 | ### Autocomplete reverse routing in your IDE 321 | 322 | [springmvc-router-ide](https://github.com/bradhouse/springmvc-router-ide) is a Maven plugin to generate template files that assist IDEs in autocompleting reverse routing with this project. 323 | 324 | ### RESThub framework 325 | 326 | This project can be used as an addon to [RESThub framework](http://resthub.org/). 327 | -------------------------------------------------------------------------------- /UPGRADE-0.7.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 0.6 to 0.7 2 | ======================= 3 | 4 | ### Spring backwards compatibility 5 | 6 | This project no longer supports Spring MVC 3.0.x . Spring MVC 3.1.0++ is now a requirement. 7 | 8 | [@RequestMapping support changed in Spring MVC 3.1](http://static.springsource.org/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-requestmapping-31-vs-30), and springmvc-router follows that direction. 9 | 10 | ### Spring MVC configuration 11 | 12 | The HandlerAdapter part of springmvc-router is no longer needed, because we rely on Spring MVC default implementation `RequestMappingHandlerAdapter`. 13 | So you should delete this part from *-servlet.xml your configuration: 14 | 15 | 20 | 22 | 23 | RouterHandlerMapping declaration has changed. 24 | It now supports multiple configuration files. Even if your application uses only one configuration file, you have to update your RouterHandlerMapping declaration for the property "routeFiles". 25 | 26 | 28 | 29 | 30 | 31 | routes.conf 32 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /UPGRADE-1.0.0.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 0.7 to 1.0.0 2 | ========================= 3 | 4 | ### Spring MVC configuration 5 | 6 | RouterHandlerMapping declaration has changed. 7 | "servletPrefix" configuration is now useless and so you should remove that part from your *-servlet.xml. 8 | 9 | 10 | 11 | This is now done automatically for you at runtime, for the context path and the servlet path. 12 | 13 | ### Javaconfig support 14 | 15 | If you want full Javaconfig support - for example, adding interceptors by implementing [WebMvcConfigurer](http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.html) methods: 16 | 17 | * you SHOULD NOT use the [@EnableWebMvc](http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/EnableWebMvc.html) annotation 18 | * you SHOULD make your javaconfig class extend RouterConfigurationSupport 19 | * no need to declare springmvc-router related beans in XMLs :-D 20 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.resthub 5 | springmvc-router 6 | jar 7 | 2.0.0-SNAPSHOT 8 | springmvc-router 9 | Adds route mapping capacity to any "Spring MVC based" webapp. 10 | https://github.com/resthub/springmvc-router/ 11 | 12 | 13 | org.sonatype.oss 14 | oss-parent 15 | 7 16 | 17 | 18 | 19 | 4.0.2.RELEASE 20 | 3.2.1.RELEASE 21 | 3.1 22 | 2.4 23 | 1.7 24 | 1.2.17 25 | 1.7.6 26 | 4.11 27 | 1.5.0 28 | 1.1.5 29 | UTF-8 30 | 31 | 32 | 33 | 34 | Apache License, Version 2 35 | http://www.apache.org/licenses/LICENSE-2.0 36 | This project's main license 37 | 38 | 39 | Apache 2 License 40 | http://www.apache.org/licenses/LICENSE-2.0 41 | PlayFramework Router implementation 42 | 43 | 44 | BSD License 45 | http://jregex.sourceforge.net/license.txt 46 | JRegex system dependency 47 | 48 | 49 | 50 | 2010 51 | 52 | 53 | bclozel 54 | Brian Clozel 55 | +1 56 | 57 | 58 | 59 | scm:git:git@github.com:bclozel/springmvc-router.git 60 | scm:git:git@github.com:bclozel/springmvc-router.git 61 | git@github.com:bclozel/springmvc-router.git 62 | 63 | 64 | Spring MVC Router issue tracker 65 | https://github.com/bclozel/springmvc-router/issues 66 | 67 | 68 | 69 | 73 | 74 | org.springframework 75 | spring-aop 76 | ${spring-version} 77 | true 78 | 79 | 80 | org.springframework 81 | spring-beans 82 | ${spring-version} 83 | true 84 | 85 | 86 | org.springframework 87 | spring-webmvc 88 | ${spring-version} 89 | true 90 | 91 | 92 | 93 | 94 | org.slf4j 95 | slf4j-api 96 | ${slf4j.version} 97 | 98 | 99 | log4j 100 | log4j 101 | ${log4j.version} 102 | runtime 103 | true 104 | 105 | 106 | org.slf4j 107 | slf4j-log4j12 108 | ${slf4j.version} 109 | runtime 110 | true 111 | 112 | 113 | 114 | 115 | net.sourceforge.jregex 116 | jregex 117 | 1.2_01 118 | 119 | 120 | 121 | 122 | commons-io 123 | commons-io 124 | ${apache-commons-io-version} 125 | 126 | 127 | 128 | javax.inject 129 | javax.inject 130 | 1 131 | 132 | 133 | 134 | 135 | javax.servlet 136 | jsp-api 137 | 2.0 138 | true 139 | 140 | 141 | 142 | 143 | org.apache.velocity 144 | velocity 145 | ${velocity-version} 146 | true 147 | 148 | 149 | 150 | 151 | org.springframework.hateoas 152 | spring-hateoas 153 | 0.9.0.RELEASE 154 | true 155 | 156 | 157 | 158 | 159 | javax.servlet 160 | javax.servlet-api 161 | 3.1.0 162 | provided 163 | 164 | 165 | 166 | 167 | org.springframework 168 | spring-test 169 | ${spring-version} 170 | test 171 | 172 | 173 | 174 | org.assertj 175 | assertj-core 176 | ${assertj.version} 177 | test 178 | 179 | 180 | 181 | info.cukes 182 | cucumber-picocontainer 183 | ${cucumber.jvm.version} 184 | test 185 | 186 | 187 | info.cukes 188 | cucumber-junit 189 | ${cucumber.jvm.version} 190 | test 191 | 192 | 193 | junit 194 | junit 195 | ${junit.version} 196 | test 197 | 198 | 199 | 200 | 203 | 204 | org.springframework.security 205 | spring-security-core 206 | ${spring-security-version} 207 | test 208 | 209 | 210 | org.springframework.security 211 | spring-security-config 212 | ${spring-security-version} 213 | test 214 | 215 | 216 | org.springframework.security 217 | spring-security-acl 218 | ${spring-security-version} 219 | test 220 | 221 | 222 | org.springframework.security 223 | spring-security-web 224 | ${spring-security-version} 225 | test 226 | 227 | 228 | cglib 229 | cglib-nodep 230 | ${cglib-version} 231 | test 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | org.apache.maven.plugins 241 | maven-compiler-plugin 242 | 3.1 243 | 244 | UTF-8 245 | 1.6 246 | 1.6 247 | 248 | 249 | 250 | org.apache.maven.plugins 251 | maven-surefire-plugin 252 | 253 | 254 | 255 | 256 | 257 | org.apache.maven.plugins 258 | maven-compiler-plugin 259 | 260 | 261 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/HTTPRequestAdapter.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.util.Assert; 6 | import org.springframework.web.context.request.RequestAttributes; 7 | import org.springframework.web.context.request.RequestContextHolder; 8 | import org.springframework.web.context.request.ServletRequestAttributes; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import java.lang.reflect.Method; 12 | import java.net.URI; 13 | import java.util.*; 14 | 15 | /** 16 | * Adapter class for HTTP class defined in Play! Framework Maps 17 | * HTTPServletRequest to HTTP.Request and HTTP.Header 18 | * 19 | * @author Brian Clozel 20 | * @see org.resthub.web.springmvc.router.Router 21 | */ 22 | public class HTTPRequestAdapter { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(HTTPRequestAdapter.class); 25 | 26 | /** 27 | * Server host 28 | */ 29 | public String host; 30 | /** 31 | * Request path 32 | */ 33 | public String path; 34 | /** 35 | * Context path 36 | */ 37 | public String contextPath; 38 | /** 39 | * Servlet path 40 | */ 41 | public String servletPath; 42 | /** 43 | * QueryString 44 | */ 45 | public String querystring; 46 | /** 47 | * Full url 48 | */ 49 | public String url; 50 | /** 51 | * HTTP method 52 | */ 53 | public String method; 54 | /** 55 | * Server domain 56 | */ 57 | public String domain; 58 | /** 59 | * Client address 60 | */ 61 | public String remoteAddress; 62 | /** 63 | * Request content-type 64 | */ 65 | public String contentType; 66 | /** 67 | * Controller to invoke 68 | */ 69 | public String controller; 70 | /** 71 | * Action method name 72 | */ 73 | public String actionMethod; 74 | /** 75 | * HTTP port 76 | */ 77 | public Integer port; 78 | /** 79 | * HTTP Headers 80 | */ 81 | public Map headers = new HashMap(); 82 | /** 83 | * Additinal HTTP params extracted from route 84 | */ 85 | public Map routeArgs; 86 | /** 87 | * Format (html,xml,json,text) 88 | */ 89 | public String format = null; 90 | /** 91 | * Full action (ex: Application.index) 92 | */ 93 | public String action; 94 | /** 95 | * The really invoker Java methid 96 | */ 97 | public transient Method invokedMethod; 98 | /** 99 | * The invoked controller class 100 | */ 101 | public transient Class controllerClass; 102 | /** 103 | * Free space to store your request specific data 104 | */ 105 | public Map args = new HashMap(); 106 | /** 107 | * When the request has been received 108 | */ 109 | public Date date = new Date(); 110 | /** 111 | * is HTTPS ? 112 | */ 113 | public Boolean secure = false; 114 | 115 | public HTTPRequestAdapter() { 116 | 117 | this.headers = new HashMap(); 118 | } 119 | 120 | public void setFormat(String _format) { 121 | 122 | this.format = _format; 123 | } 124 | 125 | public boolean isSecure() { 126 | return secure; 127 | } 128 | 129 | public void setSecure(boolean secure) { 130 | this.secure = secure; 131 | } 132 | 133 | public String getContentType() { 134 | return contentType; 135 | } 136 | 137 | public void setContentType(String contentType) { 138 | this.contentType = contentType; 139 | } 140 | 141 | public String getQueryString() { 142 | return querystring; 143 | } 144 | 145 | public void setQueryString(String queryString) { 146 | this.querystring = queryString; 147 | } 148 | 149 | /** 150 | * Get the request base (ex: http://localhost:9000 151 | * 152 | * @return the request base of the url (protocol, host and port) 153 | */ 154 | public String getBase() { 155 | if (port == 80 || port == 443) { 156 | return String.format("%s://%s", secure ? "https" : "http", domain).intern(); 157 | } 158 | return String.format("%s://%s:%s", secure ? "https" : "http", domain, 159 | port).intern(); 160 | } 161 | 162 | public static HTTPRequestAdapter parseRequest( 163 | HttpServletRequest httpServletRequest) { 164 | HTTPRequestAdapter request = new HTTPRequestAdapter(); 165 | 166 | request.method = httpServletRequest.getMethod().intern(); 167 | request.path = httpServletRequest.getPathInfo() != null ? httpServletRequest.getPathInfo() : httpServletRequest.getServletPath() ; 168 | request.servletPath = httpServletRequest.getServletPath() != null ? httpServletRequest.getServletPath() : ""; 169 | request.contextPath = httpServletRequest.getContextPath() != null ? httpServletRequest.getContextPath() : ""; 170 | request.setQueryString(httpServletRequest.getQueryString() == null ? "" 171 | : httpServletRequest.getQueryString()); 172 | 173 | logger.trace("contextPath: " 174 | + request.contextPath," servletPath: " + request.servletPath); 175 | logger.trace("request.path: " + request.path 176 | + ", request.querystring: " + request.getQueryString()); 177 | 178 | if (httpServletRequest.getHeader("Content-Type") != null) { 179 | request.contentType = httpServletRequest.getHeader("Content-Type").split(";")[0].trim().toLowerCase().intern(); 180 | } else { 181 | request.contentType = "text/html".intern(); 182 | } 183 | 184 | if (httpServletRequest.getHeader("X-HTTP-Method-Override") != null) { 185 | request.method = httpServletRequest.getHeader( 186 | "X-HTTP-Method-Override").intern(); 187 | } 188 | 189 | request.setSecure(httpServletRequest.isSecure()); 190 | 191 | request.url = httpServletRequest.getRequestURI(); 192 | request.host = httpServletRequest.getHeader("host"); 193 | if (request.host != null && request.host.contains(":")) { 194 | request.port = Integer.parseInt(request.host.split(":")[1]); 195 | request.domain = request.host.split(":")[0]; 196 | } else { 197 | request.port = 80; 198 | request.domain = request.host; 199 | } 200 | 201 | request.remoteAddress = httpServletRequest.getRemoteAddr(); 202 | 203 | Enumeration headersNames = httpServletRequest.getHeaderNames(); 204 | while (headersNames.hasMoreElements()) { 205 | HTTPRequestAdapter.Header hd = request.new Header(); 206 | hd.name = (String) headersNames.nextElement(); 207 | hd.values = new ArrayList(); 208 | Enumeration enumValues = httpServletRequest.getHeaders(hd.name); 209 | while (enumValues.hasMoreElements()) { 210 | String value = (String) enumValues.nextElement(); 211 | hd.values.add(value); 212 | } 213 | request.headers.put(hd.name.toLowerCase(), hd); 214 | } 215 | 216 | request.resolveFormat(); 217 | 218 | return request; 219 | } 220 | 221 | public static HTTPRequestAdapter getCurrent() { 222 | RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); 223 | Assert.notNull(requestAttributes, "Could not find current request via RequestContextHolder"); 224 | HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); 225 | Assert.state(servletRequest != null, "Could not find current HttpServletRequest"); 226 | return HTTPRequestAdapter.parseRequest(servletRequest); 227 | } 228 | 229 | /** 230 | * Automatically resolve request format from the Accept header (in this 231 | * order : html > xml > json > text) 232 | */ 233 | public void resolveFormat() { 234 | 235 | if (format != null) { 236 | return; 237 | } 238 | 239 | if (headers.get("accept") == null) { 240 | format = "html".intern(); 241 | return; 242 | } 243 | 244 | String accept = headers.get("accept").value(); 245 | 246 | if (accept.contains("application/xhtml") 247 | || accept.contains("text/html") 248 | || accept.startsWith("*/*")) { 249 | format = "html".intern(); 250 | return; 251 | } 252 | 253 | if (accept.contains("application/xml") 254 | || accept.contains("text/xml")) { 255 | format = "xml".intern(); 256 | return; 257 | } 258 | 259 | if (accept.contains("text/plain")) { 260 | format = "txt".intern(); 261 | return; 262 | } 263 | 264 | if (accept.contains("application/json") 265 | || accept.contains("text/javascript")) { 266 | format = "json".intern(); 267 | return; 268 | } 269 | 270 | if (accept.endsWith("*/*")) { 271 | format = "html".intern(); 272 | } 273 | } 274 | 275 | public class Header { 276 | 277 | public String name; 278 | public List values; 279 | 280 | public Header() { 281 | } 282 | 283 | /** 284 | * First value 285 | * 286 | * @return The first value 287 | */ 288 | public String value() { 289 | return values.get(0); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/Router.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router; 2 | 3 | import java.io.IOException; 4 | import java.io.UnsupportedEncodingException; 5 | import java.net.URLEncoder; 6 | import java.util.*; 7 | 8 | import jregex.Matcher; 9 | import jregex.Pattern; 10 | import jregex.REFlags; 11 | 12 | import org.apache.commons.io.FileUtils; 13 | import org.apache.commons.io.IOUtils; 14 | import org.resthub.web.springmvc.router.exceptions.NoHandlerFoundException; 15 | import org.resthub.web.springmvc.router.exceptions.NoRouteFoundException; 16 | import org.resthub.web.springmvc.router.exceptions.RouteFileParsingException; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.core.io.Resource; 20 | 21 | /** 22 | *

The router matches HTTP requests to action invocations. 23 | *

Courtesy of Play! Framework Router 24 | * 25 | * @author Play! Framework developers 26 | * @author Brian Clozel 27 | * @see org.resthub.web.springmvc.router.RouterHandlerMapping 28 | */ 29 | public class Router { 30 | 31 | static Pattern routePattern = new Pattern("^({method}GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|\\*)[(]?({headers}[^)]*)(\\))?\\s+({path}.*/[^\\s]*)\\s+({action}[^\\s(]+)({params}.+)?(\\s*)$"); 32 | /** 33 | * Pattern used to locate a method override instruction 34 | */ 35 | static Pattern methodOverride = new Pattern("^.*x-http-method-override=({method}GET|PUT|POST|DELETE|PATCH).*$"); 36 | 37 | /** 38 | * Timestamp the routes file was last loaded at. 39 | */ 40 | public static long lastLoading = -1; 41 | private static final Logger logger = LoggerFactory.getLogger(Router.class); 42 | 43 | public static void clear() { 44 | routes.clear(); 45 | } 46 | 47 | /** 48 | * Parse the routes file. This is called at startup. 49 | * 50 | */ 51 | public static void load(List fileResources) throws IOException { 52 | routes.clear(); 53 | for (Resource res : fileResources) { 54 | parse(res); 55 | } 56 | 57 | lastLoading = System.currentTimeMillis(); 58 | } 59 | 60 | /** 61 | * This one can be called to add new route. Last added is first in the route 62 | * list. 63 | */ 64 | public static void prependRoute(String method, String path, String action, String headers) { 65 | prependRoute(method, path, action, null, headers); 66 | } 67 | 68 | /** 69 | * This one can be called to add new route. Last added is first in the route 70 | * list. 71 | */ 72 | public static void prependRoute(String method, String path, String action) { 73 | prependRoute(method, path, action, null, null); 74 | } 75 | 76 | /** 77 | * Add a route at the given position 78 | */ 79 | public static void addRoute(int position, String method, String path, String action, String params, String headers) { 80 | if (position > routes.size()) { 81 | position = routes.size(); 82 | } 83 | routes.add(position, getRoute(method, path, action, params, headers)); 84 | } 85 | 86 | /** 87 | * Add a route at the given position 88 | */ 89 | public static void addRoute(int position, String method, String path, String headers) { 90 | addRoute(position, method, path, null, null, headers); 91 | } 92 | 93 | /** 94 | * Add a route at the given position 95 | */ 96 | public static void addRoute(int position, String method, String path, String action, String headers) { 97 | addRoute(position, method, path, action, null, headers); 98 | } 99 | 100 | /** 101 | * Add a new route. Will be first in the route list 102 | */ 103 | public static void addRoute(String method, String path, String action) { 104 | prependRoute(method, path, action); 105 | } 106 | 107 | /** 108 | * Add a route at the given position 109 | */ 110 | public static void addRoute(String method, String path, String action, String headers) { 111 | addRoute(method, path, action, null, headers); 112 | } 113 | 114 | /** 115 | * Add a route 116 | */ 117 | public static void addRoute(String method, String path, String action, String params, String headers) { 118 | appendRoute(method, path, action, params, headers, null, 0); 119 | } 120 | 121 | /** 122 | * This is used internally when reading the route file. The order the routes 123 | * are added matters and we want the method to append the routes to the 124 | * list. 125 | */ 126 | public static void appendRoute(String method, String path, String action, String params, String headers, String sourceFile, int line) { 127 | routes.add(getRoute(method, path, action, params, headers, sourceFile, line)); 128 | } 129 | 130 | public static Route getRoute(String method, String path, String action, String params, String headers) { 131 | return getRoute(method, path, action, params, headers, null, 0); 132 | } 133 | 134 | public static Route getRoute(String method, String path, String action, String params, String headers, String sourceFile, int line) { 135 | Route route = new Route(); 136 | route.method = method; 137 | route.path = path.replace("//", "/"); 138 | route.action = action; 139 | route.routesFile = sourceFile; 140 | route.routesFileLine = line; 141 | route.addFormat(headers); 142 | route.addParams(params); 143 | route.compute(); 144 | if (logger.isDebugEnabled()) { 145 | logger.debug("Adding [" + route.toString() + "] with params [" + params + "] and headers [" + headers + "]"); 146 | } 147 | 148 | return route; 149 | } 150 | 151 | /** 152 | * Add a new route at the beginning of the route list 153 | */ 154 | public static void prependRoute(String method, String path, String action, String params, String headers) { 155 | routes.add(0, getRoute(method, path, action, params, headers)); 156 | } 157 | 158 | /** 159 | * Parse a route file. 160 | * 161 | * @param fileResource 162 | * @throws IOException 163 | */ 164 | static void parse(Resource fileResource) throws IOException { 165 | 166 | String fileAbsolutePath = fileResource.getFile().getAbsolutePath(); 167 | String content = IOUtils.toString(fileResource.getInputStream()); 168 | 169 | parse(content, fileAbsolutePath); 170 | } 171 | 172 | static void parse(String content, String fileAbsolutePath) throws IOException { 173 | int lineNumber = 0; 174 | for (String line : content.split("\n")) { 175 | lineNumber++; 176 | line = line.trim().replaceAll("\\s+", " "); 177 | if (line.length() == 0 || line.startsWith("#")) { 178 | continue; 179 | } 180 | Matcher matcher = routePattern.matcher(line); 181 | if (matcher.matches()) { 182 | 183 | String action = matcher.group("action"); 184 | String method = matcher.group("method"); 185 | String path = matcher.group("path"); 186 | String params = matcher.group("params"); 187 | String headers = matcher.group("headers"); 188 | appendRoute(method, path, action, params, headers, fileAbsolutePath, lineNumber); 189 | } else { 190 | logger.error("Invalid route definition : " + line); 191 | } 192 | } 193 | } 194 | 195 | public static void detectChanges(List fileResources) throws IOException { 196 | 197 | boolean hasChanged = false; 198 | 199 | for (Resource res : fileResources) { 200 | if (FileUtils.isFileNewer(res.getFile(), lastLoading)) { 201 | hasChanged = true; 202 | break; 203 | } 204 | } 205 | 206 | if (hasChanged) { 207 | load(fileResources); 208 | } 209 | } 210 | 211 | public static List routes = new ArrayList(500); 212 | 213 | public static Route route(HTTPRequestAdapter request) { 214 | if (logger.isTraceEnabled()) { 215 | logger.trace("Route: " + request.path + " - " + request.querystring); 216 | } 217 | // request method may be overriden if a x-http-method-override parameter is given 218 | if (request.querystring != null && methodOverride.matches(request.querystring)) { 219 | Matcher matcher = methodOverride.matcher(request.querystring); 220 | if (matcher.matches()) { 221 | if (logger.isTraceEnabled()) { 222 | logger.trace("request method %s overriden to %s ", request.method, matcher.group("method")); 223 | } 224 | request.method = matcher.group("method"); 225 | } 226 | } 227 | 228 | for (Route route : routes) { 229 | String format = request.format; 230 | String host = request.host; 231 | Map args = route.matches(request.method, request.path, format, host); 232 | 233 | if (args != null) { 234 | request.routeArgs = args; 235 | request.action = route.action; 236 | if (args.containsKey("format")) { 237 | request.setFormat(args.get("format")); 238 | } 239 | if (request.action.indexOf("{") > -1) { // more optimization ? 240 | for (String arg : request.routeArgs.keySet()) { 241 | request.action = request.action.replace("{" + arg + "}", request.routeArgs.get(arg)); 242 | } 243 | } 244 | return route; 245 | } 246 | } 247 | // Not found - if the request was a HEAD, let's see if we can find a corresponding GET 248 | if (request.method.equalsIgnoreCase("head")) { 249 | request.method = "GET"; 250 | Route route = route(request); 251 | request.method = "HEAD"; 252 | if (route != null) { 253 | return route; 254 | } 255 | } 256 | throw new NoRouteFoundException(request.method, request.path); 257 | } 258 | 259 | public static Map route(String method, String path) { 260 | return route(method, path, null, null); 261 | } 262 | 263 | public static Map route(String method, String path, String headers) { 264 | return route(method, path, headers, null); 265 | } 266 | 267 | public static Map route(String method, String path, String headers, String host) { 268 | for (Route route : routes) { 269 | Map args = route.matches(method, path, headers, host); 270 | if (args != null) { 271 | args.put("action", route.action); 272 | return args; 273 | } 274 | } 275 | return new HashMap(16); 276 | } 277 | 278 | public static ActionDefinition reverse(String action) { 279 | // Note the map is not Collections.EMPTY_MAP because it will be copied and changed. 280 | return reverse(action, new HashMap(16)); 281 | } 282 | 283 | public static String getFullUrl(String action, Map args) { 284 | return HTTPRequestAdapter.getCurrent().getBase() + reverse(action, args); 285 | } 286 | 287 | public static String getFullUrl(String action) { 288 | // Note the map is not Collections.EMPTY_MAP because it will be copied and changed. 289 | return getFullUrl(action, new HashMap(16)); 290 | } 291 | 292 | public static Collection resolveActions(String action) { 293 | 294 | List candidateRoutes = new ArrayList(3); 295 | 296 | for (Route route : routes) { 297 | if (route.actionPattern != null) { 298 | Matcher matcher = route.actionPattern.matcher(action); 299 | if (matcher.matches()) { 300 | candidateRoutes.add(route); 301 | } 302 | } 303 | } 304 | 305 | return candidateRoutes; 306 | } 307 | 308 | public static ActionDefinition reverse(String action, Map args) { 309 | 310 | HTTPRequestAdapter currentRequest = HTTPRequestAdapter.getCurrent(); 311 | 312 | Map argsbackup = new HashMap(args); 313 | for (Route route : routes) { 314 | if (route.actionPattern != null) { 315 | Matcher matcher = route.actionPattern.matcher(action); 316 | if (matcher.matches()) { 317 | for (String group : route.actionArgs) { 318 | String v = matcher.group(group); 319 | if (v == null) { 320 | continue; 321 | } 322 | args.put(group, v.toLowerCase()); 323 | } 324 | List inPathArgs = new ArrayList(16); 325 | boolean allRequiredArgsAreHere = true; 326 | // les noms de parametres matchent ils ? 327 | for (Route.Arg arg : route.args) { 328 | inPathArgs.add(arg.name); 329 | Object value = args.get(arg.name); 330 | if (value == null) { 331 | // This is a hack for reverting on hostname that are a regex expression. 332 | // See [#344] for more into. This is not optimal and should retough. However, 333 | // it allows us to do things like {(.*)}.domain.com 334 | String host = route.host.replaceAll("\\{", "").replaceAll("\\}", ""); 335 | if (host.equals(arg.name) || host.matches(arg.name)) { 336 | args.put(arg.name, ""); 337 | value = ""; 338 | } else { 339 | allRequiredArgsAreHere = false; 340 | break; 341 | } 342 | } else { 343 | if (value instanceof List) { 344 | @SuppressWarnings("unchecked") 345 | List l = (List) value; 346 | value = l.get(0); 347 | } 348 | if (!value.toString().startsWith(":") && !arg.constraint.matches(value.toString())) { 349 | allRequiredArgsAreHere = false; 350 | break; 351 | } 352 | } 353 | } 354 | // les parametres codes en dur dans la route matchent-ils ? 355 | for (String staticKey : route.staticArgs.keySet()) { 356 | if (staticKey.equals("format")) { 357 | if (!currentRequest.format.equals(route.staticArgs.get("format"))) { 358 | allRequiredArgsAreHere = false; 359 | break; 360 | } 361 | continue; // format is a special key 362 | } 363 | if (!args.containsKey(staticKey) || (args.get(staticKey) == null) 364 | || !args.get(staticKey).toString().equals(route.staticArgs.get(staticKey))) { 365 | allRequiredArgsAreHere = false; 366 | break; 367 | } 368 | } 369 | if (allRequiredArgsAreHere) { 370 | StringBuilder queryString = new StringBuilder(); 371 | String path = route.path; 372 | //add contextPath and servletPath if set in the current request 373 | if( currentRequest != null) { 374 | 375 | if(!currentRequest.servletPath.isEmpty() && !currentRequest.servletPath.equals("/")) { 376 | String servletPath = currentRequest.servletPath; 377 | path = (servletPath.startsWith("/") ? servletPath : "/" + servletPath) + path; 378 | } 379 | if(!currentRequest.contextPath.isEmpty() && !currentRequest.contextPath.equals("/")) { 380 | String contextPath = currentRequest.contextPath; 381 | path = (contextPath.startsWith("/") ? contextPath : "/" + contextPath) + path; 382 | } 383 | } 384 | String host = route.host; 385 | if (path.endsWith("/?")) { 386 | path = path.substring(0, path.length() - 2); 387 | } 388 | for (Map.Entry entry : args.entrySet()) { 389 | String key = entry.getKey(); 390 | Object value = entry.getValue(); 391 | if (inPathArgs.contains(key) && value != null) { 392 | if (List.class.isAssignableFrom(value.getClass())) { 393 | @SuppressWarnings("unchecked") 394 | List vals = (List) value; 395 | try { 396 | path = path.replaceAll("\\{(<[^>]+>)?" + key + "\\}", URLEncoder.encode(vals.get(0).toString().replace("$", "\\$"), "utf-8")); 397 | } catch (UnsupportedEncodingException e) { 398 | throw new RouteFileParsingException("RouteFile encoding exception", e); 399 | } 400 | } else { 401 | try { 402 | path = path.replaceAll("\\{(<[^>]+>)?" + key + "\\}", URLEncoder.encode(value.toString().replace("$", "\\$"), "utf-8")); 403 | host = host.replaceAll("\\{(<[^>]+>)?" + key + "\\}", URLEncoder.encode(value.toString().replace("$", "\\$"), "utf-8")); 404 | } catch (UnsupportedEncodingException e) { 405 | throw new RouteFileParsingException("RouteFile encoding exception", e); 406 | } 407 | } 408 | } else if (route.staticArgs.containsKey(key)) { 409 | // Do nothing -> The key is static 410 | } else if (value != null) { 411 | if (List.class.isAssignableFrom(value.getClass())) { 412 | @SuppressWarnings("unchecked") 413 | List vals = (List) value; 414 | for (Object object : vals) { 415 | try { 416 | queryString.append(URLEncoder.encode(key, "utf-8")); 417 | queryString.append("="); 418 | if (object.toString().startsWith(":")) { 419 | queryString.append(object.toString()); 420 | } else { 421 | queryString.append(URLEncoder.encode(object.toString() + "", "utf-8")); 422 | } 423 | queryString.append("&"); 424 | } catch (UnsupportedEncodingException ex) { 425 | } 426 | } 427 | // } else if (value.getClass().equals(Default.class)) { 428 | // // Skip defaults in queryString 429 | } else { 430 | try { 431 | queryString.append(URLEncoder.encode(key, "utf-8")); 432 | queryString.append("="); 433 | if (value.toString().startsWith(":")) { 434 | queryString.append(value.toString()); 435 | } else { 436 | queryString.append(URLEncoder.encode(value.toString() + "", "utf-8")); 437 | } 438 | queryString.append("&"); 439 | } catch (UnsupportedEncodingException ex) { 440 | } 441 | } 442 | } 443 | } 444 | String qs = queryString.toString(); 445 | if (qs.endsWith("&")) { 446 | qs = qs.substring(0, qs.length() - 1); 447 | } 448 | ActionDefinition actionDefinition = new ActionDefinition(); 449 | actionDefinition.url = qs.length() == 0 ? path : path + "?" + qs; 450 | actionDefinition.method = route.method == null || route.method.equals("*") ? "GET" : route.method.toUpperCase(); 451 | actionDefinition.star = "*".equals(route.method); 452 | actionDefinition.action = action; 453 | actionDefinition.args = argsbackup; 454 | actionDefinition.host = host; 455 | return actionDefinition; 456 | } 457 | } 458 | } 459 | } 460 | throw new NoHandlerFoundException(action, args); 461 | } 462 | 463 | private static void addToQuerystring(StringBuilder queryString, String key, Object value) { 464 | if (List.class.isAssignableFrom(value.getClass())) { 465 | @SuppressWarnings("unchecked") 466 | List vals = (List) value; 467 | for (Object object : vals) { 468 | try { 469 | queryString.append(URLEncoder.encode(key, "utf-8")); 470 | queryString.append("="); 471 | if (object.toString().startsWith(":")) { 472 | queryString.append(object.toString()); 473 | } else { 474 | queryString.append(URLEncoder.encode(object.toString() + "", "utf-8")); 475 | } 476 | queryString.append("&"); 477 | } catch (UnsupportedEncodingException ex) { 478 | } 479 | } 480 | // } else if (value.getClass().equals(Default.class)) { 481 | // // Skip defaults in queryString 482 | } else { 483 | try { 484 | queryString.append(URLEncoder.encode(key, "utf-8")); 485 | queryString.append("="); 486 | if (value.toString().startsWith(":")) { 487 | queryString.append(value.toString()); 488 | } else { 489 | queryString.append(URLEncoder.encode(value.toString() + "", "utf-8")); 490 | } 491 | queryString.append("&"); 492 | } catch (UnsupportedEncodingException ex) { 493 | } 494 | } 495 | } 496 | 497 | public static class ActionDefinition { 498 | 499 | /** 500 | * The domain/host name. 501 | */ 502 | public String host; 503 | /** 504 | * The HTTP method, e.g. "GET". 505 | */ 506 | public String method; 507 | /** 508 | * @todo - what is this? does it include the domain? 509 | */ 510 | public String url; 511 | /** 512 | * Whether the route contains an astericks *. 513 | */ 514 | public boolean star; 515 | /** 516 | * @todo - what is this? does it include the class and package? 517 | */ 518 | public String action; 519 | /** 520 | * @todo - are these the required args in the routing file, or the query 521 | * string in a request? 522 | */ 523 | public Map args; 524 | 525 | public ActionDefinition add(String key, Object value) { 526 | args.put(key, value); 527 | return reverse(action, args); 528 | } 529 | 530 | public ActionDefinition remove(String key) { 531 | args.remove(key); 532 | return reverse(action, args); 533 | } 534 | 535 | public ActionDefinition addRef(String fragment) { 536 | url += "#" + fragment; 537 | return this; 538 | } 539 | 540 | @Override 541 | public String toString() { 542 | return url; 543 | } 544 | 545 | public void absolute() { 546 | HTTPRequestAdapter currentRequest = HTTPRequestAdapter.getCurrent(); 547 | if (!url.startsWith("http")) { 548 | if (host == null || host.isEmpty()) { 549 | url = currentRequest.getBase() + url; 550 | } else { 551 | url = (currentRequest.secure ? "https://" : "http://") + host + url; 552 | } 553 | } 554 | } 555 | 556 | public ActionDefinition secure() { 557 | if (!url.contains("http://") && !url.contains("https://")) { 558 | absolute(); 559 | } 560 | url = url.replace("http:", "https:"); 561 | return this; 562 | } 563 | } 564 | 565 | public static class Route { 566 | 567 | public String getAction() { 568 | return action; 569 | } 570 | 571 | public String getHost() { 572 | return host; 573 | } 574 | 575 | public String getMethod() { 576 | return method; 577 | } 578 | 579 | public String getPath() { 580 | return path; 581 | } 582 | 583 | public List getArgs() { 584 | return args; 585 | } 586 | 587 | public Map getStaticArgs() { 588 | return staticArgs; 589 | } 590 | 591 | 592 | 593 | /** 594 | * HTTP method, e.g. "GET". 595 | */ 596 | public String method; 597 | public String path; 598 | public String action; 599 | Pattern actionPattern; 600 | List actionArgs = new ArrayList(3); 601 | Pattern pattern; 602 | Pattern hostPattern; 603 | List args = new ArrayList(3); 604 | Map staticArgs = new HashMap(3); 605 | List formats = new ArrayList(1); 606 | String host; 607 | Arg hostArg = null; 608 | public int routesFileLine; 609 | public String routesFile; 610 | static Pattern customRegexPattern = new Pattern("\\{([a-zA-Z_0-9]+)\\}"); 611 | static Pattern argsPattern = new Pattern("\\{<([^>]+)>([a-zA-Z_0-9]+)\\}"); 612 | static Pattern paramPattern = new Pattern("\\s*([a-zA-Z_0-9]+)\\s*:\\s*'(.*)'\\s*"); 613 | 614 | public void compute() { 615 | this.host = ""; 616 | this.hostPattern = new Pattern(".*"); 617 | 618 | 619 | // URL pattern 620 | // Is there is a host argument, append it. 621 | if (!path.startsWith("/")) { 622 | String p = this.path; 623 | this.path = p.substring(p.indexOf("/")); 624 | this.host = p.substring(0, p.indexOf("/")); 625 | String pattern = host.replaceAll("\\.", "\\\\.").replaceAll("\\{.*\\}", "(.*)"); 626 | 627 | if (logger.isTraceEnabled()) { 628 | logger.trace("pattern [" + pattern + "]"); 629 | logger.trace("host [" + host + "]"); 630 | } 631 | 632 | Matcher m = new Pattern(pattern).matcher(host); 633 | this.hostPattern = new Pattern(pattern); 634 | 635 | if (m.matches()) { 636 | if (this.host.contains("{")) { 637 | String name = m.group(1).replace("{", "").replace("}", ""); 638 | hostArg = new Arg(); 639 | hostArg.name = name; 640 | if (logger.isTraceEnabled()) { 641 | logger.trace("hostArg name [" + name + "]"); 642 | } 643 | // The default value contains the route version of the host ie {client}.bla.com 644 | // It is temporary and it indicates it is an url route. 645 | // TODO Check that default value is actually used for other cases. 646 | hostArg.defaultValue = host; 647 | hostArg.constraint = new Pattern(".*"); 648 | 649 | if (logger.isTraceEnabled()) { 650 | logger.trace("adding hostArg [" + hostArg + "]"); 651 | } 652 | 653 | args.add(hostArg); 654 | } 655 | } 656 | } 657 | String patternString = path; 658 | patternString = customRegexPattern.replacer("\\{<[^/]+>$1\\}").replace(patternString); 659 | Matcher matcher = argsPattern.matcher(patternString); 660 | while (matcher.find()) { 661 | Arg arg = new Arg(); 662 | arg.name = matcher.group(2); 663 | arg.constraint = new Pattern(matcher.group(1)); 664 | args.add(arg); 665 | } 666 | 667 | patternString = argsPattern.replacer("({$2}$1)").replace(patternString); 668 | this.pattern = new Pattern(patternString); 669 | // Action pattern 670 | patternString = action; 671 | patternString = patternString.replace(".", "[.]"); 672 | for (Arg arg : args) { 673 | if (patternString.contains("{" + arg.name + "}")) { 674 | patternString = patternString.replace("{" + arg.name + "}", "({" + arg.name + "}" + arg.constraint.toString() + ")"); 675 | actionArgs.add(arg.name); 676 | } 677 | } 678 | actionPattern = new Pattern(patternString, REFlags.IGNORE_CASE); 679 | } 680 | 681 | public void addParams(String params) { 682 | if (params == null || params.length() < 1) { 683 | return; 684 | } 685 | params = params.substring(1, params.length() - 1); 686 | for (String param : params.split(",")) { 687 | Matcher matcher = paramPattern.matcher(param); 688 | if (matcher.matches()) { 689 | staticArgs.put(matcher.group(1), matcher.group(2)); 690 | } else { 691 | logger.warn("Ignoring " + param + " (static params must be specified as key:'value',...)"); 692 | } 693 | } 694 | } 695 | 696 | // TODO: Add args names 697 | public void addFormat(String params) { 698 | if (params == null || params.length() < 1) { 699 | return; 700 | } 701 | params = params.trim(); 702 | formats.addAll(Arrays.asList(params.split(","))); 703 | } 704 | 705 | private boolean contains(String accept) { 706 | boolean contains = (accept == null); 707 | if (accept != null) { 708 | if (this.formats.isEmpty()) { 709 | return true; 710 | } 711 | for (String format : this.formats) { 712 | contains = format.startsWith(accept); 713 | if (contains) { 714 | break; 715 | } 716 | } 717 | } 718 | return contains; 719 | } 720 | 721 | public Map matches(String method, String path) { 722 | return matches(method, path, null, null); 723 | } 724 | 725 | public Map matches(String method, String path, String accept) { 726 | return matches(method, path, accept, null); 727 | } 728 | 729 | /** 730 | * Check if the parts of a HTTP request equal this Route. 731 | * 732 | * @param method GET/POST/etc. 733 | * @param path Part after domain and before query-string. Starts with a 734 | * "/". 735 | * @param accept Format, e.g. html. 736 | * @param domain the domain. 737 | * @return ??? 738 | */ 739 | public Map matches(String method, String path, String accept, String domain) { 740 | // If method is HEAD and we have a GET 741 | if (method == null || this.method.equals("*") || method.equalsIgnoreCase(this.method) || (method.equalsIgnoreCase("head") && ("get").equalsIgnoreCase(this.method))) { 742 | 743 | Matcher matcher = pattern.matcher(path); 744 | 745 | boolean hostMatches = (domain == null); 746 | if (domain != null) { 747 | Matcher hostMatcher = hostPattern.matcher(domain); 748 | hostMatches = hostMatcher.matches(); 749 | } 750 | // Extract the host variable 751 | if (matcher.matches() && contains(accept) && hostMatches) { 752 | 753 | Map localArgs = new HashMap(); 754 | for (Arg arg : args) { 755 | // FIXME: Careful with the arguments that are not matching as they are part of the hostname 756 | // Defaultvalue indicates it is a one of these urls. This is a trick and should be changed. 757 | if (arg.defaultValue == null) { 758 | localArgs.put(arg.name, matcher.group(arg.name)); 759 | } 760 | } 761 | if (hostArg != null && domain != null) { 762 | // Parse the hostname and get only the part we are interested in 763 | String routeValue = hostArg.defaultValue.replaceAll("\\{.*}", ""); 764 | domain = domain.replace(routeValue, ""); 765 | localArgs.put(hostArg.name, domain); 766 | } 767 | localArgs.putAll(staticArgs); 768 | return localArgs; 769 | } 770 | } 771 | return null; 772 | } 773 | 774 | public static class Arg { 775 | 776 | String name; 777 | Pattern constraint; 778 | String defaultValue; 779 | Boolean optional = false; 780 | 781 | public String getName() { 782 | return name; 783 | } 784 | 785 | public String getDefaultValue() { 786 | return defaultValue; 787 | } 788 | } 789 | 790 | @Override 791 | public String toString() { 792 | return method + " " + path + " -> " + action; 793 | } 794 | } 795 | 796 | } 797 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/RouterConfigurationSupport.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 6 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; 7 | 8 | import java.util.List; 9 | 10 | 11 | /** 12 | * This class provides MVC Java config support for the {@link RouterHandlerMapping}, 13 | * on top of the existing {@link WebMvcConfigurationSupport}. 14 | * 15 | * Unlike {@link WebMvcConfigurationSupport}, you SHOULD NOT import it using 16 | * {@link org.springframework.web.servlet.config.annotation.EnableWebMvc @EnableWebMvc} within an application 17 | * {@link org.springframework.context.annotation.Configuration @Configuration} class. 18 | * 19 | * Extending this class and adding {@link org.springframework.context.annotation.Configuration @Configuration} 20 | * is enough. You should also implement the configureRouteFiles method to add the list of route 21 | * configuration files. 22 | * 23 | * You can then instantiate your own beans and override {@link WebMvcConfigurationSupport} methods. 24 | * 25 | *

This class registers the following {@link org.springframework.web.servlet.HandlerMapping}s:

26 | *
    27 | *
  • {@link RouterHandlerMapping} 28 | * ordered at 0 for mapping requests to annotated controller methods. 29 | *
  • {@link org.springframework.web.servlet.HandlerMapping} 30 | * ordered at 1 to map URL paths directly to view names. 31 | *
  • {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping} 32 | * ordered at 2 to map URL paths to controller bean names. 33 | *
  • {@link RequestMappingHandlerMapping} 34 | * ordered at 3 for mapping requests to annotated controller methods. 35 | *
  • {@link org.springframework.web.servlet.HandlerMapping} 36 | * ordered at {@code Integer.MAX_VALUE-1} to serve static resource requests. 37 | *
  • {@link org.springframework.web.servlet.HandlerMapping} 38 | * ordered at {@code Integer.MAX_VALUE} to forward requests to the default servlet. 39 | *
40 | * 41 | * @see WebMvcConfigurationSupport 42 | * 43 | * @author Brian Clozel 44 | */ 45 | public abstract class RouterConfigurationSupport extends DelegatingWebMvcConfiguration { 46 | 47 | /** 48 | * Return a {@link RouterHandlerMapping} ordered at 0 for mapping 49 | * requests to controllers' actions mapped by routes. 50 | */ 51 | @Bean 52 | public RouterHandlerMapping createRouterHandlerMapping() { 53 | 54 | RouterHandlerMapping handlerMapping = new RouterHandlerMapping(); 55 | handlerMapping.setRouteFiles(listRouteFiles()); 56 | handlerMapping.setAutoReloadEnabled(isHandlerMappingReloadEnabled()); 57 | handlerMapping.setInterceptors(getInterceptors()); 58 | handlerMapping.setOrder(0); 59 | return handlerMapping; 60 | } 61 | 62 | /** 63 | * By default, route configuration files auto-reload is not enabled. 64 | * You can override this method to enable this feature. 65 | * @see RouterHandlerMapping 66 | */ 67 | protected boolean isHandlerMappingReloadEnabled() { 68 | return false; 69 | } 70 | 71 | /** 72 | * Return a {@link RequestMappingHandlerMapping} ordered at 3 for mapping 73 | * requests to annotated controllers. 74 | */ 75 | @Bean 76 | @Override 77 | public RequestMappingHandlerMapping requestMappingHandlerMapping() { 78 | RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); 79 | handlerMapping.setOrder(3); 80 | handlerMapping.setInterceptors(getInterceptors()); 81 | handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); 82 | return handlerMapping; 83 | } 84 | 85 | /** 86 | * Return the ordered list of route configuration files to be loaded 87 | * by the Router at startup. 88 | */ 89 | public abstract List listRouteFiles(); 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/RouterHandlerMapping.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router; 2 | 3 | import org.resthub.web.springmvc.router.exceptions.NoRouteFoundException; 4 | import org.resthub.web.springmvc.router.exceptions.RouteFileParsingException; 5 | import org.resthub.web.springmvc.router.support.RouterHandlerResolver; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.BeansException; 9 | import org.springframework.core.io.Resource; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.util.Assert; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.method.HandlerMethod; 14 | import org.springframework.web.servlet.HandlerMapping; 15 | import org.springframework.web.servlet.handler.AbstractHandlerMapping; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | import java.io.IOException; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | 23 | /** 24 | * Implementation of the {@link org.springframework.web.servlet.HandlerMapping} 25 | * interface that maps handlers based on HTTP routes defined in a route 26 | * configuration file. 27 | * 28 | *

RouterHandlerMapping is not the default HandlerMapping registered in 29 | * {@link org.springframework.web.servlet.DispatcherServlet} in SpringMVC. You 30 | * need to declare and configure it in your DispatcherServlet context, by adding 31 | * a RouterHandlerMapping bean explicitly. RouterHandlerMapping needs the name 32 | * of the route configuration file (available in the application classpath); it 33 | * also allows for registering custom interceptors: 34 | * 35 | *

 <bean
 36 |  * class="org.resthub.web.springmvc.router.RouterHandlerMapping">
 37 |  * <property name="routeFiles"> 
 38 |  * <list>
 39 |  *   <value>bindingroutes.conf</value>
 40 |  *   <value>addroutes.conf</value>
 41 |  * </list>
 42 |  * </property> <property
 43 |  * name="interceptors" > ... </property> </bean>
 44 |  * 
45 | * 46 | *

Annotated controllers should be marked with the {@link Controller} 47 | * stereotype at the type level. This is not strictly necessary because the 48 | * methodeInvoker will try to map the Controller.invoker anyway using the 49 | * current ApplicationContext. The {@link RequestMapping} is not taken into 50 | * account here. 51 | * 52 | *

RouterHandlerMapping loads routes configuration from a file for route 53 | * configuration syntax (the Router implementation is adapted from Play! 54 | * Framework {@link http://www.playframework.org/documentation/1.0.3/routes#syntax}). 55 | * 56 | * Example: 57 | * 58 | *

 GET /home PageController.showPage(id:'home') GET
 59 |  * /page/{id} PageController.showPage POST /customer/{<[0-9]+>customerid}
 60 |  * CustomerController.createCustomer
 61 |  * 

The {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter} is responsible for choosing and 62 | * invoking the right controller method, as mapped by this HandlerMapping. 63 | * 64 | * @author Brian Clozel 65 | * @see org.springframework.web.servlet.handler.AbstractHandlerMapping 66 | */ 67 | public class RouterHandlerMapping extends AbstractHandlerMapping { 68 | 69 | private static final Logger logger = LoggerFactory.getLogger(RouterHandlerMapping.class); 70 | private List routeFiles; 71 | private boolean autoReloadEnabled = false; 72 | private RouterHandlerResolver methodResolver; 73 | 74 | public RouterHandlerMapping() { 75 | this.methodResolver = new RouterHandlerResolver(); 76 | } 77 | 78 | /** 79 | * Routes configuration Files names Injected by bean configuration (in 80 | * servlet.xml) 81 | */ 82 | public List getRouteFiles() { 83 | return routeFiles; 84 | } 85 | 86 | public void setRouteFiles(List routeFiles) { 87 | Assert.notEmpty(routeFiles,"routes configuration files list should not be empty"); 88 | this.routeFiles = routeFiles; 89 | } 90 | 91 | /** 92 | * Route files auto-reloading 93 | * Injected by bean configuration (in servlet.xml) 94 | */ 95 | public boolean isAutoReloadEnabled() { 96 | return autoReloadEnabled; 97 | } 98 | 99 | public void setAutoReloadEnabled(boolean autoReloadEnabled) { 100 | this.autoReloadEnabled = autoReloadEnabled; 101 | } 102 | 103 | /** 104 | * Reload routes configuration at runtime. No-op if configuration files 105 | * didn't change since last reload. 106 | */ 107 | public void reloadRoutesConfiguration() { 108 | List fileResources = new ArrayList(); 109 | 110 | try { 111 | for (String fileName : this.routeFiles) { 112 | fileResources.addAll(Arrays.asList(getApplicationContext().getResources(fileName))); 113 | } 114 | 115 | Router.detectChanges(fileResources); 116 | } catch (IOException ex) { 117 | throw new RouteFileParsingException( 118 | "Could not read route configuration files", ex); 119 | } 120 | } 121 | 122 | /** 123 | * Inits Routes from route configuration file 124 | */ 125 | @Override 126 | protected void initApplicationContext() throws BeansException { 127 | 128 | super.initApplicationContext(); 129 | 130 | // Scan beans for Controllers 131 | this.methodResolver.setCachedControllers(getApplicationContext().getBeansWithAnnotation(Controller.class)); 132 | List fileResources = new ArrayList(); 133 | 134 | try { 135 | for(String fileName : this.routeFiles) { 136 | fileResources.addAll(Arrays.asList(getApplicationContext().getResources(fileName))); 137 | } 138 | Router.load(fileResources); 139 | 140 | } catch (IOException e) { 141 | throw new RouteFileParsingException( 142 | "Could not read route configuration files", e); 143 | } 144 | } 145 | 146 | /** 147 | * Resolves a HandlerMethod (of type RouterHandler) given the current HTTP 148 | * request, using the Router instance. 149 | * 150 | * @param request the HTTP Servlet request 151 | * @return a RouterHandler, containing matching route + wrapped request 152 | */ 153 | @Override 154 | protected Object getHandlerInternal(HttpServletRequest request) 155 | throws Exception { 156 | 157 | HandlerMethod handler; 158 | 159 | // reload routes files if configured in servlet-context 160 | if(this.autoReloadEnabled) { 161 | this.reloadRoutesConfiguration(); 162 | } 163 | 164 | try { 165 | // Adapt HTTPServletRequest for Router 166 | HTTPRequestAdapter rq = HTTPRequestAdapter.parseRequest(request); 167 | // Route request and resolve format 168 | Router.Route route = Router.route(rq); 169 | logger.debug("Looking up handler method for path {} ({} {} {})", route.path, route.method, route.path, route.action); 170 | handler = this.methodResolver.resolveHandler(route, rq.action, rq); 171 | // Add resolved route arguments to the request 172 | request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, rq.routeArgs); 173 | request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, route.pattern.toString()); 174 | 175 | } catch (NoRouteFoundException nrfe) { 176 | handler = null; 177 | logger.trace("no route found for method[" + nrfe.method 178 | + "] and path[" + nrfe.path + "]"); 179 | } 180 | 181 | return handler; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/exceptions/ActionNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.exceptions; 2 | 3 | 4 | public class ActionNotFoundException extends Exception { 5 | 6 | private String action; 7 | 8 | public ActionNotFoundException(String action, Throwable cause) { 9 | super(String.format("Action %s not found", action), cause); 10 | this.action = action; 11 | } 12 | 13 | public ActionNotFoundException(String action, String message) { 14 | super(String.format("Action %s not found", action)); 15 | this.action = action; 16 | } 17 | 18 | public String getAction() { 19 | return action; 20 | } 21 | 22 | public String getErrorDescription() { 23 | return String.format( 24 | "Action %s could not be found. Error raised is %s", 25 | action, 26 | getCause() instanceof ClassNotFoundException ? "ClassNotFound: "+getCause().getMessage() : getCause().getMessage() 27 | ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/exceptions/NoHandlerFoundException.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.exceptions; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Exception: No handler found (during routing) 7 | * @author Brian Clozel 8 | * @see org.resthub.web.springmvc.router.Router 9 | */ 10 | public class NoHandlerFoundException extends RuntimeException { 11 | 12 | String action; 13 | Map args; 14 | 15 | public NoHandlerFoundException(String action, Map args) { 16 | super("No handler found"); 17 | this.action = action; 18 | this.args = args; 19 | } 20 | 21 | public String getAction() { 22 | return action; 23 | } 24 | 25 | public Map getArgs() { 26 | return args; 27 | } 28 | 29 | public String toString() { 30 | 31 | return this.getMessage()+" action["+this.action+"] args["+this.args+"]"; 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/exceptions/NoRouteFoundException.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.exceptions; 2 | 3 | /** 4 | * Exception: No route found (during reverse routing) 5 | * @author Brian Clozel 6 | * @see org.resthub.web.springmvc.router.Router 7 | */ 8 | public class NoRouteFoundException extends RuntimeException { 9 | 10 | public String method; 11 | public String path; 12 | 13 | public NoRouteFoundException(String method, String path) { 14 | super("No route found"); 15 | this.method = method; 16 | this.path = path; 17 | } 18 | 19 | public String toString() { 20 | 21 | return this.getMessage() + " method[" + this.method + "] path[" + this.path + "]"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/exceptions/RouteFileParsingException.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.exceptions; 2 | 3 | import org.springframework.beans.BeansException; 4 | 5 | /** 6 | * Exception: Error while parsing route file 7 | * @author Brian Clozel 8 | * @see org.resthub.web.springmvc.router.Router 9 | */ 10 | public class RouteFileParsingException extends BeansException { 11 | 12 | public RouteFileParsingException(String msg) { 13 | super(msg); 14 | } 15 | 16 | public RouteFileParsingException(String msg, Throwable e) { 17 | super(msg, e); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/hateoas/RouterLinkBuilder.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.hateoas; 2 | 3 | import org.resthub.web.springmvc.router.Router; 4 | import org.springframework.hateoas.Identifiable; 5 | import org.springframework.hateoas.Link; 6 | import org.springframework.hateoas.LinkBuilder; 7 | import org.springframework.util.Assert; 8 | 9 | import java.net.URI; 10 | import java.net.URISyntaxException; 11 | import java.util.*; 12 | 13 | /** 14 | * SpringMVC-Router's own take on Spring-HATEOAS LinkBuilder. 15 | * The RouterLinkBuilder helps you build links to Controller+Action+Arguments 16 | * when using Spring HATEOAS. 17 | * 18 | * This class implements the LinkBuilder interface, but you should use methods with 19 | * action names and/or controller names for better performance and determinism. 20 | * 21 | * @author Brian Clozel 22 | * @see Spring HATEOAS 23 | * @see LinkBuilder 24 | */ 25 | public class RouterLinkBuilder implements LinkBuilder { 26 | 27 | private String controllerName; 28 | private String actionName; 29 | private List unresolvedArgs = new ArrayList(3); 30 | private Map resolvedArgs = new HashMap(3); 31 | 32 | public RouterLinkBuilder(String controllerAction) { 33 | 34 | Assert.hasLength(controllerAction, "Controller.action should not be empty"); 35 | String[] names = controllerAction.split("\\."); 36 | 37 | this.controllerName = names[0]; 38 | if (names.length == 2) { 39 | this.actionName = names[1]; 40 | } 41 | } 42 | 43 | public static RouterLinkBuilder linkTo(String controllerAction) { 44 | 45 | return new RouterLinkBuilder(controllerAction); 46 | } 47 | 48 | public static RouterLinkBuilder linkTo(String controllerName, String actionName) { 49 | 50 | return new RouterLinkBuilder(controllerName + "." + actionName); 51 | } 52 | 53 | public static RouterLinkBuilder linkTo(Class controller) { 54 | 55 | return new RouterLinkBuilder(controller.getSimpleName()); 56 | } 57 | 58 | public RouterLinkBuilder action(String actionName) { 59 | this.actionName = actionName; 60 | return this; 61 | } 62 | 63 | @Override 64 | public RouterLinkBuilder slash(Object object) { 65 | 66 | if (object == null) { 67 | return this; 68 | } 69 | 70 | if (object instanceof Identifiable) { 71 | return slash((Identifiable) object); 72 | } 73 | 74 | this.unresolvedArgs.add(object); 75 | return this; 76 | } 77 | 78 | public RouterLinkBuilder slash(String name, Object object) { 79 | 80 | if (object == null) { 81 | return this; 82 | } 83 | 84 | if (object instanceof Identifiable) { 85 | return slash(name, (Identifiable) object); 86 | } 87 | 88 | this.resolvedArgs.put(name, object); 89 | return this; 90 | } 91 | 92 | @Override 93 | public RouterLinkBuilder slash(Identifiable identifiable) { 94 | return slash(identifiable.getId()); 95 | } 96 | 97 | public RouterLinkBuilder slash(String name, Identifiable identifiable) { 98 | return slash(name, identifiable.getId()); 99 | } 100 | 101 | @Override 102 | public URI toUri() { 103 | 104 | try { 105 | return new URI(toString()); 106 | } catch (URISyntaxException e) { 107 | throw new IllegalArgumentException("Could not reverse controller.action to an URI", e); 108 | } 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return Router.getFullUrl(controllerName + "." + actionName,resolveArgs()); 114 | } 115 | 116 | @Override 117 | public Link withRel(String rel) { 118 | return new Link(this.toString(), rel); 119 | } 120 | 121 | @Override 122 | public Link withSelfRel() { 123 | return new Link(this.toString()); 124 | } 125 | 126 | private Map resolveArgs() { 127 | 128 | if (unresolvedArgs.size() > 0) { 129 | 130 | Collection routes = Router.resolveActions(controllerName + "." + actionName); 131 | int argsCount = unresolvedArgs.size() + resolvedArgs.size(); 132 | 133 | for (Router.Route route : routes) { 134 | 135 | int routeArgsCount = route.getArgs().size(); 136 | 137 | // if args number doesn't match, check next route 138 | if (argsCount != routeArgsCount) { 139 | continue; 140 | } 141 | 142 | List argNames = new ArrayList(); 143 | for (Router.Route.Arg arg : route.getArgs()) { 144 | argNames.add(arg.getName()); 145 | } 146 | 147 | // if all resolved args aren't in this route, check next route 148 | if(!argNames.containsAll(resolvedArgs.keySet())) { 149 | continue; 150 | } 151 | 152 | // resolve missing arguments 153 | for(String argName : argNames) { 154 | //this is an unresolved arg 155 | if(!resolvedArgs.containsKey(argName)) { 156 | resolvedArgs.put(argName,unresolvedArgs.remove(0)); 157 | } 158 | 159 | } 160 | 161 | } 162 | } 163 | return resolvedArgs; 164 | } 165 | 166 | } -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/support/RouterHandler.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.support; 2 | 3 | import java.lang.reflect.Method; 4 | import org.resthub.web.springmvc.router.Router; 5 | import org.resthub.web.springmvc.router.Router.Route; 6 | import org.springframework.web.method.HandlerMethod; 7 | 8 | /** 9 | * Bears the request mapping information to be handled by the RequestMappingHandlerAdapter 10 | * to invoke the corresponding Controller.action. 11 | *

Encapsulates a HandlerMethod object + additional info: 12 | *

    13 | *
  • the matching Controller
  • 14 | *
  • the Matching Method
  • 15 | *
  • the Route that matches that request
  • 16 | *
  • the HTTPRequestAdapter, containing the actual request
  • 17 | * 18 | * @see HandlerMethod 19 | * @author Brian Clozel 20 | */ 21 | public class RouterHandler extends HandlerMethod { 22 | 23 | private Router.Route route; 24 | 25 | public RouterHandler(Object bean, Method method, Router.Route route) { 26 | // calling the actual HandlerMethod constructor 27 | super(bean, method); 28 | this.route = route; 29 | } 30 | 31 | public Route getRoute() { 32 | return route; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/router/support/RouterHandlerResolver.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.support; 2 | 3 | import org.resthub.web.springmvc.router.HTTPRequestAdapter; 4 | import org.resthub.web.springmvc.router.Router; 5 | import org.resthub.web.springmvc.router.exceptions.ActionNotFoundException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.aop.support.AopUtils; 9 | import org.springframework.core.BridgeMethodResolver; 10 | import org.springframework.web.method.HandlerMethod; 11 | 12 | import java.lang.reflect.Method; 13 | import java.lang.reflect.Modifier; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * Resolve Controller and Action for the given route (that contains the 19 | * fullAction "controller.action") 20 | * 21 | * @author Brian Clozel 22 | */ 23 | public class RouterHandlerResolver { 24 | 25 | private Map cachedControllers = new LinkedHashMap(); 26 | 27 | private final Map cachedHandlers = new LinkedHashMap(); 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(RouterHandlerResolver.class); 30 | 31 | public void setCachedControllers(Map controllers) { 32 | 33 | for(String key : controllers.keySet()) { 34 | this.cachedControllers.put(key.toLowerCase(), controllers.get(key)); 35 | } 36 | } 37 | 38 | /** 39 | * Returns a proper HandlerMethod given the matching Route 40 | * @param route the matching Route for the current request 41 | * @param fullAction string "controller.action" 42 | * @return HandlerMethod to be used by the RequestAdapter 43 | * @throws ActionNotFoundException 44 | */ 45 | public HandlerMethod resolveHandler(Router.Route route, String fullAction, HTTPRequestAdapter req) throws ActionNotFoundException { 46 | 47 | HandlerMethod handlerMethod; 48 | 49 | // check if the Handler is already cached 50 | if(this.cachedHandlers.containsKey(fullAction)) { 51 | handlerMethod = this.cachedHandlers.get(fullAction); 52 | } else { 53 | handlerMethod = this.doResolveHandler(route, fullAction); 54 | this.cachedHandlers.put(fullAction, handlerMethod); 55 | } 56 | 57 | return handlerMethod; 58 | } 59 | 60 | private HandlerMethod doResolveHandler(Router.Route route, String fullAction) throws ActionNotFoundException { 61 | 62 | Method actionMethod; 63 | Object controllerObject; 64 | 65 | String controller = fullAction.substring(0, fullAction.lastIndexOf(".")).toLowerCase(); 66 | String action = fullAction.substring(fullAction.lastIndexOf(".") + 1); 67 | controllerObject = cachedControllers.get(controller); 68 | 69 | if (controllerObject == null) { 70 | logger.debug("Did not find handler {} for [{} {}]", controller, route.method, route.path); 71 | throw new ActionNotFoundException(fullAction, new Exception("Controller " + controller + " not found")); 72 | } 73 | 74 | // find actionMethod on target 75 | actionMethod = findActionMethod(action, controllerObject); 76 | 77 | if (actionMethod == null) { 78 | logger.debug("Did not find handler method {}.{} for [{} {}]", controller, action, route.method, route.path); 79 | throw new ActionNotFoundException(fullAction, new Exception("No method public static void " + action + "() was found in class " + controller)); 80 | } 81 | 82 | return new RouterHandler(controllerObject, actionMethod, route); 83 | } 84 | 85 | /** 86 | * Find the first public static method of a controller class 87 | * 88 | * @param name The method name 89 | * @param controller The controller 90 | * @return The method or null 91 | */ 92 | private Method findActionMethod(String name, Object controller) { 93 | 94 | //get the controller class 95 | //(or the corresponding target class if the current controller 96 | // instance is an AOP proxy 97 | Class clazz = AopUtils.getTargetClass(controller); 98 | 99 | while (!clazz.getName().equals("java.lang.Object")) { 100 | for (Method m : clazz.getDeclaredMethods()) { 101 | if (m.getName().equalsIgnoreCase(name) && Modifier.isPublic(m.getModifiers())) { 102 | return BridgeMethodResolver.findBridgedMethod(m); 103 | } 104 | } 105 | clazz = clazz.getSuperclass(); 106 | } 107 | return null; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/view/freemarker/RouterModelAttribute.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.view.freemarker; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | import org.resthub.web.springmvc.router.Router; 6 | import org.springframework.web.servlet.ModelAndView; 7 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 8 | 9 | /** 10 | * An interceptor to inject an instance of {@link Router} into the model for all Spring MVC views. By default the 11 | * attribute name used is "route". This can be changed using {@link #setAttributeName(String)}. 12 | */ 13 | public class RouterModelAttribute extends HandlerInterceptorAdapter { 14 | 15 | private static final String DEFAULT_ATTRIBUTE_NAME = "route"; 16 | 17 | private String attributeName = DEFAULT_ATTRIBUTE_NAME; 18 | 19 | private final Router router = new Router(); 20 | 21 | @Override 22 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 23 | ModelAndView mav) throws Exception { 24 | 25 | if (mav != null && mav.getModelMap() != null) { 26 | mav.getModelMap().addAttribute(attributeName, router); 27 | } 28 | } 29 | 30 | public void setAttributeName(String attributeName) { 31 | this.attributeName = attributeName; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/view/jsp/URLRouteTag.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.view.jsp; 2 | 3 | 4 | import java.io.IOException; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import javax.servlet.jsp.JspException; 10 | import javax.servlet.jsp.JspWriter; 11 | import javax.servlet.jsp.tagext.DynamicAttributes; 12 | import javax.servlet.jsp.tagext.SimpleTagSupport; 13 | 14 | import org.resthub.web.springmvc.router.Router; 15 | import org.resthub.web.springmvc.router.Router.ActionDefinition; 16 | 17 | public class URLRouteTag extends SimpleTagSupport implements DynamicAttributes { 18 | 19 | private String action; 20 | 21 | /** 22 | * Stores key/value pairs to pass as parameters to the URL. 23 | */ 24 | private Map attrMap = new HashMap(); 25 | 26 | public void doTag() throws JspException, IOException, IllegalArgumentException { 27 | JspWriter out = getJspContext().getOut(); 28 | 29 | ActionDefinition urlAction = Router.reverse(action, attrMap); 30 | 31 | out.println(urlAction); 32 | } 33 | 34 | @Override 35 | public void setDynamicAttribute(String uri, String name, Object value) throws JspException { 36 | attrMap.put(name, value); 37 | } 38 | 39 | public void setAction(String action) { 40 | this.action = action; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/resthub/web/springmvc/view/velocity/RouteDirective.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.view.velocity; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import jregex.Matcher; 8 | import jregex.Pattern; 9 | import org.apache.velocity.context.InternalContextAdapter; 10 | import org.apache.velocity.exception.MethodInvocationException; 11 | import org.apache.velocity.exception.ParseErrorException; 12 | import org.apache.velocity.exception.ResourceNotFoundException; 13 | import org.apache.velocity.runtime.directive.Directive; 14 | import org.apache.velocity.runtime.parser.node.Node; 15 | import org.resthub.web.springmvc.router.Router; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | /** 20 | * #route directive for the Velocity engine. 21 | * Computes an URL given a Controller.action(argName:'argValue',...) argument. 22 | * 23 | * Examples: 24 | *
      25 | *
    • #route("helloWorldController.sayHelloName(name:'yourname')") 26 | * will resolve '/hello/yourname' because the route configured is "GET /hello/{name} helloWorldController.sayHelloName" 27 | *
    • #route("helloWorldController.sayHello") 28 | * will resolve '/hello' because the route configured is "GET /hello helloWorldController.sayHello" 29 | *
    30 | * 31 | * @author Brian Clozel 32 | * @see org.resthub.web.springmvc.router.Router 33 | */ 34 | public class RouteDirective extends Directive { 35 | 36 | 37 | /** 38 | * Regex pattern that matches params (paramName:'paramValue') 39 | */ 40 | private static Pattern paramPattern = new Pattern("([a-zA-Z_0-9]+)\\s*:\\s*'(.*)'"); 41 | 42 | private static Logger logger = LoggerFactory.getLogger(RouteDirective.class); 43 | 44 | 45 | /** 46 | * Name of the Velocity directive to be called from the .vm views 47 | * #route 48 | */ 49 | @Override 50 | public String getName() { 51 | return "route"; 52 | } 53 | 54 | 55 | /** 56 | * This directive is a "LINE directive", meaning it has to be 57 | * written on a single line. 58 | * @return org.apache.velocity.runtime.directive.DirectiveConstants.LINE 59 | */ 60 | @Override 61 | public int getType() { 62 | return LINE; 63 | } 64 | 65 | /** 66 | * Renders the directive 67 | * @param context velocity context 68 | * @param writer used for writing directive result in the view 69 | * @param node body of the directive (params, content) 70 | * @return true if the directive rendered ok. 71 | * @throws IOException 72 | * @throws ResourceNotFoundException 73 | * @throws ParseErrorException 74 | * @throws MethodInvocationException 75 | */ 76 | @Override 77 | public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException { 78 | 79 | String route = null; 80 | String action = null; 81 | String params = null; 82 | Map args = new HashMap(); 83 | 84 | //reading the unique param 85 | if (node.jjtGetChild(0) != null) { 86 | 87 | route = String.valueOf(node.jjtGetChild(0).value(context)); 88 | logger.debug("-- RouteDirective " + route); 89 | 90 | // check if arguments are provided with the "controller.action" 91 | int index = route.indexOf('('); 92 | if (index > 0) { 93 | action = route.substring(0, index); 94 | params = route.substring(index+1, route.length()-1); 95 | } else { 96 | action = route; 97 | } 98 | 99 | // extract arguments if params is not null 100 | if (params != null && params.length() > 1) { 101 | 102 | for (String param : params.split(",")) { 103 | Matcher matcher = paramPattern.matcher(param); 104 | if (matcher.matches()) { 105 | // add arguments to the args map 106 | args.put(matcher.group(1), matcher.group(2)); 107 | } else { 108 | logger.warn("Ignoring " + params + " (static params must be specified as key:'value',...)"); 109 | } 110 | } 111 | 112 | } 113 | } 114 | 115 | // resolve URL and write it to the view 116 | writer.write(Router.reverse(action, args).url); 117 | 118 | return true; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/resources/url-routing.tld: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 1.0 7 | 1.1 8 | URLRouteTags 9 | /springmvc-router 10 | Tags to assist with generating URLs with the SpringMVC router plugin. 11 | 12 | 13 | reverse 14 | org.resthub.web.springmvc.view.jsp.URLRouteTag 15 | 16 | Generates a URL based on a controller and action name 17 | 18 | action 19 | true 20 | 21 | true 22 | 23 | -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/controllers/BindTestController.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.controllers; 2 | 3 | import javax.inject.Named; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | import org.springframework.security.access.annotation.Secured; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.ModelAttribute; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.servlet.HandlerMapping; 13 | import org.springframework.web.servlet.ModelAndView; 14 | 15 | @Controller 16 | @Named("bindTestController") 17 | public class BindTestController { 18 | 19 | @ModelAttribute("simpleModelAttributeOnMethod") 20 | public boolean simpleModelAttribute() { 21 | return true; 22 | } 23 | 24 | @ModelAttribute 25 | public void multipleModelAttribute(Model model) { 26 | model.addAttribute("firstModelAttributeOnMethod", true); 27 | model.addAttribute("secondModelAttributeOnMethod", true); 28 | } 29 | 30 | public ModelAndView bindNameAction(@PathVariable(value = "myName") String myName) { 31 | 32 | ModelAndView mav = new ModelAndView("testView"); 33 | mav.addObject("name", myName); 34 | 35 | return mav; 36 | } 37 | 38 | public ModelAndView bindIdAction(@PathVariable(value = "myId") Long myId) { 39 | 40 | ModelAndView mav = new ModelAndView("testView"); 41 | mav.addObject("id", myId); 42 | 43 | return mav; 44 | } 45 | 46 | public ModelAndView bindSlugAction(@PathVariable(value = "slug") String mySlug, 47 | @RequestParam(value = "hash", required = true) String myHash) { 48 | 49 | ModelAndView mav = new ModelAndView("testView"); 50 | mav.addObject("slug", mySlug); 51 | mav.addObject("hash", myHash); 52 | 53 | return mav; 54 | } 55 | 56 | public ModelAndView bindHostSlugAction(@PathVariable(value = "slug") String mySlug, 57 | @PathVariable(value = "hostname") String hostname) { 58 | 59 | ModelAndView mav = new ModelAndView("testView"); 60 | mav.addObject("slug", mySlug); 61 | mav.addObject("hostname", hostname); 62 | 63 | return mav; 64 | } 65 | 66 | public ModelAndView bindSpecificHostAction() { 67 | 68 | ModelAndView mav = new ModelAndView("testView"); 69 | mav.addObject("host", "specific"); 70 | 71 | return mav; 72 | } 73 | 74 | public ModelAndView bindRegexpHostAction(@PathVariable(value = "subdomain") String subdomain) { 75 | ModelAndView mav = new ModelAndView("testView"); 76 | mav.addObject("subdomain", subdomain); 77 | 78 | return mav; 79 | } 80 | 81 | public ModelAndView bindModelAttributeOnMethodsAction() { 82 | 83 | ModelAndView mav = new ModelAndView("testView"); 84 | 85 | return mav; 86 | } 87 | 88 | public ModelAndView addBestMatchingPatternAction(@PathVariable("value") String value, HttpServletRequest request) { 89 | 90 | ModelAndView mav = new ModelAndView("testView"); 91 | mav.addObject("pattern", (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); 92 | mav.addObject("value", value); 93 | 94 | return mav; 95 | } 96 | 97 | @Secured("ROLE_ADMIN") 98 | public ModelAndView securityAction(@PathVariable(value = "name") String name) { 99 | 100 | ModelAndView mav = new ModelAndView("testView"); 101 | mav.addObject("name", name); 102 | 103 | return mav; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/controllers/MyTestController.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.controllers; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.PathVariable; 5 | 6 | import javax.inject.Named; 7 | 8 | @Controller 9 | @Named("myTestController") 10 | public class MyTestController { 11 | 12 | public void simpleAction() { 13 | } 14 | 15 | public void additionalRouteFile() { 16 | } 17 | 18 | public void wildcardA() { 19 | } 20 | 21 | public void wildcardB() { 22 | } 23 | 24 | public void caseInsensitive() { 25 | } 26 | 27 | public void overrideMethod() { 28 | } 29 | 30 | public void paramAction(@PathVariable(value = "param") String param) { 31 | } 32 | 33 | public void httpAction(@PathVariable(value = "type") String type) { 34 | } 35 | 36 | public void regexNumberAction(@PathVariable(value = "number") int number) { 37 | } 38 | 39 | public void regexStringAction(@PathVariable(value = "string") String string) { 40 | } 41 | 42 | public void hostAction(@PathVariable(value = "host") String host) { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/javaconfig/WebAppConfig.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.javaconfig; 2 | 3 | import org.resthub.web.springmvc.router.RouterConfigurationSupport; 4 | import org.resthub.web.springmvc.router.RouterHandlerMapping; 5 | import org.resthub.web.springmvc.router.support.TeapotHandlerInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @Configuration 15 | @ComponentScan(basePackages = "org.resthub.web.springmvc.router.controllers") 16 | public class WebAppConfig extends RouterConfigurationSupport { 17 | 18 | @Override 19 | protected void addInterceptors(InterceptorRegistry registry) { 20 | 21 | registry.addInterceptor(new TeapotHandlerInterceptor()); 22 | } 23 | 24 | @Override 25 | public List listRouteFiles() { 26 | 27 | List routeFiles = new ArrayList(); 28 | routeFiles.add("mappingroutes.conf"); 29 | 30 | return routeFiles; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/support/TeapotHandlerInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.support; 2 | 3 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | public class TeapotHandlerInterceptor extends HandlerInterceptorAdapter { 9 | 10 | @Override 11 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 12 | 13 | if(request.getParameter("teapot").equals("true")) { 14 | // I'm a teapot 15 | response.sendError(418); 16 | return false; 17 | } 18 | return true; 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/test/HandlersStepdefs.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.test; 2 | 3 | import cucumber.api.java.en.Given; 4 | import cucumber.api.java.en.Then; 5 | import cucumber.api.java.en.When; 6 | import org.resthub.web.springmvc.router.HTTPRequestAdapter; 7 | import org.resthub.web.springmvc.router.RouterHandlerMapping; 8 | import org.resthub.web.springmvc.router.hateoas.RouterLinkBuilder; 9 | import org.resthub.web.springmvc.router.support.RouterHandler; 10 | import org.springframework.mock.web.MockHttpServletRequest; 11 | import org.springframework.mock.web.MockHttpServletResponse; 12 | import org.springframework.mock.web.MockServletContext; 13 | import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; 14 | import org.springframework.web.context.request.RequestContextHolder; 15 | import org.springframework.web.context.request.ServletRequestAttributes; 16 | import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext; 17 | import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 18 | import org.springframework.web.context.support.XmlWebApplicationContext; 19 | import org.springframework.web.servlet.*; 20 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | public class HandlersStepdefs { 29 | 30 | private AbstractRefreshableWebApplicationContext wac; 31 | private HandlerMapping hm; 32 | private HandlerAdapter ha; 33 | 34 | private RouterLinkBuilder linkBuilder; 35 | 36 | private String servletPath = ""; 37 | private String contextPath = ""; 38 | private List queryParams = new ArrayList(); 39 | private List headers = new ArrayList(); 40 | 41 | private MockHttpServletRequest request; 42 | 43 | private String host = "example.org"; 44 | 45 | private HandlerExecutionChain chain; 46 | 47 | @Given("^I have a web application with the config locations \"([^\"]*)\"$") 48 | public void I_have_a_web_applications_with_the_config_locations(String locations) throws Throwable { 49 | I_have_a_web_application_configured_locations_servletPath_contextPath(locations,"",""); 50 | } 51 | 52 | 53 | @Given("^I have a web application configured locations \"([^\"]*)\" servletPath \"([^\"]*)\" contextPath \"([^\"]*)\"$") 54 | public void I_have_a_web_application_configured_locations_servletPath_contextPath(String locations, String servletPath, String contextPath) throws Throwable { 55 | 56 | this.servletPath = servletPath; 57 | this.contextPath = contextPath; 58 | 59 | MockServletContext sc = new MockServletContext(); 60 | sc.setContextPath(contextPath); 61 | 62 | this.wac = new XmlWebApplicationContext(); 63 | this.wac.setServletContext(sc); 64 | this.wac.setConfigLocations(locations.split(",")); 65 | this.wac.refresh(); 66 | 67 | this.hm = this.wac.getBean(RouterHandlerMapping.class); 68 | this.ha = this.wac.getBean(RequestMappingHandlerAdapter.class); 69 | } 70 | 71 | @Given("^I have a web application with javaconfig in package \"([^\"]*)\"$") 72 | public void I_have_a_web_application_with_javaconfig_in_package(String scanPackage) throws Throwable { 73 | MockServletContext sc = new MockServletContext(""); 74 | AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext(); 75 | appContext.scan(scanPackage); 76 | appContext.setServletContext(sc); 77 | appContext.refresh(); 78 | 79 | this.wac = appContext; 80 | 81 | this.hm = appContext.getBean(RouterHandlerMapping.class); 82 | this.ha = appContext.getBean(RequestMappingHandlerAdapter.class); 83 | } 84 | 85 | @Given("^a current request \"([^\"]*)\" \"([^\"]*)\" with servlet path \"([^\"]*)\" and context path \"([^\"]*)\"$") 86 | public void a_current_request_with_servlet_path_and_context_path(String method, String url, String servletPath, String contextPath) throws Throwable { 87 | 88 | MockServletContext sc = new MockServletContext(); 89 | sc.setContextPath(contextPath); 90 | 91 | int pathLength = 0; 92 | if(contextPath.length() > 0) { 93 | pathLength += contextPath.length(); 94 | } 95 | 96 | if(servletPath.length() > 0) { 97 | pathLength += servletPath.length(); 98 | } 99 | 100 | request = new MockHttpServletRequest(sc, method, url); 101 | request.setContextPath(contextPath); 102 | request.setServletPath(servletPath); 103 | request.addHeader("host", host); 104 | 105 | request.setPathInfo(url.substring(pathLength)); 106 | 107 | ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); 108 | RequestContextHolder.setRequestAttributes(requestAttributes); 109 | 110 | HTTPRequestAdapter.parseRequest(request); 111 | } 112 | 113 | 114 | 115 | @When("^I send the HTTP request \"([^\"]*)\" \"([^\"]*)\"$") 116 | public void I_send_the_HTTP_request(String method, String url) throws Throwable { 117 | 118 | int pathLength = 0; 119 | if(this.contextPath.length() > 0) { 120 | pathLength += this.contextPath.length(); 121 | } 122 | 123 | if(this.servletPath.length() > 0) { 124 | pathLength += this.servletPath.length(); 125 | } 126 | 127 | request = new MockHttpServletRequest(this.wac.getServletContext(),method, url); 128 | request.setContextPath(this.contextPath); 129 | request.setServletPath(this.servletPath); 130 | request.addHeader("host", host); 131 | 132 | ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); 133 | RequestContextHolder.setRequestAttributes(requestAttributes); 134 | 135 | for (HTTPHeader header : headers) { 136 | request.addHeader(header.name, header.value); 137 | } 138 | 139 | for (HTTPParam param : queryParams) { 140 | request.addParameter(param.name, param.value); 141 | } 142 | 143 | request.setPathInfo(url.substring(pathLength)); 144 | chain = this.hm.getHandler(request); 145 | } 146 | 147 | @When("^I send the HTTP request \"([^\"]*)\" \"([^\"]*)\" with a null pathInfo$") 148 | public void I_send_the_HTTP_request_with_a_null_pathInfo(String method, String url) throws Throwable { 149 | 150 | request = new MockHttpServletRequest(this.wac.getServletContext()); 151 | request.setMethod(method); 152 | request.setContextPath(this.contextPath); 153 | request.setServletPath(url.replaceFirst(this.contextPath,"")); 154 | request.addHeader("host", host); 155 | 156 | for (HTTPHeader header : headers) { 157 | request.addHeader(header.name, header.value); 158 | } 159 | 160 | for (HTTPParam param : queryParams) { 161 | request.addParameter(param.name, param.value); 162 | } 163 | 164 | request.setPathInfo(null); 165 | 166 | ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); 167 | RequestContextHolder.setRequestAttributes(requestAttributes); 168 | 169 | chain = this.hm.getHandler(request); 170 | } 171 | 172 | 173 | 174 | @When("^I send the HTTP request \"([^\"]*)\" \"([^\"]*)\" to host \"([^\"]*)\"$") 175 | public void I_send_the_HTTP_request_to_host(String method, String url, String host) throws Throwable { 176 | 177 | this.host = host; 178 | I_send_the_HTTP_request(method,url); 179 | } 180 | 181 | @When("^I send the HTTP request \"([^\"]*)\" \"([^\"]*)\" with query params:$") 182 | public void I_send_the_HTTP_request_with_query_params(String method, String url, List queryParams) throws Throwable { 183 | 184 | this.queryParams = queryParams; 185 | I_send_the_HTTP_request(method,url); 186 | } 187 | 188 | @When("^I send the HTTP request \"([^\"]*)\" \"([^\"]*)\" with headers:$") 189 | public void I_send_the_HTTP_request_with_headers(String method, String url, List headers) throws Throwable { 190 | 191 | this.headers = headers; 192 | I_send_the_HTTP_request(method,url); 193 | } 194 | 195 | @When("^I build a link for controller \"([^\"]*)\" and action \"([^\"]*)\"$") 196 | public void I_build_a_link_for_controller_and_action(String controller, String action) throws Throwable { 197 | 198 | linkBuilder = RouterLinkBuilder.linkTo(controller,action); 199 | } 200 | 201 | @When("^I add an argument named \"([^\"]*)\" with value \"([^\"]*)\"$") 202 | public void I_add_a_argument_named_with_value(String name, String value) throws Throwable { 203 | linkBuilder = linkBuilder.slash(name, value); 204 | } 205 | 206 | @When("^I add an argument \"([^\"]*)\"$") 207 | public void I_add_a_argument(String argument) throws Throwable { 208 | linkBuilder = linkBuilder.slash(argument); 209 | } 210 | 211 | @Then("^no handler should be found$") 212 | public void no_handler_should_be_found() throws Throwable { 213 | 214 | assertThat(chain).isNull(); 215 | } 216 | 217 | @Then("^the request should be handled by \"([^\"]*)\"$") 218 | public void the_request_should_be_handled_by(String controllerAction) throws Throwable { 219 | 220 | assertThat(chain).isNotNull(); 221 | RouterHandler handler = (RouterHandler) chain.getHandler(); 222 | 223 | assertThat(handler).isNotNull(); 224 | assertThat(handler.getRoute()).isNotNull(); 225 | assertThat(handler.getRoute().action).isNotNull().isEqualToIgnoringCase(controllerAction); 226 | } 227 | 228 | 229 | @Then("^the handler should raise a security exception$") 230 | public void the_handler_should_raise_a_security_exception() throws Throwable { 231 | 232 | assertThat(chain).isNotNull(); 233 | RouterHandler handler = (RouterHandler) chain.getHandler(); 234 | 235 | Exception securityException = null; 236 | 237 | try { 238 | ha.handle(request, new MockHttpServletResponse(), handler); 239 | } catch(Exception exc) { 240 | securityException = exc; 241 | } 242 | 243 | assertThat(securityException).isNotNull().isInstanceOf(AuthenticationCredentialsNotFoundException.class); 244 | } 245 | 246 | @Then("^the controller should respond with a ModelAndView containing:$") 247 | public void the_controller_should_respond_with_a_ModelAndView_containing(List mavparams) throws Throwable { 248 | 249 | assertThat(chain).isNotNull(); 250 | RouterHandler handler = (RouterHandler) chain.getHandler(); 251 | 252 | ModelAndView mv = ha.handle(request, new MockHttpServletResponse(), handler); 253 | 254 | for (MaVParams param : mavparams) { 255 | assertThat(param.value).isEqualTo(mv.getModel().get(param.key).toString()); 256 | } 257 | } 258 | 259 | @Then("^the server should send an HTTP response with status \"([^\"]*)\"$") 260 | public void the_server_should_send_an_HTTP_response_with_status(int status) throws Throwable { 261 | 262 | RouterHandler handler = null; 263 | MockHttpServletResponse response = new MockHttpServletResponse(); 264 | 265 | if(chain != null) { 266 | handler = (RouterHandler) chain.getHandler(); 267 | } 268 | 269 | HandlerInterceptor[] interceptors = chain.getInterceptors(); 270 | 271 | for(HandlerInterceptor interceptor : Arrays.asList(interceptors)) { 272 | interceptor.preHandle(request,response, handler); 273 | } 274 | 275 | ha.handle(request, response, handler); 276 | assertThat(response.getStatus()).isEqualTo(status); 277 | } 278 | 279 | @Then("^the raw link should be \"([^\"]*)\"$") 280 | public void the_raw_link_should_be(String link) throws Throwable { 281 | 282 | assertThat(linkBuilder.toString()).isEqualTo(link); 283 | } 284 | 285 | @Then("^the self rel link should be \"(.*)\"$") 286 | public void the_self_rel_link_should_be(String link) throws Throwable { 287 | 288 | assertThat(linkBuilder.withSelfRel().toString()).isEqualTo(link); 289 | } 290 | 291 | public static class HTTPHeader { 292 | public String name; 293 | public String value; 294 | } 295 | 296 | public static class HTTPParam { 297 | public String name; 298 | public String value; 299 | } 300 | 301 | public static class MaVParams { 302 | public String key; 303 | public String value; 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/test/ReverseRoutingStepdefs.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.test; 2 | 3 | import cucumber.api.java.en.Given; 4 | import cucumber.api.java.en.Then; 5 | import cucumber.api.java.en.When; 6 | import org.resthub.web.springmvc.router.HTTPRequestAdapter; 7 | import org.resthub.web.springmvc.router.Router; 8 | import org.resthub.web.springmvc.router.exceptions.NoHandlerFoundException; 9 | import org.springframework.mock.web.MockHttpServletRequest; 10 | import org.springframework.web.context.request.RequestContextHolder; 11 | import org.springframework.web.context.request.ServletRequestAttributes; 12 | 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | public class ReverseRoutingStepdefs { 20 | 21 | private HTTPRequestAdapter requestAdapter; 22 | 23 | private Router.ActionDefinition resolvedAction; 24 | 25 | private Exception thrownException; 26 | 27 | @Given("^an empty Router$") 28 | public void an_empty_Router() throws Throwable { 29 | // clear routes from the static Router 30 | Router.clear(); 31 | // clear RequestContextHolder from previous tests 32 | MockHttpServletRequest request = new MockHttpServletRequest("GET","http://localhost/"); 33 | request.addHeader("host", "localhost"); 34 | ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); 35 | RequestContextHolder.setRequestAttributes(requestAttributes); 36 | } 37 | 38 | @Given("^I have a route with method \"([^\"]*)\" path \"([^\"]*)\" action \"([^\"]*)\"$") 39 | public void I_have_a_route_with_method_url_action(String method, String path, String action) throws Throwable { 40 | Router.prependRoute(method, path, action); 41 | } 42 | 43 | @Given("^I have routes:$") 44 | public void I_have_routes(List routes) throws Throwable { 45 | for(RouteItem item : routes) { 46 | Router.addRoute(item.method,item.path,item.action,item.params,null); 47 | } 48 | } 49 | 50 | @Given("^the current request is processed within a context path \"([^\"]*)\" and servlet path \"([^\"]*)\"$") 51 | public void the_current_request_is_processed_within_a_context_path_and_servlet_path(String contextPath, String servletPath) throws Throwable { 52 | 53 | MockHttpServletRequest request = new MockHttpServletRequest("GET", "/reverse-routing"); 54 | request.addHeader("host","example.org"); 55 | request.setContextPath(contextPath); 56 | request.setServletPath(servletPath); 57 | 58 | ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); 59 | RequestContextHolder.setRequestAttributes(requestAttributes); 60 | 61 | this.requestAdapter = HTTPRequestAdapter.parseRequest(request); 62 | } 63 | 64 | @When("^I try to reverse route \"([^\"]*)\" with params:$") 65 | public void I_try_to_reverse_route_with_params(String path, List params) throws Throwable { 66 | Map routeParams = new HashMap(); 67 | for(ParamItem param : params) { 68 | routeParams.put(param.key,param.value); 69 | } 70 | try { 71 | resolvedAction = Router.reverse(path,routeParams); 72 | } catch(Exception exc) { 73 | this.thrownException = exc; 74 | } 75 | } 76 | 77 | @When("^I try to reverse route \"([^\"]*)\"$") 78 | public void I_try_to_reverse_route(String action) throws Throwable { 79 | resolvedAction = Router.reverse(action); 80 | } 81 | 82 | @Then("^I should get an action with path \"([^\"]*)\"$") 83 | public void I_should_get_an_action_with_URL(String path) throws Throwable { 84 | assertThat(path).isEqualTo(resolvedAction.url); 85 | } 86 | 87 | @Then("^I should get an action with path \"([^\"]*)\" and host \"([^\"]*)\"$") 88 | public void I_should_get_an_action_with_path_and_host(String path, String host) throws Throwable { 89 | assertThat(path).isEqualTo(resolvedAction.url); 90 | assertThat(host).isEqualTo(resolvedAction.host); 91 | } 92 | 93 | @Then("^no action should match$") 94 | public void no_action_should_match() throws Throwable { 95 | assertThat(this.thrownException).isNotNull().isInstanceOf(NoHandlerFoundException.class); 96 | } 97 | 98 | public static class RouteItem { 99 | public String method; 100 | public String path; 101 | public String action; 102 | public String params; 103 | } 104 | 105 | public static class ParamItem { 106 | public String key; 107 | public String value; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/org/resthub/web/springmvc/router/test/RunCucumberTest.java: -------------------------------------------------------------------------------- 1 | package org.resthub.web.springmvc.router.test; 2 | 3 | import cucumber.api.junit.Cucumber; 4 | import org.junit.runner.RunWith; 5 | 6 | @RunWith(Cucumber.class) 7 | public class RunCucumberTest { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/addroutes.conf: -------------------------------------------------------------------------------- 1 | # mapping tests 2 | 3 | GET /additionalroute myTestController.additionalRouteFile -------------------------------------------------------------------------------- /src/test/resources/bindingTestContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 24 | 26 | 27 | 28 | 39 | 40 | 42 | 43 | 44 | bindingroutes.conf 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/resources/bindingroutes.conf: -------------------------------------------------------------------------------- 1 | # binding tests 2 | 3 | GET /bind/name/{<[a-z]+>myName} bindTestController.bindNameAction 4 | POST /bind/id/{<[0-9]+>myId} bindTestController.bindIdAction 5 | DELETE /bind/slug/{<[a-z0-9\-_]+>slug} bindTestController.bindSlugAction 6 | GET {hostname}/bind/hostslug/{<[a-z0-9\-]+>slug} bindTestController.bindHostSlugAction 7 | 8 | GET myhost.com/bind/specifichost bindTestController.bindSpecificHostAction 9 | GET {subdomain}.domain.org/bind/regexphost bindTestController.bindRegexpHostAction 10 | GET /security/{name} bindTestController.securityAction 11 | GET /bind/modelattribute bindTestController.bindModelAttributeOnMethodsAction 12 | GET /bestpattern/{<[0-9]+>value} bindTestController.addBestMatchingPatternAction -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootCategory=WARN, CONSOLE 2 | 3 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 4 | log4j.appender.CONSOLE.Threshold=DEBUG 5 | log4j.appender.CONSOLE.layout=org.apache.log4j.EnhancedPatternLayout 6 | log4j.appender.CONSOLE.layout.ConversionPattern=[%p] [%F:%L] %m%n 7 | 8 | # For production, use folowing pattern, with have 2 advantages ; 9 | # - It uses GMT unambigous date print thnaks to EnhancedPatternLayou improvements 10 | # - It removes [%F:%L] known to be extremely slow 11 | # 12 | # More details at http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/EnhancedPatternLayout.html 13 | # 14 | # log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601}{GMT} [%p] - %m%n 15 | 16 | log4j.logger.org.resthub.web.springmvc.router = WARN 17 | log4j.logger.org.resthub.web.springmvc.view = WARN -------------------------------------------------------------------------------- /src/test/resources/mappingroutes.conf: -------------------------------------------------------------------------------- 1 | # mapping tests 2 | GET /simpleaction myTestController.simpleAction 3 | GET /param myTestController.paramAction(param:'default') 4 | GET /param/{param} myTestController.paramAction 5 | GET /http myTestController.httpAction(type:'GET') 6 | PUT /http myTestController.httpAction(type:'PUT') 7 | POST /http myTestController.httpAction(type:'POST') 8 | DELETE /http myTestController.httpAction(type:'DELETE') 9 | PATCH /http myTestController.httpAction(type:'PATCH') 10 | PUT /overridemethod myTestController.overrideMethod 11 | GET /regex/{<[0-9]+>number} myTestController.regexNumberAction 12 | GET /regex/{<[a-z]+>string} myTestController.regexStringAction 13 | GET /caseinsensitive MyTestCONTROLLER.caseInsensitive -------------------------------------------------------------------------------- /src/test/resources/multiplefilesTestContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 24 | 26 | 27 | 28 | 39 | 40 | 42 | 43 | 44 | mappingroutes.conf 45 | addroutes.conf 46 | classpath:wildcard-*.conf 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/test/resources/org/resthub/web/springmvc/router/test/handler_adapter.feature: -------------------------------------------------------------------------------- 1 | Feature: Handler adapter support 2 | As a developer coding a application 3 | I want HTTP requests to be processed by controllers 4 | In order to map HTTP responses back to the client 5 | 6 | Scenario: Binding a string within the HTTP request 7 | Given I have a web application with the config locations "/bindingTestContext.xml" 8 | When I send the HTTP request "GET" "/bind/name/test" 9 | Then the controller should respond with a ModelAndView containing: 10 | | key | value | 11 | | name | test | 12 | 13 | Scenario: Binding a Long within the HTTP request 14 | Given I have a web application with the config locations "/bindingTestContext.xml" 15 | When I send the HTTP request "POST" "/bind/id/42" 16 | Then the controller should respond with a ModelAndView containing: 17 | | key | value | 18 | | id | 42 | 19 | 20 | Scenario: Binding a regexp and a hostname within the HTTP request 21 | Given I have a web application with the config locations "/bindingTestContext.xml" 22 | When I send the HTTP request "GET" "/bind/hostslug/my-slug-number-1" 23 | Then the controller should respond with a ModelAndView containing: 24 | | key | value | 25 | | hostname | example.org | 26 | | slug | my-slug-number-1 | 27 | 28 | Scenario: Binding a specific host within the HTTP request 29 | Given I have a web application with the config locations "/bindingTestContext.xml" 30 | When I send the HTTP request "GET" "/bind/specifichost" to host "myhost.com" 31 | Then the controller should respond with a ModelAndView containing: 32 | | key | value | 33 | | host | specific | 34 | 35 | Scenario: Checking routes bound to a specific host won't work with other hosts 36 | Given I have a web application with the config locations "/bindingTestContext.xml" 37 | When I send the HTTP request "GET" "/bind/specifichost" to host "myotherhost.com" 38 | Then no handler should be found 39 | 40 | Scenario: Binding a subdomain within the HTTP request 41 | Given I have a web application with the config locations "/bindingTestContext.xml" 42 | When I send the HTTP request "GET" "/bind/regexphost" to host "myhost.domain.org" 43 | Then the controller should respond with a ModelAndView containing: 44 | | key | value | 45 | | subdomain | myhost | 46 | 47 | Scenario: Binding modelattributes within the HTTP request 48 | Given I have a web application with the config locations "/bindingTestContext.xml" 49 | When I send the HTTP request "GET" "/bind/modelattribute" to host "myhost.domain.org" 50 | Then the controller should respond with a ModelAndView containing: 51 | | key | value | 52 | | simpleModelAttributeOnMethod | true | 53 | | firstModelAttributeOnMethod | true | 54 | | secondModelAttributeOnMethod | true | 55 | 56 | Scenario: Adding the best matched pattern to the request 57 | Given I have a web application with the config locations "/bindingTestContext.xml" 58 | When I send the HTTP request "GET" "/bestpattern/55" 59 | Then the controller should respond with a ModelAndView containing: 60 | | key | value | 61 | | pattern | /bestpattern/({value}[0-9]+) | 62 | | value | 55 | 63 | 64 | Scenario: Checking that secured routes are protected 65 | Given I have a web application with the config locations "/bindingTestContext.xml,/securityContext.xml" 66 | When I send the HTTP request "GET" "/security/test" to host "myotherhost.com" 67 | Then the handler should raise a security exception 68 | 69 | Scenario: Binding a regexp and queryParam within the HTTP request 70 | Given I have a web application with the config locations "/bindingTestContext.xml" 71 | When I send the HTTP request "DELETE" "/bind/slug/my-slug-number-1" with query params: 72 | | name | value | 73 | | hash | slughash | 74 | Then the controller should respond with a ModelAndView containing: 75 | | key | value | 76 | | slug | my-slug-number-1 | 77 | | hash | slughash | -------------------------------------------------------------------------------- /src/test/resources/org/resthub/web/springmvc/router/test/handler_mapping.feature: -------------------------------------------------------------------------------- 1 | Feature: Handler mapping support 2 | As a developer coding a application 3 | I want HTTP requests to be mapped to controllers 4 | In order to implement Controller behaviour accordingly 5 | 6 | 7 | Scenario: No route defined for a request 8 | Given I have a web application with the config locations "/simpleTestContext.xml" 9 | When I send the HTTP request "GET" "/noroute" 10 | Then no handler should be found 11 | 12 | Scenario: Mapping a simple request 13 | Given I have a web application with the config locations "/simpleTestContext.xml" 14 | When I send the HTTP request "GET" "/simpleaction" 15 | Then the request should be handled by "myTestController.simpleAction" 16 | 17 | Scenario: Mapping a request with a default param 18 | Given I have a web application with the config locations "/simpleTestContext.xml" 19 | When I send the HTTP request "GET" "/param" 20 | Then the request should be handled by "myTestController.paramAction" 21 | 22 | Scenario: Mapping a request with a given param 23 | Given I have a web application with the config locations "/simpleTestContext.xml" 24 | When I send the HTTP request "GET" "/param/myparam" 25 | Then the request should be handled by "myTestController.paramAction" 26 | 27 | Scenario: Mapping a GET request 28 | Given I have a web application with the config locations "/simpleTestContext.xml" 29 | When I send the HTTP request "GET" "/http" 30 | Then the request should be handled by "myTestController.httpAction" 31 | 32 | Scenario: Mapping a PUT request 33 | Given I have a web application with the config locations "/simpleTestContext.xml" 34 | When I send the HTTP request "PUT" "/http" 35 | Then the request should be handled by "myTestController.httpAction" 36 | 37 | Scenario: Mapping a POST request 38 | Given I have a web application with the config locations "/simpleTestContext.xml" 39 | When I send the HTTP request "POST" "/http" 40 | Then the request should be handled by "myTestController.httpAction" 41 | 42 | Scenario: Mapping a DELETE request 43 | Given I have a web application with the config locations "/simpleTestContext.xml" 44 | When I send the HTTP request "DELETE" "/http" 45 | Then the request should be handled by "myTestController.httpAction" 46 | 47 | Scenario: Mapping a PATCH request 48 | Given I have a web application with the config locations "/simpleTestContext.xml" 49 | When I send the HTTP request "PATCH" "/http" 50 | Then the request should be handled by "myTestController.httpAction" 51 | 52 | Scenario: Mapping a HEAD request 53 | Given I have a web application with the config locations "/simpleTestContext.xml" 54 | When I send the HTTP request "HEAD" "/http" 55 | Then the request should be handled by "myTestController.httpAction" 56 | 57 | Scenario: Mapping a request overriden by its HTTP Header 58 | Given I have a web application with the config locations "/simpleTestContext.xml" 59 | When I send the HTTP request "GET" "/overridemethod" with headers: 60 | | name | value | 61 | | x-http-method-override | PUT | 62 | Then the request should be handled by "myTestController.overrideMethod" 63 | 64 | Scenario: Mapping a request with a number regexp 65 | Given I have a web application with the config locations "/simpleTestContext.xml" 66 | When I send the HTTP request "GET" "/regex/42" 67 | Then the request should be handled by "myTestController.regexNumberAction" 68 | 69 | Scenario: Mapping a request with a string regexp 70 | Given I have a web application with the config locations "/simpleTestContext.xml" 71 | When I send the HTTP request "GET" "/regex/marvin" 72 | Then the request should be handled by "myTestController.regexStringAction" 73 | 74 | Scenario: Mapping a request with a string regexp 75 | Given I have a web application with the config locations "/simpleTestContext.xml" 76 | When I send the HTTP request "GET" "/caseinsensitive" 77 | Then the request should be handled by "myTestController.caseInsensitive" 78 | 79 | Scenario: Mapping a request with a route defined in an another configured file 80 | Given I have a web application with the config locations "/multiplefilesTestContext.xml" 81 | When I send the HTTP request "GET" "/additionalroute" 82 | Then the request should be handled by "myTestController.additionalRouteFile" 83 | 84 | Scenario: Mapping a request with a route defined in a wildcard-configured file 85 | Given I have a web application with the config locations "/multiplefilesTestContext.xml" 86 | When I send the HTTP request "GET" "/wildcard-b" 87 | Then the request should be handled by "myTestController.wildcardB" 88 | 89 | Scenario: Mapping a simple request with a servlet path and a context path 90 | Given I have a web application configured locations "/simpleTestContext.xml" servletPath "/servlet" contextPath "/context" 91 | When I send the HTTP request "GET" "/context/servlet/simpleaction" 92 | Then the request should be handled by "myTestController.simpleAction" 93 | 94 | # Testing issue https://github.com/resthub/springmvc-router/issues/41 95 | Scenario: Mapping a request to the index with a null pathInfo 96 | Given I have a web application configured locations "/simpleTestContext.xml" servletPath "/" contextPath "/context" 97 | When I send the HTTP request "GET" "/context/simpleaction" with a null pathInfo 98 | Then the request should be handled by "myTestController.simpleAction" -------------------------------------------------------------------------------- /src/test/resources/org/resthub/web/springmvc/router/test/hateoas.feature: -------------------------------------------------------------------------------- 1 | Feature: HATEOAS support 2 | As a developer coding a application 3 | I want to use a LinkBuilder 4 | In order to build HATEOAS links 5 | 6 | Background: 7 | Given a current request "GET" "/simpleaction" with servlet path "/" and context path "/" 8 | 9 | Scenario: Reverse routing a simple link using the linkbuilder 10 | Given I have a web application with the config locations "/simpleTestContext.xml" 11 | When I build a link for controller "myTestController" and action "simpleAction" 12 | Then the raw link should be "http://example.org/simpleaction" 13 | 14 | Scenario: Build a self rel link to a simple route 15 | Given I have a web application with the config locations "/simpleTestContext.xml" 16 | When I build a link for controller "myTestController" and action "simpleAction" 17 | Then the self rel link should be ";rel="self"" 18 | 19 | Scenario: Build a self rel link to a route with a named argument 20 | Given I have a web application with the config locations "/simpleTestContext.xml" 21 | When I build a link for controller "myTestController" and action "regexStringAction" 22 | And I add an argument named "string" with value "testlinkbuilder" 23 | Then the raw link should be "http://example.org/regex/testlinkbuilder" 24 | 25 | Scenario: Build a self rel link to a route with a named argument 26 | Given I have a web application with the config locations "/simpleTestContext.xml" 27 | When I build a link for controller "myTestController" and action "regexStringAction" 28 | And I add an argument "testlinkbuilder" 29 | Then the raw link should be "http://example.org/regex/testlinkbuilder" 30 | -------------------------------------------------------------------------------- /src/test/resources/org/resthub/web/springmvc/router/test/javaconfig.feature: -------------------------------------------------------------------------------- 1 | Feature: java config support 2 | As a developer coding a application 3 | I want to configure beans using java config 4 | In order to setup my application 5 | 6 | Scenario: Adding HandlerInterceptors using javaconfig 7 | Given I have a web application with javaconfig in package "org.resthub.web.springmvc.router.javaconfig" 8 | When I send the HTTP request "GET" "/simpleaction" with query params: 9 | | name | value | 10 | | teapot | true | 11 | Then the server should send an HTTP response with status "418" -------------------------------------------------------------------------------- /src/test/resources/org/resthub/web/springmvc/router/test/reverse_routing.feature: -------------------------------------------------------------------------------- 1 | Feature: Reverse routing support 2 | As a developer coding a Controller 3 | I want to do reverse routing 4 | In order to generate URLs within my application 5 | 6 | Background: 7 | Given an empty Router 8 | 9 | Scenario: Reverse routing a simple URL 10 | Given I have a route with method "GET" path "/simpleaction" action "myTestController.simpleAction" 11 | When I try to reverse route "myTestController.simpleAction" 12 | Then I should get an action with path "/simpleaction" 13 | 14 | Scenario: Reverse routing a simple URL with PATCH method 15 | Given I have a route with method "PATCH" path "/simpleaction" action "myTestController.simpleAction" 16 | When I try to reverse route "myTestController.simpleAction" 17 | Then I should get an action with path "/simpleaction" 18 | 19 | Scenario: Reverse routing an URL with params 20 | Given I have routes: 21 | | method | path | action | params | 22 | | GET | /param | myTestController.paramAction | (param:'default') | 23 | | GET | /param/{param} | myTestController.paramAction | | 24 | When I try to reverse route "myTestController.paramAction" with params: 25 | | key | value | 26 | | param | testparam | 27 | Then I should get an action with path "/param/testparam" 28 | 29 | Scenario: Reverse routing an URL with a regexp 30 | Given I have routes: 31 | | method | path | action | params | 32 | | GET | /bind/slug/{<[a-z0-9\-_]+>slug} | myTestController.bindSlugAction | | 33 | When I try to reverse route "myTestController.bindSlugAction" with params: 34 | | key | value | 35 | | slug | slug_01 | 36 | Then I should get an action with path "/bind/slug/slug_01" 37 | 38 | Scenario: Reverse routing an URL with a regexp 39 | Given I have routes: 40 | | method | path | action | params | 41 | | GET | /bind/slug/{<[a-z0-9\-_]+>slug} | myTestController.bindSlugAction | | 42 | When I try to reverse route "myTestController.bindSlugAction" with params: 43 | | key | value | 44 | | slug | slug_01 | 45 | Then I should get an action with path "/bind/slug/slug_01" 46 | 47 | Scenario: Reverse routing an URL with a regexp 48 | Given I have routes: 49 | | method | path | action | params | 50 | | GET | /bind/name/{<[a-z]+>myName} | myTestController.bindNameAction | | 51 | When I try to reverse route "myTestController.bindNameAction" with params: 52 | | key | value | 53 | | name | name01 | 54 | Then no action should match 55 | 56 | Scenario: Reverse routing an URL and a given domain 57 | Given I have routes: 58 | | method | path | action | params | 59 | | GET | {host}/bind/host | myTestController.bindHostAction | | 60 | When I try to reverse route "myTestController.bindHostAction" with params: 61 | | key | value | 62 | | host | example.org | 63 | Then I should get an action with path "/bind/host" and host "example.org" 64 | 65 | Scenario: Reverse routing an URL and a specific domain 66 | Given I have routes: 67 | | method | path | action | params | 68 | | GET | myhost.com/bind/specifichost | myTestController.bindSpecificHostAction | | 69 | When I try to reverse route "myTestController.bindSpecificHostAction" 70 | Then I should get an action with path "/bind/specifichost" and host "myhost.com" 71 | 72 | Scenario: Reverse routing an URL and a given subdomain 73 | Given I have routes: 74 | | method | path | action | params | 75 | | GET | {subdomain}.domain.org/bind/regexphost | myTestController.bindRegexpHostAction | | 76 | When I try to reverse route "myTestController.bindRegexpHostAction" with params: 77 | | key | value | 78 | | subdomain | sub | 79 | Then I should get an action with path "/bind/regexphost" and host "sub.domain.org" 80 | 81 | Scenario: Reverse routing a simple URL with a servlet path and context path 82 | Given I have a route with method "GET" path "/simpleaction" action "myTestController.simpleAction" 83 | And the current request is processed within a context path "context" and servlet path "servlet" 84 | When I try to reverse route "myTestController.simpleAction" 85 | Then I should get an action with path "/context/servlet/simpleaction" 86 | 87 | Scenario: Reverse routing a simple URL with servlet and context paths prepended by slash 88 | Given I have a route with method "GET" path "/simpleaction" action "myTestController.simpleAction" 89 | And the current request is processed within a context path "/context" and servlet path "/servlet" 90 | When I try to reverse route "myTestController.simpleAction" 91 | Then I should get an action with path "/context/servlet/simpleaction" -------------------------------------------------------------------------------- /src/test/resources/securityContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/simpleTestContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 24 | 26 | 27 | 28 | 39 | 40 | 42 | 43 | 44 | mappingroutes.conf 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/resources/wildcard-a.conf: -------------------------------------------------------------------------------- 1 | # mapping tests 2 | 3 | GET /wildcard-a myTestController.wildcardA -------------------------------------------------------------------------------- /src/test/resources/wildcard-b.conf: -------------------------------------------------------------------------------- 1 | # mapping tests 2 | 3 | GET /wildcard-b myTestController.wildcardB --------------------------------------------------------------------------------