Thursday, August 25, 2016

Creating an APEX plugin for an Oracle JET component - Part 2

In my previous blogpost I showed how you can embed an Oracle JET component in your APEX application. Now it is time to make a plugin out of the wisdom we gained doing so.
First of all a disclaimer. My intention is to make this plugin and the inner workings as simple as possible. So you can add a lot more functionality, checks etc and therefore add complexity. But this is intended to be as simple as possible.

The plugin consists of three parts: a PL/SQL render function, a snippet of JavaScript and a PL/SQL ajax function.

The render function is defined as :

function render 
( p_region                in  apex_plugin.t_region
, p_plugin                in  apex_plugin.t_plugin
, p_is_printer_friendly   in  boolean 
) return apex_plugin.t_region_render_result 
is

  c_region_static_id      constant varchar2(255)  := apex_escape.html_attribute( p_region.static_id );

begin
  -- Add placeholder div
  sys.htp.p (
     '<div class="a-JET-PictoChart" id="' || c_region_static_id || '_region">' ||
       '<div class="a-JET-PictoChart-container" id="' || c_region_static_id || '_chart"></div>' ||
     '</div>' );
     
  -- Load the JavaScript library   
  apex_javascript.add_library 
  ( p_name      => 'pictoChart'
  , p_directory => p_plugin.file_prefix
  );
  
  -- Initialize the chart
  apex_javascript.add_onload_code
  ( p_code => 'jet.picto.init('||
                  '"#'||c_region_static_id||'_chart", '          || -- pRegionId
                  '"' || apex_plugin.get_ajax_identifier ||'"'   || -- pApexAjaxIdentifier
                 ')'
  );
  
  return null;
end render;

So what it does in these three steps is :
1. Generate a DIV placeholder, just as we saw in that previous post
2. Load the JavaScript file "pictoChart.js",
3. Execute the JavaScript function "jet.picto.init" providing two parameters, the regionId of the DIV and the (internal) identifier of the PL/SQL ajax function.

The contents of the JavaScript file is :

! function (jet, $, server, util, debug) {
    "use strict";
    requirejs.config({
        baseUrl: apex_img_dir + "oraclejet/js/libs",
        paths: {
            "jquery": "jquery/jquery-2.1.3.min",
            "jqueryui-amd": "jquery/jqueryui-amd-1.11.4.min",
            "ojs": "oj/v2.0.0/min",
            "ojL10n": "oj/v2.0.0/ojL10n",
            "ojtranslations": "oj/v2.0.0/resources",
            "promise": "es6-promise/promise-1.0.0.min"
        },
        shim: {
            jquery: {
                exports: ["jQuery", "$"]
            }
        }
    }), jet.picto = {
        init: function (pRegionId, pApexAjaxIdentifier) {
            require(["ojs/ojcore", "jquery", "ojs/ojpictochart"], function (oj, $) {
                server.plugin(pApexAjaxIdentifier, {}, {
                    success: function (pData) {
                        $(pRegionId)
                            .ojPictoChart(pData);
                    }
                });
            });
        }
    }
}(window.jet = window.jet || {}, apex.jQuery, apex.server, apex.util, apex.debug);
// To keep ThemeRoller working properly:
define("jquery", [], function () {
    return apex.jQuery
});

The first function call (requirejs.config) is again the same is we did earlier. Please note that the paths mentioned in here might differ in your environment. You could define those paths as (Application level) Component Settings, but for simplicity I keep them hardcoded in this example.
Notice there is a "jet" namespace defined and within that namespace a nested "picto" namespace. So we could easily extend this file (after renaming it) to other chart types. The "init" method defines the required files and then calls apex.server.plugin passing the PL/SQL ajax function as a parameter. After this ajax function is called, the result - a JSON object - is passed to the ojPictoChart function.
On the last line, as Kyle Hu correctly commented in my previous post, we have to define jquery in order to make ThemeRoller work (again).
So in fact, the JavaScript is very straightforward: in the end just an ajax call passing the result into the ojPictoChart function.

So what is the magic of the last piece, the ajax PL/SQL function? Here it is::

function ajax
( p_region    in  apex_plugin.t_region
, p_plugin    in  apex_plugin.t_plugin 
) return apex_plugin.t_region_ajax_result
is
  c       sys_refcursor;
  l_query varchar2(32767);
