Why is Jersey's UriBuilder.build method encoding #'s and %'s, but not /'s? - rest

I have a REST API which is fairly typical, except that the id's of resources are not integers, but strings, which often contain / characters. So if a customer's id is string/with/slashes then the URI for that customer should be http://localhost/customers/string%2Fwith%2Fslashes. When returning a list of customers, I want to construct that URI with a UriBuilder so I can put it in the href of ATOM-style link elements. But it doesn't quite work; here's a small test class that shows what I mean:
#Path("/customers")
public class JerseyTest {
#Path("{id}")
public Customer getCustomer(#PathParam("{id}") String id) {
return null;
}
public static void main(String[] args) {
buildURI("string#with#hashes"); // => http://localhost/customers/string%23with%23hashes
buildURI("string/with/slashes"); // => http://localhost/customers/string/with/slashes
}
public static void buildURI(String id) {
UriBuilder builder = UriBuilder.fromUri("http://localhost");
builder.path(JerseyTest.class).path(JerseyTest.class, "getCustomer");
URI uri = builder.build(id);
System.out.println(uri);
}
}
The #'s get encoded as I would expect but the /'s don't. I tried using builder.build(URLEncoder.encode(id)) instead, but then the UriBuilder encodes the %'s so you get .../string%252Fwith%252Fslashes!
It seems inconsistent to me that it encodes # and % but not /, but I suspect there is a good reason for it which I am not seeing. So my question is:
How can I get UriBuilder to give me .../string%2Fwith%2Fslashes, which is the URI that causes Jersey to call getCustomer with id equal to string/with/slashes? edit: I discovered a way to solve this: builder.buildFromEncoded(URLEncoder.encode(id)). Leaving this question open though, in hopes of getting an answer to the second part...
More generally, why does UriBuilder.build encode some special characters, but not others?
I found How do I encode URI parameter values?, where the accepted answer says "Use UriBuilder." Well, I am using it, but apparently I'm using it incorrectly.

This seem to be a confirmed issue:
http://java.net/jira/browse/JAX_RS_SPEC-70
Your workaround sounds good.

Related

Jaxrs QueryParam as a sub or a root item

