Overblog
Folge diesem Blog Administration + Create my blog

jrXml to PDF - a declarative, pure PL/SQL reporting engine

16. April 2012 , Geschrieben von Andreas Weiden Veröffentlicht in #APEX

The task

I'm working on APEX since half a year now and i really like it. I also think its quite "complete" regarding its functionality, besides... well, besides the reporting possibilities.

There are several available solutions, but either they require additional "middleware" (as BI Publisher or Apache FOP), or they are database driven, but not declarative (like PL/PDF or AS_PDF3 by Anton Scheffer).

I don't want that "middleware"-overhead, so i somehow have to deal with the database-driven pdf-generation as a starting point. But what i want is a graphical editor to define my report and then run and render it inside the database.

And there are graphical editors which are very powerful and easy to use, my favorite one is iReport for JasperReports.

The approach

So my approach is quite easy. I want to design my report in a graphical way, then transfer the resulting report-definition to the database, and then render the report using a PL/SQL-package to PDF.And of course i do not want to reinvent the wheel but use already existing code, if possible.

  • There already is a tool i would like to use to design my report, that's iReport.
  • There already is a tool which is capable of creating PDF-files from inside the database, the one i choose here is AS_PDF3 from Anton Scheffler ( i like it for its simplicity).

There is only one part missing, that's the "thing in the middle" which is capable of reading my report-definition and translate the single pieces to statement-call's to the procedures inside the AS_PDF3-package.

The good news is that reading the report-definition is quite simple, as iReport stores the definition as xml-file.

Here's a simple Report-definition which just renders a thin blue line.

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="simple" language="groovy" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20">
    <queryString language="SQL">
        <![CDATA[SELECt 1 X FROM DUAL]]>
    </queryString>
    <field name="X" class="java.math.BigDecimal"/>
    <background>
        <band splitType="Stretch"/>
    </background>
    <title>
        <band height="79" splitType="Stretch">
            <line>
                <reportElement x="202" y="38" width="100" height="1" forecolor="#0000CC"/>
            </line>
        </band>
    </title>
    <pageHeader>
        <band splitType="Stretch"/>
    </pageHeader>
    <columnHeader>
        <band splitType="Stretch"/>
    </columnHeader>
    <detail>
        <band splitType="Stretch"/>
    </detail>
    <columnFooter>
        <band splitType="Stretch"/>
    </columnFooter>
    <pageFooter>
        <band splitType="Stretch"/>
    </pageFooter>
    <summary>
        <band splitType="Stretch"/>
    </summary>
</jasperReport>

The result looks as simple.

jrxml2pdf simple

A "real" report in iReport basically exists of

  • a title-region rendered once for the report
  • a pageheader-region rendered ony for each page at the top of the page
  • a columnheader-region rendered once on each page before the data
  • a detail-region rendered once for each record of data
  • a columnheader-region rendered once on each page after the data
  • a pagefooter-region rendered ony for each page at the bottom of the page
  • a summary-region once for the report

each of this region maybe there or maybe not.

Each region consists of graphical objects, like

  • lines with different colors and line-widths
  • rectangles with different colors and line-widths
  • static text, with or without border
  • textfields, which take the data from specific fields of a record from the report-query and show them
  • images, either static or taken from the database (a blob-column)
  • subreports, which can be used to implement master-detail-reports

There is more, but for a first version of my package this is enough to implement.

The implementation

My package consists of three parts

  • the logic for recursively reading the xml-structure of the report-definition.
  • the rendering part which works out the logic of the different objects of the report-definition and calls the render-procedures for the different objects.
  • the render-methods, which "translate" the logical object (e.g. a line) to its physical implementation.

The reading logic

