How to Build APEX Plug-ins in PL/SQL Using APEX_PLUGIN and APEX_PLUGIN_UTIL

A tested guide to APEX_PLUGIN and APEX_PLUGIN_UTIL in Oracle APEX 26.1, from render callbacks and Ajax to data, substitutions, and REST sources.

A plug-in adds a new item type, region type, dynamic action, process type, authentication or authorization scheme, REST Data Source type, or AI tool to Oracle APEX. Its PL/SQL code is a set of callbacks (render, Ajax, validation, execution, and others) that APEX calls with records describing the component and the plug-in. APEX_PLUGIN declares those records, and APEX_PLUGIN_UTIL provides the helpers most callbacks need.

This guide walks through both packages with tested examples: the records a callback receives, printing item HTML safely, answering an Ajax call with an LOV as JSON, reading a query with searching and paging, and evaluating the code developers type into plug-in attributes.

Quick Reference

TaskSubprogram
Read the attributes developers setattributes.get_varchar2, get_number, get_boolean
Name the HTML input and the Ajax callAPEX_PLUGIN.GET_INPUT_NAME_FOR_ITEM, GET_AJAX_IDENTIFIER
Print the element's attributes and hidden valuesGET_ELEMENT_ATTRIBUTES, PRINT_HIDDEN, PRINT_READ_ONLY
Answer an Ajax call with an LOVPRINT_LOV_AS_JSON
Read a query with searching and pagingGET_DATA, GET_DATA2, GET_SEARCH_STRING
Look up display valuesGET_DISPLAY_DATA
Run the code developers enteredGET_PLSQL_EXPRESSION_RESULT, GET_PLSQL_FUNCTION_RESULT, and their variants
Substitute, escape, and compareREPLACE_SUBSTITUTIONS, ESCAPE, GET_HTML_ATTR, IS_EQUAL
Do the HTTP work of a REST Data Source plug-inGET_WEB_SOURCE_OPERATION, MAKE_REST_REQUEST, BUILD_REQUEST_BODY

How to Run These Examples

The examples ran in Oracle APEX 26.1 against a test application with ID 200. They need an APEX session of application 200, page 20, created with APEX_SESSION.CREATE_SESSION as shown in the guide to creating APEX sessions and managing session state from PL/SQL. Page 20 has a number item, P20_NUMBER, which one example sets. The products, categories, and orders come from the Orbit Outfitters sample schema in the orb_tables repository on GitHub.

Plug-in callbacks normally run while APEX renders a page or answers an Ajax request. To show what they write, the examples set up a web request buffer with OWA and HTP themselves, call the code, and print the buffer. The output under each example is exactly what the database printed. To create and install a plug-in from scratch, see the Oracle APEX plug-in tutorial.

The Interface: APEX_PLUGIN

Records and Results

Each callback receives the plug-in (apex_plugin.t_plugin), the component, and parameters, and returns a result record. The attributes developers set are in attributes, an apex_t_plugin_attributes object. Read them by the attribute's static ID with get_varchar2, get_number, and get_boolean. The old attribute_01 to attribute_25 fields are deprecated.

RecordDescribes
t_pluginThe plug-in: name, file_prefix, and attributes with scope Application.
t_item, t_item_render_param, t_item_render_resultAn item: id, name, label, placeholder, format_mask, is_required, the LOV settings, element_css_classes, attributes, and more. The render parameter holds the value, is_readonly, and is_printer_friendly. The result holds is_navigable and navigable_dom_id.
t_item_ajax_param, t_item_ajax_result, t_item_validation_param, t_item_validation_resultThe item's Ajax and validation callbacks.
t_region, t_region_render_param, t_region_ajax_param, and their resultsA region: id, static_id, name, source, ajax_items_to_submit, fetched_rows, and attributes.
t_dynamic_action, t_dynamic_action_render_resultA dynamic action: action and attributes. The result names the JavaScript function to run.
t_process, t_process_exec_resultA process: name, success_message, and attributes. The result holds success_message and execution_skipped.
t_authentication, t_authorization, t_web_source, and othersThe other plug-in types.

