The Oracle APEX URL construct can be a bit cryptic for the uninvited, people and spiders alike. Here is a (a, not THE) technique to alleviate this. It is loosely based on a similar solution I made for an ancient Oracle Portal solution quite a few years back. You need an Apache (OHS/OAS or regular Apache) in front for this to work. The API is tested for Apache 2.2 and Oracle XE/11g, but it should run equally well on OHS/OAS (Apache 1.3-2.0).
Why Do You Need Nice URLs?
This is the place to start; Why would you consider rewriting the native APEX URL? There are a few good reasons why, but generalizations won't work here. The question is why do YOU need this for YOUR application? Consider it, and reconsider it, because there are no silver bullets here. Any path you choose will inevitably cause more work, add complexity and even more gray hairs when your carefully thought out assumptions suddenly changes. Not to mention adding overhead to your webserver when processing requests. Perhaps you will even start loosing hair for all I know. The more sophisticated application, the more work added for beautifying the URLs.
For an intranet environment I can see no good reasons to do this. None.
The Alternatives
There are already some designs out there to help you on your way. Patrick Wolf has a list of the more popular ones. Kris Rice's post Better APEX URLs also uses a map file. The <iframe> solution is also worth considering, depending on your need. There are also some very creative Apache rewrite solutions described in the OTN APEX-forum.
My Attempt
Let's just call it that, an attempt. It did not turn out as streamlined as I had hoped for, but for my need (and the effort I was prepared to put into it), it suffices.
I wanted to create content based nice URLs, and the content in question here resides in an Oracle database table. I also wanted to control the output from the database to reduce the number of moving parts.
This is how it works:
- You can define a "page type" based on a table with a primary key, and create mapping between APEX URL and a nice URL based on that
- You can manually create mapping between APEX URL and a nice URL to fully control both APEX and nice URL
- The APEX to nice URL mapping will be written to an Apache map file
- Apache checks the content of the map file for a potential rewrite
- If the map file changes, Apache will cache the new version automagically
The Database Code
The following script will create two tables, one to hold the mapping between source table (where content resides) and the APEX page(s) where content will be displayed. The other table is where the actual nice to APEX URL-mapping resides, the content of this table is what gets written to the Apache rewrite map file.
The script will also create a directory where the URL map file will be written, and a package to handle all the in-betweens. The schema user must have the appropriate privileges to create these objects for the script to succeed. Replace path to APACHECONF directory to desired location.
-- change path to suite your environment create or replace directory APACHECONF as 'C:\Oracle\OraXE\Apache\conf' / create table app_page_type ( page_id number , source_table varchar2(32) , source_table_pk_col varchar2(32) , url_path varchar2(255) , pk_page_parameter varchar2(255) , name_column varchar2(32) ) / create unique index app_page_type_uk on app_page_type (page_id, source_table) / create table app_page_url_mapping ( page_id number not null , source_table varchar2(32) , source_table_id number , apex_url varchar2(4000) not null , nice_url varchar2(4000) not null , nice_title varchar2(255) ) / create unique index app_page_url_mapping_uk on app_page_url_mapping (page_id, source_table, source_table_id) / create or replace package app_page_p as -- create url-friendly construct, 255 chars long -- based on a name. function get_nice_name ( p_name in varchar2 , p_source_table_id in varchar2 default null , p_url_end in varchar2 default '.html' ) return varchar2; -- get nice url based on source table, can be used -- directly from APEX application function get_nice_url ( p_page_id in number , p_source_table in varchar2 , p_source_table_id in varchar2 ) return varchar2; -- get nice url from APEX page_id, will only return -- mappings without source table defined function get_nice_url ( p_page_id in number ) return varchar2; -- create mapping based on page type and source table procedure create_mapping ( p_source_table in varchar2 , p_source_table_id in varchar2 , p_name in varchar2 default null); -- create complete manual mapping procedure create_manual_mapping ( p_page_id in number , p_apex_url in varchar2 , p_nice_url in varchar2 , p_nice_title in varchar2 default null); --remove mapping when source is removed procedure remove_mapping ( p_source_table in varchar2 , p_source_table_id in number ); -- remove manual mapping to page_id procedure remove_manual_mapping ( p_page_id in number ); -- synchronize map file with mapping table procedure synchronize_mapfile; end; / create or replace package body app_page_p as -- global variables -- change variables to suite your environment g_app_id number := 1000; --APEX app_id for public application g_app_name varchar2(255) := 'My test app'; --descriptive name of application g_dad_path varchar2(255) := '/pls/apex'; --dad or location g_map_file varchar2(255) := 'map.txt'; --name of map file g_tmp_map_file varchar2(255) := 'map_tmp.txt'; --name of temporary map file g_map_dir varchar2(255) := 'APACHECONF'; --name of directory -- get nice url from source_table function get_nice_url ( p_page_id in number , p_source_table in varchar2 , p_source_table_id in varchar2 ) return varchar2 is l_ret varchar2(255); begin select tab.nice_url into l_ret from app_page_url_mapping tab where tab.page_id = p_page_id and tab.source_table = p_source_table and tab.source_table_id = p_source_table_id; return l_ret; exception when no_data_found then return null; end; -- get nice url from page_id, manually created function get_nice_url ( p_page_id in number ) return varchar2 is l_ret varchar2(255); begin for r in (select tab.nice_url from app_page_url_mapping tab where tab.page_id = p_page_id and tab.source_table_id is null) loop l_ret := r.nice_url; exit; end loop; return l_ret; exception when no_data_found then return null; end; -- create url-friendly construct, 255 chars long function get_nice_name ( p_name in varchar2 , p_source_table_id in varchar2 default null , p_url_end in varchar2 default '.html' ) return varchar2 is l_ret varchar2(4000) := p_name; l_sub number; begin -- special national translation (excerpt) l_ret := translate(lower(l_ret), 'æøå ', 'aoa_'); l_ret := regexp_replace(l_ret, '([^[:alnum:]|_])', '', 1, 0, 'i'); l_sub := nvl(length(to_char(p_source_table_id)||p_url_end),0); l_ret := substr(l_ret, 1, 255-l_sub); l_ret := l_ret||p_source_table_id||p_url_end; return l_ret; end; -- lookup column value for source_table and source_table_id function get_source_name ( p_source_table in varchar2 , p_source_table_id in number , p_source_pk_col in varchar2 , p_source_name_col in varchar2 ) return varchar2 is l_sql varchar2(4000) := 'select '||p_source_name_col||' pk_col'||chr(10) ||' from '||p_source_table||chr(10) ||' where '||p_source_pk_col||' = :1'; l_ret varchar2(4000); begin execute immediate l_sql into l_ret using in p_source_table_id; return l_ret; end; -- create the actual mapping procedure create_mapping ( p_source_table in varchar2 , p_source_table_id in varchar2 , p_name in varchar2 default null ) is l_orig_name varchar2(4000); l_nice_name varchar2(255); l_apex_url varchar2(4000); l_nice_url varchar2(4000); l_nice_title varchar2(4000); begin -- delete previous entries delete from app_page_url_mapping where source_table = p_source_table and source_table_id = p_source_table_id; for r in (select pt.* from app_page_type pt where pt.source_table = p_source_table) loop if p_name is null then l_orig_name := get_source_name(r.source_table, p_source_table_id, r.source_table_pk_col, r.name_column); else l_orig_name := p_name; end if; l_nice_name := get_nice_name(l_orig_name, p_source_table_id, '.html'); l_apex_url := g_dad_path||'/f?p='||g_app_id||':'||r.page_id||':0::::'||r.pk_page_parameter||':'||p_source_table_id; l_nice_url := r.url_path||'/'||l_nice_name; l_nice_title := substr(g_app_name||' - '||l_orig_name, 1, 255); -- insert new entry insert into app_page_url_mapping ( page_id , source_table , source_table_id , apex_url , nice_url , nice_title) values ( r.page_id , r.source_table , p_source_table_id , l_apex_url , l_nice_url , l_nice_title); end loop; -- synchronize url-mapping file synchronize_mapfile; end; -- create manual mapping procedure create_manual_mapping ( p_page_id in number , p_apex_url in varchar2 , p_nice_url in varchar2 , p_nice_title in varchar2 default null ) is begin -- educated guess on delete delete from app_page_url_mapping where page_id = p_page_id and source_table is null; -- insert new entry insert into app_page_url_mapping ( page_id , apex_url , nice_url , nice_title) values ( p_page_id , p_apex_url , p_nice_url , p_nice_title); -- synchronize map file synchronize_mapfile; end; procedure remove_mapping ( p_source_table in varchar2 , p_source_table_id in number ) is begin delete from app_page_url_mapping where source_table = p_source_table and source_table_id = p_source_table_id; synchronize_mapfile; end; procedure remove_manual_mapping ( p_page_id in number ) is begin delete from app_page_url_mapping where page_id = p_page_id and source_table is null; synchronize_mapfile; end; -- normal write to file procedure procedure write_to_file ( p_file_name in varchar2 , p_directory in varchar2 , p_content in clob ) is l_file utl_file.file_type; l_buffer raw(32000); l_amount binary_integer := 32000; l_pos integer := 1; l_blob blob; l_blob_left number; l_blob_length number; l_file_content blob; l_src_offset integer := 1; l_dest_offset integer := 1; l_lang_ctx integer := dbms_lob.default_lang_ctx; l_warn integer; begin -- converting from clob to blob dbms_lob.createtemporary(l_file_content, false); dbms_lob.converttoblob(l_file_content, p_content, dbms_lob.getlength(p_content), l_dest_offset, l_src_offset, 1, l_lang_ctx, l_warn); l_blob_length := dbms_lob.getlength(l_file_content); l_blob_left := l_blob_length; -- open the destination file. l_file := utl_file.fopen(p_directory,p_file_name,'WB', 32760); -- if small enough for a single write if l_blob_length < 32760 then utl_file.put_raw(l_file,l_file_content); utl_file.fflush(l_file); else -- write in pieces l_pos := 1; while l_pos < l_blob_length loop dbms_lob.read(l_file_content,l_amount,l_pos,l_buffer); utl_file.put_raw(l_file,l_buffer); utl_file.fflush(l_file); -- set the start position for the next cut l_pos := l_pos + l_amount; -- set the end position if less than 32000 bytes l_blob_left := l_blob_left - l_amount; if l_blob_left < 32000 then l_amount := l_blob_left; end if; end loop; end if; utl_file.fclose(l_file); dbms_lob.freetemporary(l_file_content); exception when others then dbms_lob.freetemporary(l_file_content); -- close the file if something goes wrong. if utl_file.is_open(l_file) then utl_file.fclose(l_file); end if; raise; end; -- synchronize map file procedure synchronize_mapfile is l_map clob; begin l_map := '# Generated file, manual changes will suddenly disappear'||chr(10); l_map := l_map || '# Last generated: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss')||chr(10); for r in (select * from app_page_url_mapping) loop l_map := l_map||r.nice_url||' '||r.apex_url||chr(10); end loop; write_to_file(g_tmp_map_file, g_map_dir, l_map); -- if everything goes well, we rename and overwrite utl_file.frename(g_map_dir, g_tmp_map_file, g_map_dir, g_map_file, true); end; end; /
Configuring Apache
Apache will handle the actual rewrites based on the generated map file. Include the following in your httpd.conf (after backing it up)
## Rewrite URLs that exists in map.txt RewriteMap nicetoapex txt:c:/oracle/oraxe/apache/conf/map.txt RewriteCond %{REQUEST_URI} /(([^/]+)(/.*)*)$ RewriteCond %2 =articles.html [OR] RewriteCond %2 =article [OR] RewriteCond %2 =albums.html [OR] RewriteCond %2 =album RewriteCond ${nicetoapex:%{REQUEST_URI}|NOT_FOUND} !NOT_FOUND RewriteRule ^(/.*) ${nicetoapex:$1}%{QUERY_STRING} [P]Note the forward slashes in the map file path (even for Windows installs). The code must be included directly into your VirtualHost-directive (if you have one), and not your default APEX Location-directive or some such. The reason is that the rewrite expects the nice-URL, not something that looks like '/pls/apex'.
Also note that I have included some criteria for when the map file will be checked, this is to exclude any number of requests not related directly to APEX pages. This is a very manual step, but can be minimized if you decide to prefix all your nice URLs with a common string. If you prefix your URLs you also will not force Apache to go through all the conditions before the attempted rewrite. A prefix kind of defeated the purpose of nice URLs for me, so I will update and reload Apache when changes are needed. A bit masochistic, I know...
The very cryptic "!NOT_FOUND" line above simply states that if the URL is not part of the map file, then don't rewrite (${nicetoapex:$1} will then have no value). There are probably easier ways to achieve this, but my knowledge of Apache is limited to what google serves me at the first page of the search result...
Apache will cache a copy of the map file for every start/restart/reload, and if the file is updated (mtime changed) it will re-read the file automagically. That is a nice feature, and saves you a ton of hassle.
You are not home free yet, APEX submits pages with relative paths (which is quite natural), but that will break your nice URL hierarchy. To fix this add:
## Handle calls to wwv_flow.accept RewriteCond %{REQUEST_URI} ^/wwv_flow.accept RewriteRule ^/(.*) /pls/apex/$1 [P]This will rewrite all requests ending with wwv_flow.accept and point it to your dad-location. Depending on your application, it may be necessary to include more rewrite conditions for this rewriterule (like wwv_flow.show).
For good measure you can also include a rewrite to handle the default "start page" of your site with the following:
## Redirect server home page to Apex application RewriteRule ^/$ http://localhost:8080/pls/apex/f?p=1000:1:0 [P,L] RewriteRule ^/index.html$ http://localhost:8080/pls/apex/f?p=1000:1:0 [P,L]Restart Apache for every change of httpd.conf for the changes to take effect.
Mapping URLs From Table Content
First off you have to define a relationship between the APEX-pages showing the content, and the database table where the content resides. The table APEX_PAGE_TYPE holds this information.
insert into app_page_type ( page_id , source_table , source_table_pk_col , url_path , pk_page_parameter , name_column) values ( 3 -- APEX page_id , 'ARTICLES' -- Source Table , 'ARTICLES_ID' -- Source table primary key , '/article' -- Desired URL-prefix , 'P3_ARTICLE_ID' -- APEX page item id that holds the primary key , 'TITLE') -- Source table column that content title / commit /In the example above I want to generate a nice URL for articles in the ARTICLES-table. APEX page_id=3 will show the content, based on the value of page item P3_ARTICLE_ID. This bit you only have to do once for each page type/source table association.
When I insert/update a row in the ARTICLES-table with ARTICLE_ID=0, I can generate a nice URL with the following statements:
begin app_page_p.create_mapping( 'ARTICLES' -- source table name , 0); -- source table id end; / commit /This is the code you normally would run following a publication of an article. If the TITLE-column has value 'APEX 4.0 - It is finally here!', your map-file should now contain the following:
# Generated file, manual changes will suddenly disappear # Last generated: 03.02.2010 22:19:22 /article/apex_40__it_is_finally_here0.html /pls/apex/f?p=1000:3:0::::P3_ARTICLE_ID:0You can have more than one page type associated with the same database table, and URLs for all pages will be generated from the statement above. If all works now, you should be able to point your browser to http://<yoursite>/article/apex_40__it_is_finally_here0.html
Manually Mappig URLs
Some APEX pages will probably not be directly linked to a database table, to accommodate this, the API offers a possibility to manually create page mappings with the following statements (note, there is no need for a page type to be defined first, as there is no content table to associate with that APEX page):
begin app_page_p.create_manual_mapping(2 -- APEX page_id , '/pls/apex/f?p=1000:2:0' -- APEX URL , '/articles.html' -- Nice URL , 'Articles'); -- Nice name end; / commit /If you ran this after the previous example, your map file should look like this:
# Generated file, manual changes will suddenly disappear # Last generated: 03.02.2010 22:21:01 /articles.html /pls/apex/f?p=1000:2:0 /article/apex_40__it_is_finally_here0.html /pls/apex/f?p=1000:3:0::::P3_ARTICLE_ID:0And likewise you should be able to see http://<yourhostname>/articles.html in a browser.
Pitfalls, Improvements and Considerations
What is the first thing you noticed about the API? No APP_ID-parameters! This version does not support more than one public application, but extending the code to accommodate this should be fairly trivial if the need should arise.
The second observation is the lack of support for more than one primary key column. The API must be modified to support this.
The third observation is the lack of APEX parameter support. Including RP or other such parameters will probably get onto your wish-list pretty soon. The API can be altered to achieve this, either through generating more mappings, or attaching parameters in a normal form to the nice-URL, and rewriting it to the correct APEX syntax in the Apache.
In the current version, there is no support for URL-hierarchy. If you want to incorporate an URL-path based on an hierarchy defined by the mapping API, you have to extend the API, or map it yourself by APP_PAGE_TYPE and APP_PAGE_URL_MAPPING.
You may also consider using the Apache httxt2dbm-utility to create a binary format DBM-file of the original map file, which is considerably faster than mapping with a txt-file. Adding a dbms_scheduler job to execute an external program should do the trick. Apache rewrite syntax is almost identical, you just have to tell it the map-type has changed.
I seriously wish there was an easy way to just delete a line from an existing file. I for one, could not conjure up a good way to do this from PL/SQL, hence the complete rewrite of the file. Maybe Perl could do a better job of it? Any old how, this portion of the API is well suited for a more asynchronous execution. In a normal production environment, the Apache is located in a DMZ, and not on the same server as the database. In that case the map file has to be moved from the database server to the webserver. The API does not support moving files through FTP/SFTP, but you can check out http://www.orafaq.com/scripts/plsql/ftpclient.txt for a plain PL/SQL FTP-solution, or be creative with an external procedure.
A Small Bonus
If you want to set the APEX page title according to the source table, you can modify the page template and include a function to return the nice_name column of the APP_PAGE_URL_MAPPING-table. The function will then be common for all pages in your application. The title of your webpage continually crops up as one of the more important tags for certain search engines.
In Conclusion
If you decide to use the API, then I suggest you start thinking about how to design your applications to use it, especially considering branching, tabs and other native APEX components that gravitate towards the normal APEX URL construct. If your application branches out with the old URL, there would be little point in attempting to rewrite the URLs. As I stated earlier, there are no silver bullets, and there will be more work than just using native APEX URLs.
If you are aiming at more sophisticated public applications, I would seriously consider NOT using it. This API (in it's present state at least) is aimed at more simple applications.
Oh, and did I mention you use this code at your own risk? Well, you do!
Enjoy :-)
I have a (very) simple solution to the "nice Apex URL problem", see the last part of my blog post at http://ora-00001.blogspot.com/2009/07/creating-rest-web-service-with-plsql.html
ReplyDeleteThis solution revolves around the "Path Alias" and "Path Alias Procedure" configuration parameters for mod_plsql (and DBMS_EPG). The really nice thing about this approach is that everything is handled via the database.
- Morten
Monkey On Oracle: Nice Url In A Public Facing Oracle Apex Application >>>>> Download Now
Delete>>>>> Download Full
Monkey On Oracle: Nice Url In A Public Facing Oracle Apex Application >>>>> Download LINK
>>>>> Download Now
Monkey On Oracle: Nice Url In A Public Facing Oracle Apex Application >>>>> Download Full
>>>>> Download LINK Z4
@morten
ReplyDeleteArgh!! Why didn't that turn up in my searches! That is sooo much easier than my solution. It basically works the same, except you do not have to use the map file (thus removing half the code in my sample).
On the other hand, you have to make an extra call to the database instead of Apache checking a map file. Which will be faster is anybodies guess. Given the reduced complexity of your code, I would use your technique over mine any day!
Well, at least my musings on "do you really want to" still holds for either technique.
I blame you for this post, not emphasizing the nice URL bonus enough for it to turn up on google! :P
Havard, thank you, very interesting. But I can't understand, what about private pages, e.g. user "profile" page, or his/her friend list?
ReplyDeleteThere is a session in url, and if we throw it away, there will be redirection to login page. Usually we have a url like /apex/f?p=103:1:3664539095781055::::: - and changing it to smth like /article/3664539095781055 looks not nicer...
- Alex
@zulkar
ReplyDeleteThis solution was intended for public pages only, and the use of APEX session zero. You will still get a session id, but set in a cookie normally called WWV_PUBLIC_SESSION_<your app id>.
I guess it should be possible to create some sort of custom cookie based authentication/session scheme, but all I see down that road is a lot of work...
Check out some of the other alternative rewrite methods out there, they would most likely be more beneficial in your case.
Hello Harvard,
ReplyDelete1. Thank you for this great article that clarified many things.
I have a feedback and a question :
2. Basically, It seems that you can achieve a similar functionality by implementing the following (for those ones that work with Apach & mod_rewrite):
http://forums.oracle.com/forums/thread.jspa?threadID=299891&start=15&tstart=0
* I also used apex pages alias (instead of pages numbers) + page zero
For Example :
Old Url :
A. http://www.postme.co.il/pls/postme/f?p=107:196
New Url :
B. http://www.postme.co.il/postme/postme.html
3. The only issue I have is with notification_msg , when notification_msg pops_up (because of a regular error , e.g invalid credentials)
It doesn't show the message on the screen.
Wonder if any workaround / explanation regarding this issue.?
Anyhow, Just for other people to know this possibility.
Regards
Etay G.
@Etay
ReplyDelete1. You're welcome :-)
2. I tried that solution, but it did not give the sort of URLs I was after (using actual table content as URL)
3. I doubt this has anything to do with the notification in it self. I have no explanation for this, as it could be any number of things. Could even be template related. Should be possible to turn up logging in Apache to see what is going in (requests).
Good luck :-)
One more Issue :
ReplyDeleteIt would be much appreciated if you can refer to point number 3 and let me know if it happens for you as well.
Regards
Etay G
@Etay
ReplyDeleteAs of now, I have no easy way to test this. You have to resort to regular old debugging (both Apache and APEX).
Sorry, you are on your own in this.
I have Oracle APEX installed on dB 12c on a linux box on my home network as a sandbox.
ReplyDeleteWhen on this box I can open firefox and goto the below link and login fine.
http://oradev.attlocal.net:7172/apex/f?p=111:LOGIN_DESKTOP:23798986661662:::::
However on any other PC on my network, or outside my network I get page can not
be displayed.
I use dyn up dater to translate my IP address to
www.aanning.com
this works fine for other applications, ie I use http://www.aanning.com:XXXX (XXXX is a port#)
and I can hit other apps (not oracle) on other PC on my network from outside, so I know this part
works.
I have port 7172 forwarded to the APEX box and try
http://www.aanning.com:7172/apex/f?p=111:LOGIN_DESKTOP:23798986661662:::::
but I get page can not be displayed.
How can I hit my APEX app from outside my network?
Please advise, thanks James
Well, Gmail comes with outstanding features and respond quickly while sending emails along with attachments too. But sometimes, you might get stuck when you see an error message “Server Down” while opening Gmail. Don’t worry you have to wait for a few minutes until the server gets repaired. Or just refresh the page and try to open it again.
ReplyDeleteGmail Support Number UK
Now in the updated version of Gmail the user can also dictate and compose the email without typing it manually using the keyboard, the user will just have to open the compose window in Gmail there, after entering the name of the recipient and also the subject of the mail the user should click on the Microphone icon given in the right-hand side of the send button the user can then simply dictate all that is to be written in the mail.
ReplyDeleteGmail Helpline Number UK
Monkey On Oracle: Nice Url In A Public Facing Oracle Apex Application >>>>> Download Now
ReplyDelete>>>>> Download Full
Monkey On Oracle: Nice Url In A Public Facing Oracle Apex Application >>>>> Download LINK
>>>>> Download Now
Monkey On Oracle: Nice Url In A Public Facing Oracle Apex Application >>>>> Download Full
>>>>> Download LINK zh