I created a report-table with an CLOB-column in which the reports xml is stored. I concentrated on the, in my opinion, are the most important objects and attributes of the report, details will be added later. I won't show the complete code, as an example here's the code of the procedure reading the main report-definition.

  FUNCTION FK_LOAD_REPORT (i_vcReportName IN VARCHAR2)
  RETURN tReport IS
    CURSOR crReport IS
      SELECT EXTRACTVALUE(XML, '/jasperReport/@pageWidth'   ) PAGE_WIDTH,
             EXTRACTVALUE(XML, '/jasperReport/@pageHeight'  ) PAGE_HEIGHT,
             EXTRACTVALUE(XML, '/jasperReport/@leftMargin'  ) LEFT_MARGIN,
             EXTRACTVALUE(XML, '/jasperReport/@rightMargin' ) RIGHT_MARGIN,
             EXTRACTVALUE(XML, '/jasperReport/@topMargin'   ) TOP_MARGIN,
             EXTRACTVALUE(XML, '/jasperReport/@bottomMargin') BOTTOM_MARGIN,
             EXTRACT     (XML, '/jasperReport/title'        ) TITLE_XML,
             EXTRACT     (XML, '/jasperReport/pageHeader'   ) PAGEHEADER_XML,
             EXTRACT     (XML, '/jasperReport/columnHeader' ) COLUMNHEADER_XML,
             EXTRACT     (XML, '/jasperReport/detail'       ) DETAIL_XML,
             EXTRACT     (XML, '/jasperReport/columnFooter' ) COLUMNFOOTER_XML,
             EXTRACT     (XML, '/jasperReport/pageFooter'   ) PAGEFOOTER_XML,
             EXTRACT     (XML, '/jasperReport/summary'      ) SUMMARY_XML,
             EXTRACTVALUE(XML, '/jasperReport/queryString'  ) QUERY_STRING
        FROM (SELECT XMLTYPE(REPORT_XML) XML
                FROM REPORTS
               WHERE REPORT_NAME =i_vcReportName
             );
    recReport crReport%ROWTYPE;
    rReport tReport;
  BEGIN
    OPEN crReport;
    FETCh crReport INTO recReport;
    IF crReport%FOUND THEN
      rReport.nPageWidth     :=recReport.PAGE_WIDTH;
      rReport.nPageHeight    :=recReport.PAGE_HEIGHT;
      rReport.nLeftMargin    :=recReport.LEFT_MARGIN;
      rReport.nRightMargin   :=recReport.RIGHT_MARGIN;
      rReport.nTopMargin     :=recReport.TOP_MARGIN;
      rReport.nBottomMargin  :=recReport.BOTTOM_MARGIN;
      rReport.xmlTitle       :=recReport.TITLE_XML;
      rReport.xmlPageHeader  :=recReport.PAGEHEADER_XML;
      rReport.xmlColumnHeader:=recReport.COLUMNHEADER_XML;
      rReport.xmlDetail      :=recReport.DETAIL_XML;
      rReport.xmlColumnFooter:=recReport.COLUMNFOOTER_XML;
      rReport.xmlPageFooter  :=recReport.PAGEFOOTER_XML;
      rReport.xmlSummary     :=recReport.SUMMARY_XML;
      rReport.vcQuery        :=recReport.QUERY_STRING;
    END IF;
    CLOSE crReport;
    RETURN rReport;
  END;

For other objects the code looks similar. So its basically translating xml to "flat" record-structures.

The rendering part