begin  
  l_query := p_region.source;
  open c for l_query;
  apex_json.open_object;
  apex_json.write('items', c);

  -- add settings
  apex_json.write('animationOnDisplay' , p_region.attribute_01);
  apex_json.write('columnCount'        , p_region.attribute_02);
  apex_json.write('layout'             , p_region.attribute_03);

  apex_json.close_object;
  return null;
end ajax;

Thus it just takes the region source - a SQL statement -, executes it returning a JSON object. And to these results three of the available options, defined as plugin attributes, are added. Again, as simple as possible. So no validation on the correctness of the SQL (as all APEX Developers can write a correct SQL statement, right). And just a minor part of the available options are exposed in the plugin.

When you create a region based on this plugin you have to provide it with a correct SQL statement - one that will return a valid JSON object according to the pictoChart docs. For example:

select ename||' - '||job||'@'||dname "name"
,      'human' "shape"
,      1 "count"
,      case job
       when 'PRESIDENT' then 'black'
       when 'ANALYST'   then 'blue'
       when 'CLERK'     then 'green'
       when 'MANAGER'   then 'red'
       when 'SALESMAN'  then 'yellow'
       end "color"
from   emp
       join dept on emp.deptno = dept.deptno
order by 2 desc, 4

And if you set the settings as :


You'll get as a beautiful result:
So knowing this, it wouldn't be hard to rebuild the plugin by yourself. But for the lazy readers out there, you can download it here too.
Or probably build another plugin for another JET component!

Saturday, August 20, 2016

Creating an APEX plugin for an Oracle JET component - Part 1

In APEX 5.1 (still Early Adaptor 1 at this moment), Oracle JET - Javascript Extension Toolkit -  is included to facilitate charting.
The APEX Development Team recently mentioned that not only the Data Visualisations part of JET will be included in APEX 5.1, but the complete installation of JET. The whole package. That won't do the size of the downloadable install file any good, but more important is: what can we do with it?

A number of the Data Visualisations will be exposed in APEX for the declarative definition of charts as we are used to now using the Anycharts library. But there are more Data Visualisations - check the JET Cookbook for all examples - and other components that might be of interest for an APEX application. So how can we use these in our apps?

As an example, I would like to use the PictoChart component in my application.
Fist of all - as we don't have APEX 5.1 yet - we need to download the Oracle JET library and install the files on either your web server or upload all of them into the APEX Static Files. As there are a lot of files - and I mean, really a lot - I would recommend storing the files on your web server.

Next, if you follow the steps of the JET Cookbook, you'll notice that they all make heavy use of the Knockout Javascript library. Knockout is extremely powerful, but also rather complex for most APEX developers. And in an APEX environment we don't really need it. So can we use Oracle JET components without Knockout, as there is no example on the (official) JET pages?
But there is an un(der)documented feature, which is the "initializer" call of an Oracle JET component, click here to open that part of the documentation of the PictoChart component. That looks more familiar to us, as it is just an (old fashioned) Javascript call with a JSON object as a parameter! So how do we get that on an APEX Page?

First of all we need to include RequireJS into our application by referencing

    #IMAGE_PREFIX#jet/js/libs/require/require.js

in the "File URLs" property of our Page. Of course, the exact location is dependent on where you stored the JET library. Next we have to tell RequireJS where all the Oracle JET files are that we need to load. Therefore we call (in the "Execute when Page Loads" property of the page) :

    requirejs.config({
        baseUrl: apex_img_dir + "oraclejet/js/libs",
        paths: {
            "jquery"         : "jquery/jquery-2.1.3.min",
            "jqueryui-amd"   : "jquery/jqueryui-amd-1.11.4.min",
            "ojs"            : "oj/v2.0.0/min",
            "ojL10n"         : "oj/v2.0.0/ojL10n",
            "ojtranslations" : "oj/v2.0.0/resources",
            "promise"        : "es6-promise/promise-1.0.0.min"
        },
        shim: {
            jquery: {
                exports: ["jQuery", "$"]
            }
        }
    })

Now, also in the "Execute when Page Loads" property of the page, we can call the initializer of the PictoChart component :

    require(['ojs/ojcore', 'jquery', 'ojs/ojpictochart'],
      function (oj, $) {
        $("#pc1").ojPictoChart(
           {items: [
              { name  : 'Have  Sleep Problems'
              , shape : 'human'
              , count : 7
              , color : '#ed6647'
              },
              { name  : 'Sleep Well'
              , shape : 'human'
              , count: 3
              }]
           , animationOnDisplay: 'zoom'
           , columnCount: 5          
        });    
    }); 

