Integrating Microsoft SharePoint Document Management with Oracle Sales Cloud

Overview

Customer Relationship Management (CRM) software, now referred to more often as Customer Experience (CX) software, tends to be customized or extended more often than HCM, ERP, or SCM applications.  The reason:  enterprise business processes in the customer experience realm tend to be non-standard and more diverse across organizations.  With wide diversity, it is impossible for CX software applications in their out-of-the-box (OOTB) states to support organizations’ sales support processes.  OOTB for CX will seldom be workable for enterprises; more often it will be necessary to customize, extend, and integrate CX applications.  Multi-faceted support for extensibility and integration has become a must-have set of features for vendors contending in the CX software space.

Oracle Sales Cloud (OSC) fully supports integrations at a number of levels with other Oracle cloud software products and with third-party software.  Integration can be accomplished through API’s and/or through UI mash-ups, depending on requirements.  Integration makes it possible for salespeople to access features, functions, and data in multiple products as if they were interacting with only one application.  Integration, if done correctly, maximizes user productivity.  Forcing users into context switches – requiring them to shell out of one application to look up a piece of data or go through a workflow in another application – lessens productivity.  Sales representatives and managers are far too busy for a fragmented, unproductive user experience.

This post provides a blueprint for integrating Sales Cloud with Microsoft SharePoint, specifically around hooking SharePoint’s powerful document management functions into Sales Cloud business processes. For organizations with existing investments in both Sales Cloud and SharePoint, this integration can reap major benefits.  If an organization has standardized on the SharePoint platform, it makes sense to leverage it in as many ways as possible.  The goal of this integration is to allow users in sales roles to create and maintain a library of document artifacts linked to (and directly accessible from) Sales Cloud opportunities, accounts, contacts, leads, and other CX objects.   Because Sales Cloud supports extending its native UI in addition to adding and modifying back-end processes, the integration will allow users to very efficiently create and populate SharePoint folders with document content as they are creating or editing Sales Cloud business objects such as opportunities or contacts.  The obvious benefit is that the user experience becomes seamless across the two applications, versus the more cumbersome and error-prone approach of requiring a sales representative or manager to create an opportunity in Sales Cloud, and then log in to SharePoint to create a folder in the correct place and populate it with documents.

There are two or three components required to support this integration, depending on Sales Cloud version (pre- or post-Rel 11):

  • SharePoint Server 2013:  In this sample integration Sales Cloud interacts with SharePoint in two ways.  First, it takes advantage of SharePoint’s public RESTful API interfaces to create folders in an object-specific hierarchy, which then become linked to Sales Cloud objects.  Second, it embeds web content inside Sales Cloud object subtabs by building dynamic URL’s that contain pointers to these object-specific folders.  The URL’s are embedded in Sales Cloud object subtabs automatically, and from within Sales Cloud users can take advantage of all of SharePoint’s drag-and-drop features to access, upload, or delete documents, all within the context of the Sales Cloud objects that are active in their workspaces.
  • SOAP to REST Wrapper Application (JCS-SX):  Releases of Sales Cloud prior to Release 11 do not support the ability to call outgoing REST endpoints.  For older Sales Cloud versions, it is therefore necessary to build and deploy a translation/wrapper application that exposes SOAP endpoint methods (accessible from Sales Cloud) and translates those methods to REST requests, which the application will pass through to the ultimate SharePoint REST endpoints on behalf of the requester.  When synchronous responses are received back from the SharePoint REST resources, they are then translated and sent back to the original caller (Sales Cloud in this case) as SOAP responses.  To minimize the amount of low level custom coding, both JAX-WS annotations and Jersey libraries, Sun’s reference implementation for JAX-RS (JSR-311), are used in the wrapper application.
  • Oracle Sales Cloud:  Extensions built with Application Composer (Sales Cloud’s extensibility toolkit) support creating object-level folders in SharePoint and then storing pointers to these folders for objects implementing this integration feature.  Conceivably, accounts, contacts, opportunities, and other sales-process-related objects could all benefit by incorporating this SharePoint integration.

The remainder of this post will cover each of these three components in detail.

