Friday, 7 March 2008

Messages from the database

My new employer asked me to show messages from triggers more user friendly than they are shown by default by Apex, i.e. in the separate error screen. I had previously been investigating this, but I wondered there should be someone with a solution out there in the Apex user's universe, represented by the Apex forum on OTN. So I posted my question, and got no response! This amazed me. Is there no one who came across this problem before?
Lot's of organizations have databases with triggers to ensure data integrity, and sure some of them want to access their databases with Apex. Are their users content with the default error screen, or even a customized screen with only an error message and a Back button to return to the screen (having to memorize the error message).

I just saw there was a reply referencing Patrcik Wolff's blog and his impressive solution for error checking in tabular forms. But I just want a simple solution without extra programming. So I took some of Patrick ideas together with some other code snippets to extract the relevant part of the error message form the whole error stack and started off...

Developing Javascript I think is a tedious process, I much rather work with PL/SQL, which can be easily debugged and traced. So I always work step by step, testing a lot along the way to make sure that everything keeps working. And at the end I had a working generic solution, which shows trigger error messages as notifications without any additional coding in Apex.

Apex: Show Database Error Message in calling form
The purpose is to show a neat error message in the notification area of the calling form.
Default Apex show the whole error stack raised by a DB trigger in a separate error page.
With acknowledgement to Patrick Wolf, I lend parts of his ApexLIB code to create this solution.


Assumptions

The error message is raised from a DB trigger using RAISE_APPLICATION_ERROR(-20000,’errormessagetext’).
  • The relevant part of the error stack is contained within the strings ‘ORA-20000’ and the next ‘ORA-‘-string, i.e. if the error stack is ‘ORA-20000 Value should not be null ORA-6502 …’, than the relevant string is ‘Value should not be null’ .
  • Cookies are enabled on the browser of the user

  • Explanation
    The solution relies heavily on the use of Javascript. On the template of the error page Javascript is added to identify the error stack and to extract the relevant error message. This message is written to a cookie, and then the control is passed back to the calling page (equal to pushing the Back button).
    In the calling page a Javascript onLoad process is added. This process determines whether an error message has been written to a cookie. If so the error message is formatted as an error and written to the notification area.
    Implementation
    The solution redefines two template pages, the two level tab (default page template) and the one level tab (error page template).
    Javascript is added to implement the solution. This Javascript is contained in a small library errorHandling.js:


    var vIndicator = "ApexErrorStack=";

    function writeMessage(vMessage)
    { document.cookie = vIndicator+vMessage+';';
    }

    function readMessage()
    { var vCookieList = document.cookie;
    var vErrorStack = null;
    var vStart = null;
    var vEnd = null;
    var vPos = null;

    vPos = vCookieList.indexOf(vIndicator);
    // No cookie found?
    if (vPos == -1) return("empty");
    //
    vStart = vPos + vIndicator.length;
    vEnd = vCookieList.indexOf(";", vStart);
    if (vEnd == -1) vEnd = vCookieList.length;
    vErrorStack = vCookieList.substring(vStart, vEnd);
    vErrorStack = decodeURIComponent(vErrorStack);
    // remove the cookie
    document.cookie = vIndicator+"; max-age=0";
    return(vErrorStack);
    }

    function getElementsByClass2(searchClass,node,tag)
    {
    var classElements = new Array();
    if ( node == null )
    node = document;
    if ( tag == null )
    tag = '*';
    var els = node.getElementsByTagName(tag);
    var elsLen = els.length;
    var pattern = new RegExp('(^\\s)'+searchClass+'(\\s$)');
    for (i = 0, j = 0; i < errorelements =" new" errorelements =" getElementsByClass2("> 0 )
    { errorText = errorElements[0].innerHTML;
    errorText = errorText.substr(errorText.indexOf("ORA-20000")+ 11);
    errorText = errorText.substr(0,errorText.indexOf("ORA")-1);
    // errorElements[0].innerHTML = errorText;
    writeMessage(errorText);
    }
    }

    function show_message()
    { var vCookieList = document.cookie;
    var vErrorStack = null;
    var vErrorName = null;
    var vErrorMessage = null;
    var vStart = null;
    var vEnd = null;
    var vPos = null;

    // get errorStack
    vErrorStack = readMessage();
    if (vErrorStack == -1) return;

    // search for our message section (eg. t7Messages)
    var notificationArea = document.getElementById("notification");
    if (notificationArea != null)
    { notificationArea.innerHTML = '
    1 error has occurred
    • ' + vErrorStack + '
    '; }
    else
    { alert(vErrorStack); }
    }



    This code is loaded as a static file in Application Express (no application associated).

    In both templates a reference to this code file is added in the Definition Header section.

    This library is called from the two level tab page template to show the error message (if any) in het onLoad event of the body tag. Therefore the function show_message() is called in the onLoad event of the page. This function reads the cookie and displays the error messages stored in the cookie.




    <body onload="javascript:show_message();">#FORM_OPEN#



    This code checks whether a message has been written to a cookie and if found displays the message in the notification area.

    In the error template page the Error section has the content:


    <script language="javascript">
    processErrorText();
    window.history.go(-1);
    </script><a href="javascript:window.history.go(-1);">Back</a>



    The call to processErrorText() looks for the error message, extracts the relevant part of it (between ‘ORA-20000’ and the next ‘ORA-‘), writes it to a cookie and returns to the previous screen.
    The link to the previous page is added should an error in the Javascript occur. It provides the user with a path back to the application.

    With these actions taken, the error messages issued from triggers are shown in the notification area of the form the user has entered his data in.
    The need for database driven messaging
    In some cases the need exists to process error messages in the database before presenting them to the user. This is the case, when the triggers return message codes, associated to error messages in a message table.

    This can be done by using a special Error Message Processing page in Apex.
    The error message page extracts the error message, redirecting to the Error Message Processing page with the error message as a parameter. In the EMP page a PL/SQL function can be called with the message as a parameter. This function returns the right message, which is written to a cookie using dynamically generated Javascript from PL/SQL. Than the contol is given back to the calling form.

    The redirect is implemented by location.replace, so that the error message page does not exist within the browsing history. The normal history(-1) will return to the calling page.

    Implementation of database driven messaging

    The solution redefines two template pages, the two level tab (default page template) and the one level tab (error page template).
    Javascript is added to implement the solution. This Javascript is contained in a small library errorHandling.js already listed in a previous paragraph.


    In the error template page the Error section has the content:


    <script language="Javascript">
    var errorText = null;

    function redirect2oracle()
    { window.location.replace("f?p=&APP_ID:500:&APP_SESSION.::::P500_MESSAGE:"+errorText); }

    function getError()
    { errorText = processErrorText(); }

    getError();
    redirect2oracle();
    </script>
    <a href="Javascript:redirect2oracle();">Go to Error Message Porcessing Page</a><br/><a href="javascript:window.history.go(-1);">Back to form</a><br/>
    <script language="Javascript">
    var errorText = null;

    function redirect2oracle()
    { window.location.replace("f?p=&APP_ID:500:&APP_SESSION.::::P500_MESSAGE:"+errorText); }

    function getError()
    { errorText = processErrorText(); }

    getError();
    redirect2oracle();
    </script>


    The call to processErrorText() looks for the error message, extracts the relevant part of it (between ‘ORA-20000’ and the next ‘ORA-‘), writes it to a cookie.
    Then the EPM-page (500) is called with the extracted message as parameter.

    The link to the EPM page and the previous page is added should an error in the Javascript occur. It provides the user with a path back to the application.

    We need to create an Error Message Processing Page.

    • Create a new page 500 with a empty HTML-region “Parameters”
    • Create an text-area P500_MESSAGE in this region
    • Create a PL/SQL region “Process Messages” with source:
      convert_message(:P500_MESSAGE);
    • Create a PL/SQL procedure convert_message like this example, that reads messages from a message table. If not found, the actual input message is returned.




    CREATE OR REPLACE procedure convert_message(i_message in varchar2) is
    v_id number := null;
    v_message varchar2(4000) := null;

    function get_message (i_message in varchar2) return varchar2 is
    v_return varchar2(4000) := null;
    begin
    select msg_text into v_return
    from messages
    where msg_code = upper(i_message);
    return(v_return);
    exception
    when no_data_found then
    return(i_message);
    end;

    begin
    v_message := get_message(i_message);

    // write the message for logging/debugging
    htp.p('Boodschap='v_message);

    // write cookie and redirect to calling page
    htp.p('<script language="Javascript">');
    htp.p('document.cookie="ApexErrorStack='v_message';";');
    htp.p('window.history.go(-1);');
    htp.p('</script>');

    // enter return link just in case
    htp.p('<a href="javascript:window.history.go(-1);">Ga terug</a>');

    end;
    /



    Note: The way the message is converted is just an example

    With these actions taken, the error messages issued from triggers are shown in the notification area of the form the user has entered his data in.