Building Your First WebServer Application:
A Tutorial
William L. Pribyl
DataCraft, Inc.
Introduction
True to web culture, this paper was written in a "just-in-time"
fashion. While every effort at accuracy has been made, please visit
http://www.datacraft.com/ for any corrections or late additions.
After spending months building a moderate-size application with Oracle's
WebServer 2.0 and 2.1, it seems to me there are a lot of things Oracle
left out of the manuals. Not secrets, really, just omissions that
would have made life easier if they'd been in there. The most frustrating
part was getting started. Installation was easy enough - as long
as you follow Oracle's instructions to the letter, and review all the log
files - but after that I was clueless. Yes, I knew PL/SQL, plus I
knew a fair amount about HTML and web servers in general, but there was
a large gray muddle between the two that the docs didn't wipe away.
(And in case you're wondering, I didn't have time to go to class, I had
a deadline.) This paper contains the information I wish I had known
when I was just starting.
The application on which I worked was almost all PL/SQL - no Java, no custom
cartridges, no SSL security, no LiveHTML. If you want to see the
finished product, there is a slightly crippled demonstration of it at http://oracle.raid.com/imagebase
(or try http://208.206.97.20/imagebase). The web site is only for
demonstration, not for download of the application, and without a privileged
userid no updates are allowed. At any rate, we used Designer/2000
as the starting point to generate the PL/SQL, which generated some functional
(and rather ugly) read-only web pages, to which we added insert/update/delete
features later. While others on the team worked on cosmetic issues
and wrote some oraperl CGI scripts to handle calls to various Unix utilities
and programs, this paper concentrates on PL/SQL, which I believe to be
easiest, and probably the most common, WebServer application development
technique (and will be until WebForms is widely available). If you
just want information on using cartridges or other approaches, you can
skip this paper.
At the time of this writing (February 1997), all of my experience has been
with WebServer version 2.1 and its predecessors. WebServer 3.0 was
in beta, and although Oracle was kind enough to let me fill out their web-based
form requesting it, they replied with an automatic rejection saying only
that the beta program was closed. I know that version 3.0 adds a
type of "transaction" support which may change your approach
to updates, but I suspect that the fundamental PL/SQL programming model
has not changed.
Acknowledgments
I am grateful to the kind folks at GeneSys LLC, an integrator
of high performance computing systems for the print and new media industries,
for their assistance in bringing a WebServer solution to reality, as well
as for their undying confidence in my Oracle skills, not to mention their
willingness to take late-night phone calls. Thanks are also due Thomas
Kyte/Oracle Government, who through his many web postings and emails helped
me understand what I was doing; and the ORAWEB-L Internet mailing list
community.
Before You Start
What Version Should I Use?
I know a lot of people have this question: Why buy WebServer
2.x when 1.0 is free? First off, an (unconfirmed) statement from an Oracles
salesperson indicated that Version 2.0 would be bundled free with the Oracle
server once Version 3.0 goes into production. By the time you read this,
this change may be old news.
Hoping for a definitive statement from Oracle, I asked the
WebServer product manager to provide some documentation on the exact differences
between the products, as well as a comment on where Oracle was headed with
respect to bundling of functionality with the core server. So far I�ve
waited a week; no reply yet. Not to be dissuaded, I tried other avenues
to get this information.
In 1.0, there is a CGI program for connecting to the database.
WebServer 2.x adds support for "cartridges" and includes the
Web Request Broker cartridge which an improvement (how much of an improvement
is something of a debate) over CGI. Version 2.x also includes SSL, which
is essential if you need secure connections; a Java virtual machine for
server-side Java programs which can call PL/SQL; and LiveHTML for Server-Side
Includes. While Version 2.0 allows you to use Netscape Fasttrack Server
as the web server, Version 2.1 has an ability to use more third-party web
servers such as Netscape Commerce Server and Microsoft IIS.
You might think that you can start practicing with 1.0 and
upgrade to 2.x later. True enough, but each new version also includes a
variety of new and enhanced PL/SQL utilities in the toolkit. Also, later
versions crash less frequently. Personally, I would hesitate to put a V1
solution into wide production use when V2 is available. It�s fine for demonstrations
of the core functionality, though.
According to Barry Johnson, a contributor on the ORAWEB-L
Internet mailing list, new features for 3.0 are mainly: Intercartrdige
Exchange (ICX); improved LiveHTML, because of ICX; a Perl cartridge; XA-compliant
Transaction Management; CORBA-compliant WRB/ORB; and database-based authentication.
Maybe next year I�ll be able to write a paper on how those things will
help your application.
Skills Assumed
This paper assumes you�ve created at least one PL/SQL package
and have created a web page of some sort. Much as I�d like to, I don't
have the space to teach these fundamentals here. To be productive in WebServer
PL/SQL, you�ll want to know PL/SQL version 2 at least well enough to write
packages, procedures, and functions; you should have a thorough knowledge
of exception handling; and you would benefit from understanding the PL/SQL
"table" data type. You�ll also want to have a bit of HTML knowledge,
some of which you can pick up as you go. Something people often ask about
is "good books about HTML;" my favorite is O�Reilly & Associates�
HTML: The Definitive Guide by Chuck Musciano & Bill Kennedy,
although it is getting a bit dated, and it does not treat CGI programming,
both of which are not a big problem if you�re a WebServer neophyte.
The Bare Minimum about Installation and Configuration
If you�ve ever installed Oracle on a Unix box, you know there
is a root.sh script that gets generated and which you must run as
root after performing the install with orainst. If you�ve installed
WebServer along with the Oracle server, then (assuming your WebServer product
directory is ows2) root.sh will call the script $ORACLE_HOME/ows2/install/fmods.sh,
which in turn will set some file permissions and then attempt to start
a special WebServer administrative listener on using a command of the form
"wlctl21 start admin $ORACLE_HOME". If you connect with a web
browser to the URL prompted by fmods.sh, probably http://myhost.mydomain:port/ows-abin/boot),
one of the first things it does is ask for the username/password combination
which you supplied to prompts during orainst. If you can�t remember it,
just look in the file $ORACLE_HOME/ows2/svadmin.cfg � it�s there in plain,
gloriously unprotected text!
Then you will confront a series of screens that lets you
configure your first listener and PL/SQL agent (previously
known as the "web agent" or "Oracle web agent" and
abbreviated OWA). The listener consists of one or more background processes
that respond to HTTP requests; the PL/SQL agent is the part of WebServer
that connects to the Oracle RDBMS using the username and password that
you designate. You can decide whether the PL/SQL agent connects via CGI,
which I think largely exists for backward compatibility, or as a cartridge,
in this case called the OWA cartridge, for the Web Request Broker.
Although the details of the architecture are outside this paper�s scope,
suffice it to say that the WRB/cartridge approach is the more efficient
alternative and has been highly touted by Oracle.
Creating a PL/SQL Agent to and Configuring it to
Use the Web Request Broker
If you, like me, are not too keen on using Oracle�s defaults,
one of the first things you will want to do is set up your own Database
Connection Descriptor (DCD) for you app rather than using those created
during the installation. The DCD is a set of information that tells Oracle
connection specifics such as username, password, connect string (TNS service
name), and log file. This information, like almost all WebServer configuration
data, is stored in an ASCII file, but it is modifiable from a web browser
through the WebServer Manager screens. The agent connect data can be found
in $ORACLE_HOME/ows2/admin/owa.cfg,
There are at least two steps here: first, setting up the
Oracle user and connection description (DCD); and second, telling the WRB
about it.
How to do it. First, choose a name for the Oracle
user and for the corresponding DCD. In our application, we chose to make
the Oracle userid invisible to the end user, so the choice of userid was
pretty arbitrary. I suggest making it a short name, though, because you
may be typing it a lot during application development. The DCD name will
show up in URLs, so you may want to choose a concise name that your users
can remember and/or type when needed. For this paper, we�ll assume you
are want a DCD name of "myapp" and that the Oracle userid will
be mywebuser.
We�ll also assume that you want to go through the OWA cartridge
rather than CGI, that the administrative listener is running on its default
port (8888), and that you want to be able to connect to your application
via ports 80 and 8888. I�m including a lot of detail in the steps below
because this is one of the areas I got massively confused on. Now, thankfully,
you won�t have to end up like me.
Creating the DCD for the PL/SQL Agent
1. From a web browser, connect to http://myhost.mydomain:8888/.
You should see the Oracle WebServer starting page. If you don�t, restart
the administrative listener by logging in as oracle and executing the command
"wlctl21 start admin" (or substitute your version of wlctl).
2. Click on WebServer manager.
3. Click on PL/SQL Agent. (This address is http://myhost.mydomain:8888/ows-abin/agadmin)
4. Click on Create New DCD.
5. Enter the following:
DCD Name: myapp
PL/SQL Agent Database User: mywebuser
Identified by Password
ORACLE_HOME should already be filled in
ORACLE_SID: your SID
Authorized ports: 80, 8888
Log file directory: whatever, or use default
Check "Install WebServer Developer�s Toolkit PL/SQL
packages"
6. Click on "Submit New Service." Eventually �
and this should take a few minutes � you will see a Success or Failure
message. This takes a while because it is creating the built-in WebServer
packages for the given user, mywebuser. Even if it says success, check
the log file indicated in the message, and ensure that you didn�t get any
errors like "out of rollback segment space."
Configuring the Agent to use the WRB
7. Navigate back to the WebServer Manager page, http://myhost.mydomain:8888/ows-adoc/Intro.html
8. Click on Web Request Broker
9. Click on Modify
10. Scroll down to Applications and Directories
11. Add the following. This will provide the physical-to-logical
mapping you want. The "/owa" is optional in the virtual path,
but I like to put it there so that if you have another category of pages
you can create another virtual subdirectory, for example, /myapp/image.
Virtual Path
/myapp/owa
App.
OWA
Physical Path
/u01/app/oracle/product/7.3.2/ows2/bin (or your physical
path to this exact directory)
12. Click on Modify Web Configuration
13. Now you have to restart the listener. Go to the Listener
Administration page at http://myhost.mydomain:8888/ows-abin/wladmin.
14. Click on STOP
15. Click on START
That ought to do it. To confirm that everything worked, you
will want to write a quick PL/SQL program to display something from the
database. See below for instructions.
About the Predefined Oracle packages
In step 6 above, you caused a script to run which built a
number of packages needed by the PL/SQL agent. This script was probably
in the file $ORACLE_HOME/ows2/admin/owains.sql. If you have a number of
PL/SQL agents, you will probably want to install only a single set of these
packages and sharing access to them by granting execute privilege to them
and creating synonyms. I think this approach is described somewhere in
Oracle�s online documentation, but if not, just do a select object_name
from user_objects where object_type = 'PACKAGE' while logged on as
the first agent, and run the grants and synonyms against this list.
These packages are extended and improved from one release
of the WebServer to the next. I have had success in back-porting them from
2.1 to 2.0 but I doubt whether doing so is supported by Oracle.
The Typical URL
You are going to be setting up applications that will be
run by pointing a web browser to a specified URL. It�s important to understand
the Oracle WebServer-specific components of the URL that follow the slash
after the hostname. Look at an example:
This URL will cause the listener to look for a virtual mapping
of media/owa which includes the DCD name, media, and the virtual
path, owa, which could be associated with either the CGI or the cartridge
OWA implementation. Here, search is the PL/SQL package name, and
listall is a procedure. This URL will pass two arguments to the
procedure, searchfor, with a value of "blue meanies" (in
an URL, the + indicates a space), and show, with a value of 20.
While some web server configurations allow the user to browse
available files in a directory, there is no way that I know of to discover
the names of existing packages and procedures using variations on this
URL.
Your First Web Page: "Hello, Web!"
Via Static HTML files
Although not a horribly interesting feature, WebServer does
allow you to display static text via conventional ".html" text
files. As with other web servers such as Apache or Netscape, you have to
map the actual on-disk directory path to a "virtual" path that
will be part of the URL after the host name. To set up a path for a static
HTML file, from the WebServer home page, go to WebServer Manager, then
Oracle Web Listener, and click on Configure for the currently running listener.
Navigate your way down to the "Directory Mappings" section and
put in something like this.
File-system directory:
/u01/app/myapp/www/
Flag:
NR
Virtual Directory:
/myapp/pages/
Note, the file-system directory is the physical path to the directory that
will contain your static page.
Let's say your myhello.html file contains the following:
�then, assuming it�s running on port 80, you can point a
browser to it using: http://myhost.mydomain/myapp/pages/myhello.html
Via PL/SQL
To get WebServer to generate and display this page via PL/SQL,
you will create a simple PL/SQL program. It�s easiest to create this as
the Oracle user who is associated with the DCD. (It could be created as
another user who has privileges to execute the htp package, as long as
the DCD user has been granted execute privilege.)
First we�ll look at an "unpackaged" version of
this procedure which uses only one of the built-ins.
CREATE OR REPLACE PROCEDURE myhello AS BEGIN htp.print('<HTML> <HEAD> <TITLE>My First Page</TITLE> </HEAD> <BODY BGCOLOR="white"> <H1>Hello, Web!</H1> </BODY> </HTML>'); END;
To oversimplify a bit, "htp.print" procedure takes a single argument
and buffers it for transfer to the user's browser. (If you execute
this procedure from SQL*Plus, you will see nothing unless you use the owa_util.showpage
utility-more on that in the "Debugging and Testing" section below).
After you have created this procedure as the Oracle userid specified for
the DCD myapp, you should be able to access the procedure from a browser
using the URL http://myhost.mydomain/myapp/owa/myhello.
Oracle provides built-ins that will generate virtually all HTML tags; you
can use these an alternative to using a straight dump of the HTML.
Here is the "builtin-ified" version which will produce identical
output to the above:
CREATE OR REPLACE PROCEDURE myhello AS BEGIN htp.htmlOpen; htp.headOpen; htp.title('My First Page'); htp.headClose; htp.bodyOpen( cattributes => 'BGCOLOR="white"'); htp.header(1, 'Hello, Web!'); htp.bodyClose; htp.htmlClose; END;
I don�t want to try to document the built-ins in this paper;
most of the documentation you need is online: follow the link from your
installation�s WebServer home page to the "Online Docs," then
follow the link to "Application Development," then click on "PL/SQL
Web Toolkit Reference." Or just type in the URL: http://myhost.mydomain:port/ows-adoc/psqlwtlk.htm.
There are advantages and disadvantages to using the built-ins.
I personally don�t agree with Oracle�s assertion that using the built-ins
will have to save you from learning HTML. The reality is that you will
want some HTML expertise to know which built-ins to call, and with what
arguments. Also, while the built-ins might insulate your programs
from changes in the HTML standard as Oracle claims, I do not consider this
a huge benefit. The benefit I see from the built-ins is that they can
allow for more compact, readable code. It�s also easy to write your own
built-ins to meet this objective.
Consider the PL/SQL example above. You can see how you could
get tired of coding all the open/close calls for the typical page, so you
might want to come up with standard PL/SQL modules that print a typical
header and footer. While we�re at it, let�s go ahead and put it in a package:
CREATE OR REPLACE PACKAGE hutil AS PROCEDURE hOpen( p_title IN VARCHAR2 DEFAULT NULL
,p_bgcolor IN VARCHAR2 DEFAULT '"white"' ); PROCEDURE hClose; END;
CREATE OR REPLACE PACKAGE BODY hutil AS PROCEDURE hOpen( p_title in varchar2
,p_bgcolor in varchar2 ) IS BEGIN htp.htmlOpen; htp.headOpen; htp.title( p_title ); htp.headClose; htp.bodyOpen( cattributes=> 'BGCOLOR='
|| p_bgcolor ); END; PROCEDURE hClose IS BEGIN htp.bodyClose; htp.htmlClose; END; END;
Quick sanity check: to convert the Hello, Web program to use the package
above, it would read:
CREATE OR REPLACE PROCEDURE myhello AS BEGIN hutil.hOpen('My First Page'); htp.header(1, 'Hello, Web!'); hutil.hClose; END;
You can see how bundling these calls into packages can save
a lot of code-writing. Using this technique, you could easily put, say,
a standard footer with a navigation bar on all your pages.
As a usage note, there are two versions of most of the modules:
a procedure and a function. For example, there is an htp.bold procedure
and an htf.bold function which achieve the same effect; the difference
is that the function generates the HTML as its return value, whereas the
procedure sends it on through to the web browser.
Displaying Data from the Database
Printing a single value. It requires no great
leap of imagination to extend the PL/SQL above to display data from a table.
To display some simple scalar data such as the maximum salary from the
archetypal EMP table, for example, you could use the following program:
CREATE OR REPLACE PROCEDURE show_max_sal as maxSal scott.emp.sal%TYPE; cursor salCur IS SELECT MAX(sal) FROM scott.emp; noEmp exception; BEGIN hutil.hOpen( 'Salary Max'); OPEN salCur; FETCH salCur INTO maxSal; if salCur%NOTFOUND then raise noEmp; end if; CLOSE salCur; htp.print('The highest salary is: ' || maxSal); hutil.hClose; EXCEPTION WHEN noEmp THEN htp.print('Warning: There are no employees.'); hutil.hClose; END;
Printing a table. If you want to print an entire
table of employee data, there are at least two ways you can go about it.
One is to generate the HTML yourself in a PL/SQL loop. Keep in mind, as
you�re looking at this code, that the tableOpen, tableData, etc. procedures
are referring to HTML tables, not Oracle tables.
CREATE OR REPLACE PROCEDURE empPrint AS BEGIN hutil.hOpen('Employees'); htp.tableOpen('BORDER');
-- <TABLE BORDER> htp.tableRowOpen;
-- <TR> htp.tableHeader('Emp#');
-- <TH>Emp#</TH> htp.tableHeader('Name');
-- <TH>Name</TH> htp.tableHeader('Sal');
-- <TH>Sal</TH> htp.tableRowClose;
-- </TR> FOR theEmp IN (SELECT empno, ename, sal
FROM scott.emp ORDER BY empno) LOOP htp.tableRowOpen;
-- <TR> htp.tableData( theEmp.empno );
-- <TD>(employee # from table)</TD> htp.tableData( theEmp.ename );
-- ditto for ename htp.tableData( theEmp.sal );
-- ditto for sal htp.tableRowClose;
-- </TR> END LOOP; htp.tableClose;
-- </TABLE> hutil.hClose; END;
Another method for printing an Oracle table in a HTML table
is use the built-in owa_util.tableprint utility. Note: WebServer
authority Thomas Kyte, a frequent poster to comp.databases.oracle.* newsgroups,
does not recommend using tableprint, although I haven�t uncovered the details
of why. He does mention that it won�t work properly if you have a single
set of WebServer developer�s packages shared among the PL/SQL agents. Although
I have not used tableprint much, I can say that you lose some formatting
control with it, but it�s OK for quick-and-dirty prototypes.
Using a Dynamic Where-Clause to Filter
Data...
Let�s add the ability to display only one department at a
time. A convenient way of passing in a variable for use in the where clause
is via the URL. We will create a page that shows all departments� employees
if there is no department number specified in the URL, or only those employees
for a specified department. That is, http://myhost.mydomain/first/empPrint
will show everyone, and http://myhost.mydomain/first/empPrint?p_deptno=20
will show employees from department 20.
Critical note: WebServer passes PL/SQL arguments by
name, so the argument in the URL above, p_deptno, must exactly match the
name of formal argument to the procedure empPrint, shown below. In addition,
the formal argument to the procedure must be defaulted or you will get
an error if you fail to supply it in the URL. This is critical to understand.
Get in the habit early: default all parameters! Even if your routine is
meaningless without a particular set of arguments, you should write error-checking
code to handle missing parameters more gracefully than WebServer�s standard
error-handling.
...With Static SQL
CREATE OR REPLACE PROCEDURE empPrint ( p_deptno IN VARCHAR2 DEFAULT
'%' ) AS BEGIN hutil.hOpen('Employees'); htp.tableOpen('BORDER'); -- the table header stuff goes here� FOR theEmp IN (SELECT empno, ename, sal FROM scott.emp
WHERE deptno like p_deptno
ORDER BY empno ) LOOP htp.tableRowOpen; htp.tableData( theEmp.empno ); htp.tableData(theEmp.ename ); htp.tableData(theEmp.sal ); htp.tableRowClose; END LOOP; htp.tableClose; hutil.hClose; END;
...With Dynamic SQL
It would probably be better to dynamically build and execute the SQL Select
statement, which gets us more flexibility in the long run and also gets
us around the forced use of ugly LIKE '%' in the where clause. (This technique
is also useful if you are giving the user a query form and you need to
build the SQL Select statement on the fly based on those search criteria
for which the user supplies a value.)
CREATE OR REPLACE PROCEDURE empPrint ( p_deptno IN VARCHAR2 default
NULL) AS l_dcurs INTEGER;
-- cursor id for dynamic SQL l_theQuery VARCHAR2(512);
-- variable to hold the query l_undefined INTEGER;
-- execute will return undefined value l_rows_fetched INTEGER;
-- result of fetch_rows function l_empno NUMBER;
-- local variable to hold table data l_ename VARCHAR2(10); l_sal
NUMBER; BEGIN IF p_deptno IS NULL THEN hutil.hOpen('All Employees'); htp.header(1, 'All Employees'); ELSE hutil.hOpen('Employees in Department
' || p_deptno ); htp.header(1, 'Employees in Department
' || p_deptno); END IF; htp.tableOpen('BORDER'); -- the table header stuff goes here� IF p_deptno IS NULL THEN l_theQuery := 'select empno, ename,
sal from scott.emp order by empno'; ELSE l_theQuery := 'select empno, ename,
sal from scott.emp '
|| 'where deptno=' || p_deptno
|| ' order by empno'; END IF; -- These calls are standard fare for dynamic SQL.
For more info, -- look at $ORACLE_HOME/rdbms/admin/dbmssql.sql l_dcurs := dbms_sql.open_cursor; dbms_sql.parse( l_dcurs, l_theQuery, dbms_sql.v7); dbms_sql.define_column( l_dcurs, 1, l_empno ); dbms_sql.define_column( l_dcurs, 2, l_ename, 10 ); dbms_sql.define_column( l_dcurs, 3, l_sal ); l_undefined := dbms_sql.execute( l_dcurs ); l_rows_fetched := dbms_sql.fetch_rows ( l_dcurs ); WHILE l_rows_fetched != 0 LOOP dbms_sql.column_value( l_dcurs, 1, l_empno); dbms_sql.column_value( l_dcurs, 2, l_ename); dbms_sql.column_value( l_dcurs, 3, l_sal); htp.tableRowOpen; htp.tableData( l_empno ); htp.tableData( l_ename ); htp.tableData( l_sal ); htp.tableRowClose; l_rows_fetched := dbms_sql.fetch_rows
( l_dcurs ); END LOOP; dbms_sql.close_cursor ( l_dcurs ); htp.tableClose; hutil.hClose; END;
Adding Links
So far, this is pretty tame stuff for your average web surfer.
Even if you add some means of allowing the user to easily modify the query
with all kinds of variations in the where clause, this technique is useful
perhaps for web-based reports but not much else. Let�s say that we want
to add to our employee list the ability to click on an employee and bring
up a detail page for that person. We�ll need two additions: the detail
page, and the HTML anchor tag to call it. First let�s look at the needed
changes to the empPrint page. In the interest of conserving space, look
at only the changed code:
-- as above, but replace the line "htp.tableData(
l_empno );" -- with the following: htp.tableData( htf.anchor( 'empDetail?p_empno=' || l_empno,
l_empno ));
As an aside, there is an alternative to putting the links in the PL/SQL:
you can code the links into a database view (I saw this suggested by someone
on the ORAWEB-L mailing list; I wish I knew whom to credit for first suggesting
it). The basic approach would be something like this:
CREATE OR REPLACE VIEW emp_view AS SELECT '<A HREF="empDetail?p_empno=' || empno
|| '">' || empno || '</A>' empno ,empno empno_base,
ename, sal FROM emp;
Using the view approach would mean you wouldn't have to change the empPrint
module; it might also mean that in a different circumstance, where you
don't need dynamic SQL, you could use a view to get more mileage out of
the owa_util.tableprint utility.
For the detail page, we will put one field per line, and we want to accept
the argument from the empPrint page. You can, of course, get a lot
fancier than this, using HTML tables for alignment and formatting.
CREATE OR REPLACE PROCEDURE empDetail ( p_empno IN VARCHAR2 DEFAULT
null ) IS CURSOR empCur IS SELECT * FROM scott.emp WHERE empno =
p_empno; empRec empCur%ROWTYPE; no_args EXCEPTION; no_emp EXCEPTION; BEGIN hUtil.hOpen('Employee Detail for ' || p_empno); IF p_empno IS NULL then RAISE no_args; END IF; OPEN empCur; FETCH empCur INTO empRec; IF empCur%NOTFOUND THEN close empCur; raise no_emp; END IF; CLOSE empCur; htp.header(1, 'Employee ' || p_empNo ); htp.para; htp.print( htf.bold( 'Name:' ) || empRec.ename || htf.br); htp.print( htf.bold( 'Dept:' ) || empRec.deptno || htf.br); --...etc. for the rest of the columns hUtil.hClose; EXCEPTION WHEN no_args THEN htp.print('Error: empDetail called with
no arguments.'); hUtil.hClose; WHEN no_emp THEN htp.print('Error: Detail requested for
unknown employee number.'); hUtil.hClose; END;
HTML Forms
A Simple HTML Form to Search the Database
The basic idea of using an HTML form is that the values supplied
by the user will get passed as parameters to the module specified in the
ACTION attribute of the FORM tag. POST is the default method and is recommended
for long argument lists.
If you generate HTML that looks like this:
<FORM ACTION="searchEmp" METHOD="POST"> Please enter the first few letters of the person's name: <INPUT TYPE="TEXT" NAME="P_SEARCH"
SIZE="20"> <INPUT TYPE="SUBMIT"> </FORM>
... then when the user clicks on the Submit button, text entered in the
p_search field will get passed as the p_search parameter to the PL/SQL
module named searchEmp. This presumes that the current page was generated
by the same agent that can execute searchEmp, since it uses a relative
URL.
The PL/SQL fragment to generate the form (skipping the standard header
and footer):
htp.formOpen('searchEmp'); htp.print('Please enter the first few letters of the person''s
name:'); htp.formText('p_ename',20); htp.formSubmit; htp.formClose;
The searchEmp module might look like this:
CREATE OR REPLACE PROCEDURE searchEmp ( p_ename IN VARCHAR2 DEFAULT
NULL ) IS l_empno scott.emp.empno%TYPE; l_rows_unshown boolean; BEGIN -- handling a possibly null input parameter is important
but is not -- shown here (see earlier code for technique) -- if only one matches, show the detail page, otherwise
show a list BEGIN -- normally don't like implicit cursors
but makes sense here because -- we want to check for multiple records
matching criteria SELECT empno INTO l_empno FROM scott.emp WHERE ename
LIKE upper(p_ename) || '%'; empDetail ( l_empno ); EXCEPTION WHEN too_many_rows THEN hutil.hOpen('Employees
Matching ' || p_ename); -- because this tablePrint
is based on the view defined earlier, -- it provides a clickable
means of clicking and drilling down -- to the employee
details l_rows_unshown :=
owa_util.tablePrint( 'scott.emp_view'
,ccolumns => 'ename, empno, sal'
,cclauses => 'where ename like upper('''
|| p_ename || '%'')' ); hutil.hClose; WHEN no_data_found THEN null; -- handle appropriately END; END;
A few things to know about passing parameters. First, all
HTML form variables get passed as varchar2. Don�t try to use anything else
in you PL/SQL formal argument lists, or you�ll get an error. Second, in
the event that the HTML form contains multiple objects with the same name
but different values (for example, a series of checkboxes, the total number
of which cannot be pre-determined), the parameter will be passed to the
module specified in the ACTION attribute as a PL/SQL table of type owa_util.ident_arr.
I wanted to give an example of this feature, which turns out to be enormously
useful, but space and time constraints require me to merely point you to
an example in Oracle�s online documentation: http://myhost.mydomain:port/ows-adoc/plaex5.htm.
Drop-Down Lists
One of the features you will very quickly want to add to
your fill-out forms is the ability to populate a drop-down list from data
in a table. This is a definite area where using the built-in utility owa_util.listprint
will save time & effort. Let�s say you want to generate a list of departments
from which the user may choose a name, and that the processing module expects
the department number as the value of the field. We want to default the
drop-down to department 20, and also want a null/blank option.
owa_util.listprint( 'select deptno, dname,'
|| ' decode(deptno,20,''SELECTED'',null)
from scott.dept'
|| ' union select to_number(null), '' '', null from dual order by 2'
,'P_DEPTNO',1,FALSE);
...which, although kind of ugly, generates the eminently useful HTML:
It�s not all that big a leap, really. You do have to keep
in mind that there is no concept of a transaction in WebServer1.0, 2.0,
or 2.1. Every database change is automatically committed unless you roll
it back in the same "call" that made the change. Neither package
nor transaction state is not preserved between page loads.
In the application that I helped build, which involved on-line
ordering, we simply invented a concept of "order approval." The
users can browse items for order at their leisure, make interim selections,
modify and remove order selections to their hearts� content. They can even
shut down their browser and return the next day to resume putting items
into their order. All the changes are committed to the database as soon
as they make them, but until they press the "Process order" button,
the order is not approved by the user, and it sits in a sort of pending
queue. (Oracle�s white paper on transactions in WebServer 3.0, which also
describes the shopping cart problem, interestingly does not acknowledge
that a user might want to save their interim order and continue working
on it later.)
When I started developing read/write forms, my first attempt
treated creation of records as something separate from query. Another way
to look at it, similar to the Oracle Forms 2.x - 4.x model, is to use the
same screen for insert and for query. My final code employed the latter
model, showing a screen with a number of fields and two buttons: Create
and Query. The user can put in a new record or query an existing one. Pseudo
code might look like this:
Show details screen with blank fields and two buttons: Create and Query User fills out one or more fields If user presses query, then pass the field values to the show details page Check in table to see if there is only one record;
if so, then display that record's data
in the details screen ...Otherwise there are multiple records, so display the records in a
clickable list If the user clicks on one,
send the value of the primary key as a parameter to the details page ...Otherwise the user has pressed Create, so Call the module that validates and insert the record
with the data provided by the user Display a confirmation page with two links: retrieve
the record, or enter a new query If the user clicks to retrieve the newly-created
record, then Bring up the details page,
which has a Save button and a Delete button. If the user presses the
Save button, validate and apply the database changes; ...Otherwise the user pressed
delete, so delete the record and display a confirmation page ...Otherwise the user wants a new query, so return
to the top (Show entry/query screen) I�m not entirely satisfied with this approach because it�s
a bit screen-happy. A more sedate user interface (my next project) might
employ frames; for example, a query that retrieved multiple rows would
show the list in one frame, and clicking on one would call up details in
another frame. Anyway, give some thought to how you want your interface
to work, and experiment until you are happy.
I�ve included some modified code fragments as an appendix.
These started their life as output from the WebServer generator in Designer/2000
version 1.2, which produces read-only WebServer pages.
Overcoming Statelessness
It won�t be long before you want to give your users the ability
to "log on" and give different users different privileges. As
has been widely discussed in the press and on the web, there is very little
intrinsic ability of web applications to preserve the state of a given
user session. While it is possible to set up WebServer-authenticated accounts,
we chose in our application to invent an application-enforced account concept,
because of the desire by the customer to allow anyone to set up their own
account. Somewhat simplified, we created a users table and a privileges
table, and in the opening screen, the user is required to put in a username
and password which the application validates against the users table.
But that is nowhere near sufficient. Once "inside"
the application, a user could merely bookmark a page and subvert such a
simpleminded scheme: in other words, the next day they could jump into
a screen behind the login screen. We adopted a rule that virtually every
PL/SQL modulemust perform user validation. While this sounds
painful and slow, it turns out that there are techniques such as client-side
"cookies" to facilitate this approach.
By the way, don�t think you can just store the user�s authentication
status in a package variable. Every time the user invokes a new PL/SQL-generated
page, the previous package state is unavailable; it is discarded immediately
after use.
Browser "cookies" are not much more than client-side
variables that will persist until a programmer-defined expiration date,
invented specifically to help overcome the basic problem of HTTP statelessness.
By default, they exist for the current "session," that is, until
the user shuts down their browser (or until the browser or the operating
system crashes), but they can be set for an infinite expiration date if
desired. Cookies are stored in a plain ASCII text file under Windows NT
and Windows 95, which I discovered cannot be edited by hand between sessions,
because it will then get completely erased when Netscape starts again later.
But all the values that you save with a non-session expiration date can
be read by anyone with access to the machine, so unencrypted passwords
are out of the question.
First you look at this module that either reads or creates
a cookie that will hold a "session id." This session id will
be stored in a table that correlates the id to a user. Although these modules
are somewhat abbreviated from their production versions, the basic idea
is the same: a temporary session id gets associated with a given user.
It would be better to randomly generate the session id rather than using
an Oracle sequence, but I didn�t have time to come up with a PL/SQL random
number generator.
CREATE OR REPLACE FUNCTION get_session_id RETURN NUMBER
IS l_session_id number; l_session_id_cookie owa_cookie.cookie; BEGIN l_session_id_cookie := owa_cookie.get('SESSION_ID'); owa_util.mime_header('text/html', FALSE); IF l_session_id_cookie.num_vals >
0 THEN owa_util.http_header_close; return l_session_id_cookie.vals(1); ELSE select session_seq.nextval
into l_session_id from dual; owa_cookie.send('SESSION_ID',
to_char(l_session_id)); owa_util.http_header_close; RETURN l_session_id; END IF; END;
Note that this must be the first function called for a given
web page, because the cookie information can only be sent and received
based on tags in the header. To see the value of the cookie you could write
a simple calling module. Note the placement of the hutil.hOpen.
CREATE OR REPLACE PROCEDURE
show_cookie AS l_value number; BEGIN l_value := get_session_id; hutil.hOpen('Cookie Demo'); htp.print('The value is: ' || l_value); hutil.hclose; END;
Here is some code that will accept a name and password and
then call a password validation module. This code merely creates
a form that is formatted within an HTML table.
CREATE OR REPLACE PROCEDURE login IS BEGIN hutil.hOpen('Login'); htp.header(1,'Please Enter Your Login
Name and Password'); htp.formOpen('validate_login'); htp.tableOpen; htp.tableRowOpen; htp.tableData(htf.bold('Login
Name:'),'right'); htp.tableData(htf.formText('p_name',18,18)); htp.tableRowClose; htp.tableRowOpen; htp.tableData(htf.bold('Password:'),'right'); htp.tableData(htf.formPassword('p_pw',18,18)); htp.tableRowClose; htp.tableRowOpen; htp.tableData; htp.tableData(htf.para
|| htf.formSubmit(null,'Login')); htp.tableRowClose; htp.tableClose; hutil.hClose; END;
The password validation module appears below: This
module uses the get_session_id function described above, and assumes that
the list of users exists in a table, and that there is a table that correlates
user ids to session ids. The tables might look like this:
SQL> desc t_users Name
Null? Type ------------------------------- -------- ---- INDIVIDUAL_ID
NOT NULL NUMBER WEB_USERNAME
NOT Null VARCHAR2(18) WEB_PASSWORD
NOT NULL VARCHAR2(18) SURNAME
VARCHAR2(35) GIVEN_NAME
VARCHAR2(25) MIDDLE_INITIAL
VARCHAR2(1) SQL> desc t_sessions Name
Null? Type ------------------------------- -------- ---- INDIVIDUAL_ID
NOT NULL NUMBER SESSION_ID
NOT NULL NUMBER CREATE OR REPLACE PROCEDURE validate_login( p_name in varchar2
,p_pw IN VARCHAR2) IS l_individual_id NUMBER; l_session_id NUMBER; l_web_password t_users.web_password%TYPE; CURSOR indCur IS SELECT individual_id,
web_password FROM t_users
WHERE web_username = UPPER(p_name); BEGIN OPEN indCur; FETCH indCur INTO
l_individual_id, l_web_password; IF indCur%FOUND THEN
CLOSE indCur;
IF l_web_password = upper(p_pw) THEN
l_session_id := get_session_id;
-- next call deletes any old session records
-- and inserts a record for the individual
t_user.create_user_session( l_individual_id, l_session_id );
-- this procedure allows the user into the opening page
home( l_individual_id );
ELSE
login;
RETURN;
END IF; ELSE
CLOSE indCur;
login;
RETURN; END IF; END;
Your modules will each begin with a call to of the form l_individual_id
:= get_individual_id, the latter of which is a function that does a quick
table lookup to see if the user's session id, as returned by the cookie,
is valid.
FUNCTION get_individual_id RETURN NUMBER IS l_session_id NUMBER; l_individual_id NUMBER; BEGIN l_session_id := get_session_id; DECLARE CURSOR indCur IS SELECT individual_id
FROM t_sessions
WHERE session_id = l_session_id; BEGIN OPEN indCur; FETCH indCur INTO l_individual_id; IF indCur%NOTFOUND THEN CLOSE indCur; LOGIN; ELSE CLOSE indCur; END IF; END; RETURN l_individual_id; END;
I should mention an alternative to cookies: passing the username/password
in the URL or in "hidden" form fields, is even less desirable
because of their lack of security. This approach is (I think) what is demonstrated
in the WebServer 1.0 "Take the Train" demo code, which is unfortunate
because people may reuse it thinking it is safe, but on the other hand
I find the code a bit hard to follow, so hopefully people will do their
own thinking before using it!
Debugging and Testing
One of the first error screens you will encounter will say:
"Request Failed. We were unable to process your request at this time.
Please try again later." This typically appears as a result of an
invalid PL/SQL procedure call. When you see this message, you�ll want to
look in the error log file, which by default will probably be $ORACLE_HOME/ows2/log/myapp.err,
where myapp is the DCD name. Write a simple script that tails that file,
for example:
And run this immediately after you get Request Failed. There�s
nothing magic about the choice of the last 40 lines, by the way. (Why Oracle
couldn�t send this error message out through the HTTP server so it shows
up in the browser is beyond me. It would make life on developers way easier...but
is "easy development" an oxymoron?)
Another gray screen you�ll get familiar with says, "The
requested URL was not found." This is an indication that either you
have mistyped the URL or there is a setup or configuration error. Unfortunately
it won�t put anything useful in the log files; solving the problem may
require some head-scratching.
A tool you will find useful during debugging is owa_util.showpage.
This allows you to see the HTML generated by a PL/SQL package without using
a web browser. Write a simple sp.sql script that does this:
rem sp.sql: show queued WebServer output rem next line is unnecessary if serveroutput already on set serveroutput on size 100000 execute owa_util.showpage
In addition to the online documentation and the well-known
Oracle newsgroups, I suggest having a look at the downloadable utilities
at http://govt.us.oracle.com, which is where WebServer maven Thomas Kyte
points on his newsgroup postings. You might want to subscribe to the ORAWEB-L
mailing list; send a message to [email protected] with
the following command in the body of the message: SUBSCRIBE oraweb-l
MY_EMAIL_ADDRESS. You can also visit http://www.datacraft.com, where
I intend to post more useful code, and where I also have some LiveWire
samples of web/database integration for the PL/SQL-impaired.
Appendix: Code Fragments for Read/Write
Screen
This code is briefly described above in the section above
on "The Big Leap." It began as Designer/2000 output, then was
modified for insert/update/delete. For this paper I started with the production
code and made some changes such as global-search-and-replace of the actual
table/column names; unfortunately I did not have a chance to test it thoroughly
before going to press. There are a couple of calls to some utility routines
that are either described elsewhere in the text, or are commented below.
CREATE OR REPLACE PACKAGE ADMDEPT IS -- FormEnter is a module that serves multiple purposes: -- 1) query screen; 2) Create-new screen; 3) update -- The available buttons vary based on the mode. PROCEDURE FormEnter ( p_deptno IN NUMBER DEFAULT NULL
,p_skip_header IN VARCHAR2 DEFAULT NULL); -- "process" takes the arguments passed from
the FormEnter -- screen and either displays search results or applies
the -- DML requested by the user PROCEDURE process(
P_DEPTNO IN VARCHAR2 DEFAULT NULL,
P_DNAME IN VARCHAR2 DEFAULT NULL,
Z_START IN VARCHAR2 DEFAULT NULL,
Z_ACTION IN VARCHAR2 DEFAULT NULL); -- ShowError is the standard Des2K-generated module PROCEDURE ShowError(
P_ERRNO IN VARCHAR2,
P_ERRM IN VARCHAR2,
P_CONTEXT IN VARCHAR2 DEFAULT NULL,
P_ACTION IN VARCHAR2 DEFAULT NULL); end; / -------------------------------------------------------------------------------- CREATE OR REPLACE PACKAGE BODY ADMDEPT IS -- these are the button captions to which multiple modules
may -- refer. Out here in global-land so they can be
changed easily. QUERY_BUT_CAPTION constant varchar2(100) :=
'Query'; SAVE_BUT_CAPTION constant varchar2(100)
:= 'Save'; CREATE_BUT_CAPTION constant varchar2(100) := 'Create'; NEW_BUT_CAPTION constant varchar2(100)
:= 'New Search'; DELETE_BUT_CAPTION constant varchar2(100) := 'Delete
Dept'; NEXT_BUT_CAPTION constant varchar2(100)
:= 'Next'; PREV_BUT_CAPTION constant varchar2(100)
:= 'Previous'; RECORD_SET_SIZE constant number(4)
:= 10; PROCEDURE FormEnter( p_deptno IN NUMBER, p_skip_header
IN VARCHAR2) is -- p_deptno: department number to display, if any -- p_skip_header: indicates whether we need to skip printing
of page header l_individual_id NUMBER; skipit EXCEPTION; CURSOR dptCur is SELECT deptno, dname
FROM dept WHERE deptno = p_deptno; dptRec dptCur%ROWTYPE; BEGIN -- always begin by validating the user l_individual_id := t_util.get_individual_id; IF l_individual_id IS NULL THEN RAISE skipit; END IF; IF p_skip_header IS NULL THEN IF p_deptno IS NOT
NULL THEN
hutil.hOpen('Modify Department');
htp.header(1,'Modify Department'); ELSE
hutil.hOpen;
htp.header(1,'Search or Create Department'); END IF; END IF; htp.formOpen('ADMDEPT.process'); -- these WSGL routines are supplied
with Designer/2000. They are -- not magic, just shortcuts to getting
tables to lay out -- according to some rules. WSGL.LayoutOpen(WSGL.LAYOUT_TABLE); WSGL.LayoutRowStart; WSGL.LayoutHeader(5, 'LEFT', NULL); WSGL.LayoutHeader(40, 'LEFT', NULL); WSGL.LayoutRowEnd; -- if the module has been called with
a deptno argument, we -- can assume they want to modify or
delete it IF p_deptno IS NOT NULL THEN OPEN dptCur; FETCH dptCur INTO
dptRec; CLOSE dptCur; WSGL.LayoutRowStart('TOP'); WSGL.LayoutData(htf.bold('ID:')); WSGL.LayoutData(htf.bold(dptRec.deptno)); WSGL.LayoutRowEnd; END IF; WSGL.LayoutRowStart('TOP'); WSGL.LayoutData(htf.bold('Department
Name:')); -- note that we can always refer to
dptRec.dname; if p_deptno -- is null, dptRec.dname will be null WSGL.LayoutData(htf.formText('P_DNAME',
'40', '40', dptRec.dname)); WSGL.LayoutRowEnd; WSGL.LayoutClose; IF p_deptno IS NULL THEN htp.formSubmit('Z_ACTION',htf.escape_sc(QUERY_BUT_CAPTION)); htp.formSubmit('Z_ACTION',htf.escape_sc(CREATE_BUT_CAPTION)); ELSE htp.formHidden('P_DEPTNO',p_deptno); htp.formSubmit('Z_ACTION',htf.escape_sc(SAVE_BUT_CAPTION)); htp.para; htp.formSubmit('Z_ACTION',htf.escape_sc(DELETE_BUT_CAPTION)); htp.formSubmit('Z_ACTION',htf.escape_sc(NEW_BUT_CAPTION)); END IF; htp.formClose; hutil.hClose; EXCEPTION WHEN skipit THEN NULL; -- failed
validation WHEN OTHERS THEN ShowError(SQLCODE,
SQLERRM, 'ADMDEPT.FormEnter:'); hutil.hClose; END; -------------------------------------------------------------------------------- -- P_DEPTNO - Department number -- P_DNAME - Department Name -- Z_START - First record to display -- Z_ACTION - what to do? this will match the strings in the
button captions PROCEDURE process(
P_DEPTNO IN VARCHAR2,
P_DNAME IN VARCHAR2,
Z_START IN VARCHAR2,
Z_ACTION IN VARCHAR2) IS L_DEPTNO
DEPT.DEPTNO%TYPE; L_DNAME
DEPT.DNAME%TYPE; l_individual_id NUMBER; skipit EXCEPTION; done EXCEPTION; BEGIN l_individual_id := t_util.get_individual_id; IF l_individual_id IS NULL THEN RAISE skipit; END IF; ------------- UPDATE
------------- IF z_action = SAVE_BUT_CAPTION then hutil.hOpen('Result
of Department Modification'); htp.header(1,'Result
of Department Modification'); -- update_dept is
just a routine that issues the update statement t_dept.update_dept(
p_deptno, p_dname ); -- if no exception
raised, we succeeded htp.p('Department
' || htf.anchor( 'ADMDEPT.formenter?p_deptno=' || p_deptno
,p_dname ) || ' has been modified.'); htp.formOpen('ADMDEPT.process'); htp.formSubmit('Z_ACTION',htf.escape_sc(NEW_BUT_CAPTION)); htp.formClose; hutil.hClose; ------------- INSERT
------------- ELSIF z_action = CREATE_BUT_CAPTION
THEN hutil.hOpen('Result
of Department Creation'); htp.header(1,'Result
of Department Creation'); -- call a routine
that creates the department l_deptno := t_dept.create_dept(
p_dname ); -- if no exception
raised, we succeeded htp.p('Department
'
|| htf.anchor( 'ADMDEPT.formenter?p_deptno=' || l_deptno
,p_dname ) || ' has been created.'); htp.formOpen('ADMDEPT.process'); htp.formSubmit('Z_ACTION',htf.escape_sc(NEW_BUT_CAPTION)); htp.formClose; ------------- NEW QUERY/CREATE
------------- ELSIF z_action = NEW_BUT_CAPTION
THEN formenter; ------------- DELETE
------------- ELSIF z_action = DELETE_BUT_CAPTION
THEN hutil.hOpen('Result
of Department Deletion'); htp.header(1,'Result
of Department Deletion'); l_dname := t_dept.delete_dept(p_deptno); htp.p('Department
' || l_dname || ' has been deleted.'); htp.formOpen('ADMDEPT.process'); htp.formSubmit('Z_ACTION',htf.escape_sc(NEW_BUT_CAPTION)); htp.formClose; hutil.hClose; ------------- QUERY
------------- else -- QUERY_BUT_CAPTION -- this section of
the code omitted for space -- reasons.
The code builds a where-clause dynamically -- and displays the
matching data. -- Visit http://www.datacraft.com
for more details. NULL; END IF; EXCEPTION WHEN skipit OR done THEN NULL; WHEN OTHERS THEN ShowError(SQLCODE,
SQLERRM, 'ADMDEPT.process:'); htuil.hClose; END; -------------------------------------------------------------------------------- -- P_ERRNO - The error number -- P_ERRM - The error message -- P_CONTEXT - The context of the error -- P_ACTION - The action to take (if known) PROCEDURE ShowError(
P_ERRNO IN VARCHAR2,
P_ERRM IN VARCHAR2,
P_CONTEXT IN VARCHAR2,
P_ACTION IN VARCHAR2) IS BEGIN hutil.hOpen('Error'); htp.para; IF P_CONTEXT IS NOT NULL THEN htp.p(P_CONTEXT); htp.para; END IF; htp.p(P_ERRM); IF P_ACTION IS NOT NULL THEN htp.para; htp.p(P_ACTION); END IF; hutil.hClose; END; END;