Showing posts with label web service attachment. Show all posts
Showing posts with label web service attachment. Show all posts

Thursday, October 17, 2024

Uploading files to SharePoint Online

Here are the steps to upload files to SharePoint Online using PeopleSoft's services/service operations.

I am running PeopleTools 8.61.x and there are some properties that are discussed below that are only available in the latest tools release. 

At a high level there are three steps

1. Generate Access Token

2. Using the access token generate Form Digest

3. Finally using both the access token and form digest upload file to SPO.

Service and Service Operation build

1. Created a Document Template (N_SPO_URI_TEMPLATE) with the following primitives defined as text.

TenantID, TenantName, SiteName, SPOFolder and FileName

2. Defined two non-rowset based messages - N_SPO_REQUEST and N_SPO_RESPONSE

3. Created a consumer REST service called as N_SPO.

4. Created three service operations all with REST method as POST - 

N_SPO_ACCESSTOKEN_POST, N_SPO_FORMDIGEST_POST and N_SPO_UPLOADFILES_POST

For the service operation N_SPO_ACCESSTOKEN_POST the REST base URL is 

https://accounts.accesscontrol.windows.net/ and template is {TenantID}/tokens/OAuth/2

For the service operation N_SPO_FORMDIGEST_POST the REST base URL is https:// and 

template is {TenantName}.sharepoint.com/sites/{SiteName}/_api/ContextInfo as we cannot provide any variables in the base URL.

Finally for the N_SPO_UPLOADFILES_POST service operation, the REST base URL is https:// and template is {TenantName}.sharepoint.com/sites/{SiteName}/_api/web/GetFolderByServerRelativeURL('/sites/{SiteName}/{SPOFolder}/')/Files/add(url='{FileName}',overwrite=true)

All other settings are default settings.

5. Updated the integrationGateway.properties file as follows - (this parameter may be available only in the latest tools release)

ig.UseDomainName.ExternalOperationNames=N_SPO_FORMDIGEST.v1,N_SPO_UPLOADFILES.v1

Had to provide external operational service name for these two. The first one related to access token generation worked without having to define it here. 

6. For the SPO site to which I want to upload the file, I need a Client ID and Secret, which is kind of like the user ID/password to make the connection. SharePoint admin can generate this. Value for &ApplicationID, &TenantID also provided by SharePoint admin. Value for variables like &TenantName, &SiteName, &SPOFolder derived by parsing the SPO URL to which I had to upload the file. &attachFileName is derived from the complete file path of the file to be uploaded.  

7. Now comes the peoplecode part to invoke these service operations. 

/* step 1 */
/* intialize service operation and update URL variables */
&reqMsg = CreateMessage(Operation.N_SPO_ACCESSTOKEN_POST);
&Doc_Tmpl = &reqMsg.GetURIDocument();
&COM_Tmpl = &Doc_Tmpl.DocumentElement;
&reqMsg.URIResourceIndex = 1;
&COM_Tmpl.GetPropertyByName("TenantID").Value = &TenantID;

/* this is how form-data can be provided */
&reqMsg.SegmentContentType = "application/x-www-form-urlencoded";
&bRet = &reqMsg.SetContentString("grant_type=client_credentials&client_id=" | EncodeURLForQueryString(&ClientID | "@" | &TenantID) | "&client_secret=" | EncodeURLForQueryString(&ClientSecret) | "&resource=" | EncodeURLForQueryString(&ApplicationID | "/" | &TenantName | ".sharepoint.com@" | &TenantID));

/* following HTTP property may only work in latest tools release */
&bRet = &reqMsg.IBInfo.LoadRESTHeaders();
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Disable_URLEncodingBody", "True", %HttpProperty);

/* submit request and the parse json response */
&respMsg = %IntBroker.SyncRequest(&reqMsg);
&sResp = &respMsg.GenXMLString();
&AccessToken will have the access token received in the response. 