SharePoint REST API Resources and SharePoint URL’s

This integration was built, deployed, and tested with Microsoft SharePoint 2013.  SharePoint 2013 exposes a rich set of API’s available for integration projects, notably in the area of document management.  Only one example, creating a folder in a specified location, is covered here in detail.  For details of other RESTful services, the most complete reference for the SharePoint 2013 REST API is here:  https://msdn.microsoft.com/en-us/library/office/jj860569.aspx

There are a few lightweight setup tasks required to prepare the SharePoint server and target site for this integration with Sales Cloud:

  1. Create or designate an admin- or super-user account to be used by the incoming REST requests. The account credentials will be needed for the wrapper application or for setting up the REST web service definition in Sales Cloud Rel12 (and later).
  2. Add a team site for the Sales employees (if one does not exist already). Add the document management library to the team site to support enhanced features. Create subfolders for business object types in Sales Cloud for which the integration will be configured.
  3. Configure cross-origin embeddable pages in SharePoint to allow the SharePoint user interface to be embedded in Sales Cloud.
  4. Optionally set up custom page layouts for use by the integration. The strategy here is to remove any unnecessary “chrome” in embedded pages, allowing more real estate for the actual document content within the object-specific folders.

Create/designate an admin- or super-user account

For organizations that are using SharePoint, more often than not user credentials will be managed in Microsoft Active Directory (AD).  With or without AD, specific enterprise guidelines can be followed when setting up these types of accounts.  There are no additional special requirements other than making sure this user has full access to the SharePoint sales team site.

Add team site, Add document management library to site, create object type subfolders for use by Sales Cloud

Most organizations set up team site locations in SharePoint for exclusive use by specific departments and/or business units.  For this integration the assumption is that a team site has been set up for the Sales organization, and that this site will be the top-level SharePoint entry point for employees in the Sales organization.  While it is possible to use the default “Documents” link for the Sales Cloud integration, this link supports only basic document management features.  If enhanced features (versioning, sharing, co-authoring, check-in/out, local sync) are desired, it is possible to add the SharePoint Document Library application to the Sales team site.  After this application has been added to the team site (it has been named “Sales Cloud Documents” in this example), the Document Library top-level page looks like this after four sub-folders for each of the supported Sales Cloud object types have been added to the document library structure:

When fully operational, the integration will programmatically create sub-folders under one of the four object type folders, depending on which object is active in the Sales Cloud UI.  Users will be able to access and interact with content contained within this folder hierarchy either from SharePoint or from within Sales Cloud.

It is necessary to note the REST endpoints for each of these folder locations, as that location is passed in the request payload when creating subfolders for Sales Cloud objects as they are created.  The format of this SharePoint site’s folders for each object (which will be dynamically constructed in Groovy script) is:

http://<hostname>/sales/_api/Web/GetFolderByServerRelativeUrl(‘/sales/Documents/‘ + <objecttype>)

Configure iFrame Embedding in Sharepoint 2013

Like other applications that are mindful of and protect against click-jacking exploits, the default behavior of SharePoint 2013 prevents SharePoint pages from being embedded in iFrames hosted by cross-domain applications.  But, the default behavior can be circumvented if required (as it is for the integration with Sales Cloud).  This workaround should of course be used with caution.

By default Sharepoint 2013 sends the X-Frame-Options: SAMEORIGIN header with its pages.  This prevents client agents from embedding any SharePoint pages hosted in origins other than where SharePoint is being hosted.  To override this default behavior site-wide, a directive needs to be added to one or more of the master page templates for the site.  In SharePoint 2013, there are two active top-level master page templates:

  • seattle.master:  used in sites with intranet team collaboration features such as social, content, site navigation, and site management shortcuts.  Navigation header and left pane navigation.
  • oslo.master: used in sites that will be published.  Emphasis on page layout and content rendering.  Left pane navigation only.

The master page layout template can be changed in the Site Settings page for the site.  The applicable link is “Change the Look” for changing from seattle to oslo or vice versa.  This also exposes several CSS options for each template.  The page edits necessary for both master page templates are identical:

