Mar
09
2010
0

Produce a sitemap.xml file for your APEX Website

This post is probably only useful for a small number of people that use APEX to generate dynamic websites. That said you may find some other uses for the file download technique documented in this post.

Here’s some code that generates a sitemap XML file for uploading to Google webmaster tools:

  PROCEDURE sitemap
  IS

    l_app_alias apex_applications.alias%TYPE;
    l_app_id    apex_applications.application_id%TYPE := v('APP_ID');

  BEGIN
    htp.init;
    owa_util.mime_header( 'application/octet', FALSE );
    htp.p('Content-Disposition: attachment; filename="sitemap.xml"');
    owa_util.http_header_close;
    htp.p(q'[<?xml version="1.0" encoding="UTF-8"?>
<urlset
      xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9

http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">

<!-- created by the e-dba website -->]');

    SELECT nvl(alias, application_id)
    INTO   l_app_alias
    FROM   apex_applications
    WHERE  application_id = l_app_id;

    FOR c IN
    ( SELECT *
      FROM   apex_application_pages
      WHERE  application_id = l_app_id
      AND    page_id NOT IN (0,101,404)
      ORDER BY page_id
    )  LOOP
      htp.p('<url><loc>'||
            lower(owa_util.get_cgi_env('REQUEST_PROTOCOL'))||'://'||
            owa_util.get_cgi_env('HTTP_HOST')||
            owa_util.get_cgi_env('SCRIPT_NAME')||
            '/f?p='||l_app_alias||':'||nvl(c.page_alias,c.page_id)||':0'||
            '</loc></url>'
      );
    END LOOP;
    htp.p('</urlset>');
    htmldb_application.g_unrecoverable_error := true;
  END;

It assumes you use a session_id of zero and that all your pages are public (you could easily add some checks into the query if you have a number of pages which require authentication, e.g. PAGE_REQUIRES_AUTHENTICATION = ‘No’).

So simply create a new APEX page, doesn’t matter what templates you use i.e. page or region template, and create a PLSQL region on the page with the above procedure call. When you run the page you’ll get the sitemap.xml file download popup, and not an APEX page which you might have assumed (it’s a nice trick to have/use when you need it).

APEX Sitemap.xml
Written by mnolan in: File Downloads | Tags: ,
Feb
19
2010
0

Using single/set of images for page layouts

I’ve been recently tasked with doing some redevelopment of our website and I was just looking at some of the unused things I implemented when it was built in October 2008. One worth mentioning was an iphone demo, which used a combination of javascript an iframe and an image to present an APEX page, google calendar, map, and promotional video all contained within a parent APEX page with a floating effect.

With CSS relative/fixed positioning I was able to embed the iframe within an iphone image and place a number of buttons below it, including a close icon and todays date above it. So hopefully it can spark a few ideas out there that you could get a very realistic looking APEX application using images. Combine this with a number of effects and you could present APEX applications in new ways.

I never progressed this any further as it was just the result of a few hours of fun and is best viewed in IE not Firefox… it’s usually the other way round (the code behind it and layout is not great/clean so I wouldn’t recommend anyone to use it). What I wanted to do was to mimic an actual iphone including all the fancy sliding effects. I also saw a post from Sujay Dutta a long time ago about building APEX applications for the iphone and thought that it would be fun demoing them in this page template. Sadly though there’s not enough hours in the day and we’re focused on commercial work, but every now and then we do get a bit of free time to have some fun and experiment :) so having just stumbled over it again I might just play a little more when I get ome spare R&D time.

We would be really interested in seeing if someone out there is doing something similar…..

P.S. we also use background images for login pages, the previous post on the Licensing application uses this technique, where the apex input items for username password are always centered within the same position on the background image.