/* step 2 */
/* intialize service operation and update URL variables */
&reqMsg = CreateMessage(Operation.N_SPO_FORMDIGEST_POST);
&Doc_Tmpl = &reqMsg.GetURIDocument();
&COM_Tmpl = &Doc_Tmpl.DocumentElement;
&reqMsg.URIResourceIndex = 1;
&COM_Tmpl.GetPropertyByName("TenantName").Value = &TenantName;
&COM_Tmpl.GetPropertyByName("SiteName").Value = &SiteName;

/* add properties to request header, &AccessToken is from step 1 */
&bRet = &reqMsg.IBInfo.LoadRESTHeaders();
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Authorization", &AccessToken, %Header);
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Accept", "application/json;odata=nometadata", %Header);

/* submit request and the parse json response */
&respMsg = %IntBroker.SyncRequest(&reqMsg);
&sResp = &respMsg.GenXMLString();
&FormDigest will be populated form the response.

/* step 3 */
/* intialize service operation and update URL variables */
&reqMsg = CreateMessage(Operation.N_SPO_UPLOADFILES_POST);
&Doc_Tmpl = &reqMsg.GetURIDocument();
&COM_Tmpl = &Doc_Tmpl.DocumentElement;
&reqMsg.URIResourceIndex = 1;
&COM_Tmpl.GetPropertyByName("TenantName").Value = &TenantName;
&COM_Tmpl.GetPropertyByName("SiteName").Value = &SiteName;
/* if SPO folder path has space in it then it has to be encoded like &SPOFolder = EncodeURL("Shared Documents/General/SPO Test"); */
&COM_Tmpl.GetPropertyByName("SPOFolder").Value = &SPOFolder;
&COM_Tmpl.GetPropertyByName("FileName").Value = &attachFileName;

/* read the file to attach and GetBase64StringFromBinary */
&MTOMFile = GetFile("filename.xlsx", "R", %FilePath_Absolute);
If &MTOMFile.IsOpen Then
   &theBase64encodedString = &MTOMFile.GetBase64StringFromBinary();
   &MTOMFile.Close();
End-If;

/* add the bas464string to request message */
If (&reqMsg.SetContentString(&theBase64encodedString)) Then
   &reqMsg.SegmentContentType = "binary";
   &reqMsg.SegmentContentTransfer = %ContentTransfer_Binary;
End-If;

/* add properties to request header, last property is a HTTP property */
&bRet = &reqMsg.IBInfo.LoadRESTHeaders();
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("X-Request-Digest", &FormDigest, %Header);
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Authorization", &AccessToken, %Header);
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Accept", "application/json;odata=nometadata", %Header);
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", %Header);
&bRet = &reqMsg.IBInfo.IBConnectorInfo.AddConnectorProperties("Base64toBinary", "Y", %HttpProperty);

/* submit request and the parse json response */
&respMsg = %IntBroker.SyncRequest(&reqMsg);
&sResp = &respMsg.GenXMLString();

Parse the response or check response status &respMsg.HTTPResponseCode
or &respMsg.ResponseStatus to complete the processing.

Hope this helps.

Tuesday, April 21, 2020

Creating JSON request for REST web services

PeopleSoft provides Document technology to be used to generate JSON request messages but in my experience they are very restrictive especially when working on integrating with 3rd party web services. So following is what I did to generate a JSON request message to post to a 3rd party REST web service. 

The request that I have to generate is in the following form.
[
   {
      "attrib1":"value1",
      "attrib2":"value2",
      "attrib3":{
         "attrib3_1":"values3_1",
         "attrib3_2":"values3_2",
         "attrib3_3":"values3_3",
         "attrib3_4":"values3_4"
      }
   }
]

I am running PT 8.57.x and at this time its not possible to build a document with the root node as an array as shown in the example below. Also I have nested compounds which is also a challenge, the parent compound does not have a label where as the child does. 
So to build something like above I am using the CreateJsonBuilder API provided by PeopleSoft.

Local JsonBuilder &jbldr = CreateJsonBuilder();
Local JsonArray &jArray;
Local string &json;
Local message &request, &response;
Local boolean &bRet;

