I need the following:
Any request to
https://localhost:8443
https://localhost:8443/
https://localhost:8443/test
https://localhost:8443/api
and so on, should be forwarded to
https://localhost:8443/a/web/index.html
Now, this is how I managed to do that:
#Controller
public class ForwardController {
#RequestMapping(value = "/*", method = RequestMethod.GET)
public String redirectRoot(HttpServletRequest request) {
return "forward:/a/web/index.html";
}
}
The problem is:
This also matches https://localhost:8443/api/ (note the / at the end).
This is a problem because that's where I want the Spring Data REST base path to be:
spring.data.rest.base-path=/api/
/api != /api/ when it comes to REST endpoints.
What should work but somehow doesn't
I have tried several different regular expressions but I am still not able to accomplish what I want. For example (demo):
#RequestMapping(value = "/[^/]+", method = RequestMethod.GET)
Will now work for Spring Data - I'm getting all the resource information I expect, but accessing https://localhost:8443/ is now broken and the web-client cannot be reached anymore.
The same goes for
#RequestMapping(value = "/{path}", method = RequestMethod.GET)
#RequestMapping(value = "/{path:[^/]+}", method = RequestMethod.GET)
which behave like /* (also matches the next /).
This issue is already haunting me for weeks and still no solution insight.
This whole question can also be seen as:
Why is "/[^/]+" not matching https://localhost:8443/whatever ?
Regex are usually not the fastest thing to try.
You can send a list of paths to
#RequestMApping(value={"", "/", "/test", "/api"}, method = RequestMethod.GET)
See Multiple Spring #RequestMapping annotations
Related
So I have a Jetty container with a URL like this: http://localhost:7071/my-app-name/ui/. The problem I'm running into is that Jetty seems to require that last trailing slash.
So this works:
http://localhost:7071/my-app-name/ui/
But this doesn't:
http://localhost:7071/my-app-name/ui
It's really weird, I would want the slash-less path to go to the same place as the path with a slash. That path is the homepage of the entire application, which is why I want it to work.
Here is some of my Jetty configuration.
PS. I'm working in Kotlin. The principles of how it works should still be the same though.
server = Server(props.getProperty(AppProps.PORT).toInt())
val handler = ServletContextHandler(server, root)
val appServletHolder = ServletHolder("AppServlet", AppServlet::class.java)
handler.addServlet(appServletHolder, "/ui/*")
handler.addEventListener(AppCore(props))
server.start()
I know I'm specifying "/ui/*" in my servlet holder configuration. But I can't seem to figure out any way to change that, I've tried a few combinations and none of them work better.
You've told the servlet spec that you want to listen on /ui/* so it mandates that your URLs must have the /ui/ portion.
But all is not lost, just tell the servlet spec the other url-patterns you are also interested in. (Servlets can be mapped to as many url-patterns as you want)
aka:
server = Server(props.getProperty(AppProps.PORT).toInt());
val handler = ServletContextHandler(server, root);
val appServletHolder = ServletHolder("AppServlet", AppServlet::class.java);
handler.addServlet(appServletHolder, "/ui"); // <-- like that
handler.addServlet(appServletHolder, "/ui/*");
handler.addEventListener(AppCore(props));
server.start();
Instead of adding a bunch of duplicate servlet mappings, I added one at the root to redirect any requests ending with "/" or "/index.html":
...
servletHandler.addServletWithMapping(RedirectServlet.class, "/*");
servletHandler.addServletWithMapping(AnotherServlet.class, "/ui");
...
public class RedirectServlet extends HttpServlet {
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String pathInfo = req.getPathInfo();
if(pathInfo.endsWith("/") || pathInfo.endsWith("/index.html")) {
String newPath = pathInfo.substring(0, pathInfo.lastIndexOf('/'));
resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
resp.setHeader("Location", newPath);
}
}
#Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) {
doGet(req, resp);
}
}
This is the method to test:
It gets an URL and return a json after sending a GET request. It is a plain function which sits in a package rather than a method from a class. Same case for the extension method below.
fun getJson (url: String): String {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
return connection.getResult()
}
This is the extension method:
It will start connecting and read from result stream.
internal fun HttpURLConnection.getResult(charset: Charset = Charsets.UTF_8): String {
this.connect()
return this.inputStream.bufferedReader(charset).use { it.readText() }
}
This is the test case:
I tried to mock the HttpURLConnection that is about to be used here and call the original method, then just call the method and assert whether the mock has been set with the expected value.
class Spike {
#Test
fun test_getJson() {
val expectedResult = "{ok: true}"
val mockConnection = mock(HttpURLConnection::class.java)
Mockito.`when`(mockConnection.getResult()).thenReturn(expectedResult)
getJson("http://www.google.com")
assertEquals("GET", mockConnection.requestMethod)
assertEquals("http://www.google.com", mockConnection.url.host)
}
}
This is the error
java.lang.IllegalStateException: this.inputStream must not be null at
my.spike.pack.http.UtilsKt.getResult(utils.kt:45)
It just like the mock is not working.
How to solve this without changing the signature of the getJson function?
This will not work because of the way Kotlin extension methods are implemented on the class / bytecode level.
What you see in source code is HttpURLConnection.getResult but on the class/bytecode level there is another file created with a static method: public final static getResult(HttpURLConnection, Charset).
Mockito cannot mock static methods. If you really have to mock one, then I think PowerMock is capable of doing that.
Edit:
If you have a module wide function then it is also generated on a class. Assuming you have a file StreamFunctions.kt with a function: doSomething then, there will be (by default) generated class StreamFunctionsKt with a static function doSomething. More details can be found here: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html
That should be as easy as
Mockito.`when`(mockConnection.inputStream).thenReturn(ByteArrayInputStream("test".toByteArray()))
I'm attempting to use WebJars-Locator with a Spring-Boot application to map JAR resources. As per their website, I created a RequestMapping like this:
#ResponseBody
#RequestMapping(method = RequestMethod.GET, value = "/webjars-locator/{webjar}/{partialPath:.+}")
public ResponseEntity<ClassPathResource> locateWebjarAsset(#PathVariable String webjar, #PathVariable String partialPath)
{
The problem with this is that the partialPath variable is supposed to include anything after the third slash. What it ends up doing, however, is limiting the mapping itself. This URI is mapped correctly:
http://localhost/webjars-locator/angular-bootstrap-datetimepicker/datetimepicker.js
But this one is not mapped to the handler at all and simply returns a 404:
http://localhost/webjars-locator/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css
The fundamental difference is simply the number of components in the path which should be handled by the regular expression (".+") but does not appear to be working when that portion has slashes.
If it helps, this is provided in the logs:
2015-03-03 23:03:53.588 INFO 15324 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/webjars-locator/{webjar}/{partialPath:.+}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity app.controllers.WebJarsLocatorController.locateWebjarAsset(java.lang.String,java.lang.String)
2
Is there some type of hidden setting in Spring-Boot to enable regular expression pattern matching on RequestMappings?
The original code in the docs wasn't prepared for the extra slashes, sorry for that!
Please try this code instead:
#ResponseBody
#RequestMapping(value="/webjarslocator/{webjar}/**", method=RequestMethod.GET)
public ResponseEntity<Resource> locateWebjarAsset(#PathVariable String webjar,
WebRequest request) {
try {
String mvcPrefix = "/webjarslocator/" + webjar + "/";
String mvcPath = (String) request.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
String fullPath = assetLocator.getFullPath(webjar,
mvcPath.substring(mvcPrefix.length()));
ClassPathResource res = new ClassPathResource(fullPath);
long lastModified = res.lastModified();
if ((lastModified > 0) && request.checkNotModified(lastModified)) {
return null;
}
return new ResponseEntity<Resource>(res, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
I will also provide an update for webjar docs shortly.
Updated 2015/08/05: Added If-Modified-Since handling
It appears that you cannot have a PathVariable to match "the remaining part of the url". You have to use ant-style path patterns, i.e. "**" as described here:
Spring 3 RequestMapping: Get path value
You can then get the entire URL of the request object and extract the "remaining part".
I am using play 2.2.2. I have apis of the form
/users?token=abcd
/users?resetToken=abcd
I have configured my route as follows:
GET /users controllers.X.method(token: String)
GET /users controllers.Y.method(resetToken: String)
Now, when I make a call to the /users?resetToken=tokenvalue, I get the following error
For request 'GET /users?resetToken=tokenvalue' [Missing parameter: token]
I could have solved this by routing both the apis to the same method and then checking the query params inside the method. But I want to route the apis to two different methods because of the access restrictions on each of them. One of the apis can be accessed only after login while the other can be accessed with/without login.
Could you please help me resolve the issue?
(Adding more information:)
I tried the following:
GET /users controllers.A.genericMethod()
GET /usersByToken/:token controllers.X.method(token: String)
GET /usersByResetToken/:token controllers.Y.method(token: String)
In controllers.A,
public static Promise<Result> genericMethod(){
Map<String, String[]> queryParams = Context.current().request().queryString();
String[] tokens = queryParams.get("token");
String[] resetTokens = queryParams.get("resetToken");
if (tokens != null && tokens.length == 1) {
return Promise.pure((Result) redirect(controllers.routes.X.method(tokens[0])));
} else if (resetTokens != null && resetTokens.length == 1) {
return Promise.pure((Result) redirect(controllers.routes.Y.method(resetTokens[0])));
} else {
ObjectNode node = ControllerHelper.error(ResponseStatus.BAD_REQUEST,
"Required params not set!");
return Promise.pure((Result) badRequest(node));
}
}
In controllers.X
#SubjectPresent
public static Promise<Result> method(){
....
}
In controllers.Y
public static Promise<Result> method(){
....
}
This works from the play framework point of view.
But I am calling these apis from ember framework through ember-data. So, if I make a call to it from ember-data, say using
this.store.find('user', {token: "abcd"});
which forms the corresponding api url
/users?token=abcd
I get a response of "303, see other" and the required data is not returned.
You can't declare two routes with the same method and path, fortunately you don't have to, you have two solutions, first is declaring 'Optional parameters' (#see: doc) like
GET /users controllers.X.action(token ?= null, resetToken ?= null)
public static Result action(String token, String resetToken){
if (token==null) debug("No token given...");
// etc
}
Other solution is declaring rout w/out params, as you can still get arguments with DynamicForm
GET /users controllers.X.action()
public static Result action(){
DynamicForm dynamicForm = Form.form().bindFromRequest();
String token = dynamicForm.get("token");
if (token==null) debug("No token given...");
// etc
}
Second approach is better especially if your API has large amount of optional params.
Note: Written from top of my head
I understand that one api is protected, requiring a login, and another does not.
You need to have two separate routes, this is the best way.
GET /users controllers.X.method(token: String)
GET /public/users controllers.Y.method(resetToken: String)
Combining it into one route will not allow you to have separate access restrictions.
You need to break it into two routes, one with a private access and one with a public access.
This will change your apis obviously.
I have got stuck with the following. Lets say I have an url like
http://localhost:8080/a/somename-anyothername-onemorename-aAttributeId.html
http://localhost:8080/a/anyothername-onemorename-aAttributeId.html
http://localhost:8080/a/anyothername-onemorename-anyothername-anyothername-aAttributeId.html
...
In general the url may have a number of parts. My goal is to set the AttributeId part of the url above as a variable in #RequestMapping as it shown below
#Controller
#RequestMapping(value = "/**/a")
public class ProductPageController extends AbstractPageController
{
#RequestMapping(value = "/{attributeId:(?:[-a])(?!.*(?:[-a])).*}.html", method = RequestMethod.GET)
public String requestHandler(#PathVariable("attributeId") final String attributeId, final Model model) {
//method body
}
}
}
My question is what I do wrong with my regex? Should I split the regex somehow to escape everything before the aAttributeId.html part as the following:
#RequestMapping(value = "/{unused:*.}{productCode:(?:[-a])(?!.*(?:[-a])).*}.html", method = RequestMethod.GET)
Thanks for ideas
Update: Answer for the question
The final mapping looks as the following:
#RequestMapping(value = "/**{attributeId:a(?:[^-]+)}.html", method = RequestMethod.GET)
How about:
-a([^-]+)\.html
NB: I don't know spring syntax.