Tuesday, September 8, 2015

OpenText Widget Customization

Below is how internally OpenText Content Widget loads different resources from Content server and renders the widget.

Here, we can see csui-base.css is downloaded from csui.js using helper.js. jquery-ui.css which imports original.css and adjustment.css has majority of the Stylesheet. 

For custom look and feel, we had downloaded entire %support\csui\lib\jquery.ui\themes\opentext% directory and made local copy of it. After that, adjustment.css can be customized using CSS overwrite rule. We could completely customize entire CSS using JQuery theme roller as well.



CSS overwrite

We had embedded content (widget) on hosted page and wanted to override the CSS which comes with hosted page. Here are a few techniques we tried:

1. Additional properties : Probably the most simple one. If we need to add properties to the style which is on hosted page. Browser will load both CSS and combine all the properties for that class.

 Hosted Page CSS:

 .my-button {  
      opacity: 1;  
      background-color: #fff;  
      border: 1px solid #999999;  
      color: #999999;  
 }   

Addition to CSS from embedded code:

 .my-button {  
      text-decoration: underline;  
 }   


2. Overwrite : When we want to overwrite the property which is already exists on hosted page's css. Here browser will give higher priority to property with !important, regardless of which of the order in which they are loaded.


 Hosted Page CSS:

 .my-button {  
      opacity: 1;  
      background-color: #fff;  
      border: 1px solid #999999;  
      color: #999999;  
      text-decoration: none;  
 }   

Addition to CSS from embedded code:

 .my-button {  
      text-decoration: underline  !important; 
 }   



3. Overwrite the Important : We had case when hosted page's css had !important in it. e.g. below, hosted page has text-decoration with !important, so it was not possible to overwrite it using another !important

 Hosted Page CSS:

 .my-button {  
      opacity: 1;  
      background-color: #fff;  
      border: 1px solid #999999;  
      color: #999999;  
      text-decoration: none  !important; 
 }   


Option a) Rely on Order

Let's say we write our write our custom CSS as below. In this case, if our custom CSS is loaded after hosted page CSS, then custom CSS will take priority over the hosted page CSS. Basically, CSS gives priority to last CSS loaded on the page if both are set to !important. However, it is not recommended to rely on order of the CSS as resource can take different time to load. Additionally if we use @import for sync loading it could potentially cause performance issues.

 .my-button {  
      text-decoration: underline  !important; 
 }   


Option b) Swap the style

Create a new style my-button-2, and use JQuery to replace all occurrences of my-button with my-button-2:

 .my-button-2 {  
      opacity: 1;  
      background-color: #fff;  
      border: 1px solid #999999;  
      color: #999999;  
      text-decoration: underline  !important; 
 }   

Swap the style for all the elements on the page:

      jQuery.noConflict();  
      jQuery( document ).ready(function( $ ) {  
           setTimeout(switchClass, 1000)  // make sure class was loaded  
      });  
      function switchClass() {  
           jQuery(".my-button").switchClass("my-button", "my-button-2", 1, "easeInOutQuad");  
      }  


Option c) disable/swap host css

Created copy of entire CSS file in the embedded code and disable the Hosted page's css.

      jQuery.noConflict();  
      jQuery( document ).ready(function( $ ) {  
           setTimeout(disableCSS, 1000)  // make sure class was loaded  
      });  
      function disableCSS() {  
           jQuery("link[href$='css-original.css']")[0].disabled = true;
      }  



Source code.

Friday, May 22, 2015

SalesForce Packaging for AppExchange

SalesForce support different package type so you can distribute your app to your customers. Here is summary of what we found:

Scenario Packaging
Unamanged
with/without namespace
Managed Beta Managed Released
Description
Unmanaged packages are typically used to distribute open-source projects or application templates to provide developers with the basic building blocks for an application. Once the components are installed from an unmanaged package, the components can be edited in the organization they are installed in.
More Information
Use to test and validate this package internally and with selected customers before release.
  • Note: this type of package can only be installed in Developer Edition, sandbox organizations, or testing organizations for registered partners
  • For a given managed package, for each version we can decide it to have it beta or released. Typically there would be multiple beta versions in between each release.
Use when you are ready to publish to Force.com AppExchange

More Information
Can Customer See your Source Code?
Yes
No
Namespace
Not required.
  • Even if namespace is added to Org which is publishing the App, it doesn't add it to customer.
  • From Scenario which were tested, it doesn't make any difference with namespace.
Required
Version Upgrade
Not supported.
  • Customer will need to uninstall and install the package
  • Customer will not be able to just reinstall (overlay) on top of old version
  • If customer code has reference to App code, then they will need to remove them before uninstall
Not supported.
  • Customer will need to uninstall and install the package
  • Customer will not be able to just reinstall (overlay) on top of old version
  • If customer code has reference to App code, then they will need to remove them before uninstall
Supported.
  • Customer will be able to just reinstall (overlay) on top of old version
  • If customer code has reference to App code, then they will not be required to change that.
  • "Push Upgrade Feature" feature is available to push the upgrade to customers More Information
Changes to the Code From App Publisher Org (APO)
Allowed.
  • Publisher Org (APO) have no limitation. APO can make any code change.
Restricted.
  • Publisher Org (APO) would have limitation on code changes
  • No signature changes on global methods allowed
  • Can not delete global classes
