среда, 31 марта 2010 г.

Зарисовка архитектуры приложения с использованием Application Express

Зарисовка архитектуры приложения, которому требуется интеграция с корпоративными информационными системами через интерфейсы Web-сервисов и Java API

Основная идеология:    В данной архитектуре вынесен "во вне" интеграционный слой - его задачи выполняет прослойка в виде JavaEE-сервера приложения (например: WebLogic, OC4J, JBoss и т.п.). Взаимодействие между ApEx-ом и сервером приложений построено на основе очередей сообщений Oracle Advanced Queuing, т.е. встроенным в СУБД механизмом организации очередей сообщений. Что даёт возможность некоторой балансировки нагрузки на сервер приложений. В нашем случае очередей сообщений две: "для запросов" и "для ответов". На стороне сервера приложения развернуты Message Driven Beans (MDB), которые слушают очередь сообщений "для запросов" и при появлении в ней сообщения обрабатывают его (парсинг; возможен вариант обработки сообщения в Session Bean). В зависимости от результата обработки вызывается метод Session Bean-а, который в свою очередь вызывает Java API или Web-Service корпоративного приложения. После получения ответа данный Session Bean отправляет ответ в очередь сообщений "для ответов", а по появлении в ней сообщения срабатывает процедура обработки в базе данных. Например, вызов Java API (аналогично и для Web-Service) корпоративного приложения:

  1. по нажатию на кнопку в ApEx(может быть любое другое действие) - вызывается хранимая процедура в СУБД;
  2. данная процедура формирует сообщение и отправляет его в очередь сообщений "для запросов";
  3. MDB-компонент слушает очередь сообщений и когда в ней появляется сообщение забирает его из очереди "для запросов" и обрабатывает его;
  4. в зависимости от результата обработки вызывается метод Session Bean-а, который в свою очередь вызывает Java API корпоративного приложения;
  5. по полученому ответу(от корпоративного приложения) данный Session Bean формирует сообщение и отравляет его в очередь сообщений "для ответов";
  6. на стороне базы данных для очередь "для ответов" выполняется процедура обработки сообщения и согласно результатам выполняется какое-либо действие (например: изменяются данные с таблице).

Плюсы данного подхода:    Интеграционная часть вынесена из базы данных. Что даёт возможность:

  • Вынесение интеграционного слоя в том числе на отдельную машину;
  • Распределения нагрузки между СУБД и сервером приложений.
  • Возможность использования всех интеграционных возможностей платформы Java EE

Минусы данного подхода:

  • Требуется дополнительное программное обеспечение ввиде сервера приложения;
  • Требуется разработка EJB-компонентов (MDB и Session Beans) на языке Java.

суббота, 6 марта 2010 г.

Установка и интеграция Oracle Application Express 3.2 и Oracle e-Business Suite R12