<head runat="server">
<meta name="GENERATOR" content="Microsoft SharePoint" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=10"/>
<meta http-equiv="Expires" content="0" />
<SharePoint:SPPinnedSiteTile runat="server" TileUrl="/_layouts/15/images/SharePointMetroAppTile.png" TileColor="#0072C6" />
<SharePoint:RobotsMetaTag runat="server"/>
<SharePoint:PageTitle runat="server"><asp:ContentPlaceHolder id="PlaceHolderPageTitle" runat="server"><SharePoint:ProjectProperty Property="Title" runat="server" /></asp:ContentPlaceHolder></SharePoint:PageTitle>
<SharePoint:SPShortcutIcon runat="server" IconUrl="/_layouts/15/images/favicon.ico?rev=23" />
<SharePoint:StartScript runat="server" />
<SharePoint:CssLink runat="server" Version="15"/>
<SharePoint:CacheManifestLink runat="server"/>
<SharePoint:ScriptLink language="javascript" name="core.js" OnDemand="true" runat="server" Localizable="false" />
<SharePoint:ScriptLink language="javascript" name="menu.js" OnDemand="true" runat="server" Localizable="false" />
<SharePoint:ScriptLink language="javascript" name="callout.js" OnDemand="true" runat="server" Localizable="false" />
<SharePoint:ScriptLink language="javascript" name="sharing.js" OnDemand="true" runat="server" Localizable="false" />
<SharePoint:ScriptLink language="javascript" name="suitelinks.js" OnDemand="true" runat="server" Localizable="false" />
<SharePoint:CustomJSUrl runat="server" />
<SharePoint:SoapDiscoveryLink runat="server" />
<SharePoint:AjaxDelta id="DeltaPlaceHolderAdditionalPageHead" Container="false" runat="server">
<asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server" />
<SharePoint:DelegateControl runat="server" ControlId="AdditionalPageHead" AllowMultipleControls="true" />
<asp:ContentPlaceHolder id="PlaceHolderBodyAreaClass" Visible="true" runat="server" />
</SharePoint:AjaxDelta>
<SharePoint:CssRegistration Name="Themable/corev15.css" runat="server" />
<WebPartPages:AllowFraming runat=”server” />
</head>

Adding the “WebPartPages:AllowFraming” directive to the active master page template’s header (line 26 in the HTML code fragment above) tells SharePoint not to send out the “X-Frame-Options: SAMEORIGIN” header for any of the site’s content, which allows pages to be embedded in Sales Cloud without cross-origin-related issues.

Custom Page Layouts

In SharePoint 2013 there are a number of ways to modify a site’s page layout.  In addition to modifying page templates and CSS, it is also possible to change specific pages.  In general, for embedded content the best practice is to remove navigation, which dedicates all of the embedded page real estate to content.  After all, navigation to Sales-Cloud-object-specific content is being done programmatically, so including it in embedded SharePoint pages would be redundant.

SharePoint also supports the concept of Device Channels, a means by which SharePoint can serve up content in alternative formats based on user device (browser or cell phone for example).  Device Channels can also be keyed off of HTTP headers, if that is the desired behavior.  This option theoretically could be leveraged to display content with navigation when users access the site from SharePoint, or without navigation when site content is embedded within another application.

Fortunately, there is an even simpler option to display pages without added chrome and navigation frames.  Including the “isdlg=1” (case-sensitive in SharePoint 2013) query parameter in the page URL will direct SharePoint to serve up content only, which is exactly the desired behavior for this integration.

SOAP to REST Wrapper Application Deployed to Oracle Java Cloud Service – SaaS Extension (JCS-SX)

NOTE:  the wrapper application is only required for pre-Rel12 versions of Sales Cloud.  Details are included here for legacy purposes and to provide this as a generic example of how to build such an application for other purposes.  This specific application becomes extraneous as Sales Cloud instances are upgraded to Release 12 and later, when support for outgoing calls to REST resources becomes available.  With one fewer component, obviously the integration will become much simpler and easier to deploy and maintain.

