Saturday 9 August 2014

InfoView Express (BOBJ SDK Demo)

InfoView Express (BOBJ SDK Demo)

The BusinessObjects SDK. A daunting thing, to be sure (for me at least). Recently I had the chance to spend a few weeks playing with it and had quite a lot of fun.
So I set out to create something useful, but what? InfoView and the CMC do everything, what more could I do? So I decided to do less. Much less.
I’m Using:
  • BusinessObjects Enterprise XI3.1
  • BusinessObjects Java Enterprise SDK
  • BusinessObjects Java ReportEngine SDK
  • BusinessObjects Java Viewers SDK
  • Eclipse IDE for Java, HTML and JavaScript work
I’m Assuming:
  • You’re interested in the SDK either as an end user or a developer.
  • You don’t have to be a Java or JavaScript programmer to get started (I’m certainly not) but the more you know the more you can put your ideas into action.


What’s the Point of this Post?
Good question.
  • For those that haven’t used the SDK, but might: it’s a demo of the kind of things that can be done. Just skip over the code samples.
  • For those that do use it: it shows a few tricks that you may find useful.
  • For me: a little bit of showing off.


Lucky
I don’t have customers, so I don’t need to allow for older browsers. For the most part, the examples below are targeted at ‘modern’ browsers: IE9+, Chrome and Firefox 5+.


Mobile
Most of what you see here is designed to be finger-friendly and works just fine on an iPad or Android. The layout is dynamic so different resolutions/rotations are handled cleanly.


Log-in
Google, Facebook, LinkedIn, Twitter. All of these services remember your password, so why not InfoView?
image

The Remember Me tick box will store your details in HTML5 local storage the first time you log in.

function submitForm(){   
    //first store values
    if(document.getElementById('remCheck').checked){
        localStorage.rememberMe='true';
        localStorage.user=document.getElementById('user').value;
        localStorage.password=document.getElementById('password').value;
        //localStorage.cms=document.getElementById('cms').value;
        //localStorage.auth=document.getElementById('auth').value;
    }   
    var rememberMe = document.getElementById('remCheck').checked;       
    localStorage.rememberMe=rememberMe;   
   
    //then submit
    document.loginform.submit();
}


If you bookmark the login page or the main screen, returning to that bookmark will check to see if you’ve said ‘remember me’ and log you in and redirect you to the main page. The code for this looks like:

function autoLoginMaybe(){
    if (<%=action%> != 'logout'){
        if(localStorage.rememberMe == 'true'){
            document.loginform.submit();
        }
    }else{ //reset the URL so a bookmark won't contain ?action=logout
        window.history.pushState('','some title goes here','loginPage.jsp');
    }
}


This feature can be turned off centrally if security is a concern.


The Main Page
Actually, it’s the only page.
image



Search
To begin with, the cursor is sitting in the search box. Let’s say I want to run the world sales report. I type ‘wo’ and the following list appears:
image

This is instant-style search, so the list is filtered with each key press. This works because while the page was loading, an AJAX call was fetching a list of all reports accessible by the user, including path and type.
The server-side java that returns the report list, in JSON format, looks like this:

String getAllReports(IInfoStore iStore, int id, int pathLevel, String[] pathArray){
    //returns an entire report list in JSON format
   
    String results = "";
    String pathName = "";
    String objectName = "";
    pathLevel++;
    IInfoObjects objects;
    IInfoObject object;

    try{
        String reportQuery = "SELECT SI_ID, SI_NAME FROM CI_INFOOBJECTS "
            + "WHERE SI_PARENTID=" + id + " AND (SI_KIND = 'CrystalReport' OR SI_KIND = 'Folder' OR SI_KIND = 'Flash' "
            + "OR (SI_KIND = 'Webi' AND SI_INSTANCE = 0)) "
            + "ORDER BY SI_NAME";
        objects = iStore.query(reportQuery);   
        Iterator objectIter = objects.iterator();

        while (objectIter.hasNext()){
            object = (IInfoObject)objectIter.next();
            objectName = cleanString(object.getTitle());
           
            if (object.getKind().equalsIgnoreCase("folder")){
                pathArray[pathLevel - 1] = objectName;
                pathName = "";
                for (int i=0; i < pathLevel; i++){
                    pathName += pathArray[i] + " > ";
                }
                results += getAllReports(iStore, object.getID(), pathLevel, pathArray);
            }else {
                pathName = "";
                for (int i=0; i < pathLevel - 1; i++){ //-1 because I don't need to report name in the path.
                    pathName += pathArray[i] + " > ";
                }
                pathName = pathName.substring(0,pathName.length() - 2);
                results += "{\"docName\":\"" + objectName;
                results += "\",\"docPath\":\"" + pathName;
                results += "\",\"docKind\":\"" + object.getKind();
                results += "\",\"docID\":\"" + object.getID() + "\"},";                   
            }
        }   
    }catch (SDKException sdke){
    }   
    return results;
}