Hi the design document I'm working off of wants me to have a URL in the pattern of
<root>/v1/installs/XYZ123/actions/next?app=1234ABCD HTTP/1.1
However the only examples I can find are extremely simple, and only show URLs that would end at the /next.
http://www.mkyong.com/webservices/jax-rs/jax-rs-queryparam-example/
I'm thinking it's something like... ? I'm missing a key step
#GET
#Produces({ "application/json" })
#Path("v1/installs/{id}/actions<SOMETHINGHERE?>/next HTTP/1.1")
public Response getSetupCommands(#PathParam("id")
String id,#QueryParam("next") String next) {
I'm using jboss and jaxrs
Why do I get the feeling that HTTP/1.1 should not be a part of the URL. You may have read/understood the design document incorrectly. If it was saying that the request should look like
GET /v1/installs/XYZ123/actions/next?app=1234ABCD HTTP/1.1
Then you only need to be worried about /v1/installs/XYZ123/actions/next?app=1234ABCD. HTTP/1.1 is simply the HTTP version that will be used implicitly with every request and response.
Your original example was fine, exception you should have replaced #QueryParam("next") with #QueryParam("app"). next is actually part of the path.
This /v1/installs/{id}/actions/next should be what's included in #Path.
The complete semantics of this request URL seems to read something like:
Get the next (resource) controller, and we will use the app query parameter as an argument to pass to this controller.
UPDATE: with example
#Path("/v1")
public class QueryResource {
#GET
#Path("/installs/{id}/actions/next")
public Response getResponse(#PathParam("id") String id,
#QueryParam("app") String app) {
StringBuilder sb = new StringBuilder();
sb.append("ID: ").append(id).append("<br/>");
sb.append("app param: ").append(app);
return Response.ok(sb.toString()).build();
}
}
Browser Test
Fire bug
v1/installs/{id}/actions/{next : .+}
public Response getSetupCommands(#PathParam("id") String id,#PathParam("next") String next) {
Way too hackish for my tastes, but I don't have any control in this situation..

Restful API and SpringMVC, should I make path variables as #RequestBody object's fields for convenience?

I own an application for collecting feedback.
The api looks like this:
POST /customer/feedback/{feedbackId} HTTP/1.1
Host: localhost:9999
Content-Type: application/json
Content-Body: {'qaSays':'Approved', 'qaRating':98}
//This results the feedback (find by feedbackId) gets updated
The application is built using spring-mvc
//in controller
#RequestMapping(value="/customer/feedback/{feedbackId}")
#ResponseStatus(OK)
public void handle(#PathVariable("feedbackId") feedbackId,
#RequestBody ApproveFeedbackCommand command) {
command.setFeedbackId(feedbackId);
commandGateway.send(command);
}
//Command
public class ApproveFeedbackCommand {
private String feedbackId;//makes no sense for client
private String qaSays;
private int qaRating;
}
The solution above works, but I have a feeling that something goes wrong here.
The question is:
1.Should I use ApproveFeedbackCommand as #RequestBody just for convenience?
The extra field "feedbackId" may confuse the other developer, and it is annoying to set path variables manually.
2.Should I add an ApproveFeedbackBody(exclude "feedbackId") for separate concerns?
But this seems to add some duplicate code and some manual command parameter extraction.
#RequestMapping(value="/customer/feedback/{feedbackId}")
#ResponseStatus(OK)
public void handle(#PathVariable("feedbackId") id,
#RequestBody ApproveFeedbackBody body) {
ApproveFeedbackCommand command = convert(id, body);//manual convert here
commandGateway.send(command);
}

Is it possible to place variables into a resource path within a sling servlet?

We are trying to provide a clean URI structure for external endpoints to pull json information from CQ5.
For example, if you want to fetch information about a particular users history (assuming you have permissions etc), ideally we would like the endpoint to be able to do the following:
/bin/api/user/abc123/phone/555-klondike-5/history.json
In the URI, we would specifying /bin/api/user/{username}/phone/{phoneNumber}/history.json so that it is very easy to leverage the dispatcher to invalidate caching changes etc without invalidating a broad swath of cached information.
We would like to use a sling servlet to handle the request, however, I am not aware as to how to put variables into the path.
It would be great if there were something like #PathParam from JaxRS to add to the sling path variable, but I suspect it's not available.
The other approach we had in mind was to use a selector to recognise when we are accessing the api, and thus could return whatever we wanted to from the path, but it would necessitate a singular sling servlet to handle all of the requests, and so I am not happy about the approach as it glues a lot of unrelated code together.
Any help with this would be appreciated.
UPDATE:
If we were to use a OptingServlet, then put some logic inside the accepts function, we could stack a series of sling servlets on and make the acceptance decisions from the path with a regex.
Then during execution, the path itself can be parsed for the variables.
If the data that you provide comes from the JCR repository, the best is to structure it exactly as you want the URLs to be, that's the recommended way of doing things with Sling.
If the data is external you can create a custom Sling ResourceProvider that you mount on the /bin/api/user path and acquires or generates the corresponding data based on the rest of the path.
The Sling test suite's PlanetsResourceProvider is a simple example of that, see http://svn.apache.org/repos/asf/sling/trunk/launchpad/test-services/src/main/java/org/apache/sling/launchpad/testservices/resourceprovider/
The Sling resources docs at https://sling.apache.org/documentation/the-sling-engine/resources.html document the general resource resolution mechanism.
It is now possible to integrate jersy(JAX-RS) with CQ. We are able to create primitive prototype to say "Hello" to the world.
https://github.com/hstaudacher/osgi-jax-rs-connector
With this we can use the #PathParam to map the requests
Thanks and Regards,
San
There is no direct way to create such dynamic paths. You could register servlet under /bin/api/user.json and provide the rest of the path as a suffix:
/bin/api/user.json/abc123/phone/555-klondike-5/history
^ ^
| |
servlet path suffix starts here
then you could parse the suffix manually:
#SlingServlet(paths = "/bin/api/user", extensions = "json")
public class UserServlet extends SlingSafeMethodsServlet {
public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) {
String suffix = request.getRequestPathInfo().getSuffix();
String[] split = StringUtils.split(suffix, '/');
// parse split path and check if the path is valid
// if path is not valid, send 404:
// response.sendError(HttpURLConnection.HTTP_NOT_FOUND);
}
}
The RESTful way to approach this would be to have the information stored in the structure that you want to use. i.e. /content/user/abc123/phone/555-klondike-5/history/ would contain all the history nodes for that path.
In that usage. you can obtain an out of the box json response by simply calling
/content/user/abc123/phone/555-klondike-5/history.json
Or if you need something in a specific json format you could use the sling resource resolution to use a custom json response.
Excited to share this! I've worked ~ a week solving this, finally have the best Answer.
First: Try to use Jersey
The osgi-jax-rs-connector suggested by kallada is best, but I couldn't get it working on Sling 8. I lost a full day trying, all I have to show for it are spooky class not found errors and dependency issues.
Solution: The ResourceProvider
Bertrand's link is for Sling 9 only, which isn't released. So here's how you do it in Sling 8 and older!
Two Files:
ResourceProvider
Servlet
The ResourceProvider
The purpose of this is only to listen to all requests at /service and then produce a "Resource" at that virtual path, which doesn't actually exist in the JCR.
#Component
#Service(value=ResourceProvider.class)
#Properties({
#Property(name = ResourceProvider.ROOTS, value = "service/image"),
#Property(name = ResourceProvider.OWNS_ROOTS, value = "true")
})
public class ImageResourceProvider implements ResourceProvider {
#Override
public Resource getResource(ResourceResolver resourceResolver, String path) {
AbstractResource abstractResource;
abstractResource = new AbstractResource() {
#Override
public String getResourceType() {
return TypeServlet.RESOURCE_TYPE;
}
#Override
public String getResourceSuperType() {
return null;
}
#Override
public String getPath() {
return path;
}
#Override
public ResourceResolver getResourceResolver() {
return resourceResolver;
}
#Override
public ResourceMetadata getResourceMetadata() {
return new ResourceMetadata();
}
};
return abstractResource;
}
#Override
public Resource getResource(ResourceResolver resourceResolver, HttpServletRequest httpServletRequest, String path) {
return getResource(resourceResolver , path);
}
#Override
public Iterator<Resource> listChildren(Resource resource) {
return null;
}
}
The Servlet
Now you just write a servlet which handles any of the resources coming from that path - but this is accomplished by handling any resources with the resource type which is produced by the ResourceProvider listening at that path.
#SlingServlet(
resourceTypes = TypeServlet.RESOURCE_TYPE,
methods = {"GET" , "POST"})
public class TypeServlet extends SlingAllMethodsServlet {
static final String RESOURCE_TYPE = "mycompany/components/service/myservice";
#Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
final String [] pathParts = request.getResource().getPath().split("/");
final String id = pathParts[pathParts.length-1];
response.setContentType("text/html");
PrintWriter out = response.getWriter();
try {
out.print("<html><body>Hello, received this id: " + id + "</body></html>");
} finally {
out.close();
}
}
}
Obviously your servlet would do something much more clever, such as process the "path" String more intelligently and probably produce JSON.