The wrapper application, developed in Oracle’s Application Development Framework (ADF), is an example of how to leverage JAX-WS annotations and the Jersey library reference implementation to make quick work of translating SOAP calls to REST calls, and vice versa. It is structured as a single ADF project; there is no need for a UI or business components project, nor is there a need for the typical ADF multi-project MVC application.

There are three different types of Java classes in the application:  a SOAP service class, a REST client class, and Plain Old Java Objects (POJO’s) to support JSON to XML transformation and vice versa.  The general flow is perhaps best described by a simple diagram:

The workhorse Java class is the JAX-WS-annotated SOAP web service, “SharepointSoapSvc”:

package oracle.sample.jcssx.sharepoint.service;
import java.io.IOException;
import javax.jws.WebMethod;
import javax.jws.WebService;
import oracle.sample.jcssx.sharepoint.pojo.AddFolderResp;
import oracle.sample.jcssx.sharepoint.pojo.FileResultsResponse;
import oracle.sample.jcssx.sharepoint.util.RestClient;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

@WebService(serviceName = "SharepointSoapSvc")
public class SharepointSoapSvc {
  private RestClient rc;

  public SharepointSoapSvc() {
    super();
    rc = new RestClient();
  }

  private Object parseRestResp(String resp, Class<?> classX) {
    System.out.println("parseRestResp: resp input parm is " + resp);
    ObjectMapper mapper = new ObjectMapper();
    Object obj = null;
    try {
      obj = mapper.readValue(resp, classX);
    } catch (JsonParseException jpe) {
      jpe.printStackTrace();
    } catch (JsonMappingException jme) {
      jme.printStackTrace();
    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
    return obj;
  }

  @WebMethod()
  public FileResultsResponse getDocs() {
    String rsp = rc.getDocs();
    return (FileResultsResponse) parseRestResp(rsp, FileResultsResponse.class);
  }

  @WebMethod
  public AddFolderResp addFolder(String folderName) {
    String rsp = rc.addFolder(folderName);
    return (AddFolderResp) parseRestResp(rsp, AddFolderResp.class);
  }

}

The “@WebService” annotation exposes the SharepointSoapSvc class as a SOAP JAX-WS service.  Within the service there are two annotated @WebMethods, getDocs() and addFolder().  The exposed addFolder() method is the one called by Sales Cloud to add a folder to the SharePoint repository.  A passed-in String parameter tells SharePoint (eventually, as we will see) what name to attach to the created folder.  (The getDoc() method is not an active part of the current basic integration covered in this post, but is included here for illustrative purposes.)

The exposed WebMethods complete their job by transforming the responses returned by the REST calls into SOAP-compatible formats that can be interpreted correctly by the calling application.

One topic that deserves detailed attention is how Jackson libraries are leveraged to transform JSON-formatted REST responses to SOAP-compatible XML formats for consumption by the application calling the SOAP web service.  Here is the JSON format for the SharePoint REST response to the addFolder request:

{
  "d": {
    "__metadata": {
      "id": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales Cloud Documents/Opportunities/TestRest007')",
      "uri": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales%20Cloud%20Documents/Opportunities/TestRest007')",
      "type": "SP.Folder"
    },
    "Files": {
      "__deferred": {
        "uri": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales%20Cloud%20Documents/Opportunities/TestRest007')/Files"
      }
    },
    "ListItemAllFields": {
      "__deferred": {
        "uri": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales%20Cloud%20Documents/Opportunities/TestRest007')/ListItemAllFields"
      }
    },
    "ParentFolder": {
      "__deferred": {
        "uri": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales%20Cloud%20Documents/Opportunities/TestRest007')/ParentFolder"
      }
    },
    "Properties": {
      "__deferred": {
        "uri": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales%20Cloud%20Documents/Opportunities/TestRest007')/Properties"
      }
    },
    "Folders": {
      "__deferred": {
        "uri": "http://<SP Host Name>/sales/_api/Web/GetFolderByServerRelativeUrl('/sales/Sales%20Cloud%20Documents/Opportunities/TestRest007')/Folders"
      }
    },
    "ItemCount": 0,
    "Name": "TestRest007",
    "ServerRelativeUrl": "/sales/Sales Cloud Documents/Opportunities/TestRest007",
    "WelcomePage": ""
  }
}

Note that the addFolder method returns an object with a custom complex class type of “AddFolderResp”.  This POJO class, and a class upon which it depends, “AddFolderBody”, is structured to reflect the format the JSON response getting returned by the SharePoint REST call:

package oracle.sample.jcssx.sharepoint.pojo;

public class AddFolderResp {
  private AddFolderBody d;