Customer Name Conflict
Fails.
  • If customer has same artifact name as the artifact name used in App, installation of package would fail.
Works.
  • Namespace are unique across SFDC, and App artifacts are in different namespace so no conflict occurs.
Customer Reference to App Code
No restriction.
  • Customer will be able to use, modify or delete the artifacts.
  • Reference to packaged code would be similar to any other code. e.g. ClassName.methodName
Restricted.
  • Customer will be only be able to use Class and Method which are declared "global" in Package.
  • Package publisher will need to define class and all the methods, and WS api to global if customer needs to consume them
  • Reference to packaged code would need to include namespace. e.g. Namespace.ClassName.methodName
Uninstall
Supported
  • Package can be uninstalled from Setup Console
  • If Customer has reference to Package artifacts, those must to be removed/de-referenced before uninstall
Supported
  • Package can be uninstalled from Setup Console
  • If Customer has reference to Package artifacts, those must to be removed/de-referenced before uninstall
Intra Upgrade
  • Unmanaged to Managed
  • Managed Beta to Managed Released
Not Applicable Not Supported
  • Unmanaged to Managed Beta
  • Customer will need to uninstall Unmanaged Package and install Managed Beta Package manually
Not Supported
  • Managed Beta to Managed Released
  • Customer will need to uninstall Managed Beta and install Managed Released Package manually(*)
* : Need to test further if there is any setting which might allow this.

Friday, May 15, 2015

iFrame content via JQuery

We can access the content of iFrame using JQuery using $("#iframe1").contents().find("html").html(), however it works only if iframe belongs to the same domain as parent container.

E.g. for below HTML, iframe1 and iframe3 is pointing to external URL and iframe2 is pointing the page with same domain:

1:  <html>  
2:       <head>  
3:            <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>  
4:       </head>  
5:       <body>  
6:            <button type="button" id="iframecontent" onclick="iframecontent_click()">IFrame Content</button>  
7:            <iframe id='iframe1' frameborder="0" height="300pt" scrolling="auto" src="http://testserver.com?func=ll&amp;objId=92043&amp;objAction=browse&amp;saptheme=default&amp;wanttabs=0" style="background-color: transparent; border: 0px none transparent;padding: 0px" width="100%"></iframe>  
8:            <iframe id='iframe2' frameborder="0" height="300pt" scrolling="auto" src="test2.html" style="background-color: transparent; border: 0px none transparent;padding: 0px" width="100%"></iframe>      
9:            <iframe id='iframe3' frameborder="0" height="300pt" scrolling="auto" src="http://www.cnn.com" style="background-color: transparent; border: 0px none transparent;padding: 0px" width="100%"></iframe>      
10:       </body>  
11:       <script>  
12:            function iframecontent_click() {  
13:                 console.log('iframe1 content : ' + $("#iframe1").contents().find("html").html());  
14:                 console.log('iframe2 content : ' + $("#iframe2").contents().find("html").html());  
15:                 console.log('iframe3 content : ' + $("#iframe3").contents().find("html").html());  
16:            }  
17:       </script>  
18:  </html>  

Here via Jquery iframe1 and iframe3 content shows up as "undefined". It works fine for iFrame2.


In SalesForce, if you include visual force page inside the standard page, it is included inside the iFrame and Salesforce includes visual force page with different domain iFrame, as below. Hence in parent container JQuery would not be able to access content of child visual force page iframe.



Thursday, May 14, 2015

SalesForce LightBox/Modal dialog on Standard Page

In visual force page, it is quite easy to implement light box using any of the jquery plugin or even with just use of css. However when we have standard page and want to open light box upon button/link click, it wasn't straightforward:

Option 1: Lightbox inside Visual Force
Include visual force page inside standard page and put light box logic inside visual force page. 
This option doesn't work well, as when we include the visualforce page in standard page, it includes that page inside iframe. Hence if you open the light box from visualforce page, it will stay inside that iFrame.

Option 2 : Modal Dialog
When we create custom button to open the visual force page, it always opens in new page, to open as modal dialg, we can use below java script:


Javascript
In below script, it opens up the visual force page using modal dialog. It updates the parent page's onFocus and onClick event so it is disabled when the child window is open.

1:  var popupWindow;  
2:  var previousOnFocus;  
3:  var previousOnClick;  
4:    
5:  function parent_disable() {   
6:   if(popupWindow && !popupWindow.closed) {  
7:     popupWindow.focus();  
8:   } else if(popupWindow && popupWindow.closed) {  
9:     window.onfocus = previousOnFocus;  
10:     window.onclick = previousOnClick;  
11:   }  
12:  }       
13:    
14:  function openModel(pageURL) {  
15:    var w = (window.innerWidth * 80 / 100);  
16:    var h = (window.innerHeight * 80 / 100);  
17:    previousOnFocus = window.onFocus;  
18:    previousOnClick = window.onClick;  
19:    window.onfocus = parent_disable;  
20:    window.onclick = parent_disable;  
21:    var left = (screen.width/2)-(w/2);  
22:    var top = (screen.height/2)-(h/2);  
23:    popupWindow = window.open(pageURL,'Workspace',   
24:    'height='+h+',width='+w+',top='+top+',left='+left+',toolbar=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,modal=yes');  
25:    popupWindow.focus();  
26:  }  
27:    
28:  openModel('/apex/WorkspaceIF?id={!Account.Id}');  