&jbldr.StartArray(""); /* no label */
 &jbldr.StartObject(""); /* no label */
  &jbldr.AddProperty("attrib1", "value1");
  &jbldr.AddProperty("attrib2", "value2");
   &jbldr.StartObject("attrib3"); /* need a label */
    &jbldr.AddProperty("attrib3_1", "value3_1");
    &jbldr.AddProperty("attrib3_2", "value3_2");
    &jbldr.AddProperty("attrib3_3", "value3_3");
    &jbldr.AddProperty("attrib3_4", "value3_4");
   &jbldr.EndObject("attrib3"); /* closing out the compound or JSONObject */
 &jbldr.EndObject("");
&jbldr.EndArray("");


/* this will return the array just like what I want */
&jArray = &jbldr.GetRootNode().GetJsonObject().GetJsonArray("");
&json = &jArray.ToString();

Created a basic non-rowset based message and assigned that as the request message in my service operation. Use this method to set the content for the message segment for a non-rowset-based message only.

&bRet = &request.SetContentString(&json);
&response = %IntBroker.SyncRequest(&request);

That's it, works like a charm.

Sunday, March 8, 2020

Event Mapping (managing customizations)

Event Mapping is a PeopleSoft delivered framework that helps applications and customers adapt PeopleSoft to meet their business needs with minimum upgrade impact. It introduces business logic as a configuration instead of customization. Event mapping allows the user to develop reusable code and artifacts and ensures that the customer’s code will not be overwritten by PeopleSoft delivered code during an upgrade.

Following is what I have tried in PeopleTools 8.56.07 and 8.57.08 on HCM 9.2 (PUM 31 and beyond). For this POC I am going to call my custom application package peoplecode on SavePostChange event on the DEPARTMENT_TBL component to perform something like sending out a notification. In the absence of the event mapping functionality I would have written this code in SavePostChange event of the DEPARTMENT_TBL component thus customizing the delivered object.

So, step one is to build the application package peoplecode. Import of PT_RCF:ServiceInterface is the key. The class name can be anything but the method name has to be "Execute".

import PT_RCF:ServiceInterface;

class FoundationDataNotify extends PT_RCF:ServiceInterface
   method Execute();
end-class;

method Execute
   /+ Extends/implements PT_RCF:ServiceInterface.execute +/

   /* custom logic to send out notification goes here */
end-method;
  
Once the package is developed navigate to PeopleTools > Portal > Related Content Service > Define Related Content Service. Create a new definition and select "URL Type" as application class and then provide the package details. For this POC I selected "Public Access" under security options. Save the service definition.

Then navigate to PeopleTools > Portal > Related Content Service > Manage Related Content Service, Event Mapping tab. Click on the "Map Application Classes to Component Events" link and then navigate the portal tree to find the component on which this event would be based on or associated with. In my case it would be the department table component located under Set Up HCM > Foundation Tables > Organization > Departments.

Under "Component Level Event Mapping" section, select "enable" check-box, event name would be SavePostChange and select the Service ID created in the earlier step. Set sequence number to 1 and processing sequence as "Post Process". Save the page and that is it. 

Navigate to the department component make a change and save the page and verify that the custom application package peoplecode fires.


The PeopleCode Editor provides an Event Mapping button in the dynamic Application Designer toolbar. For an application developer, the Event Mapping button is a visual indicator of custom PeopleCode programs mapped to events of a component, component page, component record, or component record field. This button is not available for page level event mapping.







So opened component peoplecode of component DEPARTMENT_TBL and clicked on the button to produce the following output.
















Right-clicking on the application package code and selecting "Event Mapping References" in the pop-up menu as shown below should display the component but that did not work.













"Event Mapping References" tab is empty as shown below.












So I guess this might work in future releases or maybe I don't have something configured correctly for this to work.

Saturday, March 26, 2016

Sending and Receiving MTOM-encoded binary data

PeopleSoft supports the MTOM protocol for sending and receiving binary data using service operations. While you can send and receive binary data using SOAP, doing so requires that you Base64-encode the data, which can increase message size by 33 percent or more. The MTOM protocol enables you to send and receive binary data in its original binary form, without any increase in size due to encoding.
For sending or receiving MTOM-encoded binary data, we have to use message segments to store the data. The SegmentContentType property of the message object is used to set or read the content type of each message segment.