html, body {
	font:normal 12px verdana;
	padding:0;
	border:0 none;
	overflow:hidden;
	height:100%;
}
body {
	padding: 0px;
	background-image:url(#IMAGE_PREFIX#ola/img/logo/login-screen-background.png);
	background-repeat:no-repeat;
	background-position:center;
	background-attachment: fixed;
	text-align:center;
	margin:0 auto;
	vertical-align:middle;
}
Written by mnolan in: APEX Templates, Mashlets | Tags:
Feb
12
2010
6

The start of the APEX/Ext applicaton production line

It’s been a long time coming, but our very first APEX/Ext commercial application is being rolled out. It’s an Oracle licensing tracking application for our Sales team, which will evolve into a SAAS offering for the 2nd phase of development. When I look back over the last 9 months since the start of our APEX/Ext integration, and all the interruptions/challenges/restructuring and lack of resources in between, I’m glad it’s finally here. There’s nothing better than seeing an end result of a lot of hard work, however I do apologize for the continual delay in getting the framework to market, it’s simply a resourcing issue due to the current recession!

The good news though, is the upgrade of the framework to Ext 3.1 has been completed, and whilst there’s still quite a few outstanding widgets we want to integrate we have a framework that can produce an entire APEX app with an Ext interface, simply using the APEX builtin wizards without the need to code a single line of javascript.

Some of the new additions to the framework are:

  • Replacement of the traditional APEX notification, success messages with an Ext message which fades out after a configurable amount of seconds (defined as a substitution string in the application definition). See the following example/demo .
  • Grid summaries, enabled simply by checking the sum checkbox under the report column definition.
  • Spinner Fields
  • hbox/vbox layouts based on region column settings
  • Plus a number of others….

Here’s a few screenshots of the application, it’s 95% AJAX driven, page submits only occur for form submission. For this particular application we controlled most of the layout using multi-tabs. Top level tabs are page level and encapsulated in iframes whilst the bottom level tabs are region based for the same page.

Written by mnolan in: Uncategorized |
Feb
11
2010
0

Hacking APEX safely?? Attempt 2

This is a follow up to a previous post on devising a solution to override APEX interactive reports charting functionality (and possibly other functionality), as Patrick Wolf kindly pointed out that an assumption I made was incorrect, i.e. that APEX would reference “htp.p” in its code, when in fact is uses “sys.htp.p” which basically blew up my approach. So as the motto goes “If at first you don’t succeed, try and try again!!”.

My 2nd theoretical solution is the following:

Instead of writing a wrapper around the HTP package to redirect the page output to a CLOB rather than the browser, I’ll simply use a single wrapper around apex_util.flash2 package call. So after calling apex_util.flash2 within the wrapper I’ll inspect and loop through the htp.htbuf_arr and print the results to a CLOB, reset the buffer using htp.init, manipulate the CLOB in XMLDB and then output the results using htp.p. (Don’t forget I’ll still have to implement the Apache mod_rewrite to ensure URL’s for apex_util.flash2 are redirected to my new wrapper procedure).

Whilst it’s probably a cleaner approach, I liked the previous idea better as I could apply the solution to pretty much anything produced by APEX including wrapping around the “f” procedure which meant I could have stored entire HTML pages in CLOB’s and manipulated them (Not sure why I would want to but having the flexibility is what I’m interested in).

Unfortunately the new workaround in this post will not work with “f” procedure… I previously attempted this when doing the Ext JS grid integration back in May last year. It seems the htp buffer is flushed within the “f” procedure call, so any attempt to reset it within a wrapper after calling “f” fails. The other problem with a using a wrapper around “f” directly called from the URL means that you’re outside the APEX engine and can’t access session state. The work around for this is to call your wrapper procedure through an Application Process, either via calling wwv_flow.show or via AJAX.

Anyway I’ll report back on this attempt and will let you know how it goes…..

Feb
08
2010
0

Compare a row between 2 tables

I’ve been working on our licensing application and there’s a requirement to store quotes for changes to server details. A quote is basially an update to the server details info but it’s stored in a separate table, the actual server details are not updated. Essentially our quote table is a copy of the server table with a couple of additional columns. For simplicity on the data entry page I use two “Automatic Row Processing (DML)” processes. One is responsible for maintaining the server table and the other is for adding entries to the quote table. I’m able to use 2 of these processes because they’ve got the same table structure… the only trick is that my Quote button has a Database Action: “SQL Insert action” because each quote is a new record.

Now the problem I had was that our sales team wanted to be notified via email of the data changes, i.e. only the fields that have changed… and they wanted to know what they were originally and what they are now. I started to scratch my head how I could easily do this, so after 5 mins searching on good ol Google I came across the following article by Kevin Meade: http://www.orafaq.com/node/1826

Now it didn’t quite do what I needed, as it compares data in the same table, so i made a number of modifications to it and here’s the raw end result (might have the odd bug or two, as I haven’t tested the column exclusions).

create or replace type o_column_value_difference is object
( column_name     varchar2(30)
, original_value  varchar2(4000)
, new_value       varchar2(4000)
)
/
create or replace type c_column_value_difference is table of o_column_value_difference
/
  -- ---------------------------------------------------------------------------
  --
  -- Package        : ola_diff_utils
  -- Author         : Matt Nolan
  -- Description    : This is a package for comparing the differences between two
  --                  rows from the same or different tables
  --                  (assuming the table structure is the same)
  --
  --                 Note: this code is a modified version posted by Kevin Meade
  --                 http://www.orafaq.com/node/1826
  --
  -- ---------------------------------------------------------------------------
  -- Revision History
  -- Date            Author       Reason for Change
  -- ---------------------------------------------------------------------------
  -- 05 FEB 2010     M.Nolan      Created.
  -- ---------------------------------------------------------------------------
  FUNCTION show_column_diffs
  ( p_owner                    IN VARCHAR2
  , p_a_table_name             IN VARCHAR2
  , p_a_rowid                  IN ROWID
  , p_a_excluded_columns_list  IN VARCHAR2
  , p_b_table_name             IN VARCHAR2
  , p_b_rowid                  IN ROWID
  , p_b_excluded_columns_list  IN VARCHAR2
  ) RETURN c_column_value_difference
  IS
    v_where_string_1                VARCHAR2(4000);
    v_where_string_2                VARCHAR2(4000);
    sql_v                           VARCHAR2(32000);
    col_expression_v                VARCHAR2(32000);
    c_column_value_difference_v     c_column_value_difference := c_column_value_difference();
    c_column_value_difference_f_v   c_column_value_difference := c_column_value_difference();
  BEGIN

    v_where_string_1 := 'a.'||'rowid='''||p_a_rowid||'''';
    v_where_string_2 := 'b.'||'rowid='''||p_b_rowid||'''';

    FOR i IN 1..50 LOOP
      sql_v := NULL;
      FOR r1 IN
      ( SELECT column_name
        ,      data_type
        FROM   all_tab_columns
        WHERE  owner = p_owner
        AND    table_name = p_a_table_name
        AND    data_type in ('DATE','NUMBER','VARCHAR2','CHAR')
        AND    instr(','||upper(p_a_excluded_columns_list)||',',','||column_name||',') = 0
        AND    column_id BETWEEN (i-1)*10+1 AND (i*10)
        ORDER BY column_name
      ) LOOP
        IF r1.data_type in ('NUMBER','VARCHAR2','CHAR') THEN
           col_expression_v := ''''||r1.column_name||''''||' column_name,substr(a.'||lower(r1.column_name)||',1,4000) orignal_value, substr(b.'||lower(r1.column_name)||',1,4000) new_value';
        ELSE
           col_expression_v := ''''||r1.column_name||''''||' column_name,substr(nvl(to_char(a.'||lower(r1.column_name)||',''dd-mon-rrrr hh24:mi:ss''),lpad('' '',20,'' '')),1,4000) original_value, substr(nvl(to_char(b.'||lower(r1.column_name)||',''dd-mon-rrrr hh24:mi:ss''),lpad('' '',20,'' '')),1,4000) new_value';
        END IF;
        sql_v := sql_v||' UNION ALL SELECT '||col_expression_v||' FROM '||p_owner||'.'||p_a_table_name||' a,'||p_owner||'.'||p_b_table_name||' b where '||v_where_string_1||' and '||v_where_string_2||' and decode(a.'||r1.column_name||',b.'||r1.column_name||',0,1) = 1';
      END LOOP;
      IF sql_v IS NOT NULL THEN
        sql_v := 'SELECT CAST(MULTISET(SELECT * FROM ( '||substr(sql_v,11)||' )) AS c_column_value_difference ) FROM dual';
        EXECUTE IMMEDIATE sql_v INTO c_column_value_difference_v;
        SELECT CAST(MULTISET(SELECT * FROM(
            SELECT *
            FROM   TABLE(CAST(c_column_value_difference_v AS c_column_value_difference))
            UNION ALL
            SELECT *
            FROM   TABLE(CAST(c_column_value_difference_f_v AS c_column_value_difference))
           )) AS c_column_value_difference)
        INTO c_column_value_difference_f_v
        FROM dual
        ;
      END IF;
    END LOOP;
    IF c_column_value_difference_f_v.last IS NULL THEN
       c_column_value_difference_f_v.extend;
       c_column_value_difference_f_v(c_column_value_difference_f_v.last) := o_column_value_difference(null,null,null);
    END IF;
    RETURN (c_column_value_difference_f_v);
  EXCEPTION
    WHEN OTHERS THEN
      -- Something here
      RAISE;
  END show_column_diffs;

Note: p_b_table_name can have additional columns to p_a_table_name, i.e. be a superset, but it must have all the columns from p_a_table_name as a minimum otherwise the code will error.

And here’s an example of how you use it in SQL

select c.*
from (
      select max(rowid) b_rowid
      from   OLA_COMPANY_DB_INST_QUOTES
      where  instance_id = 168509198945914487559460687942923611700
     ) b,
     (
      select rowid a_rowid
      from   OLA_COMPANY_DB_INSTANCES
      where  instance_id = 168509198945914487559460687942923611700
     ) a,
table(cast(ola_diff_utils.show_column_diffs
                ( user
                , 'OLA_COMPANY_DB_INSTANCES'
                , a_rowid
                , NULL
                , 'OLA_COMPANY_DB_INST_QUOTES'
                , b_rowid
                , NULL) as c_column_value_difference)
      ) c
order by 1,2

and here’s the output of the above query

COLUMN_NAME ORIGINAL_VALUE NEW_VALUE
UPDATED_ON 04-feb-2010 12:44:09 05-feb-2010 16:12:56
PRODUCT_ID Standard Edition Enterprise Edition

I hope you find it useful…..

Written by mnolan in: PLSQL | Tags: ,
Feb
08
2010
2

Hacking APEX safely??

NOTICE: Do not follow the information in this post, it will not work, for more information please see the comment from Patrick Wolf who’s part of the core APEX product development team.

Disclaimer: this post is directed towards APEX experienced developers and is purely theoretical (I have not implemented this as of yet). It is intended for information purposes only and to hightlight possible tips and techniques for devising solutions to problems that need to be overcome! That said it also comes with the possibility of breaking your APEX installation and is completely unsupported by Oracle.

I read Joel Kallman’s latest post on modifying the objects in the APEX schema, and whilst I completely agree, there’s occasions where we need to step in and do things that APEX doesn’t provide customizations or functionality for. Now the trick to overriding APEX functionality is to do it in a way that doesn’t interfere with the product itself.

My current problem: I need to change the charting mechanism in APEX 3.2 for Interactive reports as it’s not entirely providing what our customer wants. I need to add some additional anychart XML config. So how would I go about doing this…. and is it at all possible?

My proposed solution: I plan on implementing a seamless replacement for the apex_util.flash2 package call, well I should say I still want to use the output of apex_util.flash2 but I don’t want it to be printed to the browser. Instead I’d like to save it to a CLOB and manipulate the XML before I print it out to the screen, to make it even more complicated I only want to do this under certain specific conditions and for certain specific reports.

So the safest possible way I intend to hack APEX is:

  1. Create my own HTP package which is an exact copy of SYS.HTP and calls all of the HTP routines. I’m simply going to add some additional wrapper code around HTP.p ad HTP.prn. Note: this wrapper code will have my event condition checks to see whether I’m appending to a CLOB or simply handing off to the original HTP.p procedure call without any intervention. In 99.9999% of the cases there will be no intervention.
  2. Create a wrapper procedure around apex_util.flash2 which has the exact same arguments but has some additional code that is called before I call apex_util.flash2 which will set the environment variables under the right conditions that will invoke my override of htp.p to print to a CLOB rather than the screen. I’ll then add some additional code after calling apex_util.flash2 to switch off printing to a CLOB and will amend my chart XML Clob object with any required changes using XMLDB. I’ll then simply print it out using htp.p. In 99% of the cases there will be no intervention, only for a couple of specific reports. I will have some custom metadata which defines these conditions and what additional XML config needs to be added.
  3. Add a mod_rewrite call to my Apache config which will rewrite apex_util.flash2 to my custom package
  4. Ammend the mod_plsql security functionality for this package to be called directly from the URL, e.g. via wwv_flow_epg_include_mod_local for Oracle XE, granting exeute to the APEX_PUBLIC_USER.
  5. Here’s the ugly part, i’ll create a private synonym in the APEX schema which points to our HTP wrapper package and grant execute access. This is the action where part of the community will probably freak!! But at least all we need to do to disable this workaround is drop the synonym (and remove the redirect), nothing else! So it’s still non-intrusive… isn’t it??
  6. This will be fully documented, our customer has agreed to this, and everyone in the development team will know about this and how to disable or troubleshoot (if it does ineed work, and we implement it).

Now the reason why I’ve decided to blog about it before I actually do it, is that I’m hoping to open up a discussion around the concept, whether anyone thinks that it won’t work before I start, or whether there are any better ideas out there. If you want to post your disgust with this topic feel free, the point to take on board is that I don’t really want to do this, I’m being driven by our customer and virtually have no choice in the matter!

It’s a bit like 6 months after buying a new Ford Focus, which has 120bhp and now you want it to output 200bhp because you need it to be faster than your mates car. So you take the car to a performance specialist (not the manufacturer) who throws in a supercharger, yeah you’ll void your warranty but you don’t care because you want 200bhp not 120bhp… whats the alternative, purchase a new car?

I don’t think our customer wants to throw away their  investment in their APEX application simply because, APEX currently out of the box doesn’t provide what they need on the odd occasion! It’s also the reason why they come to us, because we’re a solutions provider (and have a reputation to maintain).

Written by mnolan in: Uncategorized |
Jan
15
2010
0

Replacing the traditional APEX tree with an Ext tree

Previously our APExt JS framework only supported an Ext tree based on existing APEX list, this week I’d been tasked with building an Ext tree based on the traditional APEX tree via a reusable template based approach in order to add some navigation flexibility in the framework.

In the previous post, I was working on building a JSON object for the Ext tree root’s children (which will be a JSON metadata object in our page header). The code I posted was based on a static query i.e. the EMP table and was purely for design testing. I’ve now adapted the code to build the JSON tree structure based on any query, well any query defined in the TREE_QUERY column in APEX_APPLICATION_TREES. I’ve used a REF CURSOR to achieve it, and it’s been possible because the tree is always defined with the same structure which means I can define my FETCH with the right record/variables e.g.

select id,
       pid,
       name,
       link,
       a1,
       a2
from "#OWNER#"."TREE"

Binds and substitution strings are supported (see the following post from Andy Tully for some more details on changing binds to V(‘TEM_NAME’) ). Here’s an extract to give you some idea of how I’m dynamically doing it (Note: l_sql is the query extracted from APEX_APPLICATION_TREES):

--
-- We need to prepare our SQL statement, i.e. do substitutions and replacements
-- we change binds to v('ITEM_NAME') replacements as we can't support binds
-- with this design
--
l_sql := regexp_replace(c.tree_query,':([a-zA-Z0-9_]*)','v(''\1'')');
l_sql := APEX_APPLICATION.DO_SUBSTITUTIONS(l_sql);
--
-- Lets build our tree hierarchy query
--
l_sql_exec := 'SELECT id, pid, name, link, a1, a2
               FROM   ('||l_sql||')
               WHERE  level = 1
               CONNECT BY PRIOR id=pid';
--
-- We have to do a check to see if our root id is null, if so the query
-- needs to do an is NULL check
--
IF c.start_with IS NULL OR (c.start_with IS NOT NULL AND c.default_value IS NULL) THEN
  l_sql_exec := l_sql_exec||' START WITH pid IS NULL';

ELSE
  l_sql_exec := l_sql_exec||' START WITH id = '||nvl(v(c.start_with),c.default_value);
END IF;
--
-- This is a query to get the root of our tree, which we'll then loop through and call our
-- function to return the children of each root node
--
OPEN c_cur1 FOR l_sql_exec;
LOOP
  FETCH c_cur1 INTO l_id, l_pid, l_name, l_link, l_a1, l_a2;
  EXIT WHEN c_cur1%NOTFOUND;

Unfortunately I can’t post the entire code for commercial reasons, but hopefully it gives you some idea of extracting a query from the APEX meta data and executing/processing it after wrapping some additional SQL around it.

So to create a dynamic tree within the framework, you simply create a traditional APEX tree, including your tree root and set the region template to “Ext.tree” thats it ( Note: a page can support unlimited tree’s… well up to the maximum number of regions that APEX allows on a page)! It also comes with the customizations in runtime development to enable tree filtering and expand/collapse buttons, plus any other Ext config parameters you wish to enable/modify. What makes this even sweeter is when we wrap a number of tree’s within an accordion or a tab panel or a combination of the two, simply using our dot notation syntax in the REGION_STATIC_ID!

The next progressive step for tree’s in the framework will be AJAX enabling them, i.e. on each node click we’ll dynamically pull back the children which will be beneficial for large tree structures.

Here’s a screenshot from development:

APExt JS - Dynamic Tree

Jan
08
2010
0

Building a JSON object for an Ext Tree

This afternoon I was searching for some easy way of generating a JSON object for a tree based on SQL and couldn’t find anything (or maybe my googling needs some work), so this is a quick post on some raw development code that you may find useful before I change it to meet our specific pruposes.

I’ve just written a basic recursive function to build a JSON tree object. It’s based on the infamous EMP table that I’m sure most of you (if not all) are familiar with. The pre-requisite is installing PLJSON, which you may not be happy with but trust me it’s the best way to work with/build JSON objects in PLSQL. (Big thanks to Jonas Krogsboll)

You simply call “p_tree_query_root_json” here’s the code:

  -- ---------------------------------------------------------------------------
  --
  -- Procedure      : p_tree_query_children_json
  -- Author         : Matt Nolan
  -- Private/Public : Public
  -- Input Params   :
  -- Output Params  :
  -- Errors Raised  :
  -- Description    : Builds a JSON meta/config object for an JS tree based on a query
  -- ---------------------------------------------------------------------------
  -- Revision History
  -- Date            Author       Reason for Change
  -- ---------------------------------------------------------------------------
  -- 04 JAN 2010     M.Nolan      Created.
  -- ---------------------------------------------------------------------------
  FUNCTION p_tree_query_children_json
  ( p_id    IN NUMBER DEFAULT NULL
  ) RETURN JSON
  AS
    --
    -- JSON Objects
    --
    v_jsonObj1                      JSON; -- Our main parent Object
    v_jsonObj2                      JSON; -- Used for building sub objects
    --
    -- JSON Lists
    --
    v_jsonList1                     JSON_LIST; -- Our main parent List
    v_jsonList2                     JSON_LIST; -- Used for building sub lists

    v_app_id                        apex_application_page_regions.application_id%TYPE := v('APP_ID');
    v_page_id                       apex_application_page_regions.page_id%TYPE := v('APP_PAGE_ID');

  BEGIN
    --
    -- Isntantiate the JSON object
    --
    v_jsonList1 := JSON_LIST();
    FOR c IN
    ( SELECT mgr, empno, level
      FROM   emp
      WHERE  level = 1
      AND    mgr   = p_id
      CONNECT BY PRIOR empno=mgr
      START WITH mgr = p_id
    ) LOOP

      FOR c1 IN
      ( SELECT ( count(*) -1 ) cn_count
        ,      root
        FROM ( SELECT connect_by_root(empno) root
               FROM   emp
               CONNECT BY mgr = prior empno
             )
        WHERE  root = c.empno
        group
        by     root
      ) LOOP

        v_jsonObj1  := JSON();

        IF c1.cn_count > 0 THEN
          v_jsonObj2 := p_tree_query_children_json
                        ( p_id    => c.empno
                        );
          v_jsonObj1.put(to_char(c.empno),v_jsonObj2);
        ELSE
          v_jsonObj1.put(to_char(c.empno), 'leaf');
        END IF;
        v_jsonList1.add_elem(v_jsonObj1.to_anydata);
      END LOOP;
    END LOOP;
    --
    -- Lets reset our JSON object as it's only used in our loop
    --
    v_jsonObj1 := JSON();
    v_jsonObj1.put('chidren',v_jsonList1);
    RETURN v_jsonObj1;

  --
  -- Exception Handling Routine
  --
  EXCEPTION
    WHEN OTHERS
    THEN
      -- put something in here
      Raise;

  END p_tree_query_children_json;
  -- ---------------------------------------------------------------------------
  --
  -- Procedure      : p_tree_query_root_json
  -- Author         : Matt Nolan
  -- Private/Public : Public
  -- Input Params   :
  -- Output Params  :
  -- Errors Raised  :
  -- Description    : Builds a JSON meta/config object for an JS tree based on a query
  -- ---------------------------------------------------------------------------
  -- Revision History
  -- Date            Author       Reason for Change
  -- ---------------------------------------------------------------------------
  -- 04 JAN 2010     M.Nolan      Created.
  -- ---------------------------------------------------------------------------
  PROCEDURE p_tree_query_root_json
  AS
    --
    -- JSON Objects
    --
    v_jsonObj1                      JSON; -- Our main parent Object
    v_jsonObj2                      JSON; -- Used for building sub objects
    --
    -- JSON Lists
    --
    v_jsonList1                     JSON_LIST; -- Our main parent List
    v_jsonList2                     JSON_LIST; -- Used for building sub lists

    v_app_id                        apex_application_page_regions.application_id%TYPE := v('APP_ID');
    v_page_id                       apex_application_page_regions.page_id%TYPE := v('APP_PAGE_ID');

  BEGIN
    --
    -- Isntantiate the JSON object
    --
    v_jsonObj1 := json();
    FOR c IN
    ( SELECT empno, level
      FROM   emp
      WHERE  level = 1
      CONNECT BY PRIOR empno=mgr
      START WITH mgr IS NULL
    ) LOOP

      v_jsonObj2 := JSON();
      FOR c1 IN
      ( SELECT ( count(*) -1 ) cn_count
        ,      root
        FROM ( SELECT connect_by_root(empno) root
               FROM   emp
               CONNECT BY mgr = prior empno
             )
        WHERE  root = c.empno
        group
        by     root
      ) LOOP

        IF c1.cn_count > 0 THEN
          v_jsonObj2 := p_tree_query_children_json
                        ( p_id    => c.empno
                        );
          v_jsonObj1.put(c.empno,v_jsonObj2);
        ELSE
          v_jsonObj1.put('root-leaf',c.empno);
        END IF;
      END LOOP;
    END LOOP;
    v_jsonObj1.print;

  --
  -- Exception Handling Routine
  --
  EXCEPTION
    WHEN OTHERS
    THEN
      -- put something in here
      Raise;

  END p_tree_query_root_json;

Which produces the following:

{
  "7839" : {
    "chidren" : [{
      "7698" : {
        "chidren" : [{
          "7499" : "leaf"
        }, {
          "7521" : "leaf"
        }, {
          "7654" : "leaf"
        }, {
          "7844" : "leaf"
        }, {
          "7900" : "leaf"
        }]
      }
    }, {
      "7782" : {
        "chidren" : [{
          "7934" : "leaf"
        }]
      }
    }, {
      "7566" : {
        "chidren" : [{
          "7788" : {
            "chidren" : [{
              "7876" : "leaf"
            }]
          }
        }, {
          "7902" : {
            "chidren" : [{
              "7369" : "leaf"
            }]
          }
        }]
      }
    }]
  }
}

I’m now planning to change it to use dynamic SQL so we can easily reuse it within the framework, based on any query, unfortunately I won’t be able to post that one though.

Written by mnolan in: PLSQL | Tags: , ,
Jan
07
2010
0

Nesting widgets using recursive PLSQL

Apologies, this post is quite long due to the inclusion of some javascript code for reference…

The other day I posted about the fact that we we’re looking to support the nesting of group level widgets, like rendering a TabPanel within an Accordion, and vice versa… and doing this recursively so we can go down x many levels. The plan was to do this simply using the REGION_STATIC_ID and a dot notation e.g. “TABPANEL1.ACCORDION1.TABPANEL1″ for. Looking at the syntax I thought it should be simple enough to implement.

Note: in the framework group level widgets are not defined as regions in APEX, they are simply defined in the REGION_STATIC_ID region fields, as they are just holders around normal widgets e.g. Grid, Tree, Form, Graph etc. this simplifies the defining of them. We add a numeric identifier on them to either group widgets together within the same tab if the identifier is the same, or seperate them on different tabs if they’e not.

The thing that I’ve found is that doing anything simply means you can make it really complex behind the scenes, especially if you try and do something recursively, my brain went into melt down after 5 coffee’s and staring at the same lines of code for over 6 hours.

A couple of things have significantly helped though:

Previously I read Simon Hunts blog on using a pipelined function to turn a delimited string into a query-able table in SQL, anyway what I use it for is to work out where I am in the recursion level and what widget I’m currently processing (However i’ve simplified it by purely using it as function call and return the row I’m interested in), i.e. in level 2 of a recursive call for the following “TABPANEL1.ACCORDION1.TABPANEL1″ I’m processing the Accordion widget. To show you how I use it and process the widgets, here’s the SQL (I don’t expect you to completely follow it but hopefully it gives you an idea of how it can work):

SELECT SUBSTR(MAX(sys_connect_by_path(region_id,':')),2,LENGTH(MAX(sys_connect_by_path(region_id,':')))) regions, max(decode(rn,1,region_name, null)) region_name
FROM
  (SELECT pr.region_id
   ,      splitRow(pr.static_id,'.',p_level) static_id
   ,      pr.region_name
   ,      row_number() OVER (PARTITION BY splitRow(pr.static_id,'.',p_level) ORDER BY pr.display_sequence) rn
  FROM    apex_application_page_regions pr
  WHERE   pr.application_id        = v('APP_ID')
  AND     pr.page_id               = v('APP_PAGE_ID')
  AND     splitRow(regexp_replace(pr.static_id,'\d',''),'.',p_level) = splitRow(p_object_type,'.',p_level)
  AND     decode
          ( pr.display_position_code
          , 'AFTER_SHOW_ITEMS' , 'BOX_BODY'
          , 'BEFORE_BOX_BODY'  , 'BOX_BODY'
          , 'BEFORE_SHOW_ITEMS', 'BOX_BODY'
          , pr.display_position_code) = p_display_position
  AND     extjs_utils.auth_condition_check(pr.condition_type,pr.condition_expression1,pr.condition_expression2,pr.authorization_scheme) = 0
  )
START WITH rn       = 1
CONNECT BY PRIOR rn = rn-1 AND PRIOR static_id = static_id
GROUP BY static_id
ORDER BY static_id

The actual results are a colon delimited string of the REGION_ID’s that belong to the group level widget, which I then use apex_util.string_to_table to process them. Here’s the actual code for splitting a string, and getting a value using a supplied row number.

create or replace TYPE "SPLIT_TBL" as table of varchar2(32767);
/
create or replace FUNCTION split
( p_list VARCHAR2
, p_del VARCHAR2 DEFAULT ','
) RETURN split_tbl PIPELINED
is

l_idx pls_integer;
l_list varchar2(32767) := p_list;
l_value varchar2(32767);

BEGIN
LOOP
l_idx := instr(l_list,p_del);
IF l_idx > 0 THEN
PIPE ROW(substr(l_list,1,l_idx-1));
l_list := substr(l_list,l_idx+length(p_del));

ELSE
PIPE ROW(l_list);
EXIT;
END IF;
END LOOP;
RETURN;
END split;
/
create or replace FUNCTION splitRow
( p_list VARCHAR2
, p_del VARCHAR2 DEFAULT ','
, p_rownum NUMBER DEFAULT 1
) RETURN VARCHAR2
is
l_value VARCHAR2(32767);
BEGIN
SELECT column_value
INTO l_value
FROM ( SELECT rownum rn, t.*
FROM TABLE(split(p_list,p_del)) t
)
WHERE rn = p_rownum;
RETURN l_value;
EXCEPTION
WHEN OTHERS THEN
RETURN 'Error';
END splitRow;
/

Anyway the point of the post is that we can now recursively support the nesting of group type widgets using a simple dot notation in the REGION_STATIC_ID, I don’t think you can code widget functionality much faster than that? The other added bonus is that the layout and widget config is completely customizable and you can use any of the allowed Ext config, and this applies to everything we have integrated thus far, so it’s really flexible and customizable.

The config is managed in runtime development mode, as per the screenshot in the last post, which we use an Ext property grid. Reminds me of one of David Peake’s comments about the APEX development team “Eating their own dog food!” i.e. the APEX development team use the APEX IDE to build APEX, similarly we reuse integrated components to manage integrated components. Anyway here’s a few screenshots of some simple, two level nesting…

A nested accordion within a tabpanel:

APExt JS - Nesting Widgets

A nested tabpanel within an accordion:

APExt JS - Nesting Widgets

The thing to note is that traditional APEX development would mean that we wouldn’t have coded that many regions on a page as we’d use navigational elements that were shared across pages, e.g. tabbed navigation lists, wizards etc. now these regions can be defined on a single page and we can use Ext layouts and components to organize them in a clean and organized way, thus cutting down on number of pages and components (it does have some maintenance benefits).

The final thing is that when you’re dealing with complexity, dynamic code generation based on underlying metadata is the best way to go (thats what APEX is, we’re just building on top of it!). If you get the design right, it removes the complexity. Just debugging all the issues and getting the design right in the first place is the struggle, but the end result is well worth it!

Here’s an example of the dynamic code generated by our viewport call for the above screenshot, which is built by the recursive function:

var apExtCenterPanel;
Ext.onReady(function () {
   Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
   apExtCenterPanel = new Ext.app.apExtiFrameTabPanel("center-tabs", {
      "region": "center",
      "closable": false,
      "hideBorders": true,
      "id": "center-pane",
      "layout": "fit",
      "tabTip": gcApExtWelcome,
      "title": "About",
      "items": [Ext.app.apExtAccordion({
         "items": [{
            "layout": "fit",
            "bodyBorder": false,
            "border": false,
            "title": "Center Tab 3",
            "items": [Ext.app.apExtTabPanel({
               "items": [{
                  "layout": "fit",
                  "bodyBorder": false,
                  "border": false,
                  "title": "Center Tab 3",
                  "items": [Ext.app.apExtPanel(extR31742036883319874)]
               },
               {
                  "layout": "fit",
                  "bodyBorder": false,
                  "border": false,
                  "title": "Products",
                  "items": [Ext.app.apExtGrid(ext17788801571748784)]
               }],
               "config": {
                  "border": false,
                  "draggable": false,
                  "frame": false,
                  "iconCls": "icon-form",
                  "shadow": false,
                  "tabPosition": "bottom"
               }
            },
            "center-pane")]
         },
         {
            "layout": "fit",
            "bodyBorder": false,
            "border": false,
            "title": "Center Tab 2",
            "items": [Ext.app.apExtPanel(extR40263958257140884)]
         }],
         "config": {
            "bodyBorder": false,
            "border": false,
            "defaults": {
               iconCls: "icon-accordion"
            },
            "iconCls": "settings",
            "layout": "accordion",
            "layoutConfig": {
               titleCollapse: false,
               animate: true,
               activeOnTop: true
            }
         }
      },
      "center-pane")]
   });
   viewport = new Ext.ux.apExtFormViewport({
      "layout": "border",
      "id": "apextjs-viewport-newc",
      "items": [new Ext.BoxComponent({
         "region": "north",
         "el": "ext-north-pane",
         "height": 65
      }), new Ext.BoxComponent({
         "region": "south",
         "el": "ext-south-pane",
         "height": 32
      }), {
         "region": "east",
         "collapseMode": "mini",
         "collapsible": true,
         "id": "east-pane",
         "layout": "fit",
         "margins": "0 5 0 0",
         "maxSize": 400,
         "minSize": 175,
         "split": true,
         "title": "East Panel",
         "useSplitTips": true,
         "width": 225,
         "items": [Ext.app.apExtAccordion({
            "items": [{
               "layout": "fit",
               "bodyBorder": false,
               "border": false,
               "title": "East Tab 1",
               "items": [Ext.app.apExtPanel(extR40263369208134543)]
            },
            {
               "layout": "fit",
               "bodyBorder": false,
               "border": false,
               "title": "East Tab 2",
               "items": [Ext.app.apExtPanel(extR40263573364135812)]
            },
            {
               "layout": "fit",
               "bodyBorder": false,
               "border": false,
               "title": "Property Grid",
               "items": [Ext.app.apExtPropertyGrid(extR33794511261603830)]
            }],
            "config": {
               "bodyBorder": false,
               "border": false,
               "defaults": {
                  iconCls: "icon-accordion"
               },
               "iconCls": "settings",
               "layout": "accordion",
               "layoutConfig": {
                  titleCollapse: false,
                  animate: true,
                  activeOnTop: true
               }
            }
         },
         "east-pane")]
      },
      {
         "region": "west",
         "collapseMode": "mini",
         "collapsible": true,
         "id": "west-pane",
         "layout": "fit",
         "layoutConfig": {
            animate: true
         },
         "maxSize": 400,
         "minSize": 175,
         "split": true,
         "title": "West Panel",
         "useSplitTips": true,
         "width": 245,
         "items": [Ext.app.apExtTabPanel({
            "items": [{
               "layout": "vbox",
               "bodyBorder": false,
               "border": false,
               "title": "Navigation",
               "items": [Ext.app.apExtTree("R40259858241590632")]
            },
            {
               "layout": "fit",
               "bodyBorder": false,
               "border": false,
               "title": "West Tab 2",
               "items": [Ext.app.apExtPanel(extR40263061243132300)]
            },
            {
               "layout": "fit",
               "bodyBorder": false,
               "border": false,
               "title": "West Tab 3",
               "items": [Ext.app.apExtPanel(extR33795216240614766)]
            },
            {
               "layout": "fit",
               "bodyBorder": false,
               "border": false,
               "title": "West Tab 4",
               "items": [Ext.app.apExtPanel(extR33795421781616407)]
            }],
            "config": {
               "border": false,
               "draggable": false,
               "frame": false,
               "iconCls": "icon-form",
               "shadow": false,
               "tabPosition": "bottom"
            }
         },
         "west-pane")]
      },
      apExtCenterPanel]
   });
});
Written by mnolan in: PLSQL, Widgets | Tags: ,
Jan
05
2010
0

New Year, New Designs!

I’ve been busy upgrading to Ext 3.1, and with that have come a number of welcomed design changes in our framework. Previously we were dynamically building a viewport and using renderTo and applyTo to create most of the widgets within, which is a bit of problem with layouts, so we’ve gone back to the drawing board and have re-engineered the viewport design.

Essentially now we build the entire viewport and components in a single function call, which when you think of it is a bit of a nightmare when you have 20-30 components on a page, so we’ve cut the amount of code/config down by calling our custom javascript functions and reusing some meta objects defined in the header, we’ve simply replaced “widget.render” with “return widget” in our js functions, meaning we haven’t had to recode much at all, as Ext supports the defining of “items” as Mixed, meaning it could be config or an instantiated object.

The benefit we now get from this is that all the widgets are managed by a Layout Manager and are dynamically resized when the size/display of the layout changes. This is the big difference between jQuery and Ext (@ the APEX community) when you see the beauty of your widgets being resized based on the change of the screen size, collapse of regions/panels etc, you’ll understand why Ext is a much better way to go than jQuery widgets, which are mostly working independently of each other.

At the moment we are supporting the grouping of regions/widgets into “Panes”, “Tab Panels”, “Accordions”, & “Panels”. However this is only at one level, i.e. we can’t support a “Tab Panel” inside an “Accordion”, however we’re working on a design change that will work recursively, so you will be able to do this infinitely by using something like a simple dot notation for the REGION_STATIC_ID e.g. tabpanel1.accordion2.panel1 which I’ll try and translate into:

  • rendering a region/widget within the first panel that is within the second accordion slot within the first tab within the pane. (We use the REGION_POSITION to define what pane the region/widget belongs to)

It all sounds well in theory but I’ll have to run some tests to see how deep we can go with the nesting of widgets. The other new functionality that has been introduced is allowing custom config to be defined for all layouts and widgets in APEX development runtime mode. We simply use a Property Grid to achieve this. We’ve separated the config between widget config and layout config for group level widgets, here’s a screenshot to give you an idea.

APExt JS - Widget Customization

The other new bit of functionality that w’ve introduced is Accordions, it’s basically the exact same design as our Tab Panel code, so it’s been really quick to implement and use. You simply set ACCORDION[Sequence No] in your REGION_STATIC_ID. Here’s a few screenshots from our dev environment.

APExt JS - Accordion

An here’s what the page looks like in the IDE

APExt JS - Behind the Scenes

The next big area of focus is back on the Editable Grid and using the row editor!

Powered by WordPress