20

Question

Running into a SalesForce REST mapping issue involving multiple wildcards in an url. Is this not supported? Is there anyway to supporting routing for nested resources?

My Setup

I have this REST structure

  • GET /teams/{teamid}/members (list team members)
  • POST /teams/{teamid}/members (create team member)
  • PUT /teams/{teamid}/members/{memberid} (update team member)
  • DELETE /teams/{teamid}/members/{memberid} (delete team member

And I setup the routing as followed:

  • REST_teams_id_members.cls @RestResource(urlMapping='/v1/teams/*/members')
  • REST_teams_id_members_id.cls @RestResource(urlMapping='/v1/teams/*/members/*')

Issue

This was working fine, but now I can only get one set of routes to work at any given time. Which ever one was compiled first gets precedence. So both GET /teams/123/members and DELETE/teams/123/members/345` get routed to the same class.

So If I compile REST_teams_id_member.cls (making the other class the first compiled), I get the following routing

  • GET /teams/{teamid}/members -> REST_teams_id_members_id.cls -> 405 Method Not Allowed
  • POST /teams/{teamid}/members -> REST_teams_id_members_id.cls -> 405 Method Not Allowed
  • PUT /teams/{teamid}/members/{memberid} -> REST_teams_id_members_id.cls -> Good
  • DELETE /teams/{teamid}/members/{memberid} -> REST_teams_id_members_id.cls -> Good

If I then compile REST_teams_id_members_id.cls (swapping the order), I get this routing

  • GET /teams/{teamid}/members -> REST_teams_id_members.cls -> Good
  • POST /teams/{teamid}/members -> REST_teams_id_members.cls -> Good
  • PUT /teams/{teamid}/members/{memberid} -> REST_teams_id_members.cls -> 405 Method Not Allowed
  • DELETE /teams/{teamid}/members/{memberid} -> REST_teams_id_members.cls -> 405 Method Not Allowed

Discussion

I feel like I'm taking crazy pills, it doesn't make any sense how /teams/123/members/345 could possible match @RestResource(UrlMapping='/teams/*/members), and vice versa, I'm at a total loss for how /teams/123/members could route to @RestResource(UrlMapping='teams/*/members/*. They seem wholly distinct!!

We've got an imminent rollout and I'm complete stuck. Praying that SFSE community can help me out.

  • what happens if you try: REST2.CLS @RestResource(urlMapping='/v1/teams/*/members/*/') – NSjonas Aug 21 '14 at 19:01
  • just tried it, no dice, /teams/123/members/345 returns 404 with Could not find a match for URL /v1/teams_fix/123123/members/123123 – Ralph Callaway Aug 21 '14 at 19:06
  • Have you tried putting both routes in the same class, so they compile togather? – NSjonas Aug 21 '14 at 19:22
  • each class can only have one route, if needed we're going to use a more generic route /v1/teams/*/members* that will match both, it just means that each function needs to do it's own internal routing management to return 405 for invalid URLs, i.e. GET /v1/teams/123/members/345 would need to be coded to return 405 GET Not Supported – Ralph Callaway Aug 21 '14 at 19:57
  • using /v1/teams/*/members* produces the following compile error: (API_V1_REST_teams_id_members) Invalid URL mapping in class API_V1_REST_teams_id_members: A wildcard in a URL mapping must be preceded by a forward slash (/). (Line: 2) – David Staley Aug 21 '14 at 21:34
8
+200

While it's not clear why that doesn't already work, to get your code out the door, my suggestion is to build your own REST dispatching service. The following pseudo code implementation works as followed:

  1. Register for all REST requests for a particular url (e.g. /teams/*)
  2. Register different controllers (which implement the interface Dispatchable)
  3. In case the url matches the url of your dispatchable class, the method execute is executed

Pseudo Code

@RestResource(urlMapping = '/teams/*')
global class RestDispatcher
{
    static Map<String, List<Dispatchable>> dispatchables;
    static
    {
        dispatchables = new Map <String, List<Dispatchable>>
        {
            'POST' => new List<Dispatchable>(),
            'GET' => new List<Dispatchable>()
        };

        // register your class here e.g. dispatchables.get('POST').add(new DispatchableClass());
    }


    global interface Dispatchable
    {
        String getURLMapping(); // e.g. /teams/{teamNumber}/members/{memberId}
        void execute(Map<String, String> parameters);
    }

    @HTTPGet
    global static void doGET()
    {
        execute('GET');
    }

    @HTTPPOST
    global static void doPOST()
    {
        execute('POST');
    }

    private static void execute(String httpMethod)
    {
        RestRequest request = RestContext.request;
        for (Dispatchable d : dispatchables.get(httpMethod))
        {
            if(match(d.getURLMapping, request.requestURI))
            {
                d.execute(getParamters(request.requestURI, d.getURLMapping()));
            }
        }
    }

    global static Map<String, String> getParamters(String requestURI, String dispatcherURI)
    {
        // To-Do: implement an extraction method here.
        return new Map<String, String>();
    }

    global static boolean match(String dispatchURI, String reuquestURI)
    {
        if(true) // To-Do: add a matching algorith here
        {
            return true;
        }
        return false;
    }

}

Code: https://gist.github.com/cdeckert/6991034790b0a4c9a34c

| improve this answer | |
  • 1
    was afraid of that, the main reason we developed an apex rest webservice (as opposed to building one ourselves and using REST/SOAP API to talk to salesforce) was because it had routing and parameter decoding out of the box. approach totally makes sense, just frustrated we have to manually code a feature that should work already – Ralph Callaway Aug 27 '14 at 0:32
  • Hi Christian, thank you for your great dispatcher. There is a bug in getParameters(): The parameter name as the map key is set with the curly bracket. E. g. "teamNumber}" instead of "teamNumber". result.put(dispachableURIList.get(i).subString(1, dispachableURIList.get(i).length()), requestURIList.get(i)); should be result.put(dispachableURIList.get(i).subString(1, dispachableURIList.get(i).length()-1), requestURIList.get(i)); – georg w. Nov 11 '15 at 11:29
5

As per the above suggestion, you will likely need to manage some of the routing yourself, however we can use regex to make this a simple process.

Below is some example code I have tested in the workbench:

@RestResource(urlMapping='/teams/*/members/*')
global with sharing class TeamMemberService {

  @HttpGet
  global static String doGet() {

    Pattern listTeamMemberPattern = Pattern.compile('/teams/(\\d+)/members');

    Matcher match = listTeamMemberPattern.matcher(RestContext.request.requestURI);
    if(match.matches()) {
        return JSON.serializePretty('GET me team ' + match.group(1));
    }
    else 
    {
        return JSON.serializePretty('ERROR - incorrect route used');
    }

    return JSON.serializePretty('ERROR - incorrect route used');

  }

  @HttpPut
  global static String doPut() {

    Pattern updateTeamMemberPattern = Pattern.compile('/teams/(\\d+)/members/(\\d+)');

    Matcher match = updateTeamMemberPattern.matcher(RestContext.request.requestURI);
    if(match.matches()) {
        return JSON.serializePretty('PUT data on member ' + match.group(2) + ' in team ' + match.group(1));
    }
    else 
    {
        return JSON.serializePretty('ERROR - incorrect route used');
    }

    return JSON.serializePretty('ERROR - incorrect route used');
  }

}

As you can see, this will allow you to match what pattern is coming into the methods so you can then run the method you require. Obviously the code needs cleaning a little but this would allow you to do fully nested resources.

EDIT

The reason Apex REST does not support nested resources/multiple wildcards is due to the matching rules. The rules in the documentation state

The rules for mapping URLs are:

  • An exact match always wins.
  • If no exact match is found, find all the patterns with wildcards that match, and then select the longest (by string length) of those.
  • If no wildcard match is found, an HTTP response status code 404 is returned.

In the case of the defined patterns above, when we add the final wildcard '/*' we redefine the pattern to be matched so that /teams/{teamid}/members does not match our defined pattern of /teams/*/members/*. Adding the extra slash /teams/{teamid}/members/ would possibly resolve this however is not a valid REST route and would still need the validation that GET /teams/123/members/456 was not being called (i.e. with a member number).

The system works as anticipated in the documentation, perhaps an idea should be promoted to make this easier to do though.

EDIT 2:

I placed some similar code in Episode 13 of the @forcedotcomcast video series which shows how you can use nested resources

| improve this answer | |
0

I wrote a lightweight framework called Apex-Rest-Route to standardize building RESTful API's in RestResources.

It lets you define and respond to deeply nested routes from a single @RestResource.

The readme shows an example of how you could implement a similar URI structure.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.