For about 1,000 reports, this will take less than a second to return to the browser. If you’re fast, and begin typing in the search before the list is loaded, the list of reports from the last time you logged in will be used. The lists are switched out seamlessly behind the scenes when the new list arrives (even if you’re halfway through typing a word).
Each key press loops through the JSON list (which is in memory on the client), filters for the search terms and prints out the results (to an HTML unordered list). Both the report name and folder path are searched, so if you want to see all the sales reports in a folder called ‘Australia’, you can just type ‘sales australia’.

function filterList(searchString){
    var startDate = new Date();   
    var matches;
    var objectCount = 0;
    searchString = searchString.toUpperCase();
    var searchTermArray = searchString.split(' ');   
    var objectArray = JSON.parse(localStorage.fullTree);
    var objectList = '<ul>';   
   
    //loops through the object array, and for each object, checks if it matches any of the search terms.
    for (i in objectArray){
        docNameUpper = objectArray[i].docName.toUpperCase() + ' ' + objectArray[i].docPath.toUpperCase()+ ' ' + objectArray[i].docKind.toUpperCase();
        matches = true;
        for (j in searchTermArray){
            searchString = searchTermArray[j];
            if (docNameUpper.indexOf(searchString) == -1){
                matches = false;
            }
        }
        if (matches == true){
            objectCount++;
            objectList += "<li class='li" + objectArray[i].docKind + "' id='r" + objectCount + "'";
            objectList += "onclick='getRepAndStore(" + objectArray[i].docID + ",0,1,\""  + objectArray[i].docName + "\",\"" + objectArray[i].docKind + "\")'>" + objectArray[i].docName + " ";
            objectList += "<span class='subtle'> " + objectArray[i].docPath + "</span></li>";
        }
    }   
    objectList += '</ul>';
    document.getElementById('filteredList').innerHTML = objectList;
       
}


Note that the report kind (objectArray[x].docKind), e.g. Webi, Crsytal, etc. is used to define the class of the HTML element. This class in turn defines the icon shown for the report. The CSS looks like this:

.liWebi{
    list-style-image: url('../images/webi16x16.png');
}
.liFlash{
    list-style-image: url('../images/xcelsius16x16.png');
}
.liCrystalReport{
    list-style-image: url('../images/crystal16x16.png');
}


It may seem inefficient to re-loop through the entire list for each new key press, but remember, this list is going to be less than a few thousand items, maybe a few hundred KB, in memory, on the client. That winds up being less than a millisecond per key-press.


Non-Rodent Navigation
The interface is keyboard friendly, so at this point, pressing enter would run the top report, or pressing the down arrow key will move down to the sales report.
image

Hitting enter will open that report. So, since clicking the bookmark to launch the app, I haven’t touched the mouse yet.
The JavaScript to manage keyboard input across all browsers was, to be honest, quite tricky.

function selectReport(evt){
    var keyCode = (evt.which) ? evt.which : window.event.keyCode;
   
    if (keyCode == 40) { //down arrow
        if (selRepPos == null){
            selRepPos = 0;
        }   
        selRepPos++;   
        selectedReport = document.getElementById('r' + selRepPos);
        selectedReport.style.background = 'rgb(220, 230, 240)';
    }else if (keyCode == 38) { //up arrow
        if (selRepPos == 1){   
            selectedReport = document.getElementById('r' + selRepPos);
            selectedReport.style.background = 'rgb(220, 230, 240)';
        }
        if (selRepPos > 1){   
            selRepPos--;   
            selectedReport = document.getElementById('r' + selRepPos);
            selectedReport.style.background = 'rgb(220, 230, 240)';
        }
    }else if (keyCode == 13) { //enter
        if (selRepPos > 0){
            selectedReport = document.getElementById('r' + selRepPos);
            if (typeof selectedReport.onclick == "function") {
                selectedReport.onclick.apply(selectedReport);
            }
        }   
    }else{
        selectedReport = document.getElementById('r1');
        selectedReport.style.background = 'rgb(220, 230, 240)';
        selRepPos = 1;
    }
}



Recent Reports
Now that I’ve run that report, it will be added to my list of ‘Recent Documents’.
image

This is stored in local storage so will be there the next time you open the application. So if you choose not to search, running one of your ten most recently used reports is a one-click operation.


Folder Structure
If you’re not opening a recent report, and not searching for it, you’ll need a folder structure. There’s not much to describe here; it’s a tree.
image

Clicking a folder will fetch a list of child objects for that folder. I considered pre-fetching this data as well, but as it turns out, each click is not too bad: over an internet connection, generally 100 – 500 milliseconds per round trip. It's on the list for version 2.
image

I considered using a JavaScript library for a folder structure, but none of them looked great. It was fiddly but not too difficult to create my own. The whole tree is a series of HTML unordered lists. Each node is a button and a span. A folder, for example, looks like this:

image

The button line is what the user sees on the screen. The span below that is where children elements will be inserted when the folder is expanded.
The getTree function uses AJAX to call a java file on the server. The java returns a list of the child items, but rather than return the list of objects in JSON or XML format, the string returned will be HTML that can be inserted directly into the span mentioned above.
This is the actual string returned by getTree.jsp

image

The java looks like this:

String getNextLevel(IInfoStore iStore, int id){
    String results = "";
    String liClass = "";
    IInfoObject object;
    IFolder folder;
    IInfoObject report;
    if (id == -1) id = 0;
    String folderQuery = "SELECT SI_ID, SI_NAME, SI_PATH FROM CI_INFOOBJECTS "
        + "WHERE SI_PARENTID=" + id + " AND SI_KIND = 'Folder' "
        + "ORDER BY SI_NAME";
   
    String reportQuery = "SELECT SI_ID, SI_NAME FROM CI_INFOOBJECTS "
    + "WHERE SI_PARENTID=" + id + " AND (SI_KIND = 'CrystalReport' OR SI_KIND = 'Flash' "
    + "OR (SI_KIND = 'Webi' AND SI_INSTANCE = 0)) "
    + "ORDER BY SI_NAME";
   
    try{
        results += "<ul>";
       
        //get an HTML list of folders
        IInfoObjects objects = iStore.query(folderQuery);   
        Iterator objectIter = objects.iterator();
        while (objectIter.hasNext()){
            //object = (IInfoObject)objectIter.next();   
            folder = (IFolder)objectIter.next();           
            int objID = folder.getID();
            String objName = folder.getTitle();
            

            results += "<li class='liFolder'>";
           
            //check for children, this defines the text style           
            IInfoObjects childObjects = iStore.query("SELECT SI_ID FROM CI_INFOOBJECTS WHERE SI_PARENTID=" + objID);
            if (childObjects.getResultSize() == 0){
                results += "<button type='button' class='folderButtonEmpty' id='";               
            }else{
                results += "<button type='button' class='folderButton' id='";                   
            }           
            results += folder.getID() + "' onclick='getTree(this.id)'>" + folder.getTitle() + "</button>";
            results += "<span id='folder" + folder.getID() + "'></span>";
            results += "</li>\n";
        }
       
        //Get an HTML list of reports.
        objects = iStore.query(reportQuery);
        objectIter = objects.iterator();
        String JSParams = "";

        while (objectIter.hasNext()){
            object = (IInfoObject)objectIter.next();
            JSParams = object.getID() + ",0,1,'" + object.getTitle() + "','" + object.getKind() + "'";
            results += "<li onclick=\"getRepAndStore(" + JSParams + ")\" class=\"li" + object.getKind() + "\">"+ object.getTitle() + "</li>\n";           
        }       
        results += "</ul>";
    }catch (SDKException sdke){
        results = "An error ocurred.";
    }
    return results;
}


