Wednesday, February 18, 2009

Drag and Drop Shopping Cart

At SQL Integrator we are just beginning to explore the possibilities of Apex. We do that by teaching each other via workshops. When somebody dives into a subject, he presents the result to the other Apex workshop participants. A good way to learn Apex.

I had no idea how to do anything with Ajax. After some web surfing (mainly at w3schools.com) I tried some simple examples myself. Most of them were taken from here and here.
A quite fancy example is the drag and drop planboard, but this was a bit too complicated for me to start with (especially since some secrets were not explicitly revealed).

So, I came up with this simple shopping cart demo. There was not much programming involved, since most of the Javascript code was downloaded from script.aculo.us.

Blogger is trying to interpret tags when you just want to publish the code, so bear in mind that you have to replace the [] pair with <> in the next pieces of code.

The left region is a report based on:

select '[div class="boxwide" id="'||product_id||'"]'||
product_name||'[/div]' product
from demo_product_info

Each product gets a unique id so that you know which product is dragged. The class boxwide is a CSS class that puts the product name in a box.

Note that is is generally not a good idea to put HTML in your query. It is a better idea to put this in the HTML Expression on the Column Attributes page. In this the query should be:

select product_id
,product_name
from demo_product_info


And the HTML Expression:

[div class="boxwide" id=#PRODUCT_ID#]#PRODUCT_NAME#[/div]

The region on the right is an empty HTML region withe a region header:

[div id="cart"]

and footer:

[/div]

This identifies the region where products can be dropped.

Next, you have to put some Javascript in the page header to call application processes and the script.aculo.us libraries.

[script src="#WORKSPACE_IMAGES#prototype.js" type="text/javascript"][/script]
[script src="#WORKSPACE_IMAGES#scriptaculous.js" type="text/javascript"][/script]
[link type="text/css" href="#WORKSPACE_IMAGES#dragdrop.css" rel="stylesheet"]
[script language="javascript" type="text/javascript"]
function addProduct(element, dropon, event) {
sendData(element.id);
}
function sendData (prod) {
var get = new htmldb_Get(null,$x('pFlowId').value,'APPLICATION_PROCESS=AddToCart',0);
get.addParam('x01',prod)
gReturn = get.get();
$x('cart').innerHTML = gReturn;
}
function clearCart () {
var get = new htmldb_Get(null,$x('pFlowId').value,'APPLICATION_PROCESS=ClearCart',0);
gReturn = get.get();
$x('cart').innerHTML = gReturn;
}
function getCart () {
var get = new htmldb_Get(null,$x('pFlowId').value,'APPLICATION_PROCESS=GetCart',0);
gReturn = get.get();
$x('cart').innerHTML = gReturn;
}
[/script]

In the footer of the page put this Javascript code to identify which elements are involved in the drag and drop process:

[script type="text/javascript"]
var products = document.getElementsByClassName('boxwide');
for (var i = 0; i < products.length; i++) {
new Draggable(products[i].id, {ghosting:false, revert:true})
}
Droppables.add('cart', {onDrop:addProduct})
[/script]

Next you define the appication process AddToCart that is called when an item is dropped on the shopping cart region:

declare
l_string varchar2(32767);
begin
insert into demo_shopping_cart(sessionid, product_id)
values(v('APP_SESSION'), to_number(wwv_flow.g_x01) );
commit;

l_string := get_shopping_cart(v('APP_SESSION'));
htp.p(l_string);
end;

Get_shopping_cart is a function in the database that returns the items currently in the cart for the session:

CREATE OR REPLACE function get_shopping_cart(i_sessionid in number)
return varchar2 is
cursor c_cart is
select '[div class="boxwide" id="cart_'||id||'"]'||
product_name||'[/div]' product_name
from demo_shopping_cart crt
, demo_product_info prd
where crt.product_id = prd.product_id
and crt.sessionid = i_sessionid
order by id;
l_string varchar2(32767);
begin
for r_cart in c_cart loop
l_string := l_string || r_cart.product_name;
end loop;
return l_string;
end;

Similarly, you can create the ClearCart and GetCart application processes that are called when you click the Clear Cart button and when you open the page.

Tuesday, February 17, 2009

JasperReports in Apex

A lot of people complain that Oracle integrated the very expensive BI Publisher in Apex. Of course there are alternatives, like the open source JasperReports. But how do you use this?

Jasper is completely new to me, so I started with a simple example. What you need is:
  • Oracle Apex (of course)
  • JasperServer
  • iReport
JasperServer and iReport can de downloaded from the Jaspersoft website.

Basically, iReport is a client/server graphical tool to design a report, and JasperServer runs the report in a web environment.

I created the simplest report I could think of in iReport using the Report Wizard:

select *
from emp
where deptno = $P{deptNo}


Publish this report in JasperServer. As a Jasper newbie I was surprised that in JasperServer you have to define the report parameter (deptNo) again as a control. This is not taken from the report definition. That took me some time to figure out, but once that was done, integration with Apex was easy!

Now back to Apex. Here I created another simple report based on:

select
"EMPNO",
"ENAME",
"DEPTNO",
"DEPTNO" "DEPTNO2"
from "EMP"


The two deptno columns are used to link to the Jasper report. I will explain the difference between those two.

For the first deptno column I created a simple link to JasperServer. Edit the report attribute so that it will become a link to a URL:




The URL is:

http://server:port/jasperserver/flow.html?_flowId=viewReportFlow&reportUnit=/reports/Apex/deptemp&output=pdf&deptNo=#DEPTNO#&j_username=un&j_password=pw


Now, as you can see this is not a very secure option. The username and password are exposed in the URL. Another option is to call the report from the database using the utl_http package. This is the procedure:


CREATE OR REPLACE procedure runJasperReport(i_deptno   in varchar2)
is
vReportURL varchar2(255);
vBlobRef blob;
vRequest Utl_Http.req;
vResponse Utl_Http.resp;
vData raw(32767);
begin
----------------------------------------------------------------------------
-- build URL to call the report
----------------------------------------------------------------------------
vReportURL := 'http://host:port/jasperserver/flow.html?_flowId=viewReportFlow'||
'&reportUnit=/reports/Apex/deptemp'||
'&output=pdf'||
'&j_username=un&j_password=pw'||
'&deptNo='||i_deptno;
----------------------------------------------------------------------------
-- get the blob reference
----------------------------------------------------------------------------
insert into demo_pdf (pdf_report)
values( empty_blob() )
returning pdf_report into vBlobRef;
----------------------------------------------------------------------------
-- Get the pdf file from JasperServer by simulating a report call from the browser
----------------------------------------------------------------------------
vRequest := Utl_Http.begin_request(vReportUrl);
Utl_Http.set_header(vRequest, 'User-Agent', 'Mozilla/4.0');
vResponse := Utl_Http.get_response(vRequest);
loop
begin
--------------------------------------------------------------------
-- read the next chunk of binary data
--------------------------------------------------------------------
Utl_Http.read_raw(vResponse, vData);
--------------------------------------------------------------------
-- append it to our blob for the pdf file
--------------------------------------------------------------------
Dbms_Lob.writeAppend
( lob_loc => vBlobRef
, amount => Utl_Raw.length(vData)
, buffer => vData
);
exception when utl_http.end_of_body then
exit; -- exit loop
end;
end loop;
Utl_Http.end_response(vResponse);

owa_util.mime_header('application/pdf',false);
htp.p('Content-length: ' || dbms_lob.getlength(vBlobRef));
owa_util.http_header_close;
wpg_docload.download_file(vBlobRef);

end runJasperReport;
/

To call this procedure you create a very simple On Demand Application Process:

runJasperReport(:deptno);

where :deptno is an Application Item.

Similar to the link in the first case, the URL is now:

f?p=&APP_ID.:&APP_PAGE_ID.:&APP_SESSION.:APPLICATION_PROCESS=runrep:::deptno:#DEPTNO2#

In a real application you will add extra parameters to the stored procedure, like logged in user. Make it re-usable by adding the report name and parameters as input parameters.

Added alternative

There is a third option to call a report. I think this is even better, since you have more authorisation control. On Demand Application Processes have the disadvantage that they do not have an authorisation control mechanism.

So, instead of calling an application process, you can call a page that has the same stored procedure as source.

Just create an empty page that calls the procedure in the On Load - Before Header process point.

In the report itself, change the link to call the page instead of the application process.

Welcome to yet another blog

Well, like so many others nowadays, I think it is time to share some good tips&trics on Oracle. My colleagues and I have started a new track with Oracle Apex after many years of using Forms, Reports and Designer.

Yep, we have tried JDeveloper and JHeadstart, and we discarded it. It seems way too complicated for most regular business type applications that focus mainly on data retrieval and entry.