This is the most complex part, as i has to deal with all the rules which apply to a report, based on its structure and attributes. As an example here is the main-procedure (shortened)

  PROCEDURE PR_RENDER_REPORT(i_rReport IN tReport) IS

    ..

  BEGIN
    -- Execute the Query and check if there are results
    lResult:=FK_EXECUTE_QUERY(i_rReport.vcQuery, i_rReport.lParams);
   
    IF lResult.COUNT>0 THEn
      -- There are records, start
      PR_INIT_PDF;
   
      PR_SETUP_PAGE(i_nWidth       =>i_rReport.nPageWidth,
                    i_nHeight      =>i_rReport.nPageHeight,
                    i_nLeftMargin  =>i_rReport.nLeftMargin,
                    i_nRightMargin =>i_rReport.nRightMargin,
                    i_nTopMargin   =>i_rReport.nTopMargin,
                    i_nBottomMargin=>i_rReport.nBottomMargin
                   );


      rArea:=FK_RENDER_REGION(i_rReport.xmlTitle, 'title', rArea, lResult, NO_RECORD, TRUE, FALSE, FALSE);
      rArea:=FK_RENDER_REGION(i_rReport.xmlPageHeader, 'pageHeader', rArea, lResult,NO_RECORD, FALSE, FALSE, FALSE);
      rArea:=FK_RENDER_REGION(i_rReport.xmlColumnHeader, 'columnHeader', rArea, lResult, NO_RECORD, TRUE, nPageHeaderheight>0, nPageFooterheight>0);


      FOR i IN 1..lResult.COUNT LOOP
        -- Check if region fits on page
        IF FK_FITS_IN_PAGE(rArea.nY, nDetailHeight, nColumnFooterHeight, nPageFooterheight) THEN
          rArea:=FK_RENDER_REGION(i_rReport.xmlDetail, 'detail', rArea, lResult, i, TRUE, nPageHeaderheight>0, nPageFooterheight>0);
        ELSE
          -- Finish page
          rArea:=FK_RENDER_REGION(i_rReport.xmlColumnFooter, 'columnFooter', rArea, lResult, i,TRUE, nPageHeaderheight>0, nPageFooterheight>0);
          -- psoition to bottom for page-footer
          rArea:=FK_RENDER_REGION(i_rReport.xmlPageFooter, 'pageFooter', rArea, lResult, i, FALSE, FALSE, FALSE);
          PR_NEW_PAGE;
          rArea:=FK_RENDER_REGION(i_rReport.xmlPageHeader, 'pageHeader', rArea, lResult,i, FALSE, FALSE, FALSE);
          rArea:=FK_RENDER_REGION(i_rReport.xmlColumnHeader, 'columnHeader', rArea, lResult, i, TRUE, nPageHeaderheight>0, nPageFooterheight>0);
          rArea:=FK_RENDER_REGION(i_rReport.xmlDetail, 'detail', rArea, lResult, i, TRUE, nPageHeaderheight>0, nPageFooterheight>0);
        END IF; 
      END LOOP;
      rArea:=FK_RENDER_REGION(i_rReport.xmlColumnFooter, 'columnFooter', rArea, lResult, NO_RECORD, TRUE, nPageHeaderheight>0, nPageFooterheight>0);
      rArea:=FK_RENDER_REGION(i_rReport.xmlSummary, 'summary', rArea, lResult, NO_RECORD, TRUE, FALSE, FALSE);

      -- psoition to bottom for page-footer
      rArea.nY:=rPageSetup.nPageHeight-rPageSetup.nBottomMargin-rPageSetup.nTopMargin-nPageFooterheight;
      rArea:=FK_RENDER_REGION(i_rReport.xmlPageFooter, 'pageFooter', rArea, lResult, NO_RECORD, FALSE, FALSE, FALSE);
      PR_FINISH_PDF;
    END IF;
  END;

The render-methods

The render-methods are used to translate a logical object from the report to the implementation of the AS_PDF3-package by Anton Scheffer. Also, here is an example-procedure, this one's for rendering a line.

  PROCEDURE PR_RENDER_LINE(i_nX          IN NUMBER,
                           i_nY          IN NUMBER,
                           i_nWidth      IN NUMBER,
                           i_nHeight     IN NUMBER,
                           i_nLineWidth  IN NUMBER,
                           i_vcLineColor IN VARCHAR2
                          ) IS
  BEGIN
    IF i_nWidth=1 THEN
      -- vertical line
      AS_PDF3.vertical_line(p_x          =>rPageSetup.nLeftMargin+i_nX,
                                p_y          =>rPageSetup.nPageHeight-i_nY-rPageSetup.nTopMargin,
                                p_height     =>-i_nHeight,
                                p_line_width =>i_nLineWidth,
                                p_line_color =>REPLACE(i_vcLineColor, '#', '')
                                );
    ELSIF i_nHeight=1 THEN
      -- horizontal line
      AS_PDF3.horizontal_line(p_x          =>rPageSetup.nLeftMargin+i_nX,
                                  p_y          =>rPageSetup.nPageHeight-rPageSetup.nTopMargin-i_nY,
                                  p_width      =>i_nWidth,
                                  p_line_width =>i_nLineWidth,
                                  p_line_color =>REPLACE(i_vcLineColor, '#', '')
                                );
    END IF;
  END;
Some examples

The following are some examples which can be generated with the current version of my package

Tabular report on DEMO_PRODUCT_INFO with images

jrxml2pdf image

tabular report on EMP built with the wizard using one of iReports predefined template (cherry).

jrxml2pdf cherry layout

