Chapter 21. I/O and PL/SQL

Many, perhaps most, of the PL/SQL programs we write need to interact only with the underlying Oracle RDBMS using SQL. However, there will inevitably be times when you will want to send information from PL/SQL to the external environment, or read information from some external source (screen, file, etc.) into PL/SQL. This chapter explores some of the most common mechanisms for I/O in PL/SQL, including the following built-in packages:

DBMS_OUTPUT

For displaying information on the screen

UTL_FILE

For reading and writing operating system files

UTL_MAIL and UTL_SMTP

For sending email from within PL/SQL

UTL_HTTP

For retrieving data from a web page

It is outside the scope of this book to provide full reference information about the built-in packages introduced in this chapter. Instead, in this chapter, we’ll demonstrate how to use them to handle the most frequently encountered requirements. Check out Oracle’s documentation for more complete coverage. You will also find Oracle Built-in Packages (O’Reilly) a helpful source for information on some of the older packages; we have put several chapters from that book on this book’s web site.

Displaying Information

Oracle provides the DBMS_OUTPUT package to give us a way to send information from our programs to a buffer. This buffer can then be read and manipulated by another PL/SQL program or by the host environment. DBMS_OUTPUT is most frequently used as a simple mechanism for displaying information on your screen.

Each user session has a DBMS_OUTPUT buffer of up to 1,000,000 bytes in size (in Oracle Database 10g and above, you can set the buffer size to UNLIMITED). You write information to this buffer by calling the DBMS_OUTPUT.PUT and DBMS_OUTPUT.PUT_LINE programs. If you are using DBMS_OUTPUT from within SQL*Plus, this information will be displayed automatically when the outermost PL/SQL block terminates. You can (optionally) explicitly retrieve information from the buffer with calls to DBMS_OUTPUT.GET and DBMS_OUTPUT.GET_LINE.

Enabling DBMS_OUTPUT

If DBMS_OUTPUT is disabled (default setting), then calls to the PUT_LINE and PUT programs are ignored; the buffer remains empty. To enable DBMS_OUTPUT, you will generally execute a command in the host environment. For example, in SQL*Plus, you will issue this command:

SET SERVEROUTPUT ON

Oracle offers a variety of options for the SERVEROUTPUT command; you should check the documentation for the latest features. Here are some things you can do (you can combine the different options into a single command):

  • Set the buffer size to the maximum allowed prior to Oracle Database 10g Release 2:

    SET SERVEROUTPUT ON SIZE 1000000
  • Set the buffer size to “unlimited” (Oracle Database 10g Release 2 only):

    SET SERVEROUTPUT ON

    In other words, the default setting in Oracle Database 10g Release 2 is an unlimited buffer size.

  • Specify that you want the text displayed by DBMS_OUTPUT wrapped at the SQL*Plus line length. The wrapping occurs regardless of word separation. This will also stop SQL*Plus from stripping leading blanks from your text.

    SET SERVEROUTPUT ON FORMAT WRAPPED
  • Specify that you want the text displayed by DBMS_OUTPUT wrapped at the SQL*Plus line length. This version respects integrity of “words.” As a result, lines will be broken in a way that keeps separate tokens intact.

    SET SERVEROUTPUT ON FORMAT WORD_WRAPPED
  • Specify that you want the text displayed by DBMS_OUTPUT to be truncated at the SQL*Plus line length; the rest of the text will not be displayed.

    SET SERVEROUTPUT ON FORMAT TRUNCATED
  • Turn off output in SQL*Plus:

    SET SERVEROUTPUT OFF

Third-party products such as Toad and PL/SQL Developer offer their own graphical interfaces to enabling and disabling DBMS_OUTPUT.

Write Lines to a Buffer

Call DBMS_OUTPUT.PUT_LINE or DBMS_OUTPUT.PUT to put information into the buffer. PUT_LINE adds a newline marker after its text. PUT places text in the buffer without a newline marker. When using DBMS_OUTPUT.PUT, you should make a call to DBMS_OUTPUT.NEW_LINE to append a newline marker to the content that you previously “put.”

If your data can be implicitly converted to a VARCHAR2 string, then you can pass it in your call to the PUT and PUT_LINE programs. Here are some examples:

BEGIN
   DBMS_OUTPUT.put_line ('Steven'),
   DBMS_OUTPUT.put_line (100);
   DBMS_OUTPUT.put_line (SYSDATE);
END;
/

Unfortunately, DBMS_OUTPUT does not yet know what to do with a variety of common PL/SQL types, most notably Booleans. You may therefore want to consider writing a small utility to make it easier to display Boolean values, such as the following procedure, which displays a string and then the Boolean:

/* File on web: plsb.sp */
CREATE OR REPLACE PROCEDURE plsb (str IN VARCHAR2, bool IN BOOLEAN)
IS
BEGIN
   IF bool
   THEN
      DBMS_OUTPUT.PUT_LINE (str || ' - TRUE'),
   ELSIF NOT bool
   THEN
      DBMS_OUTPUT.PUT_LINE (str || ' - FALSE'),
   ELSE
      DBMS_OUTPUT.PUT_LINE (str || ' - NULL'),
   END IF;
END plsb;
/

In Oracle Database 10g Release 2, the maximum size string that you can pass to DBMS_OUTPUT.PUT_LINE has been raised to 32K from the previous limit of 255.

If you pass a value larger than the maximum allowed, Oracle will raise an exception (either VALUE_ERROR or ORU-10028: line length overflow, limit of NNN chars per line). This is not much of a concern with a 32K limit, but prior to Oracle Database 10g Release 2, many of us encountered situations where we needed to display longer strings.

If you would like to avoid this problem, you might want to use an encapsulation of DBMS_OUTPUT.PUT_LINE that automatically wraps long strings. The following files, available on the book’s web site, offer variations on this theme.

pl.sp

This standalone procedure allows you to specify the length at which your string will be wrapped.

p.pks/pkb

The p package is a comprehensive encapsulation of DBMS_OUTPUT.PUT_LINE that offers many different overloadings (for example, you can display an XML document or an operating-system file by calling the p.l procedure) and also wraps long lines of text.

Read the Contents of a Buffer

The typical usage of DBMS_OUTPUT is very basic: you call DBMS_OUTPUT.PUT_LINE and view the results on the screen. Behind the scenes, the SQL*Plus host environment calls the appropriate programs in the DBMS_OUTPUT package to extract the contents of the buffer and then display it.

If you need to obtain the contents of the DBMS_OUTPUT buffer, you can call the GET_LINE and GET_LINES procedures.

The GET_LINE procedure retrieves one line of information from the buffer (up to the maximum supported by DBMS_OUTPUT), and returns a status value of 0 if successful. Here’s an example that uses this program to extract the next line from the buffer into a local PL/SQL variable:

FUNCTION get_next_line RETURN VARCHAR2
IS
   return_value VARCHAR2(255);
   get_status INTEGER;
BEGIN
   DBMS_OUTPUT.GET_LINE (return_value, get_status);
   IF get_status = 0
   THEN
      RETURN return_value;
   ELSE
      RETURN NULL;
   END IF;
END;

The GET_LINES procedure retrieves multiple lines from the buffer with one call. It reads the buffer into a PL/SQL collection of strings (maximum length 255 or 32K, depending on your version of Oracle). You specify the number of lines you want to read, and it returns those. Here is a generic program that transfers the contents of the DBMS_OUTPUT buffer into a database log table:

/* File on web: move_buffer_to_log.sp */
CREATE OR REPLACE PROCEDURE move_buffer_to_log
IS
   l_buffer      DBMS_OUTPUT.chararr;
   l_num_lines   PLS_INTEGER;
BEGIN
   LOOP
      l_num_lines := 100;
      DBMS_OUTPUT.get_lines (l_buffer, l_num_lines);

      EXIT WHEN l_buffer.COUNT = 0;

      FORALL indx IN l_buffer.FIRST .. l_buffer.LAST
         INSERT INTO logtab
                     (text
                     )
              VALUES (l_buffer (indx)
                     );
   END LOOP;
END;
/

Reading and Writing Files

The UTL_FILE package allows PL/SQL programs to both read from and write to any operating-system files that are accessible from the server on which your database instance is running. You can load data from files directly into database tables while applying the full power and flexibility of PL/SQL programming. You can generate reports directly from within PL/SQL without worrying about the maximum buffer restrictions of DBMS_OUTPUT that existed prior to Oracle Database 10g Release 2.

UTL_FILE lets you read and write files accessible from the server on which your database is running. So you could theoretically use UTL_FILE to write right over your tablespace datafiles, control files, and so on. That is of course a very bad idea. Server security requires the ability to place restrictions on where you can read and write your files. UTL_FILE implements this security by limiting access to files in one of two ways:

  • It only reads/writes files in directories that are specified by the UTL_FILE_DIR parameter in the database initialization file.

  • With Oracle Database 9i Release 2 and above, UTL_FILE reads/writes files in locations specified by database Directory objects.

The next two sections explain how to use these two approaches; we will then examine the specific capabilities of the UTL_FILE package . Many of the UTL_FILE programs are demonstrated in a handy encapsulation package found in the fileio.pkg file on the book’s web site.

The UTL_FILE_DIR Parameter

When you call FOPEN to open a file, you must specify both the location and the name of the file in separate arguments. This file location is then checked against the list of accessible directories.

Here’s the format of the parameter for file access in the database initialization file:

UTL_FILE_DIR =directory

Include a parameter for UTL_FILE_DIR for each directory you want to make accessible for UTL_FILE operations. The following entries, for example, enable four different directories in Unix:

UTL_FILE_DIR = /tmp
UTL_FILE_DIR = /ora_apps/hr/time_reporting
UTL_FILE_DIR = /ora_apps/hr/time_reporting/log
UTL_FILE_DIR = /users/test_area

To bypass server security and allow read/write access to all directories, you can use this special syntax:

UTL_FILE_DIR = *

You should not use this option on production systems. In a development system, this entry certainly makes it easier for developers to get up and running on UTL_FILE and test their code. However, you should allow access to only a few specific directories when you move the application to production.

Setting up directories