and finally we need to define component with the id "pc1" above mentioned into where the PictoChart is rendered by creating a region serving static content:

<div  id="picto-container">
  <div id='pc1'style="vertical-align:middle; margin-right:15px">
  </div>
  <div style="display:inline-block; vertical-align:middle; font-weight:bold">
    <span style="color:#333333; font-size:1.1em">7 out of 10 college students</span><br>
    <span style="color:#ed6647; font-size:1.3em">have sleep problems.</span>
  </div>
</div>

As a result we will see the exact same PictoChart as shown above in our APEX Page.
In the next blog post I will show how to convert this hard-coded data into an easy-to-use APEX plugin.

So stay tuned !








Friday, August 12, 2016

Consuming a REST Web Service returning JSON in APEX

In APEX you can define a web service that returns XML as below - all declarative, just a few steps through a wizard.


Then generate a report on top of that web service - again just a few clicks through a wizard. The generated query looks like this:

select xtab."customerName"
     , xtab."customerId"
  from apex_collections c, 
          XMLTable('/Response/S_getCustomerListTableArray/S_getCustomerListArrayItem' passing xmltype001
            COLUMNS "customerName" PATH 'customerName'
                  , "customerId"   PATH 'customerId'
          ) xtab
 where c.collection_name = 'CUSTOMERLIST'

So the result of the web service is stored in an XMLTYPE column. And it's easy to spot where you're definitions for the Response XPath and Output Parameters are used.

But what if your web service returns JSON - as more and more web services will do so? If you switch the Output Format of the web service definition to JSON, the Response XPath property and the Output Parameters are not enterable. And if you generate a report on top of that web service, the resulting query is a - disappointing - 

select c.clob001
from apex_collections c
where c.collection_name = 'CUSTOMERLIST'

and your report will show a JSON dump. Not exactly what you're hoping for. And notice the result is stored in a CLOB.

But since 12c, the JSON support in the Oracle database has improved a lot and we can easily use that in our query. So if we rewrite our query to 

select cust.*
from   apex_collections
,      json_table(clob001, '$.Response.S_getCustomerListTableArray.S_getCustomerListArrayItem[*]'
       columns ( "customerName"   PATH '$.customerName'
               , "customerId"     PATH '$.customerId'
               )) cust
where collection_name = 'CUSTOMERLIST'

we will get the same result as the - old fashioned - XML one! And as you can see the code is very much alike and could be generated by APEX itself just like the XML version.

So I expect that in a future version this functionality will be added to the APEX builder - it shouldn't be that hard to implement! And maybe the apex_collections can have a real "JSON" column as well (in fact it would still be a CLOB with an "IS JSON" check).

The only prerequisite is, you have to run APEX on version 12c of the database. If you want to know more about JSON in the database, there is a serie of blog posts about this, starting here.

Tuesday, July 05, 2016

Prevent general row based search in an APEX Interactive Report

At my current project we use Interactive Reports heavily. But in some cases the underlying queries are rather complex and a query without any filter or with just a filter on all columns doesn't perform that well (understatement...). We already implemented a solution to prevent initial loading of the data by adding an additional criterium to the query ( :P400_SEARCH = 'Y' ). The default value of this hidden item is 'N' and on Page Load the value is set to 'Y', so the next search will be executed. But as said, this is good, but not good enough...

So this morning I was looking for a solution to "force" the user to enter a query on one of the columns - either through the "search bar column list" (the magnifier on the left side of the search field) or through the Actions > Filter menu. The first attempt was to use a Dynamic Action that fires on change of the search field, but that doesn't work when the user switches existing filters on or off. So I inspected the debug output of the page and discovered an item named "APXWS_EXPR_1". This item is set when the report is refreshed and contains the value of the first column filter you apply. Subsequently you can have APXWS_EXPR_2 etc. So I added an additional restriction to the query: 

:APXWS_EXPR_1 IS NOT NULL

Thus by just adding one line to the query the user is forced to use at least one column filter - both the "contains" as any other more specific filter works. And I can even delete the previous solution that prevented running the query (actually the query is run, but returns "no data found" in no time) when the user navigates to the page.

As a side note, if you use the general filter on all columns, that value is available on the server as "APXWS_SEARCH_STRING_1" (and _2 etc.)