Master-detail-report on DEPT and EMP

jrxml2pdf dept emp

 

Whats next

 

The package is currently in a beta-state, i hope to be able to publish a first stable version in maybe a month.

Weiterlesen

A generic search-functionality

10. April 2012 , Geschrieben von Andreas Weiden Veröffentlicht in #APEX

The task

APEX has a nice search-functionality to search all objects in the application by just entering one search-condition. From the result-page of the search you can then directly navigate to the pages where you can edit the found objects. My idea was to create something similar for a self-build APEX-application.

The approach

Not all fields in an application are worth to be searched on. So my approach is to create a table with all "searchable column" in the application along with some additional information.

  • Tablename - The name of the table/view to be searched
  • Columnname - The name of the column to be searched
  • ID-Column1 - The first column of the primary key (used later in the page-url to set Page-Item-Values)
  • ID-Column2 - The first column of the primary key (used later in the page-url to set Page-Item-Values)
  • Object-description - a speaking name for the object represented by the table
  • Column-description - a speaking name of whats inside the column
  • Ident-Column - a column to select the rows identifying column (this is in general a unique key to be shown to the enduser)
  • Page-URL - URL to link to the associated APEX-Page. This must be of the format f?p=&APP_ID.:9001:&SESSION_ID.::NO::IDCOLUMN1,IDCOLUMN2:&IDVALUE1.,&IDVALUE2. The variable in that url (&APP_ID. , &SESSION_ID. , &IDVALUE1. , &IDVALUE2. ) are replaced at runtime with the appropaite value

The table-definition looks like this

 

CREATE TABLE SEARCHABLE_FIELDS (  
ID                  NUMBER(38)    NOT NULL,  
TABLE_NAME          VARCHAR2(30)  NOT NULL,  
COLUMN_NAME         VARCHAR2(30)  NOT NULL,   
IDCOLUMN1           VARCHAR2(30)  NOT NULL,  
IDCOLUMN2           VARCHAR2(30)          ,  
OBJECT_DESCRIPTION  VARCHAR2(255) NOT NULL,  
COLUMN_DESCRIPTION  VARCHAR2(255) NOT NULL,  
IDENT_COLUMN        VARCHAR2(30)  NOT NULL,  
PAGE_URL            VARCHAR2(255) NOT NULL);

 

The implementation - db-side

The search-functionality is implemented as a pipelined function in the db, so i need an object type and a collection.

The columns contain the ones from the search-definition-table along with the "found" data for one record.

CREATE OR REPLACE TYPE CO_APX_SEARCH_RESULT AS OBJECT (
  TABLE_NAME         VARCHAR2(30),
  COLUMN_NAME        VARCHAR2(30),
  OBJECT_DESCRIPTION VARCHAR2(4000),
  COLUMN_DESCRIPTION VARCHAR2(4000),
  IDCOLUMN1          VARCHAR2(30),
  IDCOLUMN2          VARCHAR2(30),
  IDVALUE1           VARCHAR2(4000),
  IDVALUE2           VARCHAR2(4000),
  OBJECT_IDENTIFIER  VARCHAR2(4000),
  SEARCH_IDENTIFIER  VARCHAR2(4000),
  PAGE_URL           VARCHAR2(4000)
);

And, as needed for the pipelined function an additional collection on that object.type

CREATE OR REPLACE TYPE COT_APX_SEARCH_RESULT AS TABLE OF CO_APX_SEARCH_RESULT;

At last, we need a function which does the real searching. The function accepts the search-strings as parameter and returns a collection of search-results to the caller. The searching itself is done by dynamic sql based on the search-definitions in the table SEARCHABLE_FIELDS. Here's the function:

CREATE OR REPLACE FUNCTION FK_SEARCH_APEX(i_vcSearchString IN VARCHAR2)
RETURN COT_APX_SEARCH_RESULT PIPELINED
AUTHID CURRENT_USER IS
  CURSOR crSearch IS
    SELECT TABLE_NAME,
           COLUMN_NAME,
           IDCOLUMN1,
           IDCOLUMN2,
           OBJECT_DESCRIPTION,
           COLUMN_DESCRIPTION,
           IDENT_COLUMN,
           REPLACE(REPLACE(PAGE_URL, '&APP_ID.', V('APP_ID')), '&SESSION_ID.', V('SESSION')) PAGE_URL
      FROM SEARCHABLE_FIELDS;
  TYPE tSearch IS TABLE OF crSearch%ROWTYPE;
 
  lSearch         tSearch;
  lResult         COT_APX_SEARCH_RESULT;
  vcQuery         VARCHAR2(32000);
  vcSearchString  VARCHAR2(32000);
  exAsserted      EXCEPTION;
  PRAGMA EXCEPTION_INIT(exAsserted, -06502);