Установка Oracle Application Express

  1. Скачиваем дистрибутив Oracle Application Express по ссылке:
    http://www.oracle.com/technology/products/database/application_express/download.html

  2. Распаковываем скаченный zip-архив (apex_3.2.zip):
     $ unzip apex_3.2.zip  
    
  3. Переходим в директорию apex (которая создастся после распаковки архива)
     $ cd  apex  
    
  4. Запускаем SQL Plus и соединаемся с БД как sys as sysdba
     $ sqlplus /nolog  
     SQL> CONNECT SYS as SYSDBA  
    
  5. В SQL Plus выполняем команду в формате:
     SQL> @apexins tablespace_apex tablespace_files tablespace_temp images  
    
    Например :
     SQL> @apexins APEX_TBS APEX_TBS APEX_TBS_TEMP /i/  
    
  6. После завершения выполнения предыдущего пункта – запускаем SQL Plus и соединяемся с БД как sys as sysdba (см. Шаг 4).
  7. В SQL Plus выполняем команду:
     SQL> @apxchpwd  
    
    Устанавливаем пароль для администратора Application Express (пользователь ADMIN)
  8. Необходимо перестартовать инстанс БД и процесс прослушивателя (listener).
  9. Запускаем SQL Plus и соединяемся с БД как sys as sysdba (см. Шаг 4).
  10. В SQL Plus выполняем команду:
     SQL> ALTER USER APEX_PUBLIC_USER ACCOUNT UNLOCK  
    
  11. Выполняем следующую команду:
     SQL> ALTER USER APEX_PUBLIC_USER IDENTIFIED BY new_password  
    
    где new_password – заменить на пароль для APEX_PUBLIC_USER
  12. Скачать дистрибутив Oracle HTTP Server для соответствующей версии СУБД используемой в Oracle e-Business Suite. Например: для Oracle 11g R1 на платформе AIX по ссылке:
    http://www.oracle.com/technology/software/products/database/oracle11g/111060_aixsoft.html

  13. Установить Oracle HTTP Server используя Oracle Universal Installer
  14. Скопировать директорию apex/images (которая создастся после распаковки архива) в директорию ORACLE_HTTPSERVER_HOME/ohs:
     $ cp –rf apex/images ORACLE_HTTPSERVER_HOME/ohs  
    
    где ORACLE_HTTPSERVER_HOME – директория, где установлен Oracle HTTP Server.
  15. Используя текстовый редактор изменям файл конфигурации Oracle HTTP Server - ORACLE_HTTPSERVER_HOME/ohs/modplsql/conf/dads.conf:
     $ vi ORACLE_HTTPSERVER_HOME/ohs/modplsql/conf/dads.conf  
    
  16. Добавляем в данный файл следующий фрагмент в конец файла:
     Alias /i/ "ORACLE_HTTPSERVER_HOME/ohs/images/"  
     AddType text/xml xbl  
     AddType text/x-component htc  
     <Location /pls/apex>  
     Order deny,allow  
     PlsqlDocumentPath docs  
     AllowOverride None  
     PlsqlDocumentProcedure wwv_flow_file_mgr.process_download  
     PlsqlDatabaseConnectString host:port:service_name ServiceNameFormat  
     PlsqlNLSLanguage AMERICAN_AMERICA.AL32UTF8  
     PlsqlAuthenticationMode Basic  
     SetHandler pls_handler  
     PlsqlDocumentTablename wwv_flow_file_objects$  
     PlsqlDatabaseUsername APEX_PUBLIC_USER  
     PlsqlDefaultPage apex  
     PlsqlDatabasePassword apex_public_user_password  
     PlsqlRequestValidationFunction wwv_flow_epg_include_modules.authorize  
     Allow from all  
     </Location>  
    
    Необходимо изменить в данной фрагменте значения ORACLE_HTTPSERVER_HOME, host, port, service_name и apex_public_user_password на соответствующие Вашей системе. И в случае наобходимости значение параметра PlsqlNLSLanguage

  17. Перезапустить Oracle HTTP Server выполнив последовательно две команды:
     ORACLE_HTTPSERVER_HOME/opmn/bin/opmnctl stopproc ias-component=HTTP_Server  
     ORACLE_HTTPSERVER_HOME/opmn/bin/opmnctl startproc ias-component=HTTP_Server  
    
  18. Если СУБД - Oracle RDBMS 11g, то выполняем скрипт в SQL Plus (от sys as sysdba):
     DECLARE  
      ACL_PATH VARCHAR2(4000);  
      ACL_ID RAW(16);  
     BEGIN  
      -- Look for the ACL currently assigned to '*' and give APEX_030200  
      -- the "connect" privilege if APEX_030200 does not have the privilege yet.  
      SELECT ACL  
      INTO ACL_PATH  
      FROM DBA_NETWORK_ACLS  
      WHERE HOST   = '*'  
      AND LOWER_PORT IS NULL  
      AND UPPER_PORT IS NULL;  
      -- Before checking the privilege, ensure that the ACL is valid  
      -- (for example, does not contain stale references to dropped users).  
      -- If it does, the following exception will be raised:  
      --  
      -- ORA-44416: Invalid ACL: Unresolved principal 'APEX_030200'  
      -- ORA-06512: at "XDB.DBMS_XDBZ", line ...  
      --  
      SELECT SYS_OP_R2O(extractValue(P.RES, '/Resource/XMLRef'))  
      INTO ACL_ID  
      FROM XDB.XDB$ACL A,  
       PATH_VIEW P  
      WHERE extractValue(P.RES, '/Resource/XMLRef') = REF(A)  
      AND EQUALS_PATH(P.RES, ACL_PATH)       = 1;  
      DBMS_XDBZ.ValidateACL(ACL_ID);  
      IF DBMS_NETWORK_ACL_ADMIN.CHECK_PRIVILEGE(ACL_PATH, 'APEX_030200', 'connect') IS NULL THEN  
       DBMS_NETWORK_ACL_ADMIN.ADD_PRIVILEGE(ACL_PATH, 'APEX_030200', TRUE, 'connect');  
      END IF;  
     EXCEPTION  
      -- When no ACL has been assigned to '*'.  
     WHEN NO_DATA_FOUND THEN  
      DBMS_NETWORK_ACL_ADMIN.CREATE_ACL('power_users.xml', 'ACL that lets power users to connect to everywhere', 'APEX_030200', TRUE, 'connect');  
      DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL('power_users.xml','*');  
     END;  
     /  
     COMMIT;  
    
  19. Если СУБД - Oracle RDBMS 11g, то выполняем скрипт в SQL Plus (от sys as sysdba):
     DECLARE  
      ACL_PATH VARCHAR2(4000);  
      ACL_ID RAW(16);  
     BEGIN  
      -- Look for the ACL currently assigned to 'localhost' and give APEX_030200  
      -- the "connect" privilege if APEX_030200 does not have the privilege yet.  
      SELECT ACL  
      INTO ACL_PATH  
      FROM DBA_NETWORK_ACLS  
      WHERE HOST   = 'localhost'  
      AND LOWER_PORT IS NULL  
      AND UPPER_PORT IS NULL;  
      -- Before checking the privilege, ensure that the ACL is valid  
      -- (for example, does not contain stale references to dropped users).  
      -- If it does, the following exception will be raised:  
      --  
      -- ORA-44416: Invalid ACL: Unresolved principal 'APEX_030200'  
      -- ORA-06512: at "XDB.DBMS_XDBZ", line ...  
      --  
      SELECT SYS_OP_R2O(extractValue(P.RES, '/Resource/XMLRef'))  
      INTO ACL_ID  
      FROM XDB.XDB$ACL A,  
       PATH_VIEW P  
      WHERE extractValue(P.RES, '/Resource/XMLRef') = REF(A)  
      AND EQUALS_PATH(P.RES, ACL_PATH)       = 1;  
      DBMS_XDBZ.ValidateACL(ACL_ID);  
      IF DBMS_NETWORK_ACL_ADMIN.CHECK_PRIVILEGE(ACL_PATH, 'APEX_030200', 'connect') IS NULL THEN  
       DBMS_NETWORK_ACL_ADMIN.ADD_PRIVILEGE(ACL_PATH, 'APEX_030200', TRUE, 'connect');  
      END IF;  
     EXCEPTION  
      -- When no ACL has been assigned to 'localhost'.  
     WHEN NO_DATA_FOUND THEN  
      DBMS_NETWORK_ACL_ADMIN.CREATE_ACL('local-access-users.xml', 'ACL that lets power users to connect to everywhere', 'APEX_030200', TRUE, 'connect');  
      DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL('local-access-users.xml','localhost');  
     END;  
     /  
     COMMIT;  
    
  20. Установка завершена