Tuesday, June 07, 2016

Create a Navigation Bar based on a SQL statement

Creating a Navigation Bar using static values is very straightforward. Just create a list similar to this example below - and you can easily define multiple levels.
Then, in the User Interface Attributes, set that list to be your application's "Navigation Bar List" and specify the template.
And the Navigation Bar pops up nicely in the upper right corner of the screen.
So far so good.

But what if you don't want a Static List, but get a list based on a SQL statement? Then you have to enter a statement that adheres to the this structure:
select level, labelValue label, 
       [targetValue]            target, 
       [is_current]             is_current_list_entry,
       [imageValue]             image, 
       [imageAttributeValue]    image_attribute,
       [imageAltValue]          image_alt_attribute,
       [attribute1]             attribute1,
       [attribute2]             attribute2,
       [attribute3]             attribute3,
       [attribute4]             attribute4,
       [attribute5]             attribute5,
       [attribute6]             attribute6,
       [attribute7]             attribute7,
       [attribute8]             attribute8,
       [attribute9]             attribute9,
       [attribute10]            attribute10
from ...
(the columns between the square brackets are optional ones). Adding any other column will fail. But it seems you can enter a "level", so if your query returns the rows in the correct order, you should be fine. You think.
Because doing so (and using the very same List Template) will not work when you have multiple (parent) entries with children: All parents will show the same children, the ones of the first parent. The issue is raised a few times on the Oracle Forums, but never answered (see here and here).

Because I ran into that very same issue this morning, I dived in a little deeper. The problem is in the generated HTML: all buttons (parents) will get the same ID. So that's why the same children are attached to all the parents. 
So where does it get it's ID from? That's defined in the "Navigation Bar" template. In there references to "#LIST_ITEM_ID#" and "#PARENT_LIST_ITEM_ID" are used. But those values are not returned by the query. Even worse, you can't even define columns with those names. 

So the solution is to copy that Navigation Bar Template and replace the references to #LIST_ITEM_ID# and #PARENT_LIST_ITEM_ID# with a column that is returned from the query (and of course contains the right value). So, for instance, use #A09# and #A10# instead of the #LIST_ITEM_ID# and #PARENT_LIST_ITEM_ID# and be sure to select "attribute9" and "attribute10" in your query.
This way you can define a multilevel Navigation Bar based on a SQL statement !


Tuesday, March 15, 2016

Me and the ODTUG Board

This afternoon my Twitter timeline exploded with retweets, likes and congratulations. It all started with this tweet:

So it's official now. As Sarah Zumbrum had to step down due to her (dream) move to Oracle, a position on the ODTUG board opened up. And I was asked whether I was still interested. And of course I was! So after an internal voting procedure, I received the good news last weekend - and had to keep quiet for a few days...which was quite hard!
Last November I wrote a blog post "I *almost* made it to the ODTUG Board of Directors...", but now the word between the asterisks can be omitted.
I am thrilled to serve on the board - even though it's initially only until the end of the year. I'll definitely run again for the board in October - so you'd better become a member a.s.a.p. to vote for me then (if you aren't already)!
Hope to see you all at Kscope16 or any other local event.
You can read the official statement on the ODTUG site.

Monday, February 22, 2016

Unexpected behaviour using SSO Authentication for APEX

A customer of mine switched to a Singe Sign On Authentication Scheme for their APEX application - using Oracle Access Manager, but that's irrelevant to this case.
In the application there's a function to create an - temporarily - "real" APEX user. That APEX user is used in another JavaScript application that uses ORDS and the built-in APEX authentication - thus requiring the existence of APEX users. That function ran flawlessly when the application was still on APEX Authentication. But after switching to SSO, it complained about "ADMIN" privileges .... although the usernames are identical: SSO and APEX Authentication used the same usernames.
So it must be something in the authorization / security realm, isn't it? Thus we tried a smart thing and moved that particular function to another - APEX Authenticated - application, while sharing the login credentials via a cookie. But to no avail. Some complaint.
So I contacted the APEX Development team and - as always - I got a quick reply. It wasn't a bug. It's a feature. They explained that you can only use functionality that needs APEX Admin privileges if you are authenticated against the APEX Repository. An - accidental (?) - match on usernames is not enough to grant ADMIN privileges. And that totally makes sense....
So although it was unexpected behaviour, it is the correct behaviour!

BTW. the solution is to create a separate job that runs using the right privileges.