GET_INPUT_NAME_FOR_ITEM, GET_AJAX_IDENTIFIER, and GET_KEEP_BACKGROUND_EXECS

GET_INPUT_NAME_FOR_ITEM returns the name the HTML input of the item being rendered must have (p_t01 and so on), so that APEX receives its value on submit. GET_AJAX_IDENTIFIER returns the identifier the plug-in's JavaScript passes to apex.server.plugin to call the Ajax callback, as shown in the guide to apex.server and Ajax calls. Both work only while APEX renders the plug-in, and return null otherwise. GET_KEEP_BACKGROUND_EXECS returns whether background executions survive an application upgrade. GET_INPUT_NAME_FOR_PAGE_ITEM is deprecated.

The example below is the render procedure of a star rating item type. It calls the procedure the way APEX would, with the records filled in by hand, once as an editable item and once as read-only, and prints the HTML it writes.

This example needs a session of application 200, page 20.

Example:

declare
    l_item   apex_plugin.t_item;
    l_plugin apex_plugin.t_plugin;
    l_param  apex_plugin.t_item_render_param;
    l_result apex_plugin.t_item_render_result;
    l_page   htp.htbuf_arr;
    l_rows   integer := 999;
    l_name   owa.vc_arr;
    l_val    owa.vc_arr;

    -- the render procedure of an item type plug-in: a star rating as radio buttons
    procedure render_rating(p_item in apex_plugin.t_item, p_plugin in apex_plugin.t_plugin,
                            p_param in apex_plugin.t_item_render_param,
                            p_result in out nocopy apex_plugin.t_item_render_result) is
        l_max number := p_item.attributes.get_number('max_stars', p_default_value => 5);
    begin
        if p_param.is_readonly then
            apex_plugin_util.print_hidden(p_item_name => p_item.name, p_value => p_param.value);
            sys.htp.p(apex_escape.html(p_param.value) || ' of ' || l_max);
            return;
        end if;
        sys.htp.p('<div ' || apex_plugin_util.get_element_attributes(p_item, p_item.name, 'orb-rating') || '>');
        for i in 1 .. l_max loop
            sys.htp.p('<input type="radio" name="' || apex_plugin.get_input_name_for_item || '" value="' || i || '"'
                      || case when i = p_param.value then ' checked' end || '>');
        end loop;
        sys.htp.p('</div>');
        p_result.is_navigable := true;
    end;
begin
    l_name(1) := 'REQUEST_CHARSET'; l_val(1) := 'AL32UTF8';          -- a web request, as ORDS sets it up
    owa.init_cgi_env(1, l_name, l_val);
    htp.init;

    -- what APEX passes: the item's properties, the plug-in, and the value
    l_item.name       := 'P20_STAR_RATING';
    l_item.attributes := apex_t_plugin_attributes(apex_t_varchar2('max_stars', '5'));
    l_param.value     := '4';
    render_rating(l_item, l_plugin, l_param, l_result);
    l_param.is_readonly := true;
    render_rating(l_item, l_plugin, l_param, l_result);

    owa.get_page(l_page, l_rows);
    for i in 3 .. l_rows loop                                          -- skip the HTTP headers
        dbms_output.put(l_page(i));
    end loop;
    dbms_output.new_line;
end;
/

Output:

<div  id="P20_STAR_RATING" name="P20_STAR_RATING" class="orb-rating&#x20;js-ignoreChange" >
<input type="radio" name="" value="1">
<input type="radio" name="" value="2">
<input type="radio" name="" value="3">
<input type="radio" name="" value="4" checked>
<input type="radio" name="" value="5">
</div>
<input type="hidden" name="" id="P20_STAR_RATING" value="4">4 of 5

The input names are empty because the example does not run inside APEX's rendering, so GET_INPUT_NAME_FOR_ITEM returned null. GET_ELEMENT_ATTRIBUTES wrote the item's ID, name, and classes, adding js-ignoreChange to the class given. In the read-only call, PRINT_HIDDEN wrote the hidden input that carries the item's value on submit. The JavaScript side of an item plug-in is apex.item.create, covered in the guide to the apex.item JavaScript API.

Rendering: APEX_PLUGIN_UTIL

SubprogramPrints or returns
GET_ELEMENT_ATTRIBUTES(p_item, p_name, p_default_class, p_add_id, p_add_required, p_add_labelledby, p_aria_describedby_id, p_add_multi_value)The id, name, class, required, ARIA, and custom attributes of the item's element.
GET_HTML_ATTR(p_name, p_value)An escaped name="value" pair with a leading space, or nothing for a null value.
GET_LINK(p_url, p_text, p_escape_text, p_attributes, p_triggering_element)An <a> element.
PRINT_ESCAPED_VALUE(p_value | p_param)A value, HTML-escaped.
PRINT_HIDDEN(p_item_name, p_value), PRINT_HIDDEN_IF_READONLY(p_item, p_param)A hidden input, always or only when the item is read-only.
PRINT_READ_ONLY(p_item, p_param, p_display_value, p_width, p_height, p_css_classes, p_protected, p_escape)The read-only display of an item, with the hidden value.
PRINT_OPTION(p_display_value, p_return_value, p_is_selected, p_attributes, p_escape)An <option> element.
PRINT_JSON_HTTP_HEADERThe HTTP header of a JSON response, in an Ajax callback.
PRINT_LOV_AS_JSON(p_sql_statement, p_component_name, p_escape, p_replace_substitutions, p_set_mime_type)An LOV query's rows as a JSON array of d and r pairs.
ITEM_NAMES_TO_JQUERY(p_item_names, p_item), ITEM_NAMES_TO_DOM(...)A comma-separated list of item names as a jQuery selector or as DOM IDs.
DEBUG_ITEM_RENDER, DEBUG_REGION, DEBUG_DYNAMIC_ACTION, DEBUG_PROCESS, and othersWrite the component's attributes to the debug log.

PRINT_DISPLAY_ONLY, PAGE_ITEM_NAMES_TO_JQUERY, and the first DEBUG_PAGE_ITEM signatures are deprecated. Use PRINT_READ_ONLY, ITEM_NAMES_TO_JQUERY, and DEBUG_ITEM_RENDER instead.

The next example is what the Ajax callback of an item plug-in would do to answer with a list of values. It prints only the content type and the JSON, leaving out the other HTTP headers.

This example needs a session of application 200, page 20.

Example:

declare
    l_page htp.htbuf_arr;
    l_rows integer := 999;
    l_name owa.vc_arr;
    l_val  owa.vc_arr;
begin
    l_name(1) := 'REQUEST_CHARSET'; l_val(1) := 'AL32UTF8';
    owa.init_cgi_env(1, l_name, l_val);
    htp.init;

    -- in the Ajax callback of an item plug-in: answer with the LOV as JSON
    apex_plugin_util.print_lov_as_json(
        p_sql_statement  => 'select category_name as d, category_id as r from orb_categories
                              where parent_category_id is null and category_id <= 3 order by 2',
        p_component_name => 'P20_CATEGORY',
        p_escape         => true);
    owa.get_page(l_page, l_rows);
    for i in 1 .. l_rows loop
        if l_page(i) like 'Content-Type%' or l_page(i) not like '%:%' or l_page(i) like '%"d"%' then
            dbms_output.put(l_page(i));            -- the content type and the JSON, not the other headers
        end if;
    end loop;
    dbms_output.new_line;
end;
/

Output:

Content-Type:application/json; charset=utf-8

[
{"d":"Camping","r":"1"}
,{"d":"Hiking","r":"2"}
,{"d":"Clothing","r":"3"}
]

PRINT_LOV_AS_JSON set the JSON content type and wrote each row as a d (display) and r (return) pair, with the return values as strings. Your plug-in's JavaScript receives this array directly from apex.server.plugin.

Reading Data: APEX_PLUGIN_UTIL

GET_DATA, GET_DATA2, and GET_DISPLAY_DATA

These functions run the SQL query or LOV of a plug-in attribute, with bind variables for page items, searching, and paging, and check that it has the expected number of columns (p_min_columns and p_max_columns). GET_DATA returns the values as strings, one array per column (t_column_value_list). GET_DATA2 returns typed values along with each column's name and data type (t_column_list), or runs a region's source when you pass p_region. GET_DISPLAY_DATA returns the display value for a return value, or for several with p_search_value_list, the way a Popup LOV shows it.

The search types are apex_plugin_util.c_search_contains_case, c_search_contains_ignore, c_search_exact_case, c_search_exact_ignore, c_search_like_case, c_search_like_ignore, and c_search_lookup. Always prepare the search string for its type with GET_SEARCH_STRING: the _ignore types expect it in uppercase and find nothing otherwise.

Syntax:

apex_plugin_util.get_data(p_sql_statement in varchar2, p_min_columns in number, p_max_columns in number,
    p_component_name in varchar2, p_search_type in varchar2 default null, p_search_column_name in varchar2 default null,
    p_search_string in varchar2 default null, p_first_row in number default null, p_max_rows in number default null,
    p_auto_bind_items in boolean default true, p_bind_list in t_bind_list default c_empty_bind_list)
  return t_column_value_list
apex_plugin_util.get_data2(p_sql_statement in varchar2, p_min_columns in number, p_max_columns in number,
    p_data_type_list in wwv_flow_global.vc_arr2 default c_empty_data_type_list, p_component_name in varchar2, ...)
  return t_column_list
apex_plugin_util.get_display_data(p_sql_statement in varchar2, p_min_columns in number, p_max_columns in number,
    p_component_name in varchar2, p_display_column_no in binary_integer default 1,
    p_search_column_no in binary_integer default 2, p_search_string in varchar2 | p_search_value_list in wwv_flow_global.vc_arr2,
    p_display_extra in boolean default true, ...) return varchar2 | wwv_flow_global.vc_arr2

This example needs a session of application 200, page 20.

Example:

declare
    l_lov  varchar2(400) := 'select product_name as d, product_id as r from orb_products order by 1';
    l_data apex_plugin_util.t_column_value_list;
    l_cols apex_plugin_util.t_column_list;
    l_list dbms_sql.varchar2a;
    l_find dbms_sql.varchar2a;
begin
    -- the LOV query of a plug-in: rows whose display column contains "tent", at most 3
    l_data := apex_plugin_util.get_data(
                  p_sql_statement      => l_lov,
                  p_min_columns        => 2,
                  p_max_columns        => 2,
                  p_component_name     => 'P20_PRODUCT',
                  p_search_type        => apex_plugin_util.c_search_contains_ignore,
                  p_search_column_name => 'D',
                  p_search_string      => apex_plugin_util.get_search_string(   -- prepared for the
                                              apex_plugin_util.c_search_contains_ignore, 'tent'),  -- search type
                  p_max_rows           => 3);
    for i in 1 .. l_data(1).count loop          -- one array per column
        dbms_output.put_line(l_data(2)(i) || ' = ' || l_data(1)(i));
    end loop;

    -- get_data2: typed values, and the columns' names and types
    l_cols := apex_plugin_util.get_data2(
                  p_sql_statement  => 'select sku, unit_price, launch_date from orb_products where sku like ''TNT%''',
                  p_min_columns    => 3,
                  p_max_columns    => 3,
                  p_component_name => 'P20_PRODUCT',
                  p_max_rows       => 2);
    for c in 1 .. l_cols.count loop
        dbms_output.put(rpad(l_cols(c).name || ' (' || l_cols(c).data_type || ')', 24));
    end loop;
    dbms_output.new_line;
    dbms_output.put_line(l_cols(1).value_list(1).varchar2_value || ' ' || l_cols(2).value_list(1).number_value
                         || ' ' || to_char(l_cols(3).value_list(1).date_value, 'DD-MON-YYYY'));

    -- the display value for a return value, as a Popup LOV shows it
    dbms_output.put_line('display of 2: ' || apex_plugin_util.get_display_data(
                             p_sql_statement => l_lov, p_min_columns => 2, p_max_columns => 2,
                             p_component_name => 'P20_PRODUCT', p_search_string => '2'));
    l_find(1) := '1'; l_find(2) := '3';
    l_list := apex_plugin_util.get_display_data(p_sql_statement => l_lov, p_min_columns => 2, p_max_columns => 2,
                  p_component_name => 'P20_PRODUCT', p_search_value_list => l_find);
    dbms_output.put_line('display of 1, 3: ' || l_list(1) || ', ' || l_list(2));
end;
/

Output:

3 = Basecamp 4-Person Tent
4 = Basecamp 6-Person Family Tent
5 = Ridge Ultralight Tent
SKU (VARCHAR2)          UNIT_PRICE (NUMBER)     LAUNCH_DATE (DATE)
TNT-1001 239.99 13-JUL-2022
display of 2: Trailblazer 2-Person Tent
display of 1, 3: Trailblazer 1-Person Tent, Basecamp 4-Person Tent

GET_DATA found the products whose name contains "tent" in any case and stopped at three rows, which is how a searchable plug-in pages through large lists. GET_DATA2 reported each column's name and type and returned a real number and date rather than strings. GET_DISPLAY_DATA translated return values back to product names, one at a time and as a list.

Reading Row by Row and Session State

SubprogramPurpose
GET_SQL_HANDLER, PREPARE_QUERY, GET_DATA or GET_DATA2 with p_sql_handler, FREE_SQL_HANDLERParse a query once and fetch it in pieces.
SET_COMPONENT_VALUES(p_column_value_list, p_row_num), CLEAR_COMPONENT_VALUESMake a row's columns available as &COLUMN. substitutions and v('COLUMN'), for per-row templates.
GET_VALUE_AS_VARCHAR2(p_data_type, p_value, p_format_mask)A typed value from GET_DATA2 as text.
SPLIT_MULTIPLE_VALUE_TO_TABLE(p_value, p_item)A multi-value item's value as a list, using its separator or JSON array setting.
GET_CURRENT_DATABASE_TYPE, GET_ORDERBY_NULLS_SUPPORTThe database type and NULLS sorting support of the region's source, for plug-ins that write SQL.

Running Developers' Code: APEX_PLUGIN_UTIL

GET_PLSQL_EXPRESSION_RESULT, GET_PLSQL_FUNCTION_RESULT, and Their Variants

These functions evaluate a PL/SQL expression or function body that a developer entered in a plug-in attribute, with bind variables for page items (p_auto_bind_items) and others (p_bind_list). They return the result as text, as a boolean (GET_PLSQL_EXPR_RESULT_BOOLEAN and GET_PLSQL_FUNC_RESULT_BOOLEAN), or as a CLOB (the _CLOB variants). The code runs as the application's parsing schema.

REPLACE_SUBSTITUTIONS, ESCAPE, IS_EQUAL, GET_ATTRIBUTE_AS_NUMBER, and DB_OPERATION_ALLOWED

REPLACE_SUBSTITUTIONS replaces &ITEM. references in an attribute's text, escaping the values when p_escape is true. ESCAPE escapes a value for HTML only if p_escape is true, which suits an attribute that lets developers choose. IS_EQUAL compares two values and treats two nulls as equal. GET_ATTRIBUTE_AS_NUMBER converts an attribute to a number, with an error message that names the attribute. DB_OPERATION_ALLOWED checks an operation against a list such as 'UD' and returns or raises an error. IS_COMPONENT_USED evaluates a build option, an authorization, and a condition.

This example needs a session of application 200, page 20.

Example:

begin
    apex_session_state.set_value('P20_NUMBER', '12');
    -- code a developer typed into a plug-in attribute, with bind variables
    dbms_output.put_line(apex_plugin_util.get_plsql_expression_result(':P20_NUMBER * 2'));
    dbms_output.put_line(case when apex_plugin_util.get_plsql_expr_result_boolean(':P20_NUMBER > 10') then 'true' end);
    dbms_output.put_line(apex_plugin_util.get_plsql_function_result(
        'declare l_n number; begin select count(*) into l_n from orb_orders where status = ''NEW''; return l_n || '' new''; end;'));
    dbms_output.put_line(dbms_lob.getlength(apex_plugin_util.get_plsql_func_result_clob(
        'declare l_c clob; begin for i in 1 .. 20 loop l_c := l_c || rpad(''x'', 2000, ''x''); end loop; return l_c; end;'))
        || ' characters');

    dbms_output.put_line(apex_plugin_util.replace_substitutions('Hello &APP_USER., you entered &P20_NUMBER.'));
    dbms_output.put_line(apex_plugin_util.escape('<b>Tent</b>', p_escape => true));
    dbms_output.put_line(apex_plugin_util.get_html_attr('data-sku', 'TNT-1002'));
    dbms_output.put_line(apex_plugin_util.get_search_string(apex_plugin_util.c_search_contains_ignore, 'Tent'));
    dbms_output.put_line(case when apex_plugin_util.is_equal(null, null) then 'null = null' end);
    dbms_output.put_line(apex_plugin_util.get_attribute_as_number('42', 'Max. Stars'));
    begin
        dbms_output.put_line(apex_plugin_util.get_attribute_as_number('four', 'Max. Stars'));
    exception when others then
        dbms_output.put_line(regexp_replace(sqlerrm, 'ORA-\d+: '));
    end;
end;
/

Output:

24
true
2 new
40000 characters
Hello ADMIN, you entered 12
&lt;b&gt;Tent&lt;&#x2F;b&gt;
 data-sku="TNT-1002"
TENT
null = null
42
APEX - Entered value four for attribute "Max. Stars" is not numeric. - Contact your application administrator.

The expressions saw :P20_NUMBER as 12 through automatic binding, and the CLOB variant returned 40,000 characters, far more than a VARCHAR2 can hold. GET_SEARCH_STRING turned "Tent" into TENT for the case-insensitive search type. The last line is the error developers see when they type text into a numeric attribute, and it names the attribute, which makes it easy to fix.

EXECUTE_PLSQL_CODE and GET_POSITION_IN_LIST are deprecated. Use APEX_EXEC.EXECUTE_PLSQL, covered in the guide to APEX_EXEC, and APEX_STRING.INDEX_OF, covered in the guide to APEX_STRING.

REST Data Source Plug-ins

A REST Data Source plug-in implements a REST API style that the built-in types do not cover, with its own paging, filters, and DML. These helpers do the HTTP work using the source's settings.

SubprogramPurpose
GET_WEB_SOURCE_OPERATION(p_web_source, p_db_operation, p_perform_init, p_preserve_headers)The operation for a database operation (fetch rows, insert, update, or delete), with URL, method, headers, and parameters.
MAKE_REST_REQUEST(p_web_source_operation, p_request_body, p_bypass_cache, p_time_budget, p_response, p_response_parameters)Runs the operation with the source's credentials, caching, and time budget.
BUILD_REQUEST_BODY(p_request_format, p_profile_columns, p_values_context, p_build_when_empty, p_request_body)The body of a DML request, from the template or the data profile.
PROCESS_DML_RESPONSE(...), PARSE_REFETCH_RESPONSE(...)Read the response of a DML request, and of a refetch for lost update detection.
CURRENT_ROW_CHANGED(p_old_row_context, p_new_row_context)Whether a row changed, by comparing two contexts.

The declarative side of REST Data Sources is covered in the guide to REST Data Sources in Oracle APEX.

Conclusion

APEX_PLUGIN declares the records that plug-in callbacks receive and return, plus the input names and Ajax identifiers they use. APEX_PLUGIN_UTIL prints item HTML, hidden values, options, and LOVs as JSON; reads SQL queries and LOVs with searching and paging; evaluates the PL/SQL and substitutions developers put into attributes; and does the HTTP work of REST Data Source plug-ins. Together they let a plug-in behave exactly like a built-in component.

Get more Vinish.dev tutorials on Google

See my articles more often when you search Google.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted