Nov 24

I’m in the process of developing a customized reporting solution using interactive reports for an existing customers application developed in APEX 3.2. Whilst they were pleased with the functionality offered by IR, they required the ability to grant access on certain reports to selected users. Now this is available in 4.0 but they are not in a position to upgrade so my hands are basically tied. To facilitate this I’ve followed the details in the following APEX forum thread and there’s also a blog post which you can reference.

In addition to the granting of reports they also require a few changes to the IR charting feature. (this also applies to 4.0)

  • Adding a title to the chart
  • Adding the point values to the chart
  • Changing the label rotation for the vertical bar chart
  • Changing colours
  • Adding in drill own links to the data

So to devise a workaround, back in February (this is when the customer first requested the change, approval to start came in November, they move quickly ;) I wrote a theoretical approach to try and override the XML chart output.

Putting this theory into practice this week I came up with the following code prototype (Unfortunately I’m not able to provide the final solution for commercial reasons). It should give you a good idea of what needs to occur, I’d recommend making a few changes to the XML_CHART_API package as it’s only written to give you an idea of what to do….

create or replace
PROCEDURE FLASH3
( p                     IN VARCHAR2
, preloaderLoadingText  IN VARCHAR2 DEFAULT NULL
, swffile               IN VARCHAR2 DEFAULT NULL
, preloaderInitText     IN VARCHAR2 DEFAULT NULL
, XMLCallDate           IN VARCHAR2 DEFAULT NULL
)
--
-- URL rewriting See - http://www.yourhtmlsource.com/sitemanagement/urlrewriting.html
--
-- RewriteEngine on
-- RewriteRule ^pls/apex/apex_util.flash2(.*) /pls/apex/flash3$1
--
-- grant execute on #OWNER#.flash3 to apex_public_user;
-- create public synonym flash3 for #OWNER#.flash3;
--
-- update wwv_flow_epg_include_mod_local
--
AS
  v_out_buf         htp.htbuf_arr;
  v_out_row_count   NUMBER := 99999999;
  v_clob            CLOB := empty_clob();
  v_xml_start_found BOOLEAN DEFAULT FALSE;

  PROCEDURE htp_buf_prn
  ( p_text              IN CLOB
  ) AS
    l_buflen  INTEGER := 256;
  BEGIN
    FOR i IN 0 .. FLOOR(LENGTH(p_text)/l_buflen) LOOP
      sys.htp.prn(substr(p_text, i * l_buflen + 1, l_buflen));
    END LOOP;
  END htp_buf_prn;

BEGIN
  dbms_lob.createtemporary(lob_loc => v_clob, cache => true, dur => dbms_lob.call);

  apex_util.flash2( p                     => p
                  , preloaderLoadingText  => preloaderLoadingText
                  , swffile               => swffile
                  , preloaderInitText     => preloaderInitText
                  , XMLCallDate           => XMLCallDate
                  );
  owa.get_page(v_out_buf, v_out_row_count);
  htp.init;

  FOR i IN 1..v_out_row_count LOOP
    --
    -- Lets look for the start of our XML document and ignore any of the mime header
    -- X-ORACLE-IGNORE etc.
    --
    IF instr(v_out_buf(i), '<?xml') > 0 OR v_xml_start_found THEN
      dbms_lob.writeappend(lob_loc => v_clob, amount => length(v_out_buf(i)), buffer => v_out_buf(i));
      v_xml_start_found := TRUE;
    END IF;
  END LOOP;
  xml_chart_api.chart_mods
  ( p_in_out_chart_xml => v_clob
  );
  htp_buf_prn(v_clob);

END FLASH3;
/
create or replace
PACKAGE XML_CHART_API AS 

  PROCEDURE chart_mods
  ( p_in_out_chart_xml IN OUT NOCOPY CLOB
  );