The AJAX to insert the result into the tree looks like this. The bold bit is what inserts the results into the tree.

function getTree(id){   
        var startDate = new Date();
        var xmlhttp; 
       
        if (window.XMLHttpRequest)
          {// code for IE7+, Firefox, Chrome, Opera, Safari
          xmlhttp=new XMLHttpRequest();
          }
        else
          {// code for IE6, IE5
          xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
          }
       
        xmlhttp.onreadystatechange=function() {       
            if (xmlhttp.readyState==4 && xmlhttp.status==200)
            {
                var endDate = new Date();
                var durationSec = (endDate.getTime() - startDate.getTime()) / 1000;
                document.getElementById('folder' + id).innerHTML=xmlhttp.responseText;
            }         
        };
        xmlhttp.open('GET','getTree.jsp?nocache=' + startDate.getTime() + '&ParentID=' + id,true);
        xmlhttp.send();
    }


There’s some extra logic that remembers if a folder is expanded or collapsed.


Show me the reports, already
You’ll notice from the screenshot above that each report in the tree structure has an onClick event called getRepAndStore. This will get the report, and while that’s happening, store the report details in the recent documents list.
The function within getRepAndStore that fetches the report looks like the following code. Note that depending on the type of report (Webi, Flash or Crystal) a different .jsp is called. There’s also some stuff in there to set the window size (the report will launch in a new window the same size as the main window). On the iPad or Android a new tab will be opened.

function getReport(docKind, docID, repID, pageNum){    //never called directly, always through getReportAndStore.
    var startDate = new Date();   
   
    var winW = 630, winH = 460;
    if (document.body && document.body.offsetWidth) {
     winW = document.body.offsetWidth;
     winH = document.body.offsetHeight;
    }
    if (document.compatMode=='CSS1Compat' &&
        document.documentElement &&
        document.documentElement.offsetWidth ) {
     winW = document.documentElement.offsetWidth;
     winH = document.documentElement.offsetHeight;
    }
    if (window.innerWidth && window.innerHeight) {
     winW = window.innerWidth;
     winH = window.innerHeight;
    }
    winW = winW - 20;       
       
    var urlString = '';
    if (docKind != "CrystalReport" && docKind != "Webi" && docKind != "Flash"){
        alert("This type of report is not supported.");       
    }else if (docKind == "CrystalReport"){
        //changed this to the new crystal jsp name
        urlString = 'getCrystalReport.jsp?nocache=' + startDate.getTime() + '&docID=' + docID;
    }else if (docKind == "Webi"){
        urlString = 'getWebiReport.jsp?nocache=' + startDate.getTime() + '&docID=' + docID + '&repID=' + repID + '&pageNum=' + pageNum;
    }else if (docKind == "Flash"){
        urlString = 'getFlashReport.jsp?nocache=' + startDate.getTime() + '&docID=' + docID;
    }   
    if (urlString != ''){
        window.open(urlString, 'repWindow', 'width=' + winW + ',height=' + winH);
    }
}



Crystal Reports
Showing a Crystal report is easy, there’s a viewer. All you need to specify is the docID.
image


