Friday, July 20, 2012

Emailing image via PLSQL


Today's example is a practical oldie on working with binary files in PLSQL.  I recently had a requirement to attach a file to an email out of Concurrent Manager but the file could be one of several types.  I frequently generated flat files as attachments but had never attached images to an email.  Additionally, my previous examples would work with files that were stored in database tables as BLOBS.  This article will explain the following two items:

1. create a temporary LOB from a BFILE
2. attach LOB to an email as a raw datatype

First, create a database directory that will contain the BLOB you want to attach to an email.  In this example we will use a .jpg of my dog, Layla:


Next, I will FTP my file to the database tier to /usr/tmp:



The setup of utl_smtp is not covered in this topic - the author assumes your DBA configured this correctly and it is available for use.  Below is the complete script to build and send the image as an attachment via utl_smtp.  We'll discuss each part of it below after we run it and see the results:


set serveroutput on size 1000000
declare
--
lv_oracle_directory            varchar2(240)        := '/usr/tmp';
lv_file_name                   varchar2(240)        := 'layla.jpg';
lv_smtp_host                   varchar2(240)        := '192.168.X.X';
lv_list                        varchar2(240)        := 'oracle-hack@hackity.com';
--
m_mail_conn                    utl_smtp.connection;
m_crlf                         varchar2(2)          := utl_tcp.crlf; -- carriage return linefeed
m_boundary                     varchar2(60)         := '6a_j9dh3sk2lao9vdse830';
m_first_boundary               varchar2(60)         := '--'|| m_boundary || m_crlf;
m_last_boundary                varchar2(60)         := '--'|| m_boundary || '--' || m_crlf;
--
lb_lob                         blob;
ln_amount                      number;
ln_position                    number;
lr_raw                         raw(2000);
--
l_bfile_source_location        bfile;
li_size                        integer;
i                              pls_integer;
li_length                      pls_integer;
--
ln_max_base64_line_width       constant pls_integer := 76 / 4 * 3;
--
begin
   --
   m_mail_conn := utl_smtp.open_connection(lv_smtp_host, 25);
   --
   utl_smtp.helo(m_mail_conn, lv_smtp_host);
   utl_smtp.mail(m_mail_conn, 'Oracle_eBiz@hackity.com'); -- needs to look and feel like an email addy with '@' and '.'
   utl_smtp.rcpt(m_mail_conn,lv_list);
   utl_smtp.open_data(m_mail_conn);
   --
   -- MIME header
   --
   utl_smtp.write_data(m_mail_conn,'MIME-Version: 1.0' || utl_tcp.crlf);
   utl_smtp.write_data(m_mail_conn, 'Content-Type: multipart/mixed; boundary="'||m_boundary||'"'||m_crlf);
   utl_smtp.write_data(m_mail_conn, 'Subject: '||'Test attachment'||m_crlf||m_crlf);
   --
   -- email body text
   --
   utl_smtp.write_data(m_mail_conn, m_first_boundary);
   utl_smtp.write_data(m_mail_conn, 'Content-Type: '||'text/plain'||m_crlf);
   utl_smtp.write_data(m_mail_conn, ' charset=US-ASCII'||m_crlf);
   utl_smtp.write_data(m_mail_conn,'Hello, world...see the attached file.'||m_crlf);
   --
   -- first attachment MIME header creation
   --
   utl_smtp.write_data(m_mail_conn, m_first_boundary);
   utl_smtp.write_data(m_mail_conn, 'Content-Type: "image/jpeg"; name="'||lv_file_name||'"'||m_crlf);
   utl_smtp.write_data(m_mail_conn, 'Content-Transfer-Encoding: base64'||m_crlf);
   utl_smtp.write_data(m_mail_conn, 'Content-Disposition: attachment; filename="'||lv_file_name||'"'||m_crlf);
   --
   -- create a temporary LOB (versus storing in a table)
   --
   dbms_lob.createtemporary(lb_lob, true, dbms_lob.session);
   l_bfile_source_location := bfilename('GVS_DOG', 'layla.jpg');
   li_size := dbms_lob.getlength(l_bfile_source_location);
   --
   dbms_output.put_line('li_size: '||li_size);
   dbms_lob.open(l_bfile_source_location);
   --
   dbms_lob.loadfromfile(dest_lob => lb_lob                  -- IN OUT NOCOPY  CLOB CHARACTER SET ANY_CS,
                        ,src_lob  => l_bfile_source_location -- IN             BFILE,
                        ,amount   => li_size);               -- IN             INTEGER,
   --
   i := 1;
   li_length := dbms_lob.getlength(lb_lob);
   dbms_output.put_line('length li_length: '||li_length);
   --
   while (i < li_length) loop
      if(i + ln_max_base64_line_width < li_length)then
         utl_smtp.write_raw_data (m_mail_conn
                                 ,utl_encode.base64_encode(dbms_lob.substr(lb_lob, ln_max_base64_line_width, i)));
      else
         utl_smtp.write_raw_data(m_mail_conn
                                ,utl_encode.base64_encode(dbms_lob.substr(lb_lob, (li_length - i),  i)));
      end if;
      utl_smtp.write_data(m_mail_conn, utl_tcp.crlf);
      i := i + ln_max_base64_line_width;
   end loop;
   --
   utl_smtp.write_data(m_mail_conn, m_crlf);
   utl_smtp.write_data(m_mail_conn, m_last_boundary);
   utl_smtp.write_data(m_mail_conn, m_crlf);
   utl_smtp.close_data(m_mail_conn);
   utl_smtp.quit(m_mail_conn);
   --
   dbms_lob.close(l_bfile_source_location);
   dbms_lob.freetemporary(lb_lob);
   commit;
   --