Интеграция с аутентификацией Oracle e-Business Suite

  1. Создать функцию (или пакет содержащий данную функцию):
     FUNCTION ebiz_suite_auth(  
       p_username IN VARCHAR2,  
       p_password IN VARCHAR2)  
      RETURN BOOLEAN  
     AS  
     BEGIN  
      IF fnd_web_sec.validate_login(p_username, p_password) = 'Y' THEN  
       RETURN true;  
      ELSE  
       RETURN false;  
      END IF;  
     END;  
    
  2. Создать новую схему аутентификации:
    1. Перейти из приложения в “Shared Components”->”Authentication Schemes”.
    2. Нажать кнопку “Create”.
    3. Выбрать тип создаваемой темы как “From scratch”. Нажать “Next”.
    4. Ввести имя схемы аутентификации, например: “eBusiness Suite Authentication”. Нажать “Next”.
    5. Оставить все значения полей по-умолчанию в шагах: Page Sentry Function, Session Verify Function, Invalid Session Target, Invalid Session Target.
    6. На шаге “Authentication Function” выбрать “Use my custom function to authenticate” и ввести в поле: “return ebiz_suite_auth;”. Нажать “Next”.
    7. Оставить все значения полей по-умолчанию в шагах: Post Authentication Process, Cookie Attributes.
    8. На шаге “Logout URL” ввести: “wwv_flow_custom_auth_std.logout?p_this_flow=&APP_ID.&p_next_flow_page_sess=&APP_ID.:1:&SESSION.”. Нажать “Next”.
    9. На следующем шаге нажать “Create Scheme”. Схема создана
  3. Необходимо изменить два параметра созданной схемы:
    1. Перейти из приложения в “Shared Components”->”Authentication Schemes”.
    2. Выбрать созданную схему. В нашем случае это “eBusiness Suite Authentication”.
    3. Изменить значение поля “Session Not Valid Page” на 101 (это номер страницы логина), а значение поля “-Or- Session Not Valid URL” должно быть пустым.
    4. Нажимаем “Apply Changes”.
  4. Сделать созданную схему аутентификации активной:
    1. Перейти из приложения в “Shared Components”->”Authentication Schemes”.
    2. Перейти на вкладку “Change Current”.
    3. Выбрать для пункта “Available Authentication Schemes” наименование созданной схемы аутентификации. В нашем случае это “eBusiness Suite Authentication”. И нажать “Next”.
    4. Нажать “Make current.”