Following is a test that I did to send a XML file as an attachment in a SOAP message and then read the attachment that is sent by the 3rd party system that I am interacting with. I am running PT 8.53.22.

Sending:

Request message is as shown below. For this test I am storing this in a html object called as MY_MESSAGE but this can be generated dynamically as needed using SOAPDoc or XMLDoc classes. The request message defined on the service operation is nonrowset based. 

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<submitLargeDocument>        
    <Document>  
         <PsftXopInclude SegmentNumber='1'/> 
    </Document>      
</submitLargeDocument>
</soapenv:Body>
</soapenv:Envelope>

document.xml is my payload which I have created in a different routine and I am just using it here. Its just like creating any other XML file in PeopleSoft. Payload can be a binary file like a pdf or a image file.
Sender node is my default local node and receiving node is the delivered WSDL_NODE. You can always create a custom receiving node if needed. Routing is using local gateway and HTTPTARGET connector. Under routing, connector properties content-type is set to text/xml as my payload is a xml file, HTTPPROPERTY MTOM is set to Y, METHOD is POST, SOAPUpContent is set to N as I have already built the SOAP wrapper in my html object above. If you need IB to create the wrapper then set this property to Y. Provided PRIMARYURL to destination 3rd party application. Took all other defaults.

PeopleCode:

&str = GetHTMLText(HTML.MY_MESSAGE);
&requestXMLDoc = CreateXmlDoc();
&ret = &requestXMLDoc.ParseXmlString(&str);

&request = CreateMessage(Operation.SEND_OPERATION);
&request.SetXmlDoc(&requestXMLDoc);

&MTOMFile = GetFile("C:\temp\document.xml", "R", %FilePath_Absolute);
If &MTOMFile.IsOpen Then   
   &theBase64encodedString = &MTOMFile.GetBase64StringFromBinary();   
   &MTOMFile.Close();
End-If;

&request.CreateNextSegment();

If (&request.SetContentString(&theBase64encodedString)) Then   
    &request.SegmentContentType = "application/xml";  
    &request.SegmentContentTransfer = %ContentTransfer_Binary;
End-If;

&response = %IntBroker.SyncRequest(&request);

Receiving:

Response message defined on the service operation is a non-rowset based message. Sender node is the default local node and receiving node is WSDL_NODE. Using local gateway and HTTPTARGET connector. Setting HEADER properties Content-Type to text/xml as the response attachment that I am receiving is a xml file, sendUncompressed is Y, HTTPPROPERTY Method is POST and SOAPUpContent is Y and finally the PRIMARYURL to the 3rd party service.

On the weblogic webserver, in the integrationGateway.properties file enable the MTOM Listening Connectors. 
ig.MTOM.enablePeopleSoftServiceListeningConnector=true
ig.MTOM.enableHttpListeningConnector=true

Bounce the webserver after making this change.

PeopleCode:

This is pretty straightforward. Once the request is made, read the response and parse out the document.

&response = %IntBroker.SyncRequest(&request);
&responseXMLDoc = &response.GetXMLDoc();

If (&response.ResponseStatus = 0) Then
     &dataNode = &responseXMLDoc.DocumentElement.GetElementByTagName("data");
     &theData = &dataNode [1].GetCDataValues();
     &responsestr = &theData.Shift();
End-If;

The &responsestr string variable will have the response SOAP envelope as well as the attachment separated by message segments as shown below. Parsed it out using string functions.

<?xml version="1.0"?>
<data psnonxml="Yes">
  <![CDATA[
------=_Part_624_1792156364.1458048147094
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: 8bit
Content-ID: <soap.xml@xfire.codehaus.org>

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
.......
.......
.......
</soap:Body>
</soap:Envelope>
------=_Part_624_1792156364.1458048147094--

------=_Part_724_1792156364.1458048147094
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: 8bit
Content-ID: <soap.xml@xfire.codehaus.org>

<Document>
........
........
........
</Document>

------=_Part_724_1792156364.1458048147094--

]]>
</data>