Option 3 : LightBox
In this option we will use javascript to insert lighbox on the fly. For this option we had to get JQuery loaded which helps with DOM manipulation (and also we had to get some content from server so had to make rest calls).



in JavaScript block, load JQuery, JQueryUI and JQueryCSS. Once JQuery is loaded, the lightbox function is called (as mentioned in line 27).

1:  lightBox_click();  
2:    
3:  function lightBox_click() {  
4:   xxLoadJQuery();  
5:  }  
6:    
7:  function xxLoadScript(url, callback) {   
8:   var head = document.getElementsByTagName('head')[0];   
9:   var script = document.createElement('script');   
10:   script.type = 'text/javascript';   
11:   script.src = url;   
12:   script.onreadystatechange = callback;   
13:   script.onload = callback;   
14:   head.appendChild(script);   
15:  }  
16:    
17:  function xxLoadJQuery() {  
18:   xxLoadScript("https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js", xxLoadJQueryUI);  
19:  }  
20:    
21:  function xxLoadJQueryUI() {  
22:   xxLoadScript("https://code.jquery.com/ui/1.11.3/jquery-ui.min.js", xxLoadJQueryCSS);   
23:  }  
24:    
25:  function xxLoadJQueryCSS() {   
26:   $("&lt;link/>", { rel: "stylesheet", type: "text/css", href: "https://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css"}).appendTo("head");   
27:   xxLightBox();  
28:  }   
29:    

Now, in xxLightBox function, required div and css are inserted to enable the lightbox.

1:  function xxLightBox() {  
2:     $("<style>.modalDialog {position: fixed;font-family: Arial, Helvetica, sans-serif;top: 0;right: 0;bottom: 0;left: 0;background: rgba(0,0,0,0.8);z-index: 99999;opacity:0;-webkit-transition: opacity 400ms ease-in;-moz-transition: opacity 400ms ease-in;transition: opacity 400ms ease-in;pointer-events: none;} .modalDialog:target {opacity:1;pointer-events: auto;} .modalDialog > div {width: 95%;position: relative;margin: 1% auto;padding: 1px 1px 1px px;border-radius: 5px;background: #fff;background: -moz-linear-gradient(#fff, #999);background: -webkit-linear-gradient(#fff, #999);background: -o-linear-gradient(#fff, #999);}.close {background: #606061;color: #FFFFFF;line-height: 25px;position: absolute;right: -12px;text-align: center;top: -10px;width: 24px;text-decoration: none;font-weight: bold;-webkit-border-radius: 12px;-moz-border-radius: 12px;border-radius: 12px;-moz-box-shadow: 1px 1px 3px #000;-webkit-box-shadow: 1px 1px 3px #000; box-shadow: 1px 1px 3px #000;} .close:hover { background: #00d9ff; } </style>").appendTo("head");  
3:     var height = ( $( document ).height() * 90 / 100 );  
4:     var iFrameURL = xxGetIFramURL();  
5:     var lighboxDiv = '<a id="openModelLink" href="#openModal"></a><div id="openModal" class="modalDialog"><div><a id="closeModelLink" href="#close" class="close">X</a><iframe frameborder="0" height="' + height + '" scrolling="auto" src="' + iFrameURL + '" style="background-color: transparent; border: 0px none transparent;padding: 0px" width="100%"></iframe></div></div>';  
6:     if( iFrameURL == null || iFrameURL == '' ) {  
7:        lighboxDiv = '<a id="openModelLink" href="#openModal"></a><div id="openModal" class="modalDialog"><div><a id="closeModelLink" href="#close" class="close">X</a>Workspace does not exist.</div></div>';  
8:     }   
9:     $( "body" ).append(lighboxDiv);  
10:    document.getElementById("openModelLink").click();  
11:  }  
12:    


Extra note here (not needed for this example), we are displaying iFrame inside lightbox and I had to get iFrame URL from server, hence I had to make Rest call using JQuery as below (also explained in detail here)

1:  function xxGetIFramURL() {  
2:     var iFrameURL = '';  
3:     $.ajax({  
4:        url:  '/services/apexrest/WorkspaceService?salesForceId={!Account.Id}',  
5:        beforeSend: function(xhr) {  
6:           xhr.setRequestHeader('Authorization', 'Bearer {!$Api.Session_ID}');  
7:        },  
8:        success: function(result) {  
9:           iFrameURL = result;  
10:       },  
11:       async:  false  
12:    });  
13:    return iFrameURL;  
14:  }  





SalesForce rest api on Standard page

In Visual Force, it is very straightforward to use server side logic (Apex Classes) using Controller. However, when working on standard page - e.g. custom button java script, we didn't have access to the controller. E.g. below we have access to object and JavaScript block. SalesForce executes this java script using JavaScript's eval function.



If we need data from server side, it is a lot easier to get that using SalesForce Rest API. We also don't need to worry about security as we can get OAUTH Bearer token using {!$Api.Session_ID}').


  • Create Apex Class and expose as rest service as below


@RestResource(urlMapping='/MyRestService/*')
global with sharing class MyRestServiceImpl {

    @HttpGet
    global static String getIFrameURL() {
        String salesForceId = RestContext.request.params.get('salesForceId');
        return 'http://www.google.com?id='+salesForceId;
    }
}


  • Test is out using workbench.developerforce.com using URL /services/apexrest/MyRestService?salesForceId=1234





  • Below is Java Script code to call this service


First load the Jquery using java script as below, it loads jquery, jquery ui and jquery-ui.css

1:  lightBox_click();   
2:    
3:  function lightBox_click() {   
4:   xxLoadJQuery();   
5:  }   
6:    
7:  function xxLoadScript(url, callback) {   
8:   var head = document.getElementsByTagName('head')[0];   
9:   var script = document.createElement('script');   
10:   script.type = 'text/javascript';   
11:   script.src = url;   
12:   script.onreadystatechange = callback;   
13:   script.onload = callback;   
14:   head.appendChild(script);   
15:  }   
16:    
17:  function xxLoadJQuery() {   
18:   xxLoadScript("https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js", xxLoadJQueryUI);   
19:  }   
20:    
21:  function xxLoadJQueryUI() {   
22:   xxLoadScript("https://code.jquery.com/ui/1.11.3/jquery-ui.min.js", xxLoadJQueryCSS);   
23:  }   
24:    
25:  function xxLoadJQueryCSS() {   
26:   $("&lt;link/>", { rel: "stylesheet", type: "text/css", href: "https://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css"}).appendTo("head");   
27:   xxLightBox();   
28:  }   


Make a call to Rest Service using Jquery. Line number 11 passes the authorization token.

1:  function xxLightBox() {   
2:   var iFrameURL = xxGetIFramURL();   
3:   alert(' iFrameURL ' + iFrameURL );  
4:  }   
5:    
6:  function xxGetIFramURL() {   
7:   var iFrameURL = '';   
8:   $.ajax({   
9:    url: '/services/apexrest/MyRestService?salesForceId={!Account.Id}',   
10:    beforeSend: function(xhr) {   
11:     xhr.setRequestHeader('Authorization', 'Bearer {!$Api.Session_ID}');   
12:    },   
13:    success: function(result) {   
14:     iFrameURL = result;   
15:    },   
16:    async: false   
17:   });   
18:   return iFrameURL;   
19:  }  



  • Test using putting this button on layout:



Tuesday, March 17, 2015

SharePoint Rest API to upload document

It was quite tricky to work with SharePoint API to upload document using Java, below is code to make it work. I had to use Apache HTTP client library for NTML authentication, rest of the work was to orchestrate different calls needed for SharePoint.

Here is the flow. You can also download the entire code from here.

1) Easy part, read the file with base64 bytes (unless you already have the bytes ready)

   private byte[] readFile(String fileName) throws Exception {  
     File file = new File(fileName);  
     FileInputStream fileInputStreamReader = new FileInputStream(file);  
     byte[] bytes = new byte[(int)file.length()];  
     fileInputStreamReader.read(bytes);  
     byte[] encodedfile = Base64.encodeBase64(bytes);  
     return encodedfile;  
   }  

2) Initialize HTTP Client with NTLM authentication

     NTCredentials credentials = new NTCredentials(userName,password,localeMachineName,domain);  
     HttpClient httpClient = new HttpClient();  
     httpClient.getState().setCredentials(AuthScope.ANY, credentials);  
     httpClient.getParams().setAuthenticationPreemptive(true);  

3) Get Form Digest value - you have to use context URL and post HTTP request in order to get Form Digest value. It is needed for the next call, otherwise SharePoint will return error message complaining about browser back button

   private String getFormDigestValue(String baseLocation, String contextInfoURI, HttpClient httpClient ) throws Exception {  
     String formDigestValue = "";  
     System.out.println("SharePointTestClient.getFormDigestValue URL: " + baseLocation + contextInfoURI );  
     PostMethod postMethod = new PostMethod(baseLocation + contextInfoURI);  
     postMethod.addRequestHeader("Accept","application/json;odata=verbose");  
     int postResponseCode = httpClient.executeMethod(postMethod);  
     System.out.println("SharePointTestClient.getFormDigestValue postResponseCode " + postResponseCode );  
     for(org.apache.commons.httpclient.Header header : postMethod.getRequestHeaders() ) {  
       System.out.print("SharePointTestClient.getFormDigestValue postMethod request header " + header );  
     }  
     String responseBody = getResponseBody(postMethod);  
     System.out.println("SharePointTestClient.getFormDigestValue responseBody " + responseBody );  
     JSONObject jsonObject = (JSONObject) new JSONParser().parse(responseBody);  
     formDigestValue = (String)( (HashMap)((HashMap) jsonObject.get("d")).get("GetContextWebInformation") ).get("FormDigestValue");  
     System.out.println("SharePointTestClient.getFormDigestValue formDigestValue " + formDigestValue );  
     return formDigestValue;  
   }  