exception
   when others then
      dbms_output.put_line('Error: '||sqlerrm);
      rollback;
end;
/

Save this off as utl_smtp and run it from SQL*Plus:



A few seconds later there is a package:



Now that we know this works, let's take a brief look at some of the script:





  1. lv_smtp_host - this needs to be defined by your mail people.  I have sometimes seen network aliases created so you can use mail.hostname.com in case the IP address changes
  2. The recipient is hard-coded as one value here and must be a valid email address format or you'll potentially see the error Error: ORA-29279: SMTP permanent error: 501 5.1.3 Bad recipient address syntax
  3. m_boundary - this arbitrary value is used throughout the mail message you build to separate each section.  The value should not exist anywhere in the mail message.  The initial boundary starts with two dashes and the last boundary of the email ends with two dashes.
  4. I copied this formula from several examples I found on the webernets.  The first time I tried to attach the file by breaking it up into 1000 byte chunks it came through garbled.  Not understanding this very well I then went to RFC 2045 (Multipurpose Internet Mail Extensions) for an explanation that states the file must be transferred in base64 encoding and also must be chunked into pieces no larger than 76 characters not including the carriage-return line feed.
  5. This address also nees to be in a valid format or you'll see error Error: ORA-29279: SMTP permanent error: 501 5.1.7 Bad sender address syntax
  6. Because this message is plain text as well as base64 you use this particular content-type  This is also where you specifiy what the boundary string will be but do not use any dashes in front of it.
  7. The body of the email is created using a plain-text content-type and an ASCII character set.  This is also where you specify the first boundary and it includes the "--"
  8. The same boundary is used but repeated here so separate the body from the attachment.  base64 is specified as the encoding as well as the attachment and filename.
  9. This line creates a temporary lob.  Previously I would pull LOBs out of tables using a SQL statement or procedure.  In this example we want to grab it from the filesystem for use as an attachment.  This line creates an empty lob ready to accept the file
  10. This will assign the file to the empty lob
  11. write the image attachment in 76 byte chunks







Tuesday, April 24, 2012

Launching XML Publisher from OA Framework

Today's example shows how to generate an XML Publisher report from OA Framework. It uses the delivered DocumentViewer bean to invoke an XML Pub report by clicking a button, instead of running a Concurrent Manager program. Rather than just create a "Hello, world" example, we'll embed our custom report in a SSHR function to show how to feed in a parameter from the page.
Lettuce use the Personal Information screen in SSHR to add a button where an employee can generate an Employment Verification letter. I'm a techie and not a generalist so this might not be a great real-world HR example but it is meant to showcase embedding an XML Publisher report inside OAF.
First, lettuce create an XML Publisher report that contains an employee's basic person, assignment and salary information. I like to avoid Oracle Reports altogether so I build my own Data Definition as follows:

Save the above as an .xml file and upload it as an XML Publisher Data Definition by browsing to the following:
(N) XML Publisher -> Data Definitions -> Create
Configure as follows, browsing for your newly created .xml file:

While the point of this exercise is to create and XML Publisher report launched from OA Framework and not from Concurrent Manager, I will also create a Concurrent Program to demonstrate that the report can be launched from two places in Oracle Applications. I also will use the generated XML to create the XML Template for the letter. Here is the configuration of my Concurrent Program that calls XDODTEXE:


Prior to running this report we need to create a PLSQL package that will transfer the P_PERSON_ID parameter into the Data Definition. Copy and paste:
create or replace package gvs_emp_verif_pkg as
--
p_person_id number;
--
gc_person_id varchar2(300);
--
function before_report return boolean;
--
end gvs_emp_verif_pkg;
/
show errors;
create or replace package body gvs_emp_verif_pkg as
--
function before_report return boolean is
--
begin
--
gc_person_id := ' and papf.person_id = '||p_person_id;
--
return true;
--
end before_report;
--
end gvs_emp_verif_pkg;
/
show errors;
Compile these as APPS and then run the report...

After running the report you'll save this output as .xml and construct your .rtf template. Only a summary of the steps necessary for merging an .xml file with a template are shown here. For an end-to-end example, see the previous article "Using XML Publisher without Report Builder":
This is the draft template I will load into XML Publisher:

Browse to (N) XML Publisher -> Templates -> Create Template and configure as follows:

Here's how the letter looks when combining the XML with the template.

Our goal is to get this to launch out of an OA Framework page rather than a Concurrent Program. Here are the steps:
1. Create a custom OA Framework page that extends delivered /oracle/apps/xdo/oa/common/webui/DocumentViewerRn.MainRegion
2. Associate a Controller .class and Application Module to this custom page that invokes method "getXMLData"
3. Create an FND Form Function for the custom page
3. Use personalizations to create a button that points at the FND Form Function
One step at a time, lettuce create our page in JDeveloper:

Right-click your MainRN and add a new region. Populate the "Extends" property with the value /oracle/apps/xdo/oa/common/webui/DocumentViewerRn.MainRegion. You will see the region information populate and be grayed-out since it is delivered by Oracle.

Right-click your MainRN and choose Set New Controller so you can write a few lines of Java code that will pass the person id to the report.

If your JDeveloper application runs as slow as mine, leave this simmer for a few minutes while you personalize (configure) a button in Self-Service HR that will invoke this Page and Controller.
(N) Employee Self-Service -> Personal Information

In the above case I created my own custom responsibility, menus and functions. Click the personalize link in the upper right hand corner:

Click the Create icon next to the cell that has the Update button:

Configure as follows:

That configuration will call our custom page and OA Framework code. Since we haven't developed nor deployed that, return to JDeveloper to complete it. Recall that we have already created a page that has just the delivered DocumentViewer bean and we set a custom Controller. Here is how you should write your Controller code:

Here's the text in case you'd like to copy/paste. I removed some white-space so it would fit on the screen print above.
package hack.oracle.apps.per.hack.webui;
import oracle.apps.fnd.common.VersionInfo;
import oracle.apps.fnd.framework.webui.OAControllerImpl;
import oracle.apps.fnd.framework.webui.OAPageContext;
import oracle.apps.fnd.framework.webui.beans.OAWebBean;
import oracle.apps.fnd.framework.OAApplicationModule;
import oracle.jbo.domain.BlobDomain;
import oracle.apps.xdo.oa.common.DocumentHelper;
import java.io.*;
public class gvsDocCO extends OAControllerImpl
{
public static final String RCS_ID="$Header$";
public static final boolean RCS_ID_RECORDED =
VersionInfo.recordClassVersion(RCS_ID, "%packagename%");
/**
* Layout and page setup logic for a region.
* @param pageContext the current OA page context
* @param webBean the web bean corresponding to the region
*/
public void processRequest(OAPageContext pageContext, OAWebBean webBean)
{
super.processRequest(pageContext, webBean);
pageContext.writeDiagnostics(this,"GVS - Version 2.0",3);
String strPersonId = pageContext.getParameter("PersonId");
pageContext.writeDiagnostics(this,"GVS - strPersonId: " + strPersonId,3);
//
OAApplicationModule oaAM = pageContext.getApplicationModule(webBean);
//
pageContext.putParameter("p_DataSource",DocumentHelper.DATA_SOURCE_TYPE_BLOB);
pageContext.putParameter("p_DataSourceCode","GVS_EMP_VERIF");
pageContext.putParameter("p_DataSourceAppsShortName","PER");
pageContext.putParameter("p_TemplateCode","GVS_EMP_VERIF");
pageContext.putParameter("p_TemplateAppsShortName","PER");
pageContext.putParameter("p_Locale","Default");
pageContext.putParameter("p_Locale","English:United States");
pageContext.putParameter("p_OutputType","PDF");
pageContext.putParameter("p_XDORegionHeight","200%");
Serializable[] oaParams = {strPersonId};
BlobDomain result = (BlobDomain)oaAM.invokeMethod("getGVSData",oaParams);
pageContext.putSessionValueDirect("XML_DATA_BLOB", result);
}
public void processFormRequest(OAPageContext pageContext, OAWebBean webBean)
{
super.processFormRequest(pageContext, webBean);
}
}
Notice that I am doing all of the code in the processRequest. This event fires when the page loads as opposed to pushing a button to generate the document. I want to click a button, launch the page and have the document launch automatically. If you would like the document creation to occur only if you press a button on your custom page just move the last two lines into the processFormRequest.
Also notice that I set the default as "PDF" using this line:
pageContext.putParameter("p_OutputType","PDF");
If you include a default output type you will not get a drop-down from Oracle that allows you to choose a different document type. I will show how it looks both ways later. For now, continue your Jdeveloper hacking by creating an Application Module.
Right-click your project and choose New Application Module...