  public AddFolderResp() {
    super();
  }

  public void setD(AddFolderBody d) {
    this.d = d;
  }

  public AddFolderBody getD() {
    return d;
  }

}

Here is the AddFolderBody class:

package oracle.sample.jcssx.sharepoint.pojo;

import org.codehaus.jackson.annotate.JsonProperty;

public class AddFolderBody {

  @JsonProperty("__metadata")
  private Metadata metadata;

  @JsonProperty("Files")
  private DeferredUri files;

  @JsonProperty("ListItemAllFields")
  private DeferredUri listItemAllFields;

  @JsonProperty("ParentFolder")
  private DeferredUri parentFolder;

  @JsonProperty("Properties")
  private DeferredUri properties;

  @JsonProperty("Folders")
  private DeferredUri folders;

  @JsonProperty("ItemCount")
  private Integer itemCount;

  @JsonProperty("Name")
  private String name;
 
  @JsonProperty("ServerRelativeUrl")
  private String serverRelativeUrl;

  @JsonProperty("WelcomePage")
  private String welcomePage;

  public AddFolderBody() {
    super();
  }

  @JsonProperty("__metadata")
  public void setMetadata(Metadata metadata) {
    this.metadata = metadata;
  }

  @JsonProperty("__metadata")
  public Metadata getMetadata() {
    return metadata;
  }

  @JsonProperty("Files")
  public void setFiles(DeferredUri files) {
    this.files = files;
  }

  @JsonProperty("Files")
  public DeferredUri getFiles() {
    return files;
  }

  @JsonProperty("ListItemAllFields")
  public void setListItemAllFields(DeferredUri listItemAllFields) {
    this.listItemAllFields = listItemAllFields;
  }

  @JsonProperty("ListItemAllFields")
  public DeferredUri getListItemAllFields() {
    return listItemAllFields;
  }

  @JsonProperty("ParentFolder")
  public void setParentFolder(DeferredUri parentFolder) {
    this.parentFolder = parentFolder;
  }

  @JsonProperty("ParentFolder")
  public DeferredUri getParentFolder() {
    return parentFolder;
  }

  @JsonProperty("Properties")
  public void setProperties(DeferredUri properties) {
    this.properties = properties;
  }

  @JsonProperty("Properties")
  public DeferredUri getProperties() {
    return properties;
  }

  @JsonProperty("Folders")
  public void setFolders(DeferredUri folders) {
    this.folders = folders;
  }

  @JsonProperty("Folders")
  public DeferredUri getFolders() {
    return folders;
  }

  @JsonProperty("ItemCount")
  public void setItemCount(Integer itemCount) {
    this.itemCount = itemCount;
  }

  @JsonProperty("ItemCount")
  public Integer getItemCount() {
    return itemCount;
  }

  @JsonProperty("Name")
  public void setName(String name) {
    this.name = name;
  }

  @JsonProperty("Name")
  public String getName() {
    return name;
  }

  @JsonProperty("ServerRelativeUrl")
  public void setServerRelativeUrl(String serverRelativeUrl) {
    this.serverRelativeUrl = serverRelativeUrl;
  }

  @JsonProperty("ServerRelativeUrl")
  public String getServerRelativeUrl() {
    return serverRelativeUrl;
  }

  @JsonProperty("WelcomePage")
  public void setWelcomePage(String welcomePage) {
    this.welcomePage = welcomePage;
  }

  @JsonProperty("WelcomePage")
  public String getWelcomePage() {
    return welcomePage;
  }

}

The JsonProperty annotations in the class work as hints to the Jackson parsing engine.  Not all of them are required in all cases; they are included here by way of example.  At any rate, detailed discussion of the Jackson libraries is somewhat out of scope here.  For more information, refer to:  https://github.com/FasterXML/jackson-docs

The exposed SOAP methods in the SharepointSoapSvc class call into methods contained in the REST client class “RestClient”:

package oracle.sample.jcssx.sharepoint.util;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.LoggingFilter;
import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import oracle.sample.jcssx.sharepoint.pojo.GetContextWebInformationResp;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.jaxrs.JacksonJsonProvider;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

public class RestClient {

  private Properties props = Config.getProperties();
  private Client client;
  private static final String URIEXT = "/_api/Web";
  private static final String URIDOCS = "/GetFolderByServerRelativeUrl('Documents')/Files";
  private static final String URIFOLDERADD = "/Folders/add";

  public RestClient() {
    super();
    ClientConfig cc = new DefaultClientConfig();
    cc.getClasses().add(JacksonJsonProvider.class);
    client = Client.create(cc);
    HTTPBasicAuthFilter authFilter = new HTTPBasicAuthFilter(props.getProperty("user1name"), props.getProperty("user1pw"));
    client.addFilter(authFilter);
  }
 
  public String getXRequestDigestValue() {
    ObjectMapper mapper = new ObjectMapper();
    GetContextWebInformationResp resp = null;
    WebResource resource = client.resource(props.getProperty("SharePointHost") + "/_api/contextinfo");
    resource.header("Accept", "application/json; odata=verbose");
    String response = resource
      .accept(MediaType.APPLICATION_JSON_TYPE)
      .header("Accept", "application/json; odata=verbose")
      .header("Content-Length", 0)
      .type(MediaType.APPLICATION_JSON_TYPE)
      .post(String.class);
    System.out.println("String contextinfo resp: " + response);
    try {
      resp = mapper.readValue(response, GetContextWebInformationResp.class);
    } catch (JsonParseException jpe) {
      jpe.printStackTrace();
    } catch (JsonMappingException jme) {
      jme.printStackTrace();
    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
    System.out.println("FormDigestValue: " + resp.getD().getGetContextWebInformation().getFormDigestValue());
    return resp.getD().getGetContextWebInformation().getFormDigestValue();
  }

  public String getDocs() {
    WebResource resource = client.resource(props.getProperty("SharePointHost") +
      "/" + props.getProperty("SharePointSite") + URIEXT + URIDOCS);
    String response = resource
      .accept(MediaType.APPLICATION_JSON_TYPE)
      .header("Accept", "application/json; odata=verbose")
      .type(MediaType.APPLICATION_JSON_TYPE)
      .get(String.class);
    return response;
  }

  public String addFolder(String folderRef) {
    String xRequestDigest = getXRequestDigestValue();
    WebResource resource = client.resource(props.getProperty("SharePointHost") +
      "/" + props.getProperty("SharePointSite") + URIEXT + URIFOLDERADD + "('" + folderRef + "')");
    String response = resource
      .accept(MediaType.APPLICATION_JSON_TYPE)
      .header("Accept", "application/json; odata=verbose")
      .header("X-RequestDigest", xRequestDigest)
      .header("Content-Length", 0)
      .type(MediaType.APPLICATION_JSON_TYPE)
      .post(String.class);
    return response;
  }
}

The class constructor creates a Jersey REST client object, adding a JacksonJsonProvider class to the client config for parsing convenience.  A BASIC authentication filter is also added to the client.  In this example, a super-user or admin-user authentication pattern is used:  hard-coded user credentials are kept in a configuration utility and are passed in to the authentication filter.  (A future blog post will explore alternatives to this super-user BASIC authentication pattern.)

Other methods in the RestClient class follow the typical Jersey REST client implementation, more or less.  A WebResource is instantiated, utilizing any context-specific passed-in parameters to build a URI compatible with the SharePoint REST service specifications.  With the WebResource instantiated, Jersey GET or POST and related methods for the resource initially add required/optional header data, and then make the REST requests.  Responses from the REST calls are harvested and get returned to the SOAP service methods that called into the REST client methods.

There is one wrinkle specific to and required by SharePoint in the code flow of the RestClient methods called by the SOAP service.  For authentication purposes, SharePoint 2013 requires an “X-RequestDigest” header value be added to incoming REST calls.  This requirement is handled by the “getXRequestDigestValue()” method (see above code sample).  Microsoft documentation for this header value is located here:  https://msdn.microsoft.com/en-us/library/microsoft.sharepoint.webcontrols.formdigest.aspx

Oracle Sales Cloud (pre-outgoing REST support) – Release 11 and earlier

The Sales Cloud Rel 11 development tasks to build what is covered in the SharePoint integration are summarized here:

  1. Create a SOAP web service endpoint definition in Application Composer. This endpoint definition points to a SharePoint site or sub-site, and it becomes available for use in functions that are called (and which are covered below) as a result of user activity in Sales Cloud’s business processes.
    https://<jcs-sx host name>/SharepointRestWrapper/SharepointSoapSvcPort?WSDL
  2. Create two global functions in Application Composer. One of these functions calls SharePoint API’s to create a folder within the SharePoint site.  The other function builds a dynamic SharePoint URL based on Sales Cloud context to display the SharePoint UI embedded within Sales Cloud.
    1. The first function, createSharePointFolderSOAP, takes two input parameters, an object type and an object name, and uses those parameters to build a folder URL which is then passed to the SharePoint web service endpoint to get the folder created. Function code (Groovy script) is here:
      println 'Entering CreateSharepointFolder Global Function'
      def folderPrefix = 'Sales%20Cloud%20Documents/' + objectType + '/'
      def objFolder = objectName.replaceAll(' ', '')
      def respMap = adf.webServices.SharepointRestWrapper.addFolder(folderPrefix + objFolder)
      println 'Response: ' + respMap
      println 'Exiting CreateSharepointFolder Global Function'
    2. The second function, getSharePointFolderUrl, takes the same two input parameters, and uses those parameters to build a URL that will display the context-specific folder for the active Sales Cloud object, whether it be Account, Contact, Lead, Opportunity, or even a custom object. Function code (Groovy script) is here:
      println 'Entering Sharepoint subtab URL definition script'
      def url = 'https://slc05mgq.us.oracle.com/oscdocs/Documents/Forms/SalesCloudAllItems.aspx?isdlg=1&RootFolder='
      def folderPrefix = '%2Fsales%2FSales%20Cloud%20Documents%2F' + objectType + '%2F'
      def objFolder = objectName.replaceAll(' ', '')
      url += folderPrefix
      url += objFolder
      println 'URL for this object\'s Sharepoint folder is ' + url
      return url
  3. Create a trigger script for the Opportunity object that calls the createSharePointFolder global function, passing the Opportunity object type along with the Opportunity name. This script will run whenever a new opportunity is created and commited to the database, which automatically creates an object-specific folder in SharePoint for the active opportunity.  Code (one line of Groovy script) is here:
    adf.util.createSharePointFolderSOAP('Opportunities', Name)
  4. Create an object script for the Opportunity object that calls the createSharePointfolder global function. This script is identical to step #3, and will be used to demonstrate how it is possible to create a folder manually by allowing the user to click an action button (created in step #5) in the Sales Cloud UI.
  5. (OPTIONAL) Create an Action for the Opportunity object that will expose the ability to create a SharePoint folder manually. Use the CreateDocFolder object function as the target of the Action.  Add the Action to the Opportunity Details page.
  6. Create a subtab in the Opportunities Details simplified UI page that will be used as the entry point for displaying the active opportunity’s dedicated SharePoint folder. This subtab will call a URL returned by the getSharePointFolderUrl global function built in step #2.2.  Code (one line of Groovy script) is here:
    return adf.util.getSharePointFolderUrl('Opportunities', Name)
  7. Repeat steps #3 – #6 for each Sales Cloud object where SharePoint folder integration is desired. Candidate object types may include Account, Contact, Lead, or any custom object that have been created.

The result of the integration for a test opportunity named ‘TestFromSalesCloudSOAP’ (after the opportunity is created) looks like this when the custom SharePoint subtab is selected:

This covers all of the required steps in Application Composer for building the SharePoint integration features.  More detailed information on Sales Cloud Application Composer is available here:  http://docs.oracle.com/cloud/latest/salescs_gs/OACEX/OACEX.pdf

Oracle Sales Cloud – Release 12 and Later

As mentioned earlier, with Release 12 of Sales Cloud it is now possible to make outgoing calls to external REST services directly instead of having to develop, deploy, and integrate a go-between SOAP-to-REST wrapper application.

Most of the logic for implementing the REST version of the Sales Cloud side of the integration is similar to the SOAP implementation summarized above.  But there is work unique to Rel 12 to construct the REST endpoints dynamically in Groovy script, and also to add the X-RequestHeader header that is required for SharePoint to recognize and process REST POST requests.