BEGIN
  IF i_vcSearchString IS NOT NULL THEN
    -- Searchstring is defined, Assert for SQL-injection
    BEGIN
      vcSearchString:=DBMS_ASSERT.ENQUOTE_LITERAL('%' || UPPER(i_vcSearchString) || '%');
    EXCEPTION
      WHEN exAsserted THEN
        vcSearchString:=NULL;
    END;
  END IF;
  IF vcSearchString IS NOT NULL THEN
    -- Searchstring is defined, load all searches
    OPEN crSearch;
    FETCH crSearch
    BULK COLLECT INTO lSearch;
    CLOSE crSearch;
    -- check for results
    IF lSearch.COUNT>0 THEN
      -- now do each search via dynamic sql
      FOR i IN 1..lSearch.COUNT LOOP
        vcQuery:='select CO_APX_SEARCH_RESULT('''  || lSearch(i).TABLE_NAME                || ''',''' ||
                                                      lSearch(i).COLUMN_NAME               || ''',''' ||
                                                      lSearch(i).OBJECT_DESCRIPTION        || ''',''' ||
                                                      lSearch(i).COLUMN_DESCRIPTION        || ''',''' ||
                                                      lSearch(i).IDCOLUMN1                 || ''','''  ||
                                                      lSearch(i).IDCOLUMN2                 || ''','  ||
                                                      lSearch(i).IDCOLUMN1                 || ',' ||
                                                      NVL(lSearch(i).IDCOLUMN2, 'NULL')    || ',' ||
                                                      lSearch(i).IDENT_COLUMN              || ',' ||
                                                      lSearch(i).COLUMN_NAME               || ',''' ||
                                                      lSearch(i).PAGE_URL                  || ''')' ||
                  '  FROM ' || lSearch(i).table_name         ||
                  ' WHERE UPPER(' || lSearch(i).column_name || ') LIKE ' || vcSearchString;
        EXECUTE IMMEDIATE vcQuery
        BULK COLLECT INTO lResult;
        FOR j IN 1..lResult.COUNT LOOP
          -- Replace ID-Values in url
          lResult(j).PAGE_URL:=REPLACE(lResult(j).PAGE_URL, '&IDVALUE1.', lResult(j).IDVALUE1);
          lResult(j).PAGE_URL:=REPLACE(lResult(j).PAGE_URL, '&IDVALUE2.', lResult(j).IDVALUE2);
          -- Pipe the result
          PIPE ROW (lResult(j));
        END LOOP;
      END LOOP;
    END IF;
  END IF;
  RETURN;
END;

 

If you now want to extend your application with search-cababilities to new objects, you only have to create addtional records in the SEARCHABLE_FIELDS-table.

 

That's it for the db-side, now let's integrate it in the APEX-application.

 

The implementation - APEX-side - The search page

The first thing we have to do is create a search-page. To do so, we create a new report-page with a report-region based on the following query

SELECT OBJECT_DESCRIPTION,
       OBJECT_IDENTIFIER,
       COLUMN_DESCRIPTION,
       SEARCH_IDENTIFIER,
       PAGE_URL
  FROM TABLE(CAST (FK_SEARCH_APEX(:P9300_SEARCH_FIELD) AS COT_APX_SEARCH_RESULT))

The passing of the search-string is done in the last line of the query as argument to the call of the function, name that item according to your requirements. In my case, i added an additional region as a "Report Filter-Single Row"  region with just that item in it.

 

Also make sure that the report-column for PAGE_URL correctly

  • Diplay As - "Standard Report column"
  • Link Text - "Show"
  • Target - "URL"
  • URL - "#PAGE_URL#"

    search column link

Now we have a search-page which is capable of searching for everything defined in the SEARCHABLE_FIELDS-table and branching to the appropiate page for each result.

 

search search page

 

You can include that page in your menu-structure or tab-bar.

The implementation - APEX-side- Page 1

But i wanted also to have the functionality of the little searchfield in upper right of the tab-bar, as you have it in the application-builder. For this i had to trick a little and tweak in the HTML of the application-builder main-page. From there i took the html-fragments, and the css.

 

First, i created my own template for the page and extended the tab-region for the search-field. In the body of the template, i added the following HTML direct behind the #TAB_CELLS#-tag

 

<div id="appl-search"><table border="0">
<tr>
  <td nowrap="nowrap" align="right"></td>
  <td  colspan="1" rowspan="1" align="left" valign="middle">
    <div class="searchbox-plugin">
      <span class="left"></span>
      <input type="text"  id="P9300_SEARCH_FIELD" value="Anwendung durchsuchen" />
      <span class="right"></span>
    </div>
  </td>
</tr>
</table>
</div>

 

Its important that the id-attribute of the input-item matches the itemname of the searchitem on my search-page.

 

search template body

 

To make this look somehow nice i took the images

  • apex-search-left.gif
  • apex-search-center.gif
  • apex-search-right.gif

from the apex-application-builder installfiles and uploaded them into my application.

Additionally the following styles has to be included either in the pages html-header-attribut or in the application's css-file.

 

<style>
#appl-search {
    width: 165px;
    margin: 9px 20px 0 0;
    float: right;
    position: relative;
}

#appl-search .left {
    display: block;
    height: 19px;
    left: 6px;
    top: 0;
    width: 10px;
    position: absolute;
    background: transparent url(#APP_IMAGES#apex-search-left.gif) no-repeat scroll 0 0;
}

#appl-search input {
    background: transparent url(#APP_IMAGES#apex-search-center.gif) repeat-x scroll center top;
    border: 0 none;
    font-size: 12px;
    color: #404040;
    margin: 0;
    padding: 2px;
    right: 18px;
    top: 0;
    width: 127px;
    outline: none;
    position: absolute;

}

#appl-search .right {
    display: block;
    height: 19px;
    right: 0;
    top: 0;
    width: 19px;
    cursor: pointer;
    position: absolute;
    background: transparent url(#APP_IMAGES#apex-search-right.gif) no-repeat scroll 0 0;
}
</style>

 

Here's the result

 

search page1

 

Obviously, this only looks nice with theme 2 "Builder Blue", for other themes you will have to create your own images and style.

 

The next step is to implement two dynamic actions to make up the functionality of the searchfield. The first action is to remove the text shown inside the search field and make it empty. So we have a dynamic action with the following properties:

  • Event - get Focus
  • Selection Type - Item
  • Item - P9300_SEARCH_FIELD (or the name of what you defined in the template)
  • Action - Execute Javascript
  • Code - $s("P9300_SEARCH_FIELD", "");

search action clear 1

search action clear 2

 

The second dynamic action is to submit the page and redirect to the search-page. This dynamic action has actually two actions, first it submits the item-content of P9300_SEARCH_FIELD to session-state, then it redirects to the search-page. The first step is necessary, because the search-item isn't a "real" page-item defined declaratively in the page-definition.

 

Here are the settings

  • Event - Change
  • Selection Type - Item
  • Item - P9300_SEARCH_FIELD (or the name of what you defined in the template)
  • Condition - Is not null

search action search 1

 

First Action

  • Action - Execute PL/SQL
  • PL/SQL-Code - NULL;
  • Items to submit - P9300_SEARCH_FIELD

search action search 2

 

Second action

  • Action - Submit page 
  • Request - SEARCH

search action search 3

 

And lastly you need a branch in the page processing going to your search-page

  • branch type - branch to page
  • Branch point - On submit .- Before computation
  • Target type - Page in the application
  • Page - 9300 (or the number of the page you use)

search action branch

 

That's it. Now upon entering a search-text in the search-field and leaving the field with TAB or ENTER will redirect to the search-page, passing the search-text. The search-page will execute the report-query, which in turn executes our search-function. The results are shown and you have a link to navigate to the appropiate page.

 

You can test out your own on apex.oracle.com

Weiterlesen