Here are some observations on working with and setting up accessible directories with UTL_FILE:

  • Access is not recursive through subdirectories. Suppose that the following lines were in your database initialization file, for example:

    UTL_FILE_DIR = c:groupdev1
    UTL_FILE_DIR = c:groupprodoe
    UTL_FILE_DIR = c:groupprodar

    You would not be able to open a file in the c:groupprodoe eports subdirectory.

  • Do not include the following entry in Unix systems:

    UTL_FILE_DIR = .

    This allows you to read/write on the current directory in the operating system.

  • Do not enclose the directory names within single or double quotes.

  • In the Unix environment, a file created by FOPEN has as its owner the shadow process running the Oracle instance. This is usually the “oracle” owner. If you try to access these files outside of UTL_FILE, you will need the correct privileges (or be logged in as “oracle”) to access or change these files.

  • You should not end your directory name with a delimiter, such as the forward slash in Unix. The following specification of a directory will result in problems when trying to read from or write to the directory:

    UTL_FILE_DIR = /tmp/orafiles/

Specifying file locations

The location of the file is an operating system-specific string that specifies the directory or area in which to open the file. The location you provide must have been listed as an accessible directory in the INIT.ORA file for the database instance.

The INIT.ORA location is a valid directory or area specification, as shown in these examples:

In Windows
'k:commondebug'
In Unix
'/usr/od2000/admin'

Notice that in Windows, the backslash character () is used as a delimiter. In Unix, the forward slash (/) is the delimiter. When you pass the location in the call to UTL_FILE.FOPEN, you provide the location specification as it appears in the INIT.ORA file (unless you just provided * for all directories in the initialization file). And remember that in case-sensitive operating systems, the case of the location specification in the initialization file must match that used in the call to UTL_ FILE.FOPEN.

Here are some examples:

In Windows
file_id := UTL_FILE.FOPEN ('k:commondebug', 'trace.lis', 'R'),
In Unix
file_id := UTL_FILE.FOPEN ('/usr/od2000/admin', 'trace.lis', 'W'),

Your location must be an explicit, complete path to the file. You cannot use operating system-specific parameters such as environment variables in Unix to specify file locations.

Work with Oracle Directories

Prior to Oracle9i Database Release 2, whenever you opened a file, you needed to specify the location of the file, as in:

BEGIN
   file_id :=
     UTL_FILE.fopen (
       '/accts/data',
       'trans.dat', 'R'),

Such a hardcoding of values is always to be avoided, however. What if the location of the accounts data changes? How many programs will I have to go fix to make sure everyone is looking in the right place? How many times will I have to make such changes?

A much better approach is to declare a variable or constant and assign it the value of the location. If you do this in a package, the constant can be referenced by any program in a schema with the EXECUTE privilege on that package. Here is an example, followed by a recoding of the earlier FOPEN call:

CREATE PACKAGE accts_pkg
IS
   c_data_location
      CONSTANT VARCHAR2(30) :=
        '/accts/data';
   ...
END accts_pkg;
/
 
BEGIN
   file_id :=
     UTL_FILE.fopen (
       accts_pkg.c_data_location,
       'trans.dat', 'R'),

That’s great. But even better is to use a schema-level object that you can define in the database: a directory. This particular type of object is also used when working with BFILEs, so you can in effect “consolidate” file location references in both DBMS_LOB and UTL_FILE by shifting to directories.

To create a directory, you will need the CREATE ANY DIRECTORY privilege . You then define a new directory as shown in this example:

CREATE OR REPLACE DIRECTORY DEVELOPMENT_DIR AS '/dev/source';

CREATE OR REPLACE DIRECTORY TEST_DIR AS '/test/source';

Here are some things to keep in mind about directories and UTL_FILE:

  • Oracle does not validate the location you specify when you specify the name of a directory. It simply associates that string with the named database object.

  • When you specify the name of a directory in a call to, say, UTL_FILE.FOPEN, it is treated as a case-sensitive string. In other words, if you do not specify the name in uppercase, the operation will fail.

  • Once created, you can grant permissions to specific users to work with that directory as follows:

    GRANT READ ON DIRECTORY DEVELOPMENT_DIR to senior_developer;
  • Finally, you can query the contents of ALL_DIRECTORIES to determine which directories are available in the currently connected schema. You can also leverage this view to build some useful utilities, which you will find in the fileio.pkg package. Here is one example: generate a UTL_FILE_DIR entry for each directory defined in the database:

    PROCEDURE fileIO.gen_utl_file_dir_entries
    IS
    BEGIN
       FOR rec IN (SELECT * FROM all_directories)
       LOOP
          DBMS_OUTPUT.put_line ('UTL_FILE_DIR = ' || rec.directory_path);
       END LOOP;
    END gen_utl_file_dir_entries;

One advantage of building utilities like those found in fileIO is that you can easily add sophisticated handling of the case of the directory to avoid “formatting errors,” such as forgetting to specify the directory name in uppercase.

Open Files

Before you can read or write a file, you must open it. The UTL_FILE.FOPEN function opens the specified file and returns a file handle you can then use to manipulate the file. Here’s the header for the function:

FUNCTION UTL_FILE.fopen (location     IN VARCHAR2
   , filename     IN VARCHAR2
   , open_mode    IN VARCHAR2
   , max_linesize IN BINARY_INTEGER DEFAULT NULL)
RETURN file_type;

Parameters are summarized in the following table:

Parameter

Description

location

Location of the file (directory in UTL_FILE_DIR or a database Directory).

filename

Name of the file.

openmode

Mode in which the file is to be opened (see the following modes).

max_linesize

The maximum number of characters per line, including the newline character, for this file. Minimum is 1; maximum is 32767. The default of NULL means that UTL_FILE determines an appropriate value from the operating system (the value has historically been around 1,024 bytes).

You can open the file in one of three modes:

R

Opens the file read-only. If you use this mode, use UTL_FILE’s GET_LINE procedure to read from the file.

W

Opens the file to read and write in replace mode. When you open in replace mode, all existing lines in the file are removed. If you use this mode, you can use any of the following UTL_FILE programs to modify the file: PUT, PUT_LINE, NEW_LINE, PUTF, and FFLUSH.

A

Opens the file to read and write in append mode. When you open in append mode, all existing lines in the file are kept intact. New lines will be appended after the last line in the file. If you use this mode, you can use any of the following UTL_FILE programs to modify the file: PUT, PUT_LINE, NEW_LINE, PUTF, and FFLUSH.

Keep the following points in mind as you attempt to open files:

  • The file location and the filename joined together must represent a legal filename on your operating system.

  • The file location specified must be accessible and must already exist; FOPEN will not create a directory or subdirectory for you in order to write a new file, for example.

  • If you want to open a file for read access, the file must already exist. If you want to open a file for write access, the file will either be created if it does not exist or emptied of all its contents if it does exist.

  • If you try to open with append, the file must already exist. UTL_FILE will not treat your append request like a write access request. If the file is not present, UTL_FILE will raise the INVALID_OPERATION exception.

The following example shows how to declare a file handle and then open a configuration file for that handle in read-only mode:

DECLARE
       config_file UTL_FILE.FILE_TYPE;
BEGIN
       config_file := UTL_FILE.FOPEN (
        '/maint/admin', 'config.txt', 'R'),

Notice that I did not provide a maximum line size when I opened this file. That parameter is, in fact, optional. If you do not provide it, the maximum length of a line you can read from or write to the file is approximately 1,024. Given this limitation, you should always include the max_linesize argument as shown below:

DECLARE
       config_file UTL_FILE.FILE_TYPE;
BEGIN
       config_file := UTL_FILE.FOPEN (
        '/maint/admin', 'config.txt', 'R', max_linesize => 32767);

Is the File Already Open?

The IS_OPEN function returns TRUE if the specified handle points to a file that is already open. Otherwise, it returns false. The header for the function is,

FUNCTION UTL_FILE.IS_OPEN (file IN UTL_FILE.FILE_TYPE) RETURN BOOLEAN;

where file is the file to be checked.

Within the context of UTL_FILE, it is important to know what this means. The IS_ OPEN function does not perform any operating system checks on the status of the file. In actuality, it merely checks to see if the id field of the file handle record is not NULL. If you don’t play around with these records and their contents, this id field is set to a non-NULL value only when you call FOPEN. It is set back to NULL when you call FCLOSE.

Close Files

Use the UTL_FILE.FCLOSE and UTL_FILE.FCLOSE_ALL procedures to close a specific file and all open files in your session, respectively.

Use FCLOSE to close an open file. The header for this procedure is:

PROCEDURE UTL_FILE.FCLOSE (file IN OUT FILE_TYPE);

where file is the file handle.

Notice that the argument to UTL_FILE.FCLOSE is an IN OUT parameter because the procedure sets the id field of the record to NULL after the file is closed.

If there is buffered data that has not yet been written to the file when you try to close it, UTL_FILE will raise the WRITE_ERROR exception.

FCLOSE_ALL closes all the opened files. The header for this procedure follows:

PROCEDURE UTL_FILE.FCLOSE_ALL;

This procedure will come in handy when you have opened a variety of files and want to make sure that none of them are left open when your program terminates.

In programs in which files have been opened, you may wish to call FCLOSE_ALL in the exception handlers of those programs. If there is an abnormal termination of the program, files will then still be closed.

EXCEPTION
   WHEN OTHERS
   THEN
      UTL_FILE.FCLOSE_ALL;
      ... other clean up activities ...
END;

When you close your files with the FCLOSE_ALL procedure, none of your file handles will be marked as closed (the id field, in other words, will still be non-NULL). The result is that any calls to IS_OPEN for those file handles will still return TRUE. You will not, however, be able to perform any read or write operations on those files (unless you reopen them).

Read from Files

The UTL_FILE.GET_LINE procedure reads a line of data from the specified file, if it is open, into the provided line buffer. Here’s the header for the procedure:

PROCEDURE UTL_FILE.GET_LINE
   (file IN UTL_FILE.FILE_TYPE,
    buffer OUT VARCHAR2);

Parameters are summarized in the following table:

Parameter

Description

file

The file handle returned by a call to FOPEN

buffer

The buffer into which the line of data is read

The variable specified for the buffer parameter must be large enough to hold all the data up to the next carriage return or end-of-file condition in the file. If not, PL/SQL will raise the VALUE_ERROR exception. The line terminator character is not included in the string passed into the buffer.

Tip

Oracle offers additional GET programs to read NVARCHAR2 data (GET_LINE_NCHAR) and raw data (GET_RAW).

Here is an example that uses GET_LINE:

DECLARE
   strbuffer VARCHAR2(32767);
   mynum NUMBER;
BEGIN
   fileID := UTL_FILE.FOPEN (
     'TEMP_DIR', 'numlist.txt', 'R', max_linesize => 32767);
   UTL_FILE.GET_LINE (fileID, strbuffer);
   mynum := TO_NUMBER (strbuffer);
END;
/

Because GET_LINE reads data only into a string variable, you will have to perform your own conversions to local variables of the appropriate datatype if your file holds numbers or dates.

GET_LINE exceptions

When GET_LINE attempts to read past the end of the file, the NO_DATA_FOUND exception is raised. This is the same exception that is raised when you:

  • Execute an implicit (SELECT INTO) cursor that returns no rows

  • Reference an undefined row of a PL/SQL collection

  • Read past the end of a BFILE (binary file) with DBMS_LOB

If you are performing more than one of these operations in the same PL/SQL block, you may need to add extra logic to distinguish between the different sources of this error. See the who_did_that.sql file on the book’s web site for a demonstration of this technique.

Handy encapsulation for GET_LINE

The GET_LINE procedure is simple and straightforward. It gets the next line from the file. If the pointer to the file is already located at the last line of the file, UTL_ FILE.GET_LINE does not return any kind of flag but instead raises the NO_DATA_FOUND exception. This design leads to poorly structured code; you might consider using an encapsulation on top of GET_LINE to improve that design. A demonstration will make the point clear.

Here is a program that reads each line from a file and then processes that line:

DECLARE
   l_file   UTL_FILE.file_type;
   l_line   VARCHAR2 (32767);
BEGIN
   l_file := UTL_FILE.fopen ('TEMP', 'names.txt', 'R'),
 
   LOOP
      UTL_FILE.get_line (l_file, l_line);
      process_line (l_line);
   END LOOP;
EXCEPTION
   WHEN NO_DATA_FOUND
   THEN
      UTL_FILE.fclose (names_file);
END;

Notice that the simple loop does not contain any explicit EXIT statement. The loop terminates implicitly and with an exception, as soon as UTL_FILE reads past the end of the file. In a small block like this one, the logic is clear. But imagine if your program is hundreds of lines long and much more complex. Suppose further that reading the contents of the file is just one step in the overall algorithm. If an exception terminates my block, I will then need to put the rest of my business logic in the exception section (bad idea) or put an anonymous BEGIN-END block wrapper around my read-file logic.

I am not comfortable with this approach. I don’t like to code infinite loops without an EXIT statement; the termination condition is not structured into the loop itself. Furthermore, the end-of-file condition is not really an exception; every file, after all, must end at some point. Why must we be forced into the exception section simply because we want to read a file in its entirety?

I believe that a better approach to handling the end-of-file condition is to build a layer of code around GET_LINE that immediately checks for end-of-file and returns a Boolean value (TRUE or FALSE). Theget_nextline procedure shown below demonstrates this approach:

/* File on web: getnext.sp */
PROCEDURE get_nextline (
   file_in IN UTL_FILE.FILE_TYPE
 , line_out OUT VARCHAR2
 , eof_out OUT BOOLEAN)
IS
BEGIN
   UTL_FILE.GET_LINE (file_in, line_out);
   eof_out := FALSE;
EXCEPTION
   WHEN NO_DATA_FOUND
   THEN
      line_out := NULL;
      eof_out  := TRUE;
END;

The get_nextline procedure accepts an already assigned file handle and returns two pieces of information: the line of text (if there is one) and a Boolean flag (set to TRUE if the end-of-file is reached, FALSE otherwise). Using get_nextline, I can now read through a file with a loop that has an EXIT statement:

DECLARE
   l_file   UTL_FILE.file_type;
   l_line   VARCHAR2 (32767);
   l_eof    BOOLEAN;
BEGIN
   l_file := UTL_FILE.fopen ('TEMP', 'names.txt', 'R'),
 
   LOOP
      get_nextline (l_file, l_line, l_eof);
      EXIT WHEN l_eof;
      process_line (l_line);
   END LOOP;

   UTL_FILE.fclose (l_file);
END;

With get_nextline, I no longer treat end-of-file as an exception. I read a line from the file until I am done, and then I close the file and exit. This is, I believe, a more straightforward and easily understood program.

Write to Files

In contrast to the simplicity of reading from a file, UTL_FILE offers a number of different procedures you can use to write to a file:

UTL_FILE.PUT

Adds the data to the current line in the opened file but does not append a line terminator. You must use the NEW_LINE procedure to terminate the current line or use PUT_LINE to write out a complete line with a line termination character.

UTL_FILE.NEW_LINE

Inserts one or more newline characters (default is 1) into the file at the current position.

UTL_FILE.PUT_LINE

Puts a string into a file, followed by a platform-specific line termination character. This is the program you are most likely to be using with UTL_FILE.

UTL_FILE.PUTF

Puts up to five strings out to the file in a format based on a template string, similar to the printf function in C.

UTL_FILE.FFLUSH

Makes sure that all pending data for the specified file is written physically out to a file.

You can use these procedures only if you have opened your file with modes Wor A; if you opened the file for read-only, the runtime engine raises the UTL_ FILE.INVALID_OPERATION exception.

Tip

Oracle offers additional PUT programs to write NVARCHAR2 data (PUT_LINE_NCHAR, PUT_NCHAR, PUTF_NCHAR) and raw data (PUT_RAW).

Let’s take a closer look at UTL_FILE.PUT_LINE. This procedure writes data to a file and then immediately appends a newline character after the text. Here’s the header for PUT_LINE:

PROCEDURE UTL_FILE.PUT_LINE (file IN UTL_FILE.FILE_TYPE,
   ,buffer IN VARCHAR2,
   ,autoflush IN BOOLEAN DEFAULT FALSE)

Parameters are summarized in the following table:

Parameter

Description

file

The file handle returned by a call to FOPEN

buffer

Text to be written to the file; maximum size allowed is 32K for 8.0. 3 and above; for earlier versions, it is 1,023 bytes

autoflush

Pass TRUE if you want this line to be flushed out to the operating system immediately

Before you can call UTL_FILE.PUT_LINE, you must have already opened the file.

Here is an example that uses PUT_LINE to dump the names of all our employees to a file:

PROCEDURE names_to_file
IS
   fileid   UTL_FILE.file_type;
BEGIN
   fileid := UTL_FILE.fopen ('TEMP', 'names.dat', 'W'),
 
   FOR emprec IN (SELECT *
                    FROM employee)
   LOOP
      UTL_FILE.put_line (
         fileid
       , emprec.first_name || ' ' || emprec.last_name);
   END LOOP;
 
   UTL_FILE.fclose (fileid);
END names_to_file;

A call to PUT_LINE is equivalent to a call to PUT followed by a call to NEW_LINE. It is also equivalent to a call to PUTF with a format string of “%s ” (see the description of PUTF in the next section).

Writing formatted text to file

Like PUT, PUTF puts data into a file, but it uses a message format (hence, the “F” in “PUTF”) to interpret the different elements to be placed in the file. You can pass between one and five different items of data to PUTF. Here’s the specification:

PROCEDURE UTL_FILE.putf
    (file IN FILE_TYPE
    ,format IN VARCHAR2
    ,arg1 IN VARCHAR2 DEFAULT NULL
    ,arg2 IN VARCHAR2 DEFAULT NULL
    ,arg3 IN VARCHAR2 DEFAULT NULL
    ,arg4 IN VARCHAR2 DEFAULT NULL
    ,arg5 IN VARCHAR2 DEFAULT NULL);

Parameters are summarized in the following table:

Parameter

Description

file

The file handle returned by a call to FOPEN

format

The string that determines the format of the items in the file; see the following options

argN

An optional argument string; up to five may be specified

The format string allows you to substitute the argN values directly into the text written to the file. In addition to “boilerplate” or literal text, the format string may contain the following patterns:

%s

Directs PUTF to put the corresponding item in the file. You can have up to five %s patterns in the format string because PUTF will take up to five items.

Directs PUTF to put a newline character in the file. There is no limit to the number of patterns you may include in a format string.

The %s formatters are replaced by the argument strings in the order provided. If you do not pass in enough values to replace all of the formatters, then the %s is simply removed from the string before writing it to the file.

The following example illustrates how to use the format string. Suppose you want the contents of the file to look like this:

Employee: Steven Feuerstein
Soc Sec #: 123-45-5678
Salary: $1000

This single call to PUTF will accomplish the task:

UTL_FILE.putf
   (file_handle, 'Employee: %s
Soc Sec #: %s
Salary: %s',
    'Steven Feuerstein',
    '123-45-5678',
    TO_CHAR (:employee.salary, '$9999'));

If you need to write out more than five items of data, you can simply call PUTF twice consecutively to finish the job.

Copy Files

UTL_FILE.FCOPY lets you easily copy the contents of one source file to another destination file. The following snippet, for example, uses UTL_FILE.FCOPY to perform a backup by copying a single file from the development directory to the archive directory:

DECLARE
   file_suffix   VARCHAR2 (100)
            := TO_CHAR (SYSDATE, 'YYYYMMDDHHMISS'),
BEGIN
   -- Copy the entire file...
   UTL_FILE.fcopy (
      src_location       => 'DEVELOPMENT_DIR',
      src_filename       => 'archive.zip',
      dest_location      => 'ARCHIVE_DIR',
      dest_filename      =>    'archive'
                            || file_suffix
                            || '.zip'
   );
END;

You can also use FCOPY to copy just a portion of a file. The program offers two additional parameters that allow you to specify the starting and ending line numbers you want to copy from the file. Suppose that I have a text file containing the names of the winners of a monthly PL/SQL quiz that started in January 2000. I would like to transfer all the names in 2001 to another file. I can do that by taking advantage of the fifth and sixth arguments of the FCOPY procedure as shown below:

DECLARE
   c_start_year PLS_INTEGER := 2000;
   c_year_of_interest PLS_INTEGER := 2001;
   l_start PLS_INTEGER;
   l_end PLS_INTEGER;
