Search My Ramblings

Wednesday, August 21, 2013

Implementing a RESTful intercepting filter pattern within Oracle Service Bus

With a RESTful design for web services, you model your primary objects using a URI hierarchy, and leverage underlying HTTP (in most cases) methods to perform CRUD operations on these objects such as getting them, deleting them, and creating/updating them.  And while I am not espousing that HTTP methods map cleanly to CRUD operations, they do conceptually make it easier to speak about various CRUD operations on objects.  A common practice is to group objects under a functional umbrella URL hierarchy, which makes the object relationships more obvious and allows versioning across a group of RESTful services that share common functions.  However, in my experience trying to map this hierarchy cleanly in a service bus isn't always the easiest or most maintainable solution.


Take the following example:
Your work wants to create a taxonomy of RESTful services that are accessible from other web site domains.  This creates a cross-site scripting violation, so you suggest using a CORS solution that uses HTTP headers with modern browsers to show which sites(domain URLs) are allowed to access the RESTful services.  You expect most sites to do an HTTP OPTIONs request to verify they can access your services and what HTTP methods are exposed to them.  If you have 20+ services already defined, how best to centralize this logic, as well as other common cross-cutting aspects like auditing, security, maybe even JSON conversion, etc.?
 The answer depends on what technologies your company is using, but primarily you need to put these concerns in a common filtering proxy that all calls access before being serviced by the RESTful services themselves.  Apache web server has some capabilities here, but in my case this wasn't in the current architecture and neither this nor another similar product was desired to make the  architecture any more complex.  What was being used was the Oracle Service Bus ("OSB"), riding on the Oracle Weblogic web/application server.

When using the OSB for RESTful web services, you typically setup proxy services that listen on a specific relative URL pattern, then inspect the HTTP method and headers for how best to respond.  If for example you have proxy service A listening on /humanresources/employee and another proxy service B listening on /humanresources/department, you cannot create a new proxy C listening at /humanresources that will get requests for both A and B before they in turn process the request.  But what you can do is create a new proxy service that uses dynamic routing based on a routing table to deliver the request onward after performing some common tasks mentioned earlier.

What I wanted to accomplish in doing this is:
  • Centralize logic common across all calls, including 
    • having a single location for overall error handling and auditing
    • conversion between JSON and XML formats for easier processing later
    • security verification eventually (not initially needed here, done later in the process)
  • Easy versioning capability (if only creating a single entry point service listening on an HTTP URL, the URL can then be changed for all subsequent services in one proxy service)
  • The ability to create/manage a filter chain pattern on RESTful service URLs in OSB.

What we did:

Created an XQuery resource to represent the routing table

Each XML "row" element contains both a relative URI to match and the physical path to the related proxy/business service and looks like:

  
      user
      MyProject_REST_v1/UserSvcs/ProxyServices/GeneralUser_ps
  

  
      user/session
      MyProject_REST_v1/UserSvcs/ProxyServices/UserSession_ps
  

 Created another XQuery resource that looks up the mapping in the previous routing table using the relative URI from the HTTP request

The intention is to try and match as much of the relative URI as possible for the request, so if the relative URI is /user/settings and the routing table contains an entry for both /user/settings and /user, it will only return the /user/settings path to the handling proxy/business server.

Here is what we came up with:
(:: pragma  parameter="$routingTable" type="anyType" ::)
(:: pragma  parameter="$relativeURI" type="string" ::)
(:: pragma  type="anyType" ::)

declare namespace xf = "http://tempuri.org/EncompassLoanPipeline_Service_QA/Resources/RoutingTableParser/";
declare namespace ctx = "http://www.bea.com/wli/sb/context";

declare function xf:RoutingTableParser($routingTable as element(*),
    $relativeURI as xs:string)
    as element(*)* {
   
    let $numPaths := count(tokenize($relativeURI,"/"))
   
    for $t at $i in tokenize($relativeURI,"/")

        let $numSegments := ($numPaths - $i + 1)
        let $regExp := concat("^/?((?:/?[\w-]+){1,", $numSegments, "})[/\w-]*")
        let $currURI := replace($relativeURI, $regExp , "$1")
   
    where exists($routingTable/row[logical/text()=$currURI]/physical) 
    return
       
              {$routingTable/row[logical/text()=$currURI]/physical/text()}
           


};

declare variable $routingTable as element(*) external;
declare variable $relativeURI as xs:string external;

xf:RoutingTableParser($routingTable,
    $relativeURI)

Created an HTTP proxy service to be the centralized entry point

Added logic in the flow to orchestrate the lookup call to the above XQuery resource.  It's as simple as assigning the routing table to a variable, then calling the second XQuery resource using the routing table variable, then checking if at least one entry was returned.  Otherwise returning a 404 error (the service error handler looks for non-BEA error codes and if completely numeric uses it as the HTTP status code).



 By putting the above login in a stage by itself, the routing logic is encapsulated and if necessary can later be called out to a database instead for the lookup if the routing table grows too large to be practical.  If using a database, leveraging Coherence as a caching solution would make sense to keep performance at a reasonable level, but that is outside the scope of this post.  Maybe a later one.  :)

No comments: