Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
engswee
Active Contributor

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:-

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

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).

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.

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

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.

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

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.

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)

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

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.

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.

Next, populate the adapter attributes and activate the channel.

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.

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

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.

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

3 Comments
Labels in this area