4) Last part is to send the HTTP Post to upload the document

     String postMethodLocation = baseLocation + "/_api/web/GetFolderByServerRelativeUrl('" + documentLibrary + "')/Files/add(url='" + uploadFileName + "',overwrite=true)";  
     System.out.println("SharePointTestClient.uploadFile postMethodLocation " + postMethodLocation );  
     PostMethod postMethod = new PostMethod(postMethodLocation);  
     postMethod.setRequestEntity(new ByteArrayRequestEntity(encodedfile));  
     postMethod.addRequestHeader("X-HTTP-Method", "POST");  
     postMethod.addRequestHeader("X-RequestDigest",formDigestValue);  
     postMethod.addRequestHeader("Accept","application/json;odata=verbose");  
     int uploadFileResponseCode = httpClient.executeMethod(postMethod);  
     System.out.println(" Upload File Response Code " + uploadFileResponseCode );  
     String responseBody = getResponseBody(postMethod);  
     System.out.println("SharePointTestClient.uploadFile Upload File responseBody " + responseBody );  
     JSONObject jsonObject = (JSONObject) new JSONParser().parse(responseBody);  
     String uri = (String)( (HashMap)((HashMap) jsonObject.get("d")).get("__metadata") ).get("uri");  
     System.out.println("SharePointTestClient.uploadFile uri " + uri );  


Testing with parameters:

 sharePointTestClient.uploadFile("test.pdf", "test.pdf", "http://ecmdv.springsoa.com", "/_api/contextinfo", "Shared%20Documents", "chshah", "***********", "localhost", "adprod.springsoa.com" );  
 Parameters  
 String fileName -  fileName to be read  
 String uploadFileName -  fileName (which will show up in SP)  
 String baseLocation -  base location, e.g. http://ecmdv.springsoa.com/contracts/na_03 or http://ecmdv.springsoa.com  
 String contextInfoURI -  context url to get form digest (e.g. /_api/contextinfo)  
 String documentLibrary -  document library or folder name (e.g. Docs )  
 String userName                 
 String password  
 String localeMachineName -  Not needed but client machine name  
 String domain -  domain name of the user  


Tuesday, January 20, 2015

Node Manger Configuration 11g/12c

Node Manager Configuration and Start up in 11g (10.3.6)

Generate Node Manager Properties (nodemanager.properties):

 cd %WLS_HOME%\server\bin  
 startNodeManager.cmd  

This will generate the %WLS_HOME%\common\nodemanager\nodemanager.properties file. We can verify the nodemanager.properties file for all properties. I usually set up SecureListener=false, as it requires more configuration if we are using Custom Identity/Trust.

Now we can start again, and new properties will take in effect.

 cd %WLS_HOME%\server\bin  
 startNodeManager.cmd  

Node Manager Configuration and Start up in 12c

Node manager properties file %DOMAIN_HOME%\nodemanager\nodemanager.properties is generated as part of the installation process or domain creation. Once it is verified, we can start the node manager with following command

 cd %DOMAIN_HOME%\bin  
 setDomainEnv.cmd  
 cd %DOMAIN_HOME%\bin  
 startNodeManager.cmd  

Admin Console (11g/12c)
Admin console part remains the same in both version. where you can verify the status of Node Manager, configure a few properties and assign machine to node manager if needed.









Friday, January 16, 2015

WebLogic/OSB one way and two way SSL - outbound calls

I found portecle quite useful when importing and exporting keys and it provides neat user interface. In standalone JVM, we can specify keystore using command line parameters as below

 -Djavax.net.ssl.keyStore  
 -Djavax.net.ssl.trustStore  
 -Djavax.net.ssl.trustStorePassword  
 -Djavax.net.ssl.keyStorePassword  
 -Djavax.net.ssl.keyStoreProvider  

In WebLogic, it is done via Admin Console.

One Way SSL
It is quite straight forward to configure one way SSL. I believe preferred way is to use Custom Identity and Custom Trust store, which is much better to manage than Demo trust and identity.

Using DemoTrust and DemoIdentity
The location of DemoTrust and DemoIdentity can be found from Admin Console:


The default passwords are:
 DemoIdentity.jks -> DemoIdentityKeyStorePassPhrase  
 DemoTrust.jks -> DemoTrustKeyStorePassPhrase  

We can open them in Portecle and import the SSL cert.

Using Custom Identity and Custom Trust
I prefer this option better than Demo. I usually use jre/lib/cacerts as starting point and import all necessary certs in cacerts. Once all certs are imported in cacerts, then weblogic configuration as below

Under KeyStores tab:

Under SSL tab:
It needs to be done on all servers which would be affected, and server restart would be necessary after this.





Two Way SSL
Using Custom Identity and Custom Trust
I imported both private key and root cert in cacerts file. The password of the cacerts file was same as password of private key, I was not sure if it was absolutely necessary but that is how it worked in standalone JVM. Once private key is imported, below is the WebLogic configuration



Enable SSL Debug
 -Dssl.debug=true  
 -Dweblogic.security.SSL.verbose=true  
 -Djavax.net.debug=all  


Loosen up Security Constraint
 -Dweblogic.webservice.client.ssl.strictcertchecking=false  
 -Dweblogic.security.SSL.allowSmallRSAExponent=true  
 -Dweblogic.security.SSL.enforceConstraints=off  
 -Dweblogic.security.SSL.enable.renegotiation=true  
 -Dsun.security.ssl.allowUnsafeRenegotiation=true  

Wednesday, January 14, 2015

webMethods NSRuntimeException