Webi Reports
Webi is more difficult. You need to define the docID, the repID (a tab) and the page number.
Also, since only the actual body of the report is returned, a toolbar showing the tabs and page numbers needs to be created. This is all done in getWebiReport.jsp. The code to generate the toolbar is this:

    Reports reports = doc.getReports();
    int iRepCount = reports.getCount();
   
    ReportMap reportMap = (ReportMap)doc.getReportMap();
    ReportMapNodes reportMapNodes = (ReportMapNodes)reportMap.getStructure();
    iRepCount = reportMapNodes.getChildCount();    

    //Get the report (tab)
    Report rep = null;
    rep = doc.getReports().getItem(iRepID);   
   
    //Prepare the toolbar   
    reportNav = "<div style='height:42px; width:100%'></div>"; // a spacer
    reportNav += "<div id='headerDiv'>";
   
    //Draws the navigation items if there is more than one report (tab)
    if (iRepCount > 1){
        for (int i = 0; i<iRepCount; i++){
            String selected = "";
            if (i == iRepID) {
                selected = " style=\"background-color:rgb(50,100,150); color:white;\" ";
            }
            reportNav += "<div class=\"textButton\" " + selected + "onClick=\"window.location.href='getWebiReport.jsp?docID=" + iDocID +"&repID=" + i + "'\">" + 

reportMapNodes.getChildAt(i).getName() + "</div>\n";
        }
    }    

    //Set the page number
    rep.getPageNavigation().setTo(iPageNum);

    //Get/set page number
    //NOTE the buttons are printed right to left
    PageNavigation pageNavigation = (PageNavigation)rep.getPageNavigation();
    int iCurrentPage = pageNavigation.getCurrent();
    int iNextPage = iCurrentPage + 1;
    int iPrevPage = iCurrentPage - 1;

    reportNav += "<div class=\"vPad\" style=\"float:right\"></div>";
    reportNav += "<img src=\"images\\drill32x32.gif\" alt\"Drill\" width=\"20px\" height=\"20px\" style=\"float:right; margin-top:8px\" />";

    if (!pageNavigation.isLast()){
        reportNav += "<div class=\"textButton\" style=\"float:right\" onClick=\"window.location.href='getWebiReport.jsp?docID=" + sDocID + "&repID=" + sRepID + 

"&pageNum=" + -1 + "'\"> Last </div>\n";
        reportNav += "<div class=\"textButton\" style=\"float:right\" onClick=\"window.location.href='getWebiReport.jsp?docID=" + sDocID + "&repID=" + sRepID + 

"&pageNum=" + iNextPage + "'\"> Next </div>\n";
    }    

    reportNav += "<div class=\"textButton\" style=\"float:right; border:none\" >Page " + iCurrentPage + "</div>\n";
    if (!pageNavigation.isFirst()){
        reportNav += "<div class=\"textButton\" style=\"float:right\" onClick=\"window.location.href='getWebiReport.jsp?docID=" + sDocID + "&repID=" + sRepID + 

"&pageNum=" + iPrevPage + "'\"> Previous </div>\n";
        reportNav += "<div class=\"textButton\" style=\"float:right\" onClick=\"window.location.href='getWebiReport.jsp?docID=" + sDocID + "&repID=" + sRepID + 

"&pageNum=" + 1 + "'\"> First </div>\n";
    }
   
    reportNav += "</div>"; //closes the header div
   


The resulting report looks like this:
image

You could simply use OpenDocument if you were deploying this next the standard BusinessObjects web apps anyway.


Xcelsius Dashboard
This simply uses the openDocument technology to link to the dashboard, which opens in a new window just as a dashboard would if you used InfoView.


Limitations
Well, there’s lots. Start any question with “Does it do…” and the answer is probably “No, use InfoView”. Think of it as your second InfoView.
If you want the full functionality of Webi, you could still use something like this as a front end, and use OpenDocument links to open your reports.


Resources
These are all for XI3.1
Developer SDK Library (web)
Enterprise Java SDK API (web)
ReportEngine (Webi) Java SDK API (web)
Viewers (Crystal) Java SDK API (web)
Enterprise Java SDK Help (zip)
ReportEngine (Webi) Java SDK Help (pdf)
Viewers (Crystal) Java SDK Help (zip)
SDK Java Tutorial (pdf)
JavaScript DOM (web)
Java 6 API (web)


That’s It!
- See more at: http://blog.davidg.com.au/2012/06/infoview-express-bobj-sdk-demo.html#sthash.Lrklvehy.dpuf

No comments:

Post a Comment