Spring MVC REST

I'm using Spring MVC and I have a controller mapped to a url lets call it example. I also have a method called show that allows me to view one of my examples based on an id.
#RequestMapping("/example")
#RequestMapping(value = "/{id}", produces = "text/html")
public String show(#PathVariable("id") String id, Model model) {
//Do some stuff and return a view
}
The problem is that the id is a URI and it has forward slashes. (e.g. test/case/version/sample might be an id so the resulting url is example/test/case/version/sample) so as a result my application gives me an error "Requested resource not found". I can't easily change the format of these ids. It's a list given to me that I have to work with. Is there a way around this? Thanks in Advance.
You can try using Regular expressions on the #PathVariable.
Like this from the Spring Docs:
#RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(#PathVariable String version, #PathVariable String extension) {
// ...
}
}
You'll just have to think on a regular expression that matches the "example/test/case/version/sample" that is your expression.
See the title: "URI Template Patterns with Regular Expressions"
on this page: http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/mvc.html for more information

Handling MVC2 variables with hyphens in their name

I'm working with some third-party software that creates querystring parameters with hyphens in their names. I was taking a look at this SO question and it seems like their solution is very close to what I need but I'm too ignorant to the underlying MVC stuff to figure out how to adapt this to do what I need. Ideally, I'd like to simply replace hyphens with underscores and that would be a good enough solution. If there's a better one, then I'm interested in hearing it.
An example of a URL I want to handle is this:
http://localhost/app/Person/List?First-Name=Bob&My-Age=3
with this Controller:
public ActionResult List(string First_Name, int My_Age)
{
{...}
}
To repeat, I cannot change the querystring being generated so I need to support it with my controller somehow. But how?
For reference, below is the custom RouteHandler that is being used to handle underscores in controller names and action names from the SO question I referenced above that we might be able to modify to accomplish what I want:
public class HyphenatedRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
Have you tried [Bind(Prefix="First-name")]? It might work...
One way would be with a custom model binder. Another way would be with an action filter. Use the model binder if you want to do this on a specific type. Use the action filter if you want to do this on a specific action or controller. So for the latter method you could do something like:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var keys = filterContext.HttpContext.Request.QueryString.AllKeys.Where(k => k.Contains('-'));
foreach(var k in keys)
{
filterContext.ActionParameters.Add(
new KeyValuePair<string, object>(
k.Replace('-', '_'), filterContext.HttpContext.Request.QueryString[k]));
}
base.OnActionExecuting(filterContext);
}
I had the same problem. In the end rather than doing something too complex I just get the query string parameters using
string First_Name = Request["First-Name"];
You may want to check for NUlls incase the parameter is not there, but this sorted it out for me. You can also include an optional parameter for the ActionResult for test purposes etc..