Skip to Content
Author's profile photo Eng Swee Yeoh

Demystifying Custom Adapter Development: Part 5 – Creating an HTTP Poller with OAuth 2.0 authentication

Introduction

In this final part of the custom adapter series, we will be looking at modifying the functionality for the sender side of the adapter. Although generally, both sender and receiver sides of an adapter will use the same transport/message protocols, it will not be the case here for the sake of demonstration.

In particular, we will change the functionality of the sender side to incorporate an HTTP poller with OAuth 2.0 authentication capability. OAuth 2.0 authentication differs in flavor, so for this case we will implement it based on the Native Flow authentication for Concur. This will be similar to the recently introduced capability in the REST adapter (Option 3) – PI REST Adapter – Connect to Concur.

As certain parts of the development have already been covered in previous parts of this series, I will omit those repetitive portions from this part. Instead, I will focus on certain aspects that might not have been covered earlier. For the complete details of the development, refer to the source codes of the Eclipse projects in GitHub equalize-xpi-adapter-sample repository.

Concur OAuth 2.0 Authentication

Access to Concur’s REST-based APIs is achieved via an OAuth 2.0 authentication method. This is a two-step approach, whereby an OAuth 2.0 access token must first be retrieved from a Token Endpoint prior to calling the target REST API. For more details, refer to the following:-

Concur Developer Portal | Authentication

Adapter Metadata Changes

First of all, we will update SampleRA.xml which defines the adapter metadata. Following are the changes:-

i) Replace old Transport Protocol with new Transport Protocol HTTP

ii) Replace old Message Protocol with new Message Protocol OAuth2

iii) Define channel attributes and their corresponding details

Modify the Inbound section of the file as below:-

/wp-content/uploads/2016/06/proto_970030.png

The adapter metadata will be defined such that the channel configuration contains the following sections as shown in the screenshot below:-

i) Target URL for the REST API

ii) Proxy settings

iii) OAuth 2.0 settings

iv) Polling settings

/wp-content/uploads/2016/06/metadata_970027.png

The full definition is provided in the source code, but I want to highlight a few additional aspects of the metadata definition.

i) Conditional parameters

Certain parameters can be set conditional depending upon the value of a reference parameter. In the section below, parameter proxyhost is only available for input when useProxy is 1 (true).

/wp-content/uploads/2016/06/useproxy_970025.png

ii) Password masking

For parameters that contain sensitive information such as password, they can be defined with an attribute isPassword=”true” so that the password is masked when it is entered in the channel.

/wp-content/uploads/2016/06/ispassword_970026.png

Source Code Changes

In order to perform HTTP calls, we will be utilising the SAP’s HTTP Client Library that is already available in the AS Java system. It is also possible to use third party libraries like Apache but that will involve downloading the relevant JARs and including them into the deployment archives.

In order to use the library, download the following JAR file and include it as an External JAR in the build path of the Eclipse project.

/usr/sap/<SID>/J<nr>/j2ee/cluster/bin/ext/httpclient/lib/sap.com~httpclient.jar

/wp-content/uploads/2016/06/httpclient_970032.png

i) XIConfiguration

Similar to the previous part for receiver adapter, we need to tweak the logic since the metadata for the adapter has changed.

In methods channelAdded() and init(), comment out the old attributes and add logic using new attribute urlEndpoint.

/wp-content/uploads/2016/06/xicfg1_970033.png

In method getChannelStatus(), comment out section that uses the old attributes and add logic using urlEndpoint.

/wp-content/uploads/2016/06/xicfg2_970034.png

ii) SPIManagedConnectionFactory

This is the core class that contains the logic for the sender adapter.

First of all, comment out the existing logic of the sample JCA adapter. In the run() method, comment out the following try-catch block within the for loop.

/wp-content/uploads/2016/06/smcf1_970098.png

After the end of the commented try-catch block, add the logic below which does the following:-

  • Retrieve polling interval configured in the sender channel
  • Use the MonitoringManager to report the process status of the channel. This in effect logs the status in the transient Memory Logs viewable in Communication Channel Monitor
  • Executes new method runChannel() to process the channel (more details below)

/wp-content/uploads/2016/06/smcf2_970099.png

The polling interval retrieved above is used in the following (existing) logic that uses a wait statement to implement the polling mechanism.

/wp-content/uploads/2016/06/smcf3_970103.png

Add the following new methods:-

runChannel() – This method retrieves all the values from the sender channel, then calls the subsequent two methods to perform channel processing.


  private void runChannel(Channel channel) throws Exception {
    // Retrieve the channel configuration values
    String urlEndpoint = channel.getValueAsString("urlEndpoint");
    boolean useProxy = channel.getValueAsBoolean("useProxy");
    String proxyhost = channel.getValueAsString("proxyhost");
    int proxyport = channel.getValueAsInt("proxyport");
    String proxyuser = channel.getValueAsString("proxyuser");
    String proxypwd = channel.getValueAsString("proxypwd");
    String tokenEndpoint = channel.getValueAsString("tokenEndpoint");
    String consumerKey = channel.getValueAsString("consumerKey");
    String user = channel.getValueAsString("user");
    String pwd = channel.getValueAsString("pwd");
    // Update channel processing status
    MonitoringManager mm = MonitoringManagerFactory.getInstance().getMonitoringManager();
    ProcessContext pc = ProcessContextFactory.getInstance().createProcessContext(ProcessContextFactory.getParamSet().channel(channel));
    mm.reportProcessStatus(this.adapterNamespace, this.adapterType, ChannelDirection.SENDER , ProcessState.OK, "Polling endpoint: " + urlEndpoint, pc);
    // Execute the HTTP polling, then create & dispatch the message to the Adapter Framework
    String output = execHTTPGet(tokenEndpoint, urlEndpoint, user, pwd, consumerKey, useProxy, proxyhost, proxyport, proxyuser, proxypwd);
    createMessage(output.getBytes("UTF-8"), channel);
  }



execHTTPGet() – This method executes the HTTP GET calls using the HTTP Client library. It first accesses the Token Endpoint to retrieve the OAuth token, then extracts the token value via XPath, and subsequently calls the target URL using OAuth authentication.


  private String execHTTPGet(String tokenEndpoint, String urlEndpoint, String user, String pwd,
      String consumerKey, boolean useProxy, String proxyhost, int proxyport,
      String proxyuser, String proxypwd) throws Exception {
    HttpClient client = new HttpClient();
    // Set proxy details
    if(useProxy) {
      HostConfiguration hostConfig = new HostConfiguration();
      hostConfig.setProxy(proxyhost, proxyport);
      client.setHostConfiguration(hostConfig);
      AuthScope ourScope = new AuthScope(proxyhost, proxyport, "realm");
      UserPassCredentials userPass = new UserPassCredentials(proxyuser, proxypwd);
      client.getState().setCredentials(ourScope, userPass);
    }
    // Retrieve the OAuth token from the token endpoint
    GET httpGet = new GET(tokenEndpoint);
    String b64encodedLogin = DatatypeConverter.printBase64Binary((user + ":" + pwd).getBytes());
    httpGet.setRequestHeader("Authorization", "Basic " + b64encodedLogin);
    httpGet.setRequestHeader("X-ConsumerKey", consumerKey);
    String token = null;
    try {
      client.executeMethod(httpGet);
      // Parse the response and retrieve the value of the token
      ConversionDOMInput domIn = new ConversionDOMInput(httpGet.getResponseBodyAsString());
      token = domIn.evaluateXPathToString("/Access_Token/Token");
    } finally {
      httpGet.releaseConnection();
    }
    // Execute the call to the target URL using the OAuth 2.0 token for authorization
    GET httpGet2 = new GET(urlEndpoint);
    httpGet2.setRequestHeader("Authorization", "OAuth " + token);
    try {
      client.executeMethod(httpGet2);
      return httpGet2.getResponseBodyAsString();
    } finally {
      httpGet2.releaseConnection();
    }
  }



createMessage() – This method creates the XI asynchronous message and sends it to the Adapter Framework to be processed by the Messaging System. Additionally, it adds some entries into the audit log of the message.


  private void createMessage(byte[] content, Channel channel) {
    try {
      // Retrieve the binding details from the channel
      Binding binding = CPAFactory.getInstance().getLookupManager().getBindingByChannelId(channel.getObjectId());
      String action = binding.getActionName();
      String actionNS = binding.getActionNamespace();
      String fromParty = binding.getFromParty();
      String fromService = binding.getFromService();
      String toParty = binding.getToParty();
      String toService = binding.getToService();
      // Normalize wildcards and null's to "non-specified" address value
      if ( (fromParty == null) || (fromParty.equals("*")) )
        fromParty = new String("");
      if ( (fromService == null) || (fromService.equals("*")) )
        fromService = new String("");
      if ( (toParty == null) || (toParty.equals("*")) )
        toParty = new String("");
      if ( (toService == null) || (toService.equals("*")) )
        toService = new String("");
      if ( (action == null) || (action.equals("*")) )
        action = new String("");
      if ( (actionNS == null) || (actionNS.equals("*")) )
        actionNS = new String("");
      // Create the XI message and populate the headers and content
      if(this.mf == null) {
        this.mf = new XIMessageFactoryImpl(channel.getAdapterType(), channel.getAdapterNamespace());
      }
      Message msg = this.mf.createMessageRecord(fromParty, toParty, fromService, toService, action, actionNS);
      msg.setDeliverySemantics(DeliverySemantics.ExactlyOnce);
      XMLPayload xp = msg.createXMLPayload();
      xp.setContent(content);
      xp.setContentType("application/xml");  
      xp.setName("MainDocument");
      xp.setDescription("EQ Adapter Polling Output");
      msg.setDocument(xp);
      // Set the message into the module for processing by the module processor
      ModuleData md = new ModuleData();
      md.setPrincipalData(msg);
      TransactionTicket txTicket = null;
      try {
        txTicket = TxManager.required();
        MessageKey amk = new MessageKey(msg.getMessageId(), MessageDirection.OUTBOUND);
        md.setSupplementalData("audit.key", amk);
        audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, "Asynchronous message was polled and will be forwarded to the XI AF MS now.");
        audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, "Name of the polled URL: {0}.", new Object[] {channel.getValueAsString("urlEndpoint")});
        audit.addAuditLogEntry(amk, AuditLogStatus.WARNING, "Demo: This is a warning audit log message");
        // And flush them into the DB
        audit.flushAuditLogEntries(amk);
  
        // Process the module
        ModuleProcessorFactory.getModuleProcessor(true, 1, 1000).process(channel.getObjectId(), md);
        // Update the channel status
        MonitoringManager mm = MonitoringManagerFactory.getInstance().getMonitoringManager();
        ProcessContext pc = ProcessContextFactory.getInstance().createProcessContext(ProcessContextFactory.getParamSet().channel(channel).message(msg));
        mm.reportProcessStatus(this.adapterNamespace, this.adapterType, ChannelDirection.SENDER , ProcessState.OK, "Message sent to AF", pc);
  
      } catch (TxRollbackException e) {
      } catch (TxException e) {
      } catch (Exception e) {
        TxManager.setRollbackOnly();
      } finally {
        if(txTicket != null)
          try {
            TxManager.commitLevel(txTicket);
          } catch (Exception e) {      
          }
      }
    } catch (Exception e) {
      TRACE.errorT("createMessage()", XIAdapterCategories.CONNECT_AF, "Received exception: " + e.getMessage());
    }
  }



Other Changes

Now that we are done with the biggie changes, just a couple more changes and we can test out our new adapter!

I’ll increment the minor version of the adapter in both ra.xml and SAP_MANIFEST.MF.

/wp-content/uploads/2016/06/ra_11_970138.png

/wp-content/uploads/2016/06/sap_mani_11_970166.png

Once this is complete, perform the normal export and deployment steps.

Configuration and Testing

Now we can configure the adapter and test it out.

Configure a sender channel using the custom adapter. The transport and message protocol will be automatically reflected.

/wp-content/uploads/2016/06/channel1_970035.png

Next, populate the adapter attributes and activate the channel.

/wp-content/uploads/2016/06/channel2_970036.png

Configure an integration scenario (classical, ICO or IFlow) that uses the sender channel. This step will not be covered in this post.

Once the scenario has been configured and activated/deployed, we can check the Communication Channel Monitor to see the processing state of the channel. As shown below, the memory logs are populated during each polling cycle of the channel.

/wp-content/uploads/2016/06/channel_log_970079.png

When we view the message generated, we can see the audit log entries in the message log.

/wp-content/uploads/2016/06/audit_log_970080.png

Finally, we can view the payload that was received from the REST API via the polling channel. This confirms that the adapter logic is able to perform OAuth authentication and access Concur’s REST API successfully.

/wp-content/uploads/2016/06/payload_970081.png

Conclusion

Finally, at the end of this custom adapter series, we have covered most of the main areas and aspects of custom adapter development. In this example, I have demonstrated the possibility of resolving another integration requirement using custom adapter. OAuth or multi-step HTTP calls are getting common these days with services such as SFDC and Successfactors.

Other Parts of this Series

Demystifying Custom Adapter Development: Part 1a – Cloning the Sample JCA Adapter

Demystifying Custom Adapter Development: Part 1b – Cloning the Sample JCA Adapter

Demystifying Custom Adapter Development: Part 2 – Examining the Adapter’s Key Classes and Methods

Demystifying Custom Adapter Development: Part 3 – Examining the Deployment and Metadata Files

Demystifying Custom Adapter Development: Part 4 – Modifying the Adapter’s Functionality

Assigned Tags

      3 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Samuel Shen
      Samuel Shen

      Hi Eng

      I Google but I cannot find any example for REST sender/receiver for OAUTH. Is it possible to do OAUTH without creating JAVA code? I mean just using ESR and IB objects to connect using OAUTH? Is it possible to do a proof of concept to connect to Google using OAUTH? thanks

      Author's profile photo Eng Swee Yeoh
      Eng Swee Yeoh
      Blog Post Author

      Hi Samuel

      If your system has the SAP REST adapter, you can check try using the OAuth functionality there to connect to Google. However, do note that different service providers implement OAuth differently, so no guarantee that it will work until you try it out.

      If you have further issue with using the REST adapter, I'd suggest you open a discussion thread instead of commenting on this post.

      Regards

      Eng Swee

      Author's profile photo Tchalkov Rayko
      Tchalkov Rayko

      Hi Eng,

       

      Can you highlight the steps to develop a custom sender adapter that accepts HTTP POST requests?

       

      Kind Regards,

      Rayko