In webMethods, I was facing "com.wm.app.b2b.client.ns.NSRuntimeException". It is quite generic exception caused in many different situations. The solution in each case quite same, drop the variables in pipeline, especially some of the variables which are sensitive to Java and Notification service, e.g. document, properties were causing the problems.

Monday, January 5, 2015

Consuming EBS events (WF_BPEL_Q) using Java/PLSQL AQ API

When we raise event in EBS, it is enqueued in durable subscriber queue (WF_BPEL_Q). We could use EBS API or pure AQ (java/plsql) API to dequeue them. The message type stored in this queue is object type (WF_EVENT_T)

Use below query to check list of subscribers and subscriber number. Both queries shows subscriber list, but second query shows subscriber number, which is very helpful to troubleshoot later.

 SELECT * FROM ALL_QUEUE_SUBSCRIBERS WHERE QUEUE_NAME = 'WF_BPEL_Q';  
 SELECT * FROM AQ$_WF_BPEL_QTAB_S WHERE NAME IS NOT NULL;  


In WF_BPEL_QTAB, we can see all messages, and AQ$_WF_BPEL_QTAB_I would store copy of the message for each subscriber. The durable subscription is explained here in detail.

 SELECT * FROM AQ$_WF_BPEL_QTAB_I;  


Enqueue using PLSQL (instead of raise event)
 DECLARE  
   enqueue_options   dbms_aq.enqueue_options_t;  
   message_properties dbms_aq.message_properties_t;  
   message_handle   RAW(16);  
   message       APPS.WF_EVENT_T;  
 BEGIN  
   message := APPS.WF_EVENT_T(50,SYSDATE,SYSDATE,NULL,APPS.WF_PARAMETER_LIST_T(APPS.WF_PARAMETER_T('USER_ID','20421'),APPS.WF_PARAMETER_T('RESP_ID','51378'),APPS.WF_PARAMETER_T('RESP_APPL_ID','800'),APPS.WF_PARAMETER_T('SECURITY_GROUP_ID','0'),APPS.WF_PARAMETER_T('ORG_ID','122'),APPS.WF_PARAMETER_T('PARTY_ID','3667135'),APPS.WF_PARAMETER_T('#CURRENT_PHASE','101')),'oracle.apps.ar.hz.Person.create','oracle.apps.ar.hz.Person.create659162',NULL,APPS.WF_AGENT_T('WF_BPEL_QAGENT','EBIZPROD.SPRINGSOA.COM'),NULL,NULL,NULL,NULL);  
   dbms_aq.enqueue(queue_name => 'WF_BPEL_Q',        
      enqueue_options   => enqueue_options,      
      message_properties  => message_properties,     
      payload       => message,          
      msgid        => message_handle);  
   COMMIT;  
 END;  



Register Custom Subscriber
Below code can be used to register custom durable subscriber to the queue.

 DECLARE  
   SUBSCRIBER SYS.AQ$_AGENT;  
 BEGIN  
   SUBSCRIBER := SYS.AQ$_AGENT('CUSTOM', NULL, NULL);  
   DBMS_AQADM.ADD_SUBSCRIBER(QUEUE_NAME => 'WF_BPEL_Q',SUBSCRIBER => SUBSCRIBER);  
 END;       

Dequeue using PLSQL
To Dequeue this using PLSQL, was a lot easier than Java, as WF_EVENT_T object is available at database level under APPS schema. PLSQL code below:

 DECLARE  
   DEQUEUE_OPTIONS   dbms_aq.dequeue_options_t;  
   MESSAGE_PROPERTIES dbms_aq.message_properties_t;  
   MESSAGE_HANDLE   RAW(16);  
   MESSAGE       APPS.WF_EVENT_T;  
 BEGIN  
      DEQUEUE_OPTIONS.WAIT := DBMS_AQ.NO_WAIT;  
      DEQUEUE_OPTIONS.CONSUMER_NAME := 'CUSTOM';  
      DEQUEUE_OPTIONS.NAVIGATION := DBMS_AQ.FIRST_MESSAGE;  
      DBMS_AQ.DEQUEUE(QUEUE_NAME => 'WF_BPEL_Q',  
       DEQUEUE_OPTIONS  => DEQUEUE_OPTIONS,  
       MESSAGE_PROPERTIES => MESSAGE_PROPERTIES,  
       PAYLOAD      => MESSAGE,  
       MSGID       => MESSAGE_HANDLE);  
      DBMS_OUTPUT.PUT_LINE ('Message: ' || MESSAGE );  
      DEQUEUE_OPTIONS.NAVIGATION := DBMS_AQ.NEXT_MESSAGE;  
      COMMIT;  
 END;  

Dequeue using Java API
To dequeue using Java using pure AQ API, I used JPublisher to convert WF_EVENT_T data type to WF_EVENT_T java class as below:

1) Running JPublisher on DOS prompt to generate Java object for WF_EVENT_T

 set DATABASE_HOME=C:\oracledb\product\11.2.0\dbhome_1  
 set JAVA_HOME=C:\Oracle\Java\hotspot\jdk  
 set CLASSPATH=%DATABASE_HOME%\jdbc\lib\ojdbc5.jar;%DATABASE_HOME%\sqlj\lib\translator.jar;%DATABASE_HOME%\sqlj\lib\runtime12.jar  
 %JAVA_HOME%\bin\java -classpath %CLASSPATH% oracle.jpub.Doit -url=jdbc:oracle:thin:@ebiz.springsoa.com:1521:ebiz -user=apps/***** -sql=WF_EVENT_T -usertypes=oracle -methods=false -package=com.springsoa.jpub -usertypes=jdbc  

This generate WF_EVENT_T.java, which you can copy to your project under right package folder (e.g. com.springsoa.jpub) in this case.

2) Writing Java code for Dequeue

 package com.springsoa.util;  
 import com.springsoa.jpub.WF_EVENT_T;  
 import java.sql.Connection;  
 import java.sql.DriverManager;  
 import java.util.HashMap;  
 import java.util.Map;  
 import oracle.AQ.AQAgent;  
 import oracle.AQ.AQDequeueOption;  
 import oracle.AQ.AQDriverManager;  
 import oracle.AQ.AQMessage;  
 import oracle.AQ.AQQueue;  
 import oracle.AQ.AQSession;  
 import oracle.sql.STRUCT;  
 public class AQQueueConsumer {  
   public AQSession createSession() {  
     Connection connection;  
     AQSession aqSession = null;  
     try {  
       Class.forName("oracle.jdbc.driver.OracleDriver");  
       connection = DriverManager.getConnection("jdbc:oracle:thin:@ebiz.springsoa.com:1521:ebiz","apps", "******");  
       connection.setAutoCommit(true);  
       Class.forName("oracle.AQ.AQOracleDriver");  
       aqSession = AQDriverManager.createAQSession(connection);  
     } catch (Exception ex) {  
       ex.printStackTrace();  
     }   
     return aqSession;  
   }  
   public void listDurableSubscribers(AQSession aqSession, String queueOwner, String queueName) throws Exception {  
     AQQueue queue = aqSession.getQueue(queueOwner, queueName);        // select * from all_queues  
     AQAgent[] agents = queue.getSubscribers();  
     for(int i=0; i<agents.length; i++) {  
       System.out.println(" Consumers : " + agents[i].getName() );  
     }  
   }  
   public Map<String,String> dequeue(AQSession aqSession, String queueOwner, String queueName) throws Exception {  
     Map<String,String> messageMap = new HashMap<String,String>();  
     AQQueue queue = aqSession.getQueue(queueOwner, queueName);        // select * from all_queues  
     AQDequeueOption aqDequeueOption = new AQDequeueOption();        // https://docs.oracle.com/cd/B19306_01/server.102/b14257/aq_views.htm  
     aqDequeueOption.setConsumerName("CUSTOM");   // ORA_6H0JECQ66OO48D1N6SRJIC1N61 ORA_70skccq26kok8cq6752j0da665      
     aqDequeueOption.setNavigationMode(AQDequeueOption.NAVIGATION_FIRST_MESSAGE);  
     //aqDequeueOption.setCondition("tab.user_data.event_name = 'oracle.apps.xxbmc.employeePublish.updateEvent'");  
     aqDequeueOption.setWaitTime(AQDequeueOption.WAIT_NONE);  
     AQMessage message = queue.dequeue(aqDequeueOption, WF_EVENT_T.class);  
     if( message != null && message.getObjectPayload() != null && message.getObjectPayload().getPayloadData() != null ) {  
       WF_EVENT_T wfEventT = (WF_EVENT_T) message.getObjectPayload().getPayloadData();  
       messageMap.put("eventName",wfEventT.getEventName());  
       messageMap.put("eventKey",wfEventT.getEventKey());  
       if( wfEventT.getParameterList() != null && wfEventT.getParameterList().getArray() != null ) {  
         Object[] parameters = (Object[]) wfEventT.getParameterList().getArray();  
         for(int i=0;i<parameters.length;i++) {  
           //System.out.println(" parameter[" + i + "] : " + ( (oracle.sql.STRUCT)parameters[i] ).dump() );  
           STRUCT parameter = (oracle.sql.STRUCT)parameters[i];  
           Object[] attributes = parameter.getAttributes();  
           if(attributes.length == 2) {  
             messageMap.put((String)attributes[0],(String)attributes[1]);  
           }  
         }  
       }  
     }  
     return messageMap;  
   }  
   public static void main(String[] args) {  
     AQSession aq_sess = null;  
     try {  
       AQQueueConsumer aqQueueConsumer = new AQQueueConsumer();  
       AQSession aqSession = aqQueueConsumer.createSession();  
       aqQueueConsumer.listDurableSubscribers(aqSession,"APPS", "WF_BPEL_Q");  
       Map map = aqQueueConsumer.dequeue(aqSession,"APPS", "WF_BPEL_Q");  
       System.out.println(" map " + map );  
     } catch (Exception ex) {  
       System.out.println("Exception: " + ex);  
       ex.printStackTrace();  
     }  
   }  
 }  

In above code, dequeue method does the dequeue, code is quite similar to what we have in PLSQL, however some extra work needs to be done to get all parameters from WF_EVENT_T as WF_PARAMETER is not given as SQL object.

Please note that message in WF_BPEL_QTAB will not be removed or marked as status 2 until all durable subscribers consumes the message. However, message will be deleted for a specific subscriber ("CUSTOM" in above case) in AQ$_WF_BPEL_QTAB_I. 

Purge records

 EXECUTE DBMS_AQADM.PURGE_QUEUE_TABLE('APPS.WF_BPEL_QTAB', NULL, NULL);  

Friday, January 2, 2015

AQ Durable Subscripton

AQ Durable subscription works like durable topic subscription, and below are some tweaks we can use to check how it works internally and might help troubleshoot further.

 EXECUTE DBMS_AQADM.STOP_QUEUE ( QUEUE_NAME => 'CS_MULTI_Q');  
 EXECUTE DBMS_AQADM.DROP_QUEUE ( QUEUE_NAME => 'CS_MULTI_Q');  
 EXECUTE DBMS_AQADM.DROP_QUEUE_TABLE ( QUEUE_TABLE => 'CS_MULTI_QTAB');  
 EXECUTE DBMS_AQADM.CREATE_QUEUE_TABLE ( QUEUE_TABLE     => 'CS_MULTI_QTAB', MULTIPLE_CONSUMERS => TRUE, QUEUE_PAYLOAD_TYPE => 'RAW' );   
 EXECUTE DBMS_AQADM.CREATE_QUEUE ( QUEUE_NAME => 'CS_MULTI_Q', QUEUE_TABLE => 'CS_MULTI_QTAB', RETENTION_TIME => DBMS_AQADM.INFINITE );   
 EXECUTE DBMS_AQADM.START_QUEUE ( QUEUE_NAME     => 'CS_MULTI_Q');   

Create durable subscriber

 DECLARE  
   subscriber sys.aq$_agent;  
 BEGIN  
   subscriber := sys.aq$_agent('ONE', NULL, NULL);  
   DBMS_AQADM.ADD_SUBSCRIBER(queue_name => 'CS_MULTI_Q',subscriber => subscriber);  
   subscriber := sys.aq$_agent('TWO', NULL, NULL);  
   DBMS_AQADM.ADD_SUBSCRIBER(queue_name => 'CS_MULTI_Q',subscriber => subscriber);   
 END;  

En-queue Messages:

 DECLARE  
   ENQUEUE_OPTIONS   DBMS_AQ.ENQUEUE_OPTIONS_T;  
   MESSAGE_PROPERTIES DBMS_AQ.MESSAGE_PROPERTIES_T;  
   MESSAGE_HANDLE   RAW(16);  
   MESSAGE       RAW(4096);  
 BEGIN  
   MESSAGE := HEXTORAW(RPAD('FF',4095,'FF'));   
   DBMS_AQ.ENQUEUE(QUEUE_NAME => 'CS_MULTI_Q',  
      ENQUEUE_OPTIONS   => ENQUEUE_OPTIONS,  
      MESSAGE_PROPERTIES  => MESSAGE_PROPERTIES,     
      PAYLOAD       => MESSAGE,          
      MSGID        => MESSAGE_HANDLE);  
   COMMIT;  
 END;  


Now if we see, below queries shows the all the subscribers to this queue (ONE and TWO), and their numbers. (This query shows different results than select * from all_subscribers)
 SELECT * FROM AQ$_CS_MULTI_QTAB_S WHERE NAME IS NOT NULL;  













Below query shows the multiple copy of the messages - one per each subscriber.
 SELECT * FROM AQ$_CS_MULTI_QTAB_I;  












Dequeue the message as subscriber "TWO" using below PLSQL block:

 DECLARE  
   DEQUEUE_OPTIONS   DBMS_AQ.DEQUEUE_OPTIONS_T;  
   MESSAGE_PROPERTIES DBMS_AQ.MESSAGE_PROPERTIES_T;  
   MESSAGE_HANDLE   RAW(16);  
   MESSAGE       RAW(4096);  
 BEGIN  
      DEQUEUE_OPTIONS.WAIT := DBMS_AQ.NO_WAIT;  
      DEQUEUE_OPTIONS.CONSUMER_NAME := 'TWO';  
      DEQUEUE_OPTIONS.NAVIGATION := DBMS_AQ.FIRST_MESSAGE;  
      DBMS_AQ.DEQUEUE(QUEUE_NAME => 'CS_MULTI_Q',  
       DEQUEUE_OPTIONS  => DEQUEUE_OPTIONS,  
       MESSAGE_PROPERTIES => MESSAGE_PROPERTIES,  
       PAYLOAD      => MESSAGE,  
       MSGID       => MESSAGE_HANDLE);  
      DBMS_OUTPUT.PUT_LINE ('Message: ' || message );  
      DEQUEUE_OPTIONS.NAVIGATION := DBMS_AQ.NEXT_MESSAGE;  
      COMMIT;  
 END;    

After we dequeue this message, we can see that in queue table (CS_MULTI_QTAB), status field is still set to 0. This is because other subscribers (e.g. ONE) has not dequeued the message. Also we can see that in table AQ$_CS_MULTI_QTAB_I, the message for subscriber "TWO" is deleted.






Once all subscribers dequeues the message, the status field in Queue table (CS_MULTI_QTAB) will be set to 2 (if retention time is set), or message from queue table will be deleted.


Purge Records

 EXECUTE DBMS_AQADM.PURGE_QUEUE_TABLE('CS_MULTI_QTAB', NULL, NULL);