  1. Create two REST web service definitions in Sales Cloud App Composer. (Since this Sales Cloud feature is relatively new, it will be covered here in a fair amount of detail.)
    1. The first web service definition, when called and run from a Groovy script, will return a token for the time-sensitive X-RequestDigest header value as part of the response payload.

      Returning an XML-formatted response enables a Groovy script that calls this REST web service to automatically parse the response and retrieve any important data that is returned. In this case, what is needed is the value of the X-RequestDigest token.
    2. The second web service definition, when called and run from a Groovy script, will create a folder in SharePoint that is tied to the active Sales Cloud business object. The Opportunity object is used as an example here.

      This REST web service will create the folder for the active object. The placeholder variable, ##FolderRef##, is populated by the Groovy script that calls the REST service.
  2. Create a global function in Sales Cloud App Composer that will utilize the two REST web service definitions to create the object-specific SharePoint folder:
    // straight REST implementation
    def xReq = adf.webServices.SharepointRestApiContextInfoXML
    def xReq1 = adf.webServices.SharepointRestApiAddFolder3
    try {
      def xResp = xReq.POST()
      println ('Response: ' + xResp)
      def digestVal = xResp.FormDigestValue
      println ('xResp.FormDigestValue: ' + digestVal)
      def httpHdrs1 = ['X-RequestDigest':digestVal,'Content-Encoding':'gzip','Vary':'Accept-Encoding']
      def FolderRef = '\'/sales/OracleSalesCloudDocs/' + objectType + '/' + objectName + '\''
      println ('FolderRef: ' + FolderRef)
      xReq1.requestHTTPHeaders = httpHdrs1
      def xResp1 = xReq1.POST(FolderRef, '')
      println ( "Req Headers: " + xReq1.requestHTTPHeaders) 
      println ('xResp1 Response: ' + xResp1)
    } catch (Exception e) {
      println ( "Headers:" + xReq1.responseHTTPHeaders)
      println ( "-----------------------------------------------------")
      println ( "Status:" + xReq1.statusCode)
      println ( "-----------------------------------------------------")
      println ( "Error:" + e)
      println ( "-----------------------------------------------------")
    }

    This function makes an initial call to the REST contextInfo endpoint to retrieve the X-RequestDigest value. When that step completes, the key/value pair is added to the HTTP header for the second REST call, which makes the REST call to create a SharePoint folder specific to the active object type and object name.

  3. Create trigger functions for each Sales Cloud object where supporting documents may need to be maintained in SharePoint. This step is identical to the SOAP example (see step #3 above).
  4. Create the global function for creating the dynamic URL along with creating the subtab(s) which will call this global function.  These steps are identical to the SOAP example (see steps #2.2 and #6 above).

Summary

Sales support processes normally generate a lot of documents at various stages.  If salespeople are given faster and more efficient means of maintaining sales-related documentation, everyone wins.  Organizations benefit when salespeople can maintain libraries of supporting documents for various workflows as they are involved in the workflows.  By integrating Oracle Sales Cloud and Microsoft SharePoint to support sales and customer relationship processes, enterprises can realize additional value from both software applications.  This value comes in the form of giving sales representatives and sales managers a more efficient, simpler, and less error-prone means of managing documents that need to be tied to business objects, whether they be sales opportunities, accounts, contacts, or activities in Oracle Sales Cloud.

Add Your Comment