As part of a project I am working on, we have got budget to purchase additional licences for our chosen reporting tool – Business Objects Edge.
This means that we can move most of our Crystal Reports on to the Business Objects Edge server, it can run both Crystal and WebI reports. This will remove the need for most of our staff to use a custom application written to run Crystal Reports which is deployed to each staff members computer.
An added benefit is that as Business Objects Edge client interaction is designed around being run within a web-browser, so we can launch reports from Smart Office with context data via a URL.
But with every silver lining, there is a cloud…
Business Objects licensing isn’t traditionally great for this sort of thing – each URL launch consumes a license. If you are running concurrent licenses a single user can quickly eat through your licenses, in our instance we will be named user licenses. Even though we will be named user licenses soon, I still would have to obtain a login token with each report which gets launched, so my plan is to store the login token to reduce the number of calls we need to make to run a report. If we weren’t using named user licenses, we would need to control our browser session and logout when the user closes the window.
There’s a fair amount of code that is needed to log in (we don’t have single sign-on yet) to run our report and it’s not something that I really want to per program where I am creating a report link.
To resolve this, I have created a script called boeRunReport.js – we will call this script from another script with the arguments determining the name of the report and its arguments.
The basic process is as follows:
- Run Script which retrieves report context information
- Call the boeRunReport script passing the reports arguments
- Retrieve the location of our Business Objects Edge server from a config file
-
Check to see if we have an existing login token saved to the SessionCache
- If no login token, make a Rest call to get a login token and save it to the SessionCache
- Build our URL and then call DashboardTaskService.Current.LaunchTask() to run the report
Below is a script which demonstrates calling our boilerplate code – in a real world situation we would be extracting some data from the panel and then including that in our parameters so our report becomes in-context.
If we were being really clever we’d also create the Task so we can keep track of the window that is created so we can log out. But given the licensing model I’m working with, fire and forget works and is easier 🙂
/* ** Proof of concept, running a script from another script ** ** */ import System; import System.Windows; import System.Windows.Controls; import MForms; import Mango.UI.Services; import System.Net; import System.Web; package MForms.JScript { class testingReports { public function Init(element: Object, args: Object, controller : Object, debug : Object) { // iDocID = the identifier of the report we are going to run // sIDType = this tells Business Objects Edge how to treat the iDocID // lsS<parameter name> = <parameter name> 'Order Name' is the name of the report parameter 'Order Name' // lsS<parameter name> = <parameter name> 'Attention' is another report parameter 'Attention' var strArguments : String = Uri.EscapeDataString("&iDocID=Ae392dj3K_9Dq3V_4S7QxCw&sIDType=CUID&lsSOrder Number=0000000002&lsSAttention=Scott"); // the actual launching of the task. Call the script boeRunReport and provide the arguments to determine // the script and its arguments DashboardTaskService.Current.LaunchTask(new Uri("mforms://_runscript?name=boeRunReport&args=" + strArguments)); } } }
And now we have the boilerplate code that does all of the heavy lifting:
/* ** boeRunReport.js ** ** This jscript will provide the base code to run a Business Objects Edge ** report from a URL. ** ** We will read the base URLs, Username and Password from our configuration ** file. We could use our users login/password and secLDAP authentication instead ** but in this instance we won't. ** ** We will retrieve a login token from the restful webservices, and then ** cache that so subsequent calls don't attempt to gain another license. ** ** We should have another script call this script, passing in the rest of the URL ** via the args. ** ** An example would be a call like so: ** DashboardTaskService.Current.LaunchTask(new Uri("mforms://_runscript?name=boeRunReport&args=%26iDocID=AXgm1vjEaLpOh06wgcuF9OE%26sIDType=CUID%26lsMOrder Number=0000000002%26lsSAttention=Scott")); ** ** Note: ** We don't do any logouts. We don't really care, because the session will eventually timeout ** and we have named user licenses ** ** History: ** 20150612 - initial cut */ import System; import System.Windows; import System.Windows.Controls; import MForms; import Mango.UI; import Mango.UI.Services; import Mango.Services; import System.Net; import System.Text; import System.IO; import System.Web; import System.Xml; import System.Xml.Serialization; import System.Xml.XPath; package MForms.JScript { class boeRunReport { var gstrConfigurationFileName = "citcl_boeRunReportProperties.xml"; var gstrSessionCacheKey = "boeRunReport_LoginToken"; var gstrOpenDocumentURI : String = "http://boe.indfish.co.nz:8080/BOE/OpenDocument/opendoc/openDocument.jsp?"; var gstrRestURI : String = "http://boe.indfish.co.nz:6405/biprws/logon/long"; var gUsername = null; var gPassword = null; var gToken = ""; var gSettings = null; public function Init(element: Object, args: Object, controller : Object, debug : Object) { // check to see if we have already obtained a session token, if so we will use that if(true == SessionCache.ContainsKey(gstrSessionCacheKey)) { gToken = SessionCache.Get(gstrSessionCacheKey); } else { } var profile : ISystemProfile = ApplicationServices.SystemProfile; var runtimeURI = profile.GetProperty(ApplicationGroupType.M3, ApplicationType.MForms, "RuntimeUrl"); gSettings = loadSettings(runtimeURI); if(null != gSettings) { // get the username/password from the config file gUsername = gSettings.user; gPassword = gSettings.password; gstrRestURI = gSettings.boeRestURL; gstrOpenDocumentURI = gSettings.boeOpenDocumentURL; if(true == String.IsNullOrEmpty(gToken)) { gToken = getSession(gstrRestURI); } if(false == String.IsNullOrEmpty(gToken)) { if(false == SessionCache.ContainsKey(gstrSessionCacheKey)) { if(false == String.IsNullOrEmpty(gToken) && gToken != "undefined" && gToken != "null") { SessionCache.Add(gstrSessionCacheKey, gToken); } } runReport(args); } else { } } else { ConfirmDialog.ShowErrorDialog("Error", "Failed to load settings for running reports"); } } private function runReport(args) { gUsername = gSettings.user; gPassword = gSettings.password; gstrRestURI = gSettings.boeRestURL; gstrOpenDocumentURI = gSettings.boeOpenDocumentURL; // remove the leading and trailing speechmark var strLogin : String = gToken.substring(1, gToken.length-1); // now we need to escape the data var strEscaped : String = HttpUtility.UrlEncode(strLogin, System.Text.Encoding.UTF8); //Uri.EscapeUriString(strLogin); var strFinalString : String = gstrOpenDocumentURI + args + "&token=" + strEscaped; DashboardTaskService.Current.LaunchTask(new Uri(strFinalString)); } private function getSession(astrURL : String) : String { var result : String = ""; try { var hwrRequest : HttpWebRequest = WebRequest.Create(astrURL); if(null != hwrRequest) { var credCache = new CredentialCache(); // here we're defining our actions and content types hwrRequest.ContentType = "application/xml;charset=\"utf-8\""; hwrRequest.Method = "POST"; hwrRequest.Accept = "application/xml"; credCache.Add(new Uri(astrURL), "Basic", new NetworkCredential("secEnterprise\\" + gUsername, gPassword)); hwrRequest.Credentials = credCache; var strmStream : Stream = hwrRequest.GetRequestStream(); if(null != strmStream) { // SOAP is basically just xml, so we are going to use the XML framework // to make our lives easier. // Create an XML Document var xmdDocument : XmlDocument = new XmlDocument(); if(null != xmdDocument) { // we then add the String to our XML document xmdDocument.LoadXml("<attrs xmlns=\"http://www.sap.com/rws/bip\"><attr name=\"userName\" type=\"string\">" + gUsername + "</attr><attr name=\"password\" type=\"string\">" + gPassword + "</attr><attr name=\"auth\" type=\"string\" possibilities=\"secEnterprise,secLDAP,secWinAD,secSAPR3\">secEnterprise</attr></attrs>"); // the save of the document to our stream actually sends the request // to the server xmdDocument.Save(strmStream); // close our stream strmStream.Close(); var wresponse : WebResponse = hwrRequest.GetResponse(); if(null != wresponse) { // search for the Logon token for(var i = 0; i < wresponse.Headers.Count; i++) { if("X-SAP-LogonToken" == wresponse.Headers.Keys[i]) { //gDebug.Debug(" " + wresponse.Headers.Keys[i] + ": " + wresponse.Headers[i]); result = wresponse.Headers[i]; } } wresponse.Close(); } else { //gDebug.Error("No response"); } } } } else { //gDebug.Error("No hwrRequest"); } } catch(ex) { ConfirmDialog.ShowErrorDialog("Exception", ex); //gDebug.Error(ex); } return(result); } private function loadSettings(uri : String) { try { var strFullPath : String = uri + "/xml/" + gstrConfigurationFileName; var doc : XPathDocument = new XPathDocument(strFullPath); var nav : XPathNavigator = doc.CreateNavigator(); var user = nav.Evaluate("string(//Username)"); var password = nav.Evaluate("string(//Password)"); var boeOpenDocumentURL = nav.Evaluate("string(//BOEOpenDocumentURL)"); var boeRestURL = nav.Evaluate("string(//BOERestURL)"); return{user : user, password : password, boeOpenDocumentURL : boeOpenDocumentURL, boeRestURL : boeRestURL}; } catch(ex) { //gDebug.Error("Exception loading gSettings: " + ex); } return(null); } } }
And it looks something like this:
Enjoy!
Scott