Интеграция Oracle Application Express и Google Maps

  1. Создать JavaScript-файл или добавить в Header Definition:
     function initializeGoogleMaps(){  
      try{  
      Ext.BLANK_IMAGE_URL = '/i/1px_trans.gif';  
      // Create namespace if doesn't already exist  
      Ext.namespace('Ext.ux');  
      Ext.ux.GMapPanel = Ext.extend(Ext.Panel, {  
       initComponent : function(){  
         var defConfig = {  
           plain: true,  
           zoomLevel: 3,  
           yaw: 180,  
           pitch: 0,  
           zoom: 0,  
           gmapType: 'map',  
           border: false  
         };  
         Ext.applyIf(this,defConfig);  
         Ext.ux.GMapPanel.superclass.initComponent.call(this);      
        },  
        afterRender : function(){  
         var wh = this.ownerCt.getSize();  
         Ext.applyIf(this, wh);  
         Ext.ux.GMapPanel.superclass.afterRender.call(this);    
         if (this.gmapType === 'map'){  
           this.gmap = new GMap2(this.body.dom);  
         }  
         if (this.gmapType === 'panorama'){  
           this.gmap = new GStreetviewPanorama(this.body.dom);  
         }  
         if (typeof this.addControl == 'object' && this.gmapType === 'map') {  
           this.gmap.addControl(this.addControl);  
         }  
         if (typeof this.setCenter === 'object') {  
           if (typeof this.setCenter.geoCodeAddr === 'string'){  
             this.geoCodeLookup(this.setCenter.geoCodeAddr);  
           }else{  
             if (this.gmapType === 'map'){  
               var point = new GLatLng(this.setCenter.lat,this.setCenter.lng);  
               this.gmap.setCenter(point, this.zoomLevel);    
             }  
        this.gmap.addControl(this.addControl);  
         }  
         if (typeof this.setCenter === 'object') {  
           if (typeof this.setCenter.geoCodeAddr === 'string'){  
             this.geoCodeLookup(this.setCenter.geoCodeAddr);  
           }else{  
             if (this.gmapType === 'map'){  
               var point = new GLatLng(this.setCenter.lat,this.setCenter.lng);  
               this.gmap.setCenter(point, this.zoomLevel);    
             }  
        this.gmap.addControl(this.addControl);  
         }  
         if (typeof this.setCenter === 'object') {  
           if (typeof this.setCenter.geoCodeAddr === 'string'){  
             this.geoCodeLookup(this.setCenter.geoCodeAddr);  
           }else{  
             if (this.gmapType === 'map'){  
               var point = new GLatLng(this.setCenter.lat,this.setCenter.lng);  
               this.gmap.setCenter(point, this.zoomLevel);    
             }  
        this.gmap.addControl(this.addControl);  
         }  
         if (typeof this.setCenter === 'object') {  
           if (typeof this.setCenter.geoCodeAddr === 'string'){  
             this.geoCodeLookup(this.setCenter.geoCodeAddr);  
           }else{  
             if (this.gmapType === 'map'){  
               var point = new GLatLng(this.setCenter.lat,this.setCenter.lng);  
               this.gmap.setCenter(point, this.zoomLevel);    
             }  
        this.gmap.addControl(this.addControl);  
         }  
         if (typeof this.setCenter === 'object') {  
           if (typeof this.setCenter.geoCodeAddr === 'string'){  
             this.geoCodeLookup(this.setCenter.geoCodeAddr);  
           }else{  
             if (this.gmapType === 'map'){  
               var point = new GLatLng(this.setCenter.lat,this.setCenter.lng);  
               this.gmap.setCenter(point, this.zoomLevel);    
             }  
             if (typeof this.setCenter.marker === 'object' && typeof point === 'object'){  
               this.addMarker(point,this.setCenter.marker,this.setCenter.marker.clear);  
             }  
           }  
           if (this.gmapType === 'panorama'){  
             this.gmap.setLocationAndPOV(new GLatLng(this.setCenter.lat,this.setCenter.lng), {yaw: this.yaw, pitch: this.pitch, zoom: this.zoom});  
           }  
         }  
         GEvent.bind(this.gmap, 'load', this, function(){  
           this.onMapReady();  
         });  
       },  
       onMapReady : function(){  
         this.addMarkers(this.markers);  
         this.addMapControls();  
         this.addOptions();   
       },  
       onResize : function(w, h){  
         if (typeof this.getMap() == 'object') {  
           this.gmap.checkResize();  
         }  
         Ext.ux.GMapPanel.superclass.onResize.call(this, w, h);  
       },  
       setSize : function(width, height, animate){  
         if (typeof this.getMap() == 'object') {  
           this.gmap.checkResize();  
         }  
         Ext.ux.GMapPanel.superclass.setSize.call(this, width, height, animate);  
       },  
       getMap : function(){  
         return this.gmap;  
       },  
       getCenter : function(){  
         return this.getMap().getCenter();  
       },  
       getCenterLatLng : function(){  
         var ll = this.getCenter();  
         return {lat: ll.lat(), lng: ll.lng()};  
       },  
       addMarkers : function(markers) {  
         if (Ext.isArray(markers)){  
           for (var i = 0; i < markers.length; i++) {  
             var mkr_point = new GLatLng(markers[i].lat,markers[i].lng);  
             this.addMarker(mkr_point,markers[i].marker,false,markers[i].setCenter, markers[i].listeners);  
           }  
         }  
       },  
       addMarker : function(point, marker, clear, center, listeners){  
         Ext.applyIf(marker,G_DEFAULT_ICON);  
         if (clear === true){  
           this.getMap().clearOverlays();  
         }  
         if (center === true) {  
           this.getMap().setCenter(point, this.zoomLevel);  
         }  
         var mark = new GMarker(point,marker);  
         if (typeof listeners === 'object'){  
           for (evt in listeners) {  
             GEvent.bind(mark, evt, this, listeners[evt]);  
           }  
         }  
         this.getMap().addOverlay(mark);  
       },  
       addMapControls : function(){  
         if (this.gmapType === 'map') {  
           if (Ext.isArray(this.mapControls)) {  
             for(i=0;i  
               this.addMapControl(this.mapControls[i]);  
             }  
           }else if(typeof this.mapControls === 'string'){  
             this.addMapControl(this.mapControls);  
           }else if(typeof this.mapControls === 'object'){  
             this.getMap().addControl(this.mapControls);  
           }  
         }  
       },  
       addMapControl : function(mc){  
         var mcf = window[mc];  
         if (typeof mcf === 'function') {  
           this.getMap().addControl(new mcf());  
         }    
       },  
       addOptions : function(){  
         if (Ext.isArray(this.mapConfOpts)) {  
           var mc;  
           for(i=0;i         this.addOption(this.mapConfOpts[i]);  
           }  
         }else if(typeof this.mapConfOpts === 'string'){  
           this.addOption(this.mapConfOpts);  
         }      
       },  
       addOption : function(mc){  
         var mcf = this.getMap()[mc];  
         if (typeof mcf === 'function') {  
           this.getMap()[mc]();  
         }    
       },  
       geoCodeLookup : function(addr) {  
         this.geocoder = new GClientGeocoder();  
         this.geocoder.getLocations(addr, this.addAddressToMap.createDelegate(this));;  
       },  
       addAddressToMap : function(response) {  
         if (!response || response.Status.code != 200) {  
        Ext.MessageBox.alert('Unable to Locate Address','Unable to Locate the Address you provided');  
         }else{  
           place = response.Placemark[0];  
           addressinfo = place.AddressDetails;  
           accuracy = addressinfo.Accuracy;  
           if (accuracy === 0) {  
             Ext.MessageBox.alert('Unable to Locate Address', 'Unable to Locate the Address you provided');  
           }else{  
               point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);  
               if (typeof this.setCenter.marker === 'object' && typeof point === 'object'){  
                 this.addMarker(point,this.setCenter.marker,this.setCenter.marker.clear,true, this.setCenter.listeners);  
               }  
           }  
         }  
       }  
     });  
     Ext.reg('gmappanel',Ext.ux.GMapPanel);  
     } catch(err){}   
     }  
     function showAddress(address){  
       var mapwin = new Ext.Window({  
             layout: 'fit',  
             title: 'Map',  
             closeAction: 'hide',  
             width:800,  
             height:600,  
             x: 40,  
             y: 60,  
             items: {  
               xtype: 'gmappanel',  
               region: 'center',  
               zoomLevel: 12,  
               gmapType: 'map',  
               mapConfOpts: ['enableScrollWheelZoom','enableDoubleClickZoom','enableDragging'],  
               mapControls: ['GSmallMapControl','GMapTypeControl','NonExistantControl'],  
               setCenter: {  
                 geoCodeAddr: address,  
                 marker: {title: address}  
               }  
             }  
           });   
         mapwin.show();  
     }  
    
  2. Добавить в HTML Header страницы:
     <!---script src="http://maps.google.com/maps?file=api&amp;v=2&amp;sensor=true&amp;key=&GOOGLE_MAPS_API_KEY." type="text/javascript"-->  
    
    где, &GOOGLE_MAPS_API_KEY. - значение ключа для конкретного хоста
  3. Добавить в onload body вызов данной функции или использовать метод onReady фреймворка ExtJS:
     Ext.onReady(function(){  
       Ext.BLANK_IMAGE_URL = '/i/1px_trans.gif';  
       // Create namespace if doesn't already exist  
       Ext.namespace('Ext.ux');  
       //Здесь последовательность вызовов функций, например:  
       initializeGoogleMaps();  
       makeComboboxis();  
     });  
    
  4. Добавить вызов JavaScript-функции по нажатию на кнопку или ссылку:
     javascript:showAddress(адрес);  
    
    например для "Москва, Пресненская набережная 10":
     javascript:showAddress('Москва, Пресненская набережная 10');  
    

пятница, 5 марта 2010 г.

Application Express: функция для ввода только чисел в TEXTFIELD в определённом диапазоне значений

  1. Создать JavaScript-файл или добавить в Header Definition:
     function limitNumber(elementName,minNum,maxNum) {  
       var t_field=document.getElementById(elementName).value;  
       if (isNumeric(t_field)){  
        if (!((parseInt(t_field) <= maxNum) && ((parseInt(t_field) >= minNum)))) {  
          var temp=document.getElementById(elementName).value.substring(0, document.getElementById(elementName).value.length-1);  
          document.getElementById(elementName).value=temp;  
        }  
       } else {  
        var temp=document.getElementById(elementName).value.substring(0, document.getElementById(elementName).value.length-1);  
        document.getElementById(elementName).value=temp;  
       }  
     }  
    
  2. Создать или изменить на странице Apex Item типа TEXTFIELD и указать в свойстве Element->HTML Form Element Attributes следующее значение:
     onkeyup="limitNumber('имя_данного_textfield',min_значение,max_значение)" onkeydown="limitNumber('имя_данного_textfield',min_значение,max_значение)"  
    
    например для TEXTFIELD c именем P150_PROBABILITY, минимальный значением 1 и максимальным значением 100:
     onkeyup="limitNumber('P150_PROBABILITY',1,100)" onkeydown="limitNumber('P150_PROBABILITY',1,100)"  
    

Application Express: функция для ввода только числовых значений в TEXTFIELD

  1. Создать JavaScript-файл или добавить в Header Definition:
     function checkOnlyNumbers(evt) {  
       evt = (evt) ? evt : window.event  
       var charCode = (evt.which) ? evt.which : evt.keyCode  
       if (charCode > 31 && (charCode < 48 || charCode > 57)) {  
        return false;  
       } else {  
        return true;  
       }  
     }  
    
  2. Создать или изменить на странице Apex Item типа TEXTFIELD и указать в свойстве Element->HTML Form Element Attributes следующее значение:
     onkeydown="return checkOnlyNumbers(event)"
       onkeyup="return checkOnlyNumbers(event)"  
    

Application Express: функция для ограничения длины вводимых данных для TEXTAREA

Данная функция предназначена для ограничения вводимых данных в TEXTAREA, т.к. стандартными средствами Apex 3.2 эту валидацию выполнить нельзя.
  1. Создать JavaScript-файл или добавить в Header Definition:
     function limitText(elementName,limitNum) {  
       var t_field=document.getElementById(elementName);  
       if (t_field.value.length > limitNum) {  
        t_field.value = t_field.value.substring(0, limitNum);  
       }  
     }  
    
  2. Создать или изменить на странице Apex Item типа TEXTAREA и указать в свойстве Element->HTML Form Element Attributes следующее значение:
    onKeyUp="limitText('имя_данной_textarea',максимальная длинна)" 
       onKeyDown="limitText('имя_данной_textarea',максимальная длинна)"  
    
    например для TEXTAREA c именем P115_DESCRIPTION и максимальной длинной 2000 символов(для латиницы, для кирилицы в двое меньше):
    onKeyUp="limitText('P115_DESCRIPTION',2000)" 
       onKeyDown="limitText('P115_DESCRIPTION',2000)"  
    

Рекомендации при разработке Apex-приложений

  1. Понимание архитектуры Apex и связанных технологий:
    • Oracle RDBMS - PL/SQL
    • DHTML = HTML+CSS+JavaScript+Ajax
  2. Логика отображения(View Layer) должна быть только в:
    • Templates
    • CSS
    • Item Formatting
  3. Избегать, где это возможно использования PL/SQL для генерации HTML
  4. Вынести все пользовательские JavaScript-функции в отдельный файл и минимализировать и упаковать
  5. Перенести анонимные PL/SQL процедуры в БД (нет необходимости в компиляции каждый раз) и отладка из отдельно от Apex. * передавать значения в процедуры как параметры * использовать переменные связывания
  6. Использовать схему пэйджинга(pagination scheme) "X to Y" взамен "X to Y of Z"
  7. Постоянно производить диагностику производительности вновь созданных страниц и переодически приложения в целом (возможно использование средств автоматического тестирования Web-приложений, например, Oracle Application Testing Suite).

Рекомендую ознакомиться со статьёй Майка Хичва - Оценка производительности, в которой Вы найдёте пояснение некоторых рекомендаций

Application Express: использование всплывающих модальных окон из фреймворка ExtJS

Открытие модального окна

  1. Создать JavaScript-файл или добавить в Header Definition:
     function initializePopupWindows(){  
       try{  
        // Define an iframe component, better for garbage collection  
        Ext.ux.IFrameComponent = Ext.extend(Ext.BoxComponent, {  
          url : null,  
          onRender : function(ct, position){  
           var url = this.url;  
           this.el = ct.createChild({tag: 'iframe', id: 'iframe-'+ this.id, frameBorder: 0, src: url, "SCROLLING":'NO'});  
        }  
        });  
       Ext.reg('iframe', Ext.ux.IFrameComponent);  
       Ext.ux.PopupWindow = Ext.extend(Ext.Window, {  
        url: '',  
        title: '',  
        width: 700,  
        height: 600,  
        resizable: true,  
        maximizable: true,  
        initComponent: function(){  
          this.isProcessed = false;  
          var config = {  
           border: false,  
           closable: true,  
           closeAction: 'close',  
           height: this.height,  
           items: [ new Ext.ux.IFrameComponent({ id: id, url: this.url }) ],  
           layout: 'fit',  
           modal: true,  
           plain: false,  
           title: this.title,  
           width: this.width  
        };  
        // apply config  
        Ext.apply(this, Ext.apply(this.initialConfig, config));  
        Ext.ux.PopupWindow.superclass.initComponent.call(this);  
        // define custom events  
        this.addEvents('success');  
       },  
       processSuccessful: function(){  
        this.isProcessed = true;  
        this.fireEvent("success", this);  
       },  
       hasChanged: function() {  
        return this.isProcessed;  
       },  
       show: function(){  
        this.isProcessed = false;  
        Ext.ux.PopupWindow.superclass.show.call(this);  
       },  
       // private  
       initTools : function(){  
        // over-ride Ext.Window functions adding qtips to original code  
        if(this.minimizable){  
          this.addTool({  
           id: 'minimize',  
           handler: this.minimize.createDelegate(this, []),  
           qtip: 'Minimize'  
          });  
        }  
        if(this.maximizable){  
          this.addTool({  
           id: 'maximize',  
           handler: this.maximize.createDelegate(this, []),  
           qtip: 'Maximize'  
          });  
          this.addTool({  
           id: 'restore',  
           handler: this.restore.createDelegate(this, []),  
           hidden:true,  
           qtip: 'Restore size'  
          });  
          this.header.on('dblclick', this.toggleMaximize, this);  
        }  
        if(this.closable){  
          this.addTool({  
           id: 'close',  
           handler: this[this.closeAction].createDelegate(this, []),  
           qtip: 'Close'  
          });  
        }  
       }  
       });  
       Ext.reg('PopupWindow', Ext.ux.PopupWindow);  
       } catch(err){}  
     }  
     function openModalWindow(pageNumber, windowHeight,windowWidth,windTitle){  
       var newURL = window.location.protocol + "//" + window.location.host + "/apex/f?p=" +Ext.getDom('pFlowId').value+":"+pageNumber+":"+Ext.getDom('pInstance').value+":::::";  
       var wind=new Ext.ux.PopupWindow({url: newURL , height: windowHeight,width: windowWidth, title: windTitle});  
       wind.show();  
     }  
     function openModalWindowWithParams(pageNumber, windowHeight,windowWidth,paramNames,paramValues,windTitle){  
       var newURL = window.location.protocol + "//" + window.location.host + "/apex/f?p=" +Ext.getDom('pFlowId').value+":"+pageNumber+":"+Ext.getDom('pInstance').value+"::::"+paramNames+":"+paramValues;  
       var wind=new Ext.ux.PopupWindow({url: newURL , height: windowHeight,width: windowWidth, title: windTitle});  
       wind.show();  
     }  
     function openModalWindowWithParamsNotResizable(pageNumber, windowHeight,windowWidth,paramNames,paramValues,windTitle){  
       var newURL = window.location.protocol + "//" + window.location.host + "/apex/f?p=" +Ext.getDom('pFlowId').value+":"+pageNumber+":"+Ext.getDom('pInstance').value+"::::"+paramNames+":"+paramValues;  
       var wind=new Ext.ux.PopupWindow({url: newURL , height: windowHeight,width: windowWidth, title: windTitle, resizable:false,maximizable:false});  
       wind.show();  
     }  
    
  2. Добавить в onload body вызов данной функции (initializePopupWindows) или использовать метод onReady фреймворка ExtJS:
     Ext.onReady(function(){  
       Ext.BLANK_IMAGE_URL = '/i/1px_trans.gif';  
       // Create namespace if doesn't already exist  
       Ext.namespace('Ext.ux');  
       //Здесь последовательность вызовов функций, например:  
       initializePopupWindows();  
     });  
    
  3. Чтобы открыть окно по нажатию на кнопку необходимо установить значение "URL Target" для данной кнопки:
       javascript:openModalWindow(номер_страницы,высота_окна,ширина_окна,имя_окна)  
    
    например для открытия страницы 170, с высотой окна 470, с шириной окна 660 и именем "Modal Popup Window":
       javascript:openModalWindow(170,470,660,'Modal Popup Window')  
    

Закрытие окна

  1. Создать JavaScript-файл или добавить в Header Definition:
     function closePopupWindow(){  
       try{  
        parent.Ext.WindowMgr.getActive().close();  
       }catch(err){}  
     }  
     function closePopupWindowWithRefresh(){  
       try {  
        parent.gReport.pull();  
       }catch(err){  
        parent.location.href=parent.location.href;  
       }  
       try {  
        parent.Ext.WindowMgr.getActive().close();  
       }catch(err){}  
     }  
    
  2. Добавить следующий текст в поле Success Message в процесс DML или PL/SQL всплывающего окна:
     <script type="text/javascript">  
      closePopupWindowWithRefresh();  
     </script>  
    

Роли в команде разработки ПО на Application Express

Роли в команде

Основные роли:

  • Database Developer
  • UI Designer
  • Apex Developer
  • QA Engineer
  • Team Leader
  • Integration Architect (может отсутствовать, если не требуется интеграция)

Дополнительная роль:

  • Apex Administrator - не участвует в процессе разработки

Database Developer

Задачи:

  • Разработка схемы данных
  • Разработка пакетов-утилит в БД

Необходимые навыки/знания:

  • проектирования баз данных
  • опыт программирования на PL/SQL

UI Designer

Задачи:

  • Разработка (и/или изменение) существующих тем
  • Создание кастомных компонентов (и их документирование, например на Wiki)
  • Интеграция с JavaScript-библиотеками (фреймворками)
  • Создание необходимых JavaScript функций-утилит (например: для валидации ввода значений)

Необходимые навыки/знания:

  • DHTML(HTML+JavaScript)
  • CSS
  • Понимание процесса генерации Apex-ом динамического HTML

Apex Developer

Задачи:

  • Разработка интерфейса пользователя средствами Apex
  • Использование кастомных компонентов и тем пользовательского интерфейса
  • Использование JavaScript функций-утилит
  • Использование пакетов-утилит в БД

Необходимые навыки/знания:

  • DHTML(HTML+JavaScript) + Ajax
  • Знание SQL и PL/SQL
  • Понимание процесса разработки приложений на Apex

QA Engineer

Задачи:

  • Функциональное тестирование приложения
  • Тестирование приложения на производительность

Необходимые навыки/знания:

  • Понимание процесса функционального тестирования
  • Понимание принципов автотестов и опыт их использования в Web-ориентированной среде

Team Leader

Задачи:

  • Разработка архитектуры приложения в соответствии с бизнес-требованиями
  • Участие в основных этапах разработки и тестирования системы
  • Планирование выпуска новых версий системы

Необходимые навыки/знания:

  • Навыки ролей “Database Developer”, “Apex Developer”, “QA Engineer” и “UI Engineer”
  • Мэппинг бизнес-требований на технологии и средства Apex и смежных технологий
  • Понимание принципов управления проектами

Integration Architect (может отсутствовать, если не требуется интеграция)

Задачи:

  • Разработка интеграционной архитектуры

Необходимые навыки/знания:

  • Понимание основ и принципов SOA и SaaS
  • Опыт интеграционных проектов:
  • Интеграция на уровне бизнес-процессов
  • Интеграция на основе сервисной шины
  • Интеграция на уровне данных (федерат.хранилища данных)
  • Интеграция на уровне пользовательского интерфейса

Apex Administrator

Задачи:

  • Администрирование Apex-приложений:
  1. Оптимизация и мониторинг производительности
  2. Бэкап схемы БД и самого приложения

Необходимые навыки/знания:

  • Навыки администратора СУБД Oracle
  • Начальные знания Apex

Взаимодействие ролей


Пример: роли в команде из 3-х и 5-и человек

Из 3-х:

  1. Team Leader (Apex Developer, Database Developer, UI Designer, QA Engineer)
  2. Apex Developer + Database Developer
  3. Apex Developer + QA Engineer

Из 5-и:

  1. Team Leader (Apex Developer, Database Developer, UI Designer, QA Engineer)
  2. Database Developer + Apex Developer
  3. Apex Developer + Database Developer
  4. Apex Developer + QA Engineer
  5. Apex Developer + UI Designer

Создание округлых кнопок вместо cтандартных в Application Express

  1. Home -> Application Builder -> Application 101 -> Shared Components -> Templates -> Button Definition:
     <a class="button_ND1" href="#LINK#" onclick="this.blur();">  
                                                    <span>#LABEL#</span></a>  
    
  2. Создать css-файл для описания кнопки:
     .clear { /* generic container (i.e. div) for floating buttons */  
      overflow: hidden;  
      width: 100%;  
     }  
     a.button_ND1 {  
      background: transparent url('bg_button_a.gif') no-repeat scroll top right;  
      color: #444;  
      display: block;  
      float: left;  
      font: normal 12px arial, sans-serif;  
      height: 24px;  
      margin-right: 6px;  
      padding-right: 18px; /* sliding doors padding */  
      text-decoration: none;  
     }  
     a.button_ND1 span {  
      background: transparent url('bg_button_span.gif') no-repeat;  
      display: block;  
      line-height: 14px;  
      padding: 5px 0 5px 18px;  
      text-decoration: none;  
     }  
     a.button_ND1:active {  
      background-position: bottom right;  
      color: #000;  
      outline: none; /* hide dotted outline in Firefox */  
      text-decoration: none;  
     }  
     a.button_ND1:active span {  
      background-position: bottom left;  
      padding: 6px 0 4px 18px; /* push text down 1px */  
      text-decoration: none;  
     }  
    
  3. Загрузить данный файл на Web-server (аналогично, интеграции Oracle Application Express и ExtJS).
  4. Загрузить рядом с css файлом две следующие картинки:

понедельник, 1 марта 2010 г.

Область применения Application Express

Краткие соображения на тему - для чего целесообразно, а для чего нецелесообразно использовать Application Express (APEX).

Целесообразно использовать для:
  • создания небольших корпоративных приложений (всё зависит от аппаратного обеспечения, но цифра для ориентировки: не более 500 одновременно работающих пользователей);
  • миграция/замена Microsoft Access и электронных таблиц;
  • создание модулей для Oracle E-Business Suite;
  • перенос унаследованных приложений (например: созданных на Borland Delphi) в Web-среду.

Не целесообразно использовать для:
  • Web-сайты с большим количеством пользователей;
  • корпоравные приложения с большим количеством одновременно работающих пользователей;
  • создания систем где, требуется интеграция с крупными корпоративными приложениями (на основе Java EE и .Net)