END XML_CHART_API;
/
create or replace
PACKAGE BODY XML_CHART_API AS

  PROCEDURE chart_mods
  ( p_in_out_chart_xml IN OUT NOCOPY CLOB
  )
  AS
    l_xml     XMLType;
    doc       dbms_xmldom.DOMDocument;
    nl        DBMS_XMLDOM.DOMNodeList;
    n         DBMS_XMLDOM.DOMNode;
    n2        DBMS_XMLDOM.DOMNode;
    l_node    DBMS_XMLDOM.DOMNode;
    l_attr    DBMS_XMLDOM.DOMAttr;
    e         DBMS_XMLDOM.DOMElement;
    e2        DBMS_XMLDOM.DOMElement;

    len       NUMBER;
    l_chart   VARCHAR2(100);

  BEGIN
    -------------------------------
    -- Turn our clob into XML doc
    -------------------------------

    l_xml := XMLType.createXML(p_in_out_chart_xml);
    doc   := dbms_xmldom.newDOMDocument(l_xml);

    -------------------------------
    -- Get the report type
    -------------------------------
    nl   := DBMS_XMLDOM.getElementsByTagName(doc, 'chart');
    len  := DBMS_XMLDOM.getLength(nl);

    -- loop through elements (should only be 1
    FOR i IN 1 .. len LOOP
        n := DBMS_XMLDOM.item(nl, i-1);
        e := DBMS_XMLDOM.makeelement(n);
        --
        -- We need to know what type of chart, as label rotation is only
        -- required for vertical chart
        --
        l_chart := DBMS_XMLDOM.getAttribute(e, 'plot_type');
    END LOOP;

    -------------------------------
    -- Add a report drill down link
    -------------------------------
    nl   := DBMS_XMLDOM.getElementsByTagName(doc, 'point');
    len  := DBMS_XMLDOM.getLength(nl);

    -- loop through elements
    FOR i IN 1 .. len LOOP
        n := DBMS_XMLDOM.item(nl, i-1);
        e2 := DBMS_XMLDOM.makeelement(n);
        e:= DBMS_XMLDOM.createElement( doc, 'actions' );
        --sys.htp.p('Debug: adding label');
        n2 := DBMS_XMLDOM.makeNode(e);
        e:= DBMS_XMLDOM.createElement( doc, 'action' );
        DBMS_XMLDOM.setAttribute(e, 'type', 'navigateToURL');
        --
        -- Basic example of how to build up the URL for report drill down
        -- this is where I would hand off to another function with the page number
        -- and value to work out what items need setting in the URL
        --
        DBMS_XMLDOM.setAttribute(e, 'url', 'f?p='||v('APP_ID')||':1:'||v('APP_SESSION')||':'||DBMS_XMLDOM.getAttribute(e2,'name')||'_'||DBMS_XMLDOM.getAttribute(e2,'y'));
        --
        -- Lets open the drill down in a new window
        --
        DBMS_XMLDOM.setAttribute(e, 'target', '_blank');
        l_node := DBMS_XMLDOM.appendChild(n2,dbms_xmldom.makeNode(e));
        l_node := DBMS_XMLDOM.appendChild(n,n2);

    END LOOP;
    -------------------------------
    -- Label Rotation & alignment
    -------------------------------
    nl   := DBMS_XMLDOM.getElementsByTagName(doc, 'labels');
    len  := DBMS_XMLDOM.getLength(nl);

    -- loop through elements
    FOR i IN 1 .. len LOOP
        n := DBMS_XMLDOM.item(nl, i-1);
        -- cast our node into an element which we can manipulate
        e := DBMS_XMLDOM.makeelement(n);
        --DBMS_XMLDOM.setAttribute(e, 'display_mode', 'rotated');
        CASE l_chart
          WHEN 'CategorizedHorizontal' THEN
            DBMS_XMLDOM.setAttribute(e, 'rotation', '0');
          ELSE
            DBMS_XMLDOM.setAttribute(e, 'rotation', '270');
        END CASE;
        DBMS_XMLDOM.setAttribute(e, 'align', 'inside');

    END LOOP;

    -------------------------------
    -- Add point values
    -------------------------------
    nl   := DBMS_XMLDOM.getElementsByTagName(doc, 'series');
    len  := DBMS_XMLDOM.getLength(nl);

    -- loop through elements
    FOR i IN 1 .. len LOOP
        n := DBMS_XMLDOM.item(nl, i-1);
        e:= dbms_xmldom.createElement( doc, 'label' );
        DBMS_XMLDOM.setAttribute (e, 'enabled', 'true');

        l_node := dbms_xmldom.appendChild(n,dbms_xmldom.makeNode(e));
    END LOOP;
    DBMS_XMLDOM.writeToClob(doc,p_in_out_chart_xml);
    DBMS_XMLDOM.freeDocument(doc);
  EXCEPTION
    WHEN OTHERS THEN
      DBMS_XMLDOM.freeDocument(doc);
      sys.htp.p(SQLERRM);
  END chart_mods;

END XML_CHART_API;
/

Additional Setup

  1. Apache mod_rewrite rule:
    #
    # change the PLS handler to the one defined for your DB
    #
    RewriteEngine on
    RewriteRule ^pls/apex/apex_util.flash2(.*) /pls/apex/flash3$1
  2. grant execute on #OWNER#.flash3 to apex_public_user
  3. create public synonym flash3 for #OWNER#.flash3;
  4. update #APEX_SCHEMA#.wwv_flow_epg_include_mod_local function to include FLASH3
    e.g.
    if upper(procedure_name) in (
    ‘#OWNER#.FLASH3′,’FLASH3′) then
    return TRUE;
Nov 04

I have a requirement to process uploaded CSV files into a number of application tables and provide a number of screens to efficiently manage this process. I’ve followed the following post by “Advait Deo” however I have a requirement to handle columns wrapped in double quotes or some other encapsulator.

http://avdeo.com/2008/05/21/uploading-excel-sheet-using-oracle-application-express-apex/

So before I can really get started I needed a function to do this. I did see an example in the comments but I was looking for something that could be reused for other purposes.

I looked at dbms_utility.comma_to_table but it had too many limitations and then at regex but unfortunately Oracle doesn’t support look ahead assertion, so after 30mins of frustration I jumped back into PLSQL and came up with the following after getting some inspiration from the following http://www.oratechinfo.co.uk/delimited_lists_to_collections.html:

create or replace
FUNCTION f_csv_convert_to_table
( p_in_string       IN VARCHAR2
, p_in_encapsulator IN VARCHAR2 DEFAULT '"'
)
RETURN wwv_flow_global.vc_arr2
  AS
    l_string              VARCHAR2(32767) := p_in_string || ',';
    l_quote_start_index   PLS_INTEGER := 0;
    l_quote_end_index     PLS_INTEGER := 0;
    l_comma_index         PLS_INTEGER;
    l_index               PLS_INTEGER := 1;
    l_tab                 wwv_flow_global.vc_arr2;
    i                     PLS_INTEGER := 1;
  BEGIN
    LOOP
      l_comma_index := REGEXP_INSTR(l_string, '[,'||p_in_encapsulator||']', l_index);
      EXIT WHEN l_comma_index = 0;

      CASE
        WHEN SUBSTR(l_string,l_comma_index,1) = p_in_encapsulator AND l_quote_start_index = 0 THEN
          l_quote_start_index := l_comma_index;
        WHEN SUBSTR(l_string,l_comma_index,1) = p_in_encapsulator AND l_quote_start_index <> 0 THEN
          l_quote_end_index   := l_comma_index;
        ELSE
          NULL;
      END CASE;

      IF l_quote_start_index = 0 THEN

        l_tab(i) := SUBSTR(l_string, l_index, l_comma_index - l_index);
        i := i+1;

      ELSIF l_quote_start_index <> 0 AND l_quote_end_index <> 0 THEN

        l_tab(i) := SUBSTR(l_string, l_quote_start_index+1, (l_quote_end_index - l_quote_start_index)-1);
        i := i+1;
        --
        -- Lets reset our quote check
        --
        l_quote_start_index := 0;
        l_quote_end_index   := 0;
        --
        -- We need to discard our end comma
        --
        l_comma_index := INSTR(l_string, ',', l_index);
      END IF;
      l_index := l_comma_index + 1;
    END LOOP;
    RETURN l_tab;
END f_csv_convert_to_table;

Now the above only works correctly if the encapsulator is matched i.e. start and end around fields. It will also continue to work if there are line breaks between your encapsulated fields. I’m sure someone can come up with a better solution, but this is all I need at the moment.

In Advait’s post I plan to swap the line

v_data_array := wwv_flow_utilities.string_to_table (v_line);

with

v_data_array := f_csv_convert_to_table (v_line);

Just to keep things simple.

Now back to the task at hand and changing it be completely generic using collections. I’ll follow up with another post with the entire solution in the coming week or so, when time permits.

Oct 27

I had a requirement today for an existing customer to add in some functionality to display a report listing all the users sharing the same email address when creating a new user in the application. An additional requirement was that they also wanted the user to be able to continue to create the user but be prompted to do so to confirm that they are recording a user with a duplicate email address.

I thought about popups, using a modal dialog etc. this is in APEX 3.2 btw. but none of this was simple and straight forward with minimal effort. So whilst I was thinking of possibilities I remembered previously using the “BRANCH_TO_PAGE_ACCEPT” request value which gave me the idea for the following simple solution.

On submit of the page I would have a branch that executes “On Submit: Before Processing: (After Computation, Validation and Before Processing)” with a SQL exists condition which checks for the duplicate email address and if so redirects the user to this newly created report page listing all the users with duplicate email addresses. On this page I have button labelled “Create User” which allows them to continue creating the user but first redirects to some Javascript similar to confirmDelete which prompts the user to confirm that they do indeed wish to create the user. On click of Ok they are then redirected back to the create user page with the following REQUEST value in the branch “BRANCH_TO_PAGE_ACCEPT” which automatically tells APEX to ignore the rendering phase and simply process the page. This means that I was able to reuse the entire functionality of the create users page to perform all the processing as all the values were still recorded in session state from the initial page submit when the user clicked “CREATE”.

However setting this request value is simply not enough since the Automatic DML row processing (and my PLSQL processes) requires a valid REQUEST value like “INSERT, CREATE, CREATE_AGAIN, CREATEAGAIN”. So to work around this issue I used the following PLSQL process (which runs before every other process) to switch the request value.

wwv_flow.g_request := 'CREATE';

I simply set a condition of “Request = Expression 1″ with a value of “BRANCH_TO_PAGE_ACCEPT” and my page is successfully processed and the user added. The thing I like about this sort of approach is that we can conditionally change the REQUEST value on the fly which means that we can attribute some logic to multiple independent PSLQL processes. We can update the REQUEST value in flight to either execute the next process or skip it. Please Note: this is not best practice, and will open up debugging issues, but it does have its uses here and there. It’s all about solving problems, and this one today was solved rather painlessly with a neat little trick.

Tagged with:
Sep 15

The best part about APEX, compared with most other development products, is that our applications are just metadata. This gives us the benefit of managing and building our applications using SQL and using the APEX PLSQL API. This can really benefit you on larger applications. You can either update the definition of existing item(s)/region(s)/validation(s) etc. using a SQL DML update statement, or you could add a new region or item to every page in the application using the APEX PLSQL API (The easiest approach for this would be to create a dummy APEX page and create a PLSQL process on this page to issue the APEX PLSQL API calls. This way you’ll avoid all the setup requirement of running APEX code outside of the APEX engine. Every less line of code helps :) ).

Currently I have a customer who requires a security change to their application, which affects most of the LOV definitions defined on most of the pages. The task of editing these through the APEX IDE does not excite me at all. Being the lazy developer that I am, it’s much much simpler making these changes at the backend. (As long as you have access to update data in the APEX schema). Now I’m sure most experienced APEX developers make changes through the backend rather than the frontend, especially if you need to make a consistent change across a number of pages. It’s less error prone to do this in SQL than it is to click click click paste and click.

A point I’d like to highlight with making backend updates is that using consistent naming convention can allow you to quickly update more items in a single UPDATE statement. I’m not saying that you couldn’t update an irregular naming convention with the use of regex in your update statement, it’s just going to take you longer and would require a few more tests to get it right. That is unless you’re a regex master…. me? no!

Note: before you manipulate your apps using SQL, all the obvious apply e.g. backup your applications, db, enable flashback, issue a rollback before you exit SQL*Plus after an unsuccessful change etc. etc. Just in case you screw something up, we’ve all been there before…. myself included!

So one of the tasks at hand is to update a district LOV definition (it has a foreign key which references a company table). On each of the pages the district appears three times for a “submitter”, “assignee” and “location”. the standard naming convention used was to have DISTRICT_ID in each of these three different page items (the same goes for COMPANY_ID). Therefore it made it quick and easy to write the following UPDATE statement to change the LOV definition.

e.g.

UPDATE apex_030200.wwv_flow_step_items
SET    lov = 'SELECT district_name, district_id
FROM   district
WHERE  company_id = :'||replace(name,'DISTRICT','COMPANY')||'
AND    (soft_deleted = ''N'' OR (:P'||flow_step_id||'_REQUEST = ''EDIT'' AND :'||name||' = district_id))
ORDER BY 1'
,      lov_display_null = 'YES'
,      lov_null_text = 'Please Select ...'
WHERE  name like '%_DISTRICT_ID%'
AND    flow_id =702 and flow_step_id IN (2,26,29,39,47,55,61,137)

Now this is just a simple test case for one type of LOV, I have roughly about 200+ items to change in the application as they have page specific binds defined in them. So rather than using the APEX IDE I can complete this task in minutes as opposed to hours (which I don’t think my brain could take)….

Now if only I could find a quick way to test these mass changes as I’m yet to find a similar productivity boost? Any advice would be most welcome…..

Tagged with:
preload preload preload