BEGIN
   l_start := (c_year_of_interest - c_start_year)*12 + 1;
   l_end := l_start + 11;
 
   UTL_FILE.fcopy (
      src_location       => 'WINNERS_DIR',
      src_filename       => 'names.txt',
      dest_location      => 'WINNERS_DIR',
      dest_filename      => 'names2001.txt',
      start_line         => l_start,
      end_line           => l_end
   );
END;

A useful encapsulation to UTL_FILE.FCOPY allows me to specify start and end strings instead of line numbers. We will leave the implementation of such a utility as an exercise for the reader (see the infile.sf file on the book’s web site for an implementation of an “INSTR for files” that might give you some ideas on implementation).

Delete Files

Beginning with Oracle9i Database Release 2, you can remove files using UTL_FILE.FREMOVE. The header for this program is:

PROCEDURE UTL_FILE.fremove (location IN VARCHAR2,
   filename IN VARCHAR2);

That’s simple enough. You provide the location and name of the file, and UTL_FILE attempts to delete it. What if UTL_FILE encounters a problem? You might see one of the following exceptions then raised:

Exception name

Meaning

UTL_FILE.invalid_path

Not a valid file handle

UTL_FILE.invalid_filename

File not found or filename NULL

UTL_FILE.file_open

File already open for writing/appending

UTL_FILE.access_denied

Access to the directory object is denied

UTL_FILE.remove_failed

Failed to delete file

In other words, UTL_FILE will raise an exception if you try to remove a file that doesn’t exist or if you do not have the privileges needed to remove the file. Many file-removal programs in other languages (for example, File.delete in Java) return a status code to inform you of the outcome of the removal attempt. If you prefer this approach, you can use (or copy) the fileIO.fremove program found in the fileio.pkg file on the book’s web site.

Rename and Move Files

I can combine copy and remove operations into a single step by calling the UTL_FILE.RENAME procedure. This handy utility allows me to either rename a file in the same directory or to rename a file to another name and location (in effect, moving that file).

Here is the header for FRENAME:

PROCEDURE UTL_FILE.frename (src_location   IN VARCHAR2,
   src_filename   IN VARCHAR2,
   dest_location  IN VARCHAR2,
   dest_filename  IN VARCHAR2,
   overwrite      IN BOOLEAN DEFAULT FALSE);

This program may raise one of the following exceptions:

Exception name

Meaning

UTL_FILE.invalid_path

Not a valid file handle

UTL_FILE.invalid_filename

File not found or filename NULL

UTL_FILE.rename_failed

Unable to perform the rename as requested

UTL_FILE.access_denied

Insufficient privileges to access directory object

You will find an interesting application of FRENAME in the fileIO package—the chgext procedure. This program changes the extension of the specified file.

Retrieve File Attributes

Sometimes we need to get information about the file in question: How big is this file? Does a file even exist? What is the block size of my file? Such questions are not mysteries that can only be solved with the help of an operating system command (or, in the case of the file length, the DBMS_LOB package), as they were in early Oracle releases. UTL_FILE.FGETATTR provides us with that information in a single native procedure call.

Here is the header for FGETATTR:

PROCEDURE UTL_FILE.fgetattr (location    IN VARCHAR2,
   filename    IN VARCHAR2,
   fexists     OUT BOOLEAN,
   file_length OUT NUMBER,
   block_size  OUT BINARY_INTEGER);

Thus, to use this program, we must declare three different variables to hold the Boolean flag (does the file exist?), the length of the file, and the block size. Here is a sample usage:

DECLARE
   l_fexists      BOOLEAN;
   l_file_length  PLS_INTEGER;
   _block_size    PLS_INTEGER;
BEGIN
   UTL_FILE.fgetattr (
      location    => DEVELOPMENT_DIR,
      filename    => 'bigpkg.pkg',
      fexists     => l_fexists,
      file_length => l_file_length,
      block_size  => l_block_size
   );
   ...
END;

This interface is a bit awkward. Suppose we just want to find out the length of this file? We still have to declare all those variables, obtain the length, and then work with that value. Perhaps the best way to take advantage of FGETATTR is to build some of your own functions on top of this built-in that answer a single question, such as:

FUNCTION fileIO.flength (
   location_in   IN   VARCHAR2,
   file_in       IN   VARCHAR2
   )
   RETURN PLS_INTEGER;

or:

FUNCTION fileIO.fexists (
   location_in IN VARCHAR2,
   file_in IN VARCHAR2
   )
      RETURN BOOLEAN;

As a result, you do not have to declare unneeded variables, and you can write simpler, cleaner code.

Sending Email

Over the years, Oracle has gradually made it easier to send email from within a stored procedure, and Oracle Database 10g offers the simplest API yet. Here’s a short example:

/* Requires Oracle Database 10g */
BEGIN
   UTL_MAIL.send(
      sender     => '[email protected]'
     ,recipients => '[email protected]'
     ,subject    => 'Cool new API for sending email 
'
     ,message    =>
'Dear Sirs and Madams:
 
Sending email in PL/SQL is *much* easier with UTL_MAIL in 10g. Give it a try!
 
Mailfully Yours,
Bill'
   );
 
END;

When you run this block, Oracle will attempt to send this message using whatever SMTP[*] host the DBA has configured in the initialization file (see below).

Here is the header for UTL_MAIL.SEND:

PROCEDURE send(sender     IN VARCHAR2 CHARACTER SET ANY_CS,
               recipients IN VARCHAR2 CHARACTER SET ANY_CS,
               cc         IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT NULL,
               bcc        IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT NULL,
               subject    IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT NULL,
               message    IN VARCHAR2 CHARACTER SET ANY_CS DEFAULT NULL,
               mime_type  IN VARCHAR2 CHARACTER SET ANY_CS
                             DEFAULT 'text/plain; charset=us-ascii',
               priority   IN PLS_INTEGER DEFAULT 3);

Most of the parameters are self-explanatory. One nonobvious usage hint: if you want to use more than one recipient (or cc or bcc), separate the addresses with commas, like this:

recipients => '[email protected], [email protected]'

Okay, so that’s pretty good if you have Oracle Database 10g, but what if you only have access to earlier versions, or what if you just want a little more control? You can still use the UTL_SMTP package , which is a little more complicated but nevertheless workable. If you want to code at an even lower level, you can use UTL_TCP, an external procedure, or a Java stored procedure, but I’ll leave those as an exercise for anyone who either wants to do some fun coding or who knows how to search for code using Google.

Oracle Prerequisites by Release

Unfortunately, not all versions of Oracle provide email-from-PL/SQL that works out of the box. Let’s take a look at the setup requirements for different versions:

Oracle8i Database

When Oracle first introduced the UTL_SMTP package, in 8.1.6, it was implemented in Java. That meant the DBA had to be sure that the Java virtual machine (JVM) was properly installed. Here’s the abridged version of what a DBA needed to do to install the JVM in 8.1.6:

As SYS, run:
$ORACLE_HOME/javavm/install/initjvm.sql
Run:
CALL dbms_java.loadjava('-resolve -force plsql/jlib/plsql.jar')
Run:
$ORACLE_HOME/rdbms/admin/initplsj.sql

Also note that UTL_SMTP was a little buggy prior to 8.1.7.2; check Metalink if you must run an earlier version.

Oracle9i Database

In the next release, Oracle rewrote the guts of UTL_SMTP in C, obviating the need to install the JVM just to send mail. Even better, UTL_SMTP gets installed in Oracle9i Database by default, so it should work right out of the box.

Oracle Database 10g

While UTL_SMTP still works just fine in Oracle Database 10g, the default Oracle installation does not include the new UTL_MAIL package. To set up and use UTL_MAIL, your DBA will have to do three things:

  1. Set a value for the initialization parameter SMTP_OUT_SERVER. In Oracle Database 10g Release 2, you can just do something like this:

    ALTER SYSTEM SET SMTP_OUT_SERVER = 'mailhost';

    In Oracle Database 10g Release 1, you need to edit your pfile by hand to set this parameter. The string you supply will be one or more (comma-delimited) mail hostnames that UTL_MAIL should try one at a time until it finds one it likes.

  2. After setting this parameter, you must bounce the database server for the change to take effect. Amazing but true.

  3. As SYS, run the installation scripts:

    @$ORACLE_HOME/rdbms/admin/utlmail.sql
    @$ORACLE_HOME/rdbms/admin/prvtmail.plb
  4. Grant execute to the “privileged few” who need to use it:

    GRANT EXECUTE ON UTL_MAIL TO PUBLIC;

Send a Short (32K or Less) Plaintext Message

In the previous section, the first example showed how to send a plaintext message if you have UTL_MAIL at your disposal. If, however, you are using UTL_SMTP, your program will have to communicate with the mail server at a lower programmatic level: opening the connection, composing the headers, sending the body of the message, and (ideally) examining the return codes. To give you a flavor of what this looks like, Figure 21-1 shows a sample conversation between a mail server and a PL/SQL mail client I’ve named send_mail_via_utl_smtp.

Here is the code for this simple stored procedure:

/* File on web: send_mail_via_utl_smtp.sp */
1    CREATE OR REPLACE PROCEDURE send_mail_via_utl_smtp
2      ( sender IN VARCHAR2
3       ,recipient IN VARCHAR2
4       ,subject IN VARCHAR2 DEFAULT NULL
5       ,message IN VARCHAR2
6       ,mailhost IN VARCHAR2 DEFAULT 'mailhost'
7       )
8    IS
9      mail_conn UTL_SMTP.connection;
10      crlf CONSTANT VARCHAR2(2) := CHR(13) || CHR(10);
11      smtp_tcpip_port CONSTANT PLS_INTEGER := 25;
12    BEGIN
13      mail_conn := UTL_SMTP.OPEN_CONNECTION(mailhost, smtp_tcpip_port);
14      UTL_SMTP.HELO(mail_conn, mailhost);
15      UTL_SMTP.MAIL(mail_conn, sender);
16      UTL_SMTP.RCPT(mail_conn, recipient);
17      UTL_SMTP.DATA(mail_conn, SUBSTR(
18         'Date: ' || TO_CHAR(SYSTIMESTAMP, 'Dy, dd Mon YYYY HH24:MI:SS TZHTZM')
19         || crlf || 'From: ' || sender || crlf
20         || 'Subject: ' || subject || crlf
21         || 'To: ' || recipient || crlf
22         || message
23       , 1, 32767));
24     
25      UTL_SMTP.QUIT(mail_conn);
26    END;
A “conversation” between the PL/SQL mail client and SMTP server

Figure 21-1. A “conversation” between the PL/SQL mail client and SMTP server

The following table explains a few concepts of this code:

Line(s)

Description

9

You must define a variable to handle the “connection,” which is a record of type UTL_SMTP.connection.

10

According to Internet mail standards, all header lines must end with a carriage return followed by a line feed, and you are responsible for making this happen (see lines 18-21).

16-28

These lines send specific instructions to the SMTP server in the sequence and form an Internet-compliant mail server expects.

19

This line uses SYSTIMESTAMP (introduced in Oracle9i Database) to gain access to time zone information. You can use SYSDATE and omit the TZTM in the Oracle8i Database.

If you look at lines 17-23, you’ll see that this procedure cannot send a message whose “DATA” part exceeds 32,767 bytes, which is the limit of PL/SQL variables. It’s possible to send longer emails using UTL_SMTP, but you will need to stream the data using multiple calls to UTL_SMTP.WRITE_DATA, as shown later.

Warning

By convention, most email programs limit each line of text to 78 characters plus the two line-terminating characters. In general, you’ll want to keep each line of text to a maximum of 998 characters exclusive of carriage return/line feed, or CRLF (that is, 1,000 bytes if you count the CRLF). Don’t go over 1000 bytes unless you’re sure that your server implements the relevant SMTP “Service Extension.”

Include “Friendly” Names in Email Addresses

If I were to invoke the previous procedure like this:

CALL send_mail_via_utl_smtp('[email protected]',
   '[email protected]', 'mail demo', NULL);

the “normally” visible headers of the email, as generated by lines 17-21, will show up something like this:

Date: Wed, 23 Mar 2005 17:14:30 -0600
From: [email protected]
Subject: mail demo
To: [email protected]

Most humans (and many antispam programs) prefer to see real names in the headers, in a the form such as:

Date: Wed, 23 Mar 2005 17:14:30 -0600
From:Bob Swordfish <[email protected]>
Subject: mail demo
To: "Scott Tiger, Esq." <[email protected]>

There is, of course, more than one way to make this change; perhaps the most elegant would be to add some parsing to the sender and recipient parameters. This is what Oracle has done in UTL_MAIL. So, for example, I can call UTL_MAIL.SEND with addresses of the form:

["]Friendly name["] <email_address>

as in:

BEGIN
   UTL_MAIL.send('Bob Swordfish <[email protected]>',
      '"Scott Tiger, Esq." <[email protected]>',
      subject=>'mail demo'),
END;

However, you need to realize that Oracle’s package also adds character set information, so the previous code generates an email header that looks something like this:

Date: Tue, 10 May 2005 17:40:37 -0500 (CDT)
From: =?ISO-8859-1?Q?Bob=20Swordfish=20<[email protected]>?=
To: =?ISO-8859-1?Q?"Scott=20Tiger,=20Esq."=20<[email protected]>?=
Subject: =?ISO-8859-1?Q?mail=20demo?=

While that looks odd to most ASCII speakers, it is completely acceptable in Internet-standards-land; an intelligent mail client should interpret (rather than display) the character set information anyway.

One quick and dirty modification of the send_mail_via_utl_smtp procedure would simply be to add parameters for the friendly names (or change the existing parameters to record structures).

Send a Plaintext Message of Arbitrary Length

UTL_MAIL is pretty handy, but if you want to send a text message larger than 32,767 bytes, it won’t help you. One way around this limitation would be to modify our send_mail_via_utl_smtp procedure so that the “message” parameter is a CLOB datatype. Take a look at the other changes required:

/* File on web: send_clob.sp */
CREATE OR REPLACE PROCEDURE send_clob
   ( sender IN VARCHAR2
    ,recipient IN VARCHAR2
    ,subject IN VARCHAR2 DEFAULT NULL
    ,message INCLOB
    ,mailhost IN VARCHAR2 DEFAULT 'mailhost'
    )
IS
   mail_conn  UTL_SMTP.connection;
   crlf CONSTANT VARCHAR2(2) := CHR(13) || CHR(10);
   smtp_tcpip_port CONSTANT PLS_INTEGER := 25;
   pos PLS_INTEGER := 1;
   bytes_o_data CONSTANT PLS_INTEGER := 32767;
   offset PLS_INTEGER := bytes_o_data;
   msg_length CONSTANT PLS_INTEGER := DBMS_LOB.getlength(message);
BEGIN
   mail_conn := UTL_SMTP.open_connection(mailhost, smtp_tcpip_port);
   UTL_SMTP.helo(mail_conn, mailhost);
   UTL_SMTP.mail(mail_conn, sender);
   UTL_SMTP.rcpt(mail_conn, recipient);
 
   UTL_SMTP.open_data(mail_conn);
   UTL_SMTP.write_data(mail_conn,
       'Date: ' || TO_CHAR(SYSTIMESTAMP, 'Dy, dd Mon YYYY HH24:MI:SS TZHTZM') || crlf
       || 'From: ' || sender || crlf
       || 'Subject: ' || subject || crlf
       || 'To: ' || recipient || crlf || crlf);
 
   WHILE pos < msg_length
   LOOP
      utl_smtp.write_data(mail_conn, DBMS_LOB.SUBSTR(message, offset, pos));
      pos  := pos + offset;
      offset := LEAST(bytes_o_data, msg_length - offset);
   END LOOP;
   UTL_SMTP.close_data(mail_conn);
 
   UTL_SMTP.QUIT(mail_conn);
END;

Using open_data, write_data, and close_data allows you to transmit an arbitrary number of bytes to the mail server (up to whatever limit the server imposes on email size). Note the one big assumption that this code is making: that the CLOB has been properly split into lines of the correct length.

Let’s next take a look at how to attach a file to an email.

Send a Message with a Short (< 32K) Attachment

The original email standard required all messages to be composed of seven-bit U.S. ASCII characters.[*] But we all know that emails can include attachments—such as viruses and word-processing documents—and these kinds of files are normally binary, not text. How can an ASCII message transmit a binary file? The answer, in general, is that attachments are transmitted using mail extensions known as MIME[] in combination with a binary-to-ASCII translation scheme such as base64. To see MIME in action, let’s take a look at an email that transmits a tiny binary file:

Date: Fri, 01 Apr 2005 10:16:51 -0600
From: Bob Swordfish <[email protected]>
MIME-Version: 1.0
To: Scott Tiger <[email protected]>
Subject: Attachment demo
Content-Type: multipart/mixed;
 boundary="------------060903040208010603090401"
 
This is a multi-part message in MIME format.
--------------060903040208010603090401
Content-Type: text/plain; charset=us-ascii; format=fixed
Content-Transfer-Encoding: 7bit
 
Dear Scott:
 
I'm sending a gzipped file containing the text of the first
paragraph. Hope you like it.
 
Bob
--------------060903040208010603090401
Content-Type: application/x-gzip; name="hugo.txt.gz"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="hugo.txt.gz"
 
H4sICDh/TUICA2xlc21pcy50eHQAPY5BDoJAEATvvqI/AJGDxjMaowcesbKNOwmZITsshhf7
DdGD105Vpe+K5tQc0Jm6sGScU8gjvbrmoG8Tr1qhLtSCbs3CEa/gaMWTTbABF3kqa9z42+dE
RXhYmeHcpHmtBlmIoBEpREyZLpERtjB/aUSxns5/Ci7ac/u0P9a7Dw4FECSdAAAA
--------------060903040208010603090401--

Although a lot of the text can be boilerplated, it’s still a lot of details to have to deal with when you generate the email. Fortunately, if you just want to send a “small” attachment (less than 32K), and you have Oracle Database 10g or later, UTL_MAIL comes to the rescue. In this next example, we’ll use UTL_MAIL.SEND_ATTACH_VARCHAR2, which sends attachments that are expressed as text.

The previous message and file can be sent as follows:

DECLARE
   b64 VARCHAR2(512) := 'H4sICDh/TUICA2xlc21...'; -- etc., as above
   txt VARCHAR2(512) := 'Dear Scott: ...'; -- etc., as above
BEGIN
   UTL_MAIL.send_attach_varchar2(
      sender => '[email protected]'
     ,recipients => '[email protected]'
     ,message => txt
     ,subject => 'Attachment demo'
     ,att_mime_type => 'application/x-gzip'
     ,attachment => b64
     ,att_inline => TRUE
     ,att_filename => 'hugo.txt.gz'
   );
END;

Here are the new parameters:

att_mime_type

An indication of the type of media and format of the attachment

att_inline

Directive to the mail-reading program as to whether the attachment should be displayed in the flow of the message body (TRUE), or as a separate thing (FALSE)

att_filename

The sender’s designated name of the attached file

The MIME type isn’t just something you make up; it’s loosely governed, like so many things on the Internet, by the Internet Assigned Numbers Authority (IANA) . Table 21-1 displays some common MIME types.

Table 21-1. Common MIME types

MIME content type

Description

multipart/mixed

Indicates that the email body contains more than one independent part, which should be presented in a particular order.

text/plain

“Plain text” sounds straightforward, but in reality there are a surprising number of possible variations including the character set and the means of encoding line breaks.

text/html

HTML (can also be presented in different character sets).

application/pdf

Adobe Portable Document Format.

application/msword

Microsoft Word document.

application/vnd.ms-excel

Microsoft Excel document.

application/vnd.ms-tnef or application/ms-tnef

Microsoft MAPI Transport Neutral Encoding Format; a Microsoft-defined format for attachments that is understood by MS Outlook (although there are some third-party and open source tools to interpret tnef).

application/zip

File compressed using Pkware’s PKZIP algorithm.

image/png

Image represented in Portable Network Graphics format.

message/rfc822

Attachment is itself an email (for example, a forwarded message).

application/octet-stream

This is the “punt” format that you can use when no suitable MIME type exists to describe the attachment; use this sparingly, because mail-reading programs may interpret this as a possible virus.

For more information on all MIME types and links to the RFCs that will help you create the MIME headers visit IANA’s web page: http://www.iana.org/assignments/media-types/.

Returning to PL/SQL business, you may have noticed that there was quite a bit of hand-waving earlier to attach a base64-encoded file to an email. Let’s take a closer look at the exact steps required to convert a binary file into something you can send to an inbox.

Send a Small File (< 32K) as an Attachment

To have Oracle convert a small binary file to something that can be emailed, you can read the contents of the file into a RAW variable, and use UTL_MAIL.SEND_ATTACH_RAW. This causes Oracle to convert the binary data to base64 and properly construct the MIME directives. If the file you want to send is in /tmp/hugo.txt.gz (and is less than 32K in size), you might specify:

CREATE OR REPLACE DIRECTORY tmpdir AS '/tmp';
 
DECLARE
   the_file BFILE := BFILENAME('TMPDIR', 'hugo.txt.gz'),
   rawbuf RAW(32767);
   amt PLS_INTEGER :=  32767;
   offset PLS_INTEGER := 1;
BEGIN
   DBMS_LOB.fileopen(the_file, DBMS_LOB.file_readonly);
   DBMS_LOB.read(the_file, amt, offset, rawbuf);
   UTL_MAIL.send_attach_raw 
(
      sender => '[email protected]'
     ,recipients => '[email protected]'
     ,subject => 'Attachment demo'
     ,message => 'Dear Scott...'
     ,att_mime_type => 'application/x-gzip'
     ,attachment => rawbuf
     ,att_inline => TRUE
     ,att_filename => 'hugo.txt.gz'
   );
 
   DBMS_LOB.close(the_file);
END;

If you don’t have UTL_MAIL, follow the instructions in the next section.

Attach a File of Arbitrary Size

To send a larger attachment, you can use the trusty UTL_SMTP package; if the attachment is not text, you can perform a base64 conversion with Oracle’s built-in UTL_ENCODE package . Here is an example procedure that sends a BFILE along with a short text message:

/* File on web: send_bfile.sp */
1    CREATE OR REPLACE PROCEDURE send_bfile
2      ( sender IN VARCHAR2
3       ,recipient IN VARCHAR2
4       ,subject IN VARCHAR2 DEFAULT NULL
5       ,message IN VARCHAR2 DEFAULT NULL
6       ,att_bfile IN OUT BFILE
7       ,att_mime_type IN VARCHAR2
8       ,mailhost IN VARCHAR2 DEFAULT 'mailhost'
9       )
10    IS
11      crlf CONSTANT VARCHAR2(2) := CHR(13) || CHR(10);
12      smtp_tcpip_port CONSTANT PLS_INTEGER := 25;
13      bytes_per_read CONSTANT PLS_INTEGER := 23829;
14      boundary CONSTANT VARCHAR2(78) := '-------5e9i1BxFQrgl9cOgs90-------';
15      encapsulation_boundary CONSTANT VARCHAR2(78) := '--' || boundary;
16      final_boundary CONSTANT VARCHAR2(78) := '--' || boundary || '--';
17     
18      mail_conn  UTL_SMTP.connection;
19      pos PLS_INTEGER := 1;
20      file_length PLS_INTEGER;
21     
22      diralias VARCHAR2(30);
23      bfile_filename VARCHAR2(512);
24      lines_in_bigbuf PLS_INTEGER := 0;
25     
26      PROCEDURE writedata (str IN VARCHAR2, crlfs IN PLS_INTEGER DEFAULT 1)
27      IS
28      BEGIN
29         UTL_SMTP.write_data(mail_conn, str || RPAD(crlf, 2 * crlfs, crlf));
30      END;
31     
32    BEGIN
33      DBMS_LOB.fileopen(att_bfile, DBMS_LOB.LOB_READONLY);
34      file_length := DBMS_LOB.getlength(att_bfile);
35     
36      mail_conn := UTL_SMTP.open_connection(mailhost, smtp_tcpip_port);
37      UTL_SMTP.helo(mail_conn, mailhost);
38      UTL_SMTP.mail(mail_conn, sender);
39      UTL_SMTP.rcpt(mail_conn, recipient);
40     
41      UTL_SMTP.open_data(mail_conn);
42      writedata('Date: ' || TO_CHAR(SYSTIMESTAMP,
43                   'Dy, dd Mon YYYY HH24:MI:SS TZHTZM') || crlf
44          || 'MIME-Version: 1.0' || crlf
45          || 'From: ' || sender || crlf
46          || 'Subject: ' || subject || crlf
47          || 'To: ' || recipient || crlf
48          || 'Content-Type: multipart/mixed; boundary="' || boundary || '"', 2
49     
50      writedata(encapsulation_boundary);
51      writedata('Content-Type: text/plain; charset=ISO-8859-1; format=flowed')
52      writedata('Content-Transfer-Encoding: 7bit', 2);
53      writedata(message, 2);
54     
55      DBMS_LOB.filegetname(att_bfile, diralias, bfile_filename);
56      writedata(encapsulation_boundary);
57      writedata('Content-Type: '
58         || att_mime_type || '; name="' || bfile_filename || '"'),
59      writedata('Content-Transfer-Encoding: base64'),
60      writedata('Content-Disposition: attachment; filename="'
61         || bfile_filename || '"', 2);
62     
63      WHILE pos < file_length
64      LOOP
65         writedata(UTL_RAW.cast_to_varchar2(
66                     UTL_ENCODE.base64_encode 
(
67                        DBMS_LOB.substr(att_bfile, bytes_per_read, pos))), 0);
68         pos := pos + bytes_per_read;
69      END LOOP;
70     
71      writedata(crlf || crlf || final_boundary);
72     
73      UTL_SMTP.close_data(mail_conn);
74      UTL_SMTP.QUIT(mail_conn);
75      DBMS_LOB.CLOSE(att_bfile);
76    END;

Let’s take a look at a few highlights:

Line(s)

Description

13

This constant governs how many bytes of the file to attempt to read at a time (see line 67), which should probably be as large as possible for performance reasons. It turns out that UTL_ENCODE.BASE64_ENCODE generates lines that are 64 characters wide. Because of the way base64 works, each 3 bytes of binary data gets translated into 4 bytes of character data. Add in 2 bytes of CRLF per emailed line of base64 text, and you get the largest possible read of 23,829 bytes (obtained from the expression TRUNC((0.75*64)*(32767/(64+2))-1).

14-16

You can reuse the same core boundary string throughout this email. As you can see from the code, MIME standards require that slightly different boundaries be used in different parts of the email. If you want to create an email with nested MIME parts, though, you will need a different boundary string for each level of nesting.

26-30

This is a convenience procedure to make the executable section a little cleaner. The crlfs parameter indicates the number of CRLFs to append to the line (generally 0, 1, or 2).

55

Instead of requiring a filename argument to send_bfile, we can just extract the filename from the BFILE itself.

63-69

This is the real guts of the program. It reads a portion of the file and converts it to base64, sending data out via the mail connection just before hitting the 32K limit.

I know what you’re thinking: I, too, used to think sending email was easy. And this procedure doesn’t even provide much flexibility; it lets you send one text part and attach one file. But it provides a starting point you can extend for your own application’s needs.

One more point about crafting well-formed emails: rather than reading yourself to sleep with the RFCs, you may prefer to pull out the email client you use every day, send yourself an email of the form you are trying to generate, and then view the underlying “source text” of the message. It worked for me; I did that many times while writing this section of the book!

Working with Web-Based Data (HTTP)

Let’s say you want to acquire some data from the web site of one of your business partners. There are lots of ways to retrieve a web page:

  • “By hand,” that is, by pointing your web browser to the right location

  • Using a scripting language such as Perl, which, incidentally, has lots of available gizmos and gadgets to interpret the data once you retrieve it

  • Via a command-line utility such as GNU wget (one of my favorite utilities)

  • Using Oracle’s built-in package UTL_HTTP.

Since this is a book about PL/SQL, guess which method we’ll be discussing!

Let’s start with a relatively simple means of coding the retrieval of a web page. This first method, which slices up the web page and puts the slices into an array, actually predates Oracle’s support of CLOBs.

Retrieve a Web Page in “Pieces”

One of the first procedures that Oracle ever released in the UTL_HTTP package retrieves a web page into consecutive elements of an associative array. Usage can be pretty simple:

DECLARE
   page_pieces UTL_HTTP.html_pieces; -- array of VARCHAR2(2000)
BEGIN
   page_pieces := UTL_HTTP.request_pieces(
      url => 'http://www.oreilly.com/'),
END;

I confess that I’ve never found the data retrieved in this format terribly fun to work with, because the 2,000-byte boundaries are unrelated to anything you would find on the text of the page. So if you have a parsing algorithm that needs a line-by-line approach, you will have to read and reassemble the lines. Moreover, Oracle says that it may not fill all of the (nonending) pieces to 2,000 bytes; Oracle’s algorithm does not use end-of-line boundaries as breaking points; and the maximum number of pieces is 32,767.

Even if an array-based retrieval meets your needs, you will likely encounter web sites where the above code just won’t work. For example, some sites would refuse to serve their content to such a script, because Oracle’s default HTTP “header” looks unfamiliar to the web server. In particular, the “User-Agent” header is a text string that tells the web server the browser software the client is using (or emulating), and many web sites are set up to provide content specific to certain browsers. But by default, Oracle does not send a User-Agent. A commonly used and supported header you might want to use is:

User-Agent: Mozilla/4.0

Sending this header does increase the complexity of the code you must write, because doing so means you must code at a lower level of abstraction; in particular, you must initiate a “request,” send your header, get the “response,” and retrieve the page in a loop:

DECLARE
   req UTL_HTTP.req;    -- a "request object" (actually a PL/SQL record)
   resp UTL_HTTP.resp;  -- a "response object" (also a PL/SQL record)
   buf VARCHAR2(32767); -- buffer to hold data from web page
BEGIN
   req := UTL_HTTP.begin_request('http://www.oreilly.com/',
      http_version => UTL_HTTP.http_version_1_1);
   UTL_HTTP.set_header(req, 'User-Agent', 'Mozilla/4.0'),
   resp := UTL_HTTP.get_response(req);
 
   BEGIN
      LOOP
         UTL_HTTP.read_text(resp, buf);-- process buf here; e.g., store in array
      END LOOP;
   EXCEPTION
      WHEN UTL_HTTP.end_of_body
      THEN
         NULL;
   END;
   UTL_HTTP.end_response(resp);
END;

The heart of the code above is this built-in:

PROCEDURE UTL_HTTP.read_text(r IN OUT NOCOPY UTL_HTTP.resp,
   data OUT NOCOPY VARCHAR2 CHARACTER SET ANY_CS,
   len IN PLS_INTEGER DEFAULT NULL);

If len is NULL, Oracle will fill the buffer up to its maximum size until reaching the end of the page, after which point the read operation raises the UTL_HTTP.end_of_body exception as above. (Yes, this goes against a coding practice that normal operations should not raise exceptions.) Each iteration through the loop, you will need to process the buffer, perhaps by appending it to a LOB.

You can also use the line-by-line retrieval using READ_LINE rather than READ_TEXT:

PROCEDURE UTL_HTTP.read_line(r IN OUT NOCOPY UTL_HTTP.resp,
   data OUT NOCOPY VARCHAR2 CHARACTER SET ANY_CS,
   remove_crlf IN BOOLEAN DEFAULT FALSE);

This built-in reads one line of source text at a time, optionally cutting off the end-of-line characters. The caveat with READ_LINE is that each line you fetch from the http server needs to be less than 32K in length. Such an assumption is not always a good one, so don’t use READ_LINE unless you are sure this limit won’t cause a problem.

Retrieve a Web Page into a LOB

Because reading either by “pieces” or by lines can run into various size limits, you may decide that it would make more sense to read into LOBs. Again, Oracle provides a very simple call that may meet your needs. You can retrieve an entire page at once into a single data structure using the HTTPURITYPE built-in object type:

DECLARE
   text CLOB;
BEGIN
   text := HTTPURITYPE('http://www.oreilly.com').getclob;
END;

If you are retrieving a binary file and you want to put it in a BLOB, you can use getblob():

DECLARE
   image BLOB;
BEGIN
   image :=
      HTTPURITYPE('www.oreilly.com/catalog/covers/oraclep4.s.gif').getblob;
END;

The HTTPURITYPE constructor is fairly forgiving regarding the presence of the http:// prefix (it assumes HTTP as the transport protocol).

Again, the simple solution won’t transmit a User-Agent; if you need custom headers when retrieving your LOB, you can use this:

/* File on web: url_to_clob.sql */
DECLARE
   req UTL_HTTP.req;
   resp UTL_HTTP.resp;
   buf VARCHAR2(32767);pagelob CLOB;
BEGIN
   req := UTL_HTTP.begin_request('http://www.oreilly.com/',
      http_version => UTL_HTTP.http_version_1_1);
   UTL_HTTP.set_header(req, 'User-Agent', 'Mozilla/4.0'),
   resp := UTL_HTTP.get_response(req);
   DBMS_LOB.createtemporary(pagelob, TRUE);
   BEGIN
      LOOP
         UTL_HTTP.read_text(resp, buf);
         DBMS_LOB.writeappend(pagelob, LENGTH(buf), buf);
      END LOOP;
   EXCEPTION
      WHEN UTL_HTTP.end_of_body
      THEN
         NULL;
   END;
   UTL_HTTP.end_response(resp);
 
   ...here is where you parse, store, or otherwise process the LOB
 
   DBMS_LOB.freetemporary(pagelob);
END;

Authenticate Using HTTP Username/Password

Although many web sites such as Amazon and eBay use a custom HTML form for login and authentication, there are still a lot of sites (such as Oracle’s own Metalink support site) that use HTTP authentication , more precisely known as basic authentication . You will recognize such sites by your browser client’s behavior; it will pop up a modal dialog box requesting your username and password.

According to Internet standards,[*] you should be able to bypass the dialog by inserting your username and password in the URL in the form:

http://username:password@some.site.com

If you are running 9.2.0.4 or later, both UTL_HTTP and HTTPURITYPE support this syntax. Consider the case of logging in to Oracle Metalink:

DECLARE
   webtext clob;
   user_pass VARCHAR2(64) := 'bob:swordfish'; -- replace with your own!
   url VARCHAR2(128) := 'metalink.oracle.com/metalink/plsql/ml2_gui.startup';
BEGIN
   webtext := HTTPURITYPE(user_pass || '@' || url).getclob;
END;
/

If encoding the username and password in the URL doesn’t work, try something along these lines:

...
   req := UTL_HTTP.begin_request('http://some.site.com/'),UTL_HTTP.set_authentication(req, 'bob', 'swordfish'),
   resp := UTL_HTTP.get_response(req);
...

This works as long if the site does not encrypt the login page.

Retrieve an SSL-Encrypted Web Page (Via https)

Although HTTPURITYPE does not support SSL-encrypted retrievals, UTL_HTTP will do the job if you set up an Oracle Wallet . An Oracle Wallet just a catchy name for a file that contains security certificates and, optionally, public/private key pairs . It’s the former (the certificates) that you need for https retrievals. You can store one or more wallets as files in the database server’s filesystem or in an LDAP directory service; Oracle does not install any wallets by default.

To set up one of these wallet things, you’ll want to fire up Oracle’s GUI utility known as Oracle Wallet Manager , which is probably named owm on Unix hosts or will appear on your StartOracle... menu on Microsoft Windows. Once you’ve got Oracle Wallet Manager running, the basic steps you need to follow[*] are:

  1. Click on the “New” icon or select WalletNew from the pull-down menu

  2. Give the wallet a password. In my example, the password will be “password1”. Use the default wallet type (“standard”).

  3. If it asks you “Do you want to create a certificate request at this time?,” the correct response is almost certainly “No.” You don’t need your own certificate to make an https retrieval.

  4. Click on the Save icon or choose WalletSave As from the menu to designate the directory. Oracle will name the file for you (on my machine, owm named it “ewallet.p12”).

  5. Upload or copy the wallet file to some location on the Oracle server to which the oracle processes have read access. In the next example, the directory is /oracle/wallets.

Now try something like this:

DECLARE
   req UTL_HTTP.req;
   resp UTL_HTTP.resp;
BEGIN
   UTL_HTTP.set_wallet('file:/oracle/wallets', 'password1'),
   req := UTL_HTTP.begin_request('https://www.entrust.com/'),
   UTL_HTTP.set_header(req, 'User-Agent', 'Mozilla/4.0'),
   resp := UTL_HTTP.get_response(req);
   UTL_HTTP.end_response(resp);
END;

If you don’t get an error message, you can reward yourself with a small jump for joy. This ought to work, because Entrust is one of the few authorities whose certificate Oracle includes by default when you create a wallet.

If you want to retrieve data from another https site whose public certificate doesn’t happen to be on Oracle’s list, you can fire up Oracle Wallet Manager again and “import” the certificate into your file, and again put it on the server. To download a certificate in a usable format, you can use Microsoft Internet Explorer and follow these steps:

  1. Point your (Microsoft IE) browser to the https site.

  2. Double-click on the yellow lock icon in the lower right corner of the window.

  3. Click on Details Copy to File.

  4. Follow the prompts to export a base64-encoded certificate.

  5. Open Oracle Wallet Manager.

  6. Open your “wallet” file.

  7. Import the certificate from the file you just created.

  8. Save your wallet file, and upload it to the database server as before.

Or, if you have openssl installed, you could do this:

echo '' | openssl s_client -connecthost:port

which will spew all kinds of information to stdout; just save the text between the BEGIN CERTIFICATE and END CERTIFICATE lines (inclusive) to a file. And by the way, the normal port for https is 443.

Remember that those certificates are not in an Oracle wallet until you import them via Oracle Wallet Manager. And in case that you’re wondering, a wallet can have more than one certificate, and a wallet directory can hold one or more wallets.

Submit Data to a Web Page via GET or POST

Sometimes, you’ll want to retrieve results from a web site as if you had filled out a form in your browser and pressed the Submit button. This section will show a few examples that use UTL_HTTP for this purpose, but many web sites are quirky and require quite a bit of fiddling about to get things working right. Some of the tools you may find useful while analyzing the behavior of your target site include:

  • Familiarity with HTML source code (especially as it relates to HTML forms) and possibly with JavaScript

  • A browser’s “view source” feature that lets you examine the source code of the site you’re trying to use from PL/SQL

  • A tool such as GNU wget that easily lets you try out different URLs and has an ability to show the normally hidden conversation between web client and server (use the -d switch)

First, let’s look some simple code you can use to query Google. As it turns out, Google’s main page uses a single HTML form:

<form action=/search name=f>

Because the method tag is omitted, it defaults to GET. The single text box on the form is named “q”:

<input maxLength=256 size=55 name=q value="">

You can encode the GET request directly in the URL as follows:

http://www.google.com/search?client=googlet&q= query

Given this information, here is the programmatic equivalent of searching for “oracle pl/sql programming” (including the double quotes) using Google:

SET DEFINE OFF
DECLARE
   url VARCHAR2(64)
      := 'http://www.google.com/search?client=googlet&q=';
   qry VARCHAR2(128) := UTL_URL.escape('"oracle pl/sql programming"'),
   result CLOB;
BEGIN
   result := HTTPURITYPE(url || qry).getclob;
END;
/

Oracle’s handy UTL_URL.ESCAPE function transforms the query by translating spaces and double quotes into their hex equivalents. If you’re curious, the escaped text from the example is:

%22oracle%20pl/sql%20programming%22

Let’s take a look at using POST in a slightly more complicated example. When I looked at the source HTML for http://www.apache.org, I found that their search form’s “action” was http://search.apache.org, that the form uses the POST method, and that their search box is named “query”. With POST, you cannot simply append the data to the URL as with GET; instead you send it to the web server in a particular form. Here is some code that POSTs a search for the string “oracle pl/sql” (relevant additions highlighted):

DECLARE
   req UTL_HTTP.req;
   resp UTL_HTTP.resp;
   qry VARCHAR2(512) := UTL_URL.escape('query=oracle pl/sql'),
BEGIN
   req := UTL_HTTP.begin_request('http://search.apache.org/', 'POST', 'HTTP 
/1.0');
   UTL_HTTP.set_header(req, 'User-Agent', 'Mozilla/4.0'),
   UTL_HTTP.set_header(req, 'Host', 'search.apache.org'),
   UTL_HTTP.set_header(req, 'Content-Type', 'application/x-www-form-urlencoded'),
   UTL_HTTP.set_header(req, 'Content-Length', TO_CHAR(LENGTH(qry)));
   UTL_HTTP.write_text(req, qry);
   resp := UTL_HTTP.get_response(req);
 
  ...now we can retrieve the results as before (e.g., line by line)
 
   UTL_HTTP.end_response(resp);
END;
/

In a nutshell, the BEGIN_REQUEST includes the POST directive, and the code uses write_text to transmit the form data. While POST does not allow the name/value pairs to be appended to the end of the URL (like GET queries), this site allows the x-www-form-urlencoded content type , allowing name/value pairs in the qry variable that we send to the server.

Tip

The earlier Apache example shows one other additional header that my other examples don’t use:

UTL_HTTP.set_header(req, 'Host', 'search.apache.org'),

Without this header, Apache’s site was responding with their main page, http://www.apache.org , rather than their search page, http://search.apache.org. The Host header is required for web sites that use the “virtual host” feature—that is, one IP address serves two or more hostnames—so the web server knows what site you’re looking for. The good thing is that you can always include the Host header, even if the remote site does not happen to serve virtual hosts.

By the way, if you have more than one item in the form to fill out, URL encoding says that each name/value pair must be separated with an ampersand:

name1=value1&name2=value2&name3= ...

Okay, you’ve got all your GETs and POSTs working now, so you are all set to go forth and fetch...right? Possibly. It’s likely your code will sooner or later run afoul of the HTTP “redirect.” This is a special return code that web servers send, which means “sorry, you need to go over there to find what you are looking for.” We are accustomed to letting our browsers handle redirects for us silently and automatically, but it turns out that the underlying implementation can be tricky: there are at least five different kinds of redirect, each with slightly different rules about what is “legal” for the browser to do. You may encounter redirects with any web page, but for many of them you should be able to use a feature in UTL_HTTP to follow redirects. That is:

UTL_HTTP.set_follow_redirect 
 (max_redirects IN PLS_INTEGER DEFAULT 3);

Unfortunately, while testing code to retrieve a weather forecast page from the U.S. National Weather Service, I discovered that their server responds to a POST with a 302“Found” redirect. This is an odd case in the HTTP standard, which holds that clients should not follow the redirect...and Oracle’s UTL_HTTP adheres to the letter of the standard, at least in this case.

So, I have to ignore the standard to get something useful from the weather page. My final program to retrieve the weather in Sebastopol, California appears below:

/* File on web: orawx.sp */
CREATE OR REPLACE PROCEDURE orawx
AS
   req UTL_HTTP.req;
   resp UTL_HTTP.resp;
   line VARCHAR2(32767);
   formdata VARCHAR2(512) := 'inputstring=95472';   -- zip code
   newlocation VARCHAR2(1024);
BEGIN
   req := UTL_HTTP.begin_request('http://www.srh.noaa.gov/zipcity.php',
            'POST', UTL_HTTP.http_version_1_0);
   UTL_HTTP.set_header(req, 'User-Agent', 'Mozilla/4.0'),
   UTL_HTTP.set_header(req, 'Content-Type', 'application/x-www-form-urlencoded'),
   UTL_HTTP.set_header(req, 'Content-Length', TO_CHAR(LENGTH(formdata)));
   UTL_HTTP.write_text(req, formdata);
   resp := UTL_HTTP.get_response(req);
 IF resp.status_code = UTL_HTTP.http_found
   THEN
      UTL_HTTP.get_header_by_name(resp, 'Location', newlocation);
      req := UTL_HTTP.begin_request(newlocation);
      resp := UTL_HTTP.get_response(req);
   END IF;
 
   ...process the resulting page here, as before...
 
   UTL_HTTP.end_response(resp);
END;

Figure 21-2 gives an idea of the interaction between this code and the web server.

Getting the Sebastopol weather from NOAA involves following a “302 found” redirection

Figure 21-2. Getting the Sebastopol weather from NOAA involves following a “302 found” redirection

I don’t know how common a problem that is, and my “fix” is not really a general-purpose solution for all redirects, but it gives you an idea of the kinds of quirks you may run into when writing this sort of code.

Disable Cookies or Make Cookies Persistent

For better or for worse, session-level cookie support is enabled by default in recent versions of UTL_HTTP. Oracle has set a default of 20 cookies allowed per site and a total of 300 per session. To check whether this is true for your version of UTL_HTTP, use the following:

DECLARE
   enabled BOOLEAN;
   max_total PLS_INTEGER;
   max_per_site PLS_INTEGER;
BEGIN
   UTL_HTTP.get_cookie_support(enabled, max_total, max_per_site);
   IF enabled
   THEN
      DBMS_OUTPUT.PUT('Allowing ' || max_per_site || ' per site'),
      DBMS_OUTPUT.PUT_LINE(' for total of ' || max_total || ' cookies. '),
   ELSE
      DBMS_OUTPUT.PUT_LINE('Cookie support currently disabled.'),
   END IF;
END;

Cookie support is transparent; Oracle automatically stores cookies in memory and sends them back to the server when requested.

Cookies disappear when the session ends. If you’d like to make cookies persistent, you can save them into Oracle tables and then restore them when you start a new session. To do this, have a look at the sample code that Oracle provides in the UTL_HTTP section of the Packages and Types manual.

To completely disable cookie support for all your UTL_HTTP requests for the remainder of your session, use this code:

UTL_HTTP.set_cookie_support 
(FALSE);

To disable cookies for a particular request:

UTL_HTTP.set_cookie_support(req, FALSE);

To change the number of cookies from Oracle’s default values:

UTL_HTTP.set_cookie_support(TRUE,
   max_cookies =>n,
   max_cookies_per_site => m);

Retrieve Data from an FTP Server

Oracle does not provide out-of-the-box support for retrieving data from FTP sites via PL/SQL. However, if you need to send or receive files via FTP, there is at least one open source PL/SQL solution available that you can use, written by an industrious fellow by the name of Barry Chase. His code, written with UTL_TCP and UTL_FILE (no Java required), supports FTP put, get, rename, and delete operations, and includes both binary and ASCII transport modes. Retrieved files get saved in the server’s filesystem, but it would be easy enough to add the ability to read a file into a LOB after it’s retrieved. You can find a link to Barry’s code by visiting http://plnet.org.

Use a Proxy Server

A number of corporations force all web traffic out through a proxy server . Oracle includes support for this configuration in UTL_HTTP. For example, if your proxy is running on port 8888 at 10.2.1.250:

DECLARE
   req UTL_HTTP.req;
   resp UTL_HTTP.resp;
BEGIN
   UTL_HTTP.set_proxy(proxy => '10.2.1.250:8888',
       no_proxy_domains => 'mycompany.com, hr.mycompany.com'),
 
   req := UTL_HTTP.begin_request('http://some-remote-site.com'),
 
   /* If your proxy requires authentication, use this: */
   UTL_HTTP.set_authentication(r => req,
      username => 'username',
      password => 'password',
      for_proxy => TRUE);
 
   resp := UTL_HTTP.get_response(req);
 
   ...etc.

I happened to test this code on a proxy server that uses Microsoft NTLM-based authentication. After an embarrassing amount of trial and error, I discovered that I had to prefix my username with the Microsoft server “domain name” plus a backslash. That is, if I normally log in to the “mis” domain as user bill with password swordfish, I must specify:

     username => 'misill', password => 'swordfish'

Other Types of I/O Available in PL/SQL

This chapter has focused on some of the types of I/O that we think are most useful in the real world and that aren’t well covered elsewhere. But what about these other types of I/O?

  • Database pipes, queues, and alerts

  • TCP sockets

  • Oracle’s built-in web server

Database Pipes, Queues, and Alerts

The DBMS_PIPE built-in package was originally designed as an efficient means of sending small bits of data between separate Oracle sessions. With the introduction of autonomous transactions, database pipes are no longer needed if they are simply being used to isolate transactions from each other. Database pipes can also be used to manually parallelize operations.

Database queuing is a way to pass messages asynchronously among Oracle sessions. There are many variations on queuing: single versus multiple producers, single versus multiple consumers, limited-life messages, priorities, and more. The latest incarnation of Oracle’s queuing features is covered in the Oracle manual called Oracle Streams Advanced Queuing User’s Guide and Reference.

The DBMS_ALERT package allows synchronous notification to multiple sessions that various database events have occurred. My impression is that this feature is rarely used today; Oracle provides other extensive features that fill a similar need but with more features.

You can read more about pipes and alerts in the chapter “Intersession Communication” in O’Reilly’s Oracle Built-in Packages. For your convenience, we have posted that chapter on our book’s web site.

TCP Sockets

As interesting a subject as low-level network programming may be to some strange folks (including yours truly), it’s just not a widely needed feature. In addition to the UTL_TCP built-in package, Oracle also supports invocation of the networking features in Java stored procedures, which you can invoke from PL/SQL.

Oracle’s Built-in Web Server

Even if you haven’t licensed the Oracle Application Server 10g product, you still have access to an HTTP server built in to the Oracle RDBMS. Configuration of the built-in server varies according to Oracle version, but the PL/SQL programming side of it, including the OWA_UTIL, HTP, and HTF packages, has remained relatively stable.

These packages let you generate database-driven web pages directly from PL/SQL. This is a fairly extensive topic, particularly if you want to generate and process HTML forms in your web page—not to mention the fact that HTTP is a stateless protocol, so you don’t really get to set and use package-level variables from one call to the next. O’Reilly’s Learning Oracle PL/SQL provides an introduction to PL/SQL that makes heavy use of the built-in web server and provides a number of code samples. The PL/SQL coding techniques are also applicable if you happen to be using Oracle’s separate, full-blown application server product; for more information about this product, see Oracle Application Server 10g Essentials by Rick Greenwald, Robert Stackowiak, and Donald Bales (O’Reilly).



[*] SMTP is one of many Internet acronyms governed by other acronyms. Simple Mail Transfer Protocol is governed by Request for Comment (RFC) 2821, which obsoletes RFC 821.

[*] Modern mail programs generally support 8-bit character transfer per an SMTP extension known as 8BITMIME. You can discover whether it’s supported via SMTP’s EHLO directive.

[] Multipurpose Internet Mail Extensions, as set forth in RFC 2045, 2046, 2047, 2048, and 2049, and updated by 2184, 2231, 2646, and 3023. And then some...

[*] Newer versions of Microsoft Internet Explorer do not support this syntax because of security concerns.

[*] Thanks to Tom Kyte for spelling this out in plain English on http://asktom.oracle.com.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.114.221