Here's what it looks like in JDeveloper:

And the text for copy/paste purposes:
package hack.oracle.apps.per.hack.server;
import oracle.apps.fnd.framework.server.OAApplicationModuleImpl;
import oracle.apps.fnd.framework.OAException;
import oracle.apps.fnd.framework.server.OADBTransactionImpl;
import oracle.apps.fnd.framework.server.OADBTransaction;
import oracle.jbo.domain.BlobDomain;
import oracle.apps.xdo.oa.util.DataTemplate;
import java.sql.SQLException;
import oracle.apps.xdo.XDOException;
import com.sun.java.util.collections.Hashtable;
public class gvsPubAMImpl extends OAApplicationModuleImpl {
/**This is the default constructor (do not remove)
*/
public gvsPubAMImpl() {
}
/**Sample main for debugging Business Components code using the tester.
*/
public static void main(String[] args) {
launchTester("hack.oracle.apps.per.hack.server", /* package name */
"gvsPubAMLocal" /* Configuration Name */);
}
public BlobDomain getGVSData (String strPersonId)
{
BlobDomain blobDomain = new BlobDomain();
try
{
DataTemplate datatemplate = new DataTemplate(((OADBTransactionImpl)getOADBTransaction()).getAppsContext(), "PER", "GVS_EMP_VERIF");
Hashtable parameters = new Hashtable();
parameters.put("P_PERSON_ID",strPersonId);
datatemplate.setParameters(parameters);
datatemplate.setOutput(blobDomain.getBinaryOutputStream());
datatemplate.processData();
}
catch(SQLException e)
{
throw new OAException("SQL Error=" + e.getMessage(),OAException.ERROR);
}
catch (XDOException e)
{
throw new OAException("XDOException" + e.getMessage(),OAException.ERROR);
}
catch(Exception e)
{
throw new OAException("Exception" + e.getMessage(),OAException.ERROR);
}
return blobDomain;
}
}
Notice the line "new DataTemplate" needs your Data Template code as the second parameter; the first parameter is the application short name, in my case I used "PER" for Human Resources. Also, any parameters can be supplied using a Hashtable. Remember that my Concurrent Program has one parameter defined and I called the token "P_PERSON_ID". This must match here when supplying a parameter to it. That's it as far as coding goes. In order to deploy this to Oracle Applications you must perform the following:
1. Make and Rebuild your project in JDeveloper
2. .zip the project on your computer and FTP it to $JAVA_TOP
3. Unzip the project in $JAVA_TOP
4. Import the custom page into the database
5. Bounce apache and OA Core
There are two different ways to import your custom page. One is to use jpximport on your machine. The other is to use the XML Importer on the middle tier. Both work fine; I used the jpximport locally using this command:
C:\JDEVOAFR12\jdevbin\oaext\bin\import C:\JDEVOAFR12\jdevhome\jdev\myclasses\hack\oracle\apps\per\hack\webui\gvsPubPG.xml -rootdir C:\JDEVOAFR12\jdevhome\jdev\myclasses -mmddir "C:\JDEVOAFR12\jdevbin\oaext\config\mmd" -username "apps" -password "password123" -dbconnection "(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=test.hack.com)(PORT=1521))(CONNECT_DATA=(SID=HACK)))"
Once you have completed steps 1-5, log in and press the button you configured earlier:

If you press the Export button in the above picture you can download the file:

Recall that I defaulted the document to open automatically and specified PDF. If you comment out this line:
pageContext.putParameter("p_OutputType","PDF");
You will have the choice to choose HTML, PDF, Excel or RTF:

What's it look like in HTML?

Option to download RTF:

Excel: