17.12.2014

--------------------OeBS is over

Очень интересный проект по внедрению Oracle E-Business Suite R12, на котором я трудился 938 дней, подошел к концу. Ухожу поддерживать отечественного производителя.
Для истории:

08.11.2014

Создание оконного Java-приложения

Цель: создать простейшее оконное приложение на Java, выводить текст, переданный в качестве параметра.

06.11.2014

Реализация очереди Oracle AQ

Oracle Advanced Queuing (AQ) - механизм организации очередей. В простейшем случае создается тип, на основе типа специальным API создается таблица очереди. Очередь действует по принципу FIFO. Приведенный ниже листинг создает демонстрирует вышесказанное.

13.10.2014

OeBS: Обновление параметра FND_INIT_SQL на уровне полномочий

Параметр содержит анонимный блок, выполняющийся при инициализации полномочий. Ниже - пример кода изменения значения параметра для группы полномочий.

08.08.2014

Subversion: синхронизация каталога между двумя репозитариями

Например, мы ведем java код в одном репозитарии, а объекты sql и ldt складируем в другой. Заказчик захотел в каталогах функциональных расширений видеть ещё и папку java c исходниками. Пишем batник синхроинзации.
@echo off
@echo ---------------------------------------
@echo Sync Java sources from java JSVN to source SSVN
@echo ---------------------------------------
# защита от случайного запуска. Обязательно должны запускать с -refresh
if "%1" == "refresh" (
# временный каталог для импорта
if exist c:\svn_tmp (
  <nul set /p strTemp=Cleaning a temp folder...
  rmdir c:\svn_tmp /s /q
  <nul set /p strTemp=done.
  echo.
)
# импорт содержимого репозитария
svn export https://JSVN-url/java_folder c:\svn_tmp
# если что-то появилось в каталоге, начинаем экспорт
if exist c:\svn_tmp\xxmy000(
 <nul set /p strTemp=Cleaning old revisions in SSVN...
 svn delete svn://SSVN-url/appscode/trunk/XXMY/XXMY000/java/src/oracle/apps/xxmy/xxmy000 -m "clean old rev"
 <nul set /p strTemp=done.
 echo.
 <nul set /p strTemp=Importing new revisions to SSVN...
 svn import c:\svn_tmp\xxmy000 svn://SSVN-url/appscode/trunk/XXMY/XXMY000/java/src/oracle/apps/xxmy/xxmy000 -m "actualize"
 <nul set /p strTemp=done.
 echo.
 <nul set /p strTemp=Cleaning a temp folder...
 rmdir c:\svn_tmp /s /q
 <nul set /p strTemp=done.
) else (
 @echo --------!! Error during import
)
) else (
 @echo !Error: Parameter 1 not found. Nothing done.
)
pause

07.07.2014

Особенность retainAM при использовании нескольких AM

Простой эксперимент позволит нам увидеть разницу между корневым и некорневыми Application Modules (AM).

15.06.2014

WebADI: атрибуты интерфейса

Цель: ограничивать элементы в атрибуте интерфейса (выпадающий список) в зависимости от входных параметров. В нашем примере - три атрибута CAUSE_1, CAUSE_2, CAUSE_3, содержащих один и тот же комбо-бокс. Нужно выводить только те элементы, которые актуальны на текущий момент (в параметрах интегратора вводится месяц и год)

10.06.2014

JBO-27122: SQL error during statement preparation

Ошибка появляется, если VO выполняется сначала с ограчением в n1 предикатов, затем в n2 предикатов, причем n2 < n1. Пример:
CausesVOImpl causeVO = getAm().getCausesVO1();
// первое выполнение
if (!causeVO.isPreparedForExecution()){
    causeVO.setMaxFetchSize(999);
}
causeVO.executeEmptyRowSet();
causeVO.setWhereClause("CAUSE_NUMBER > :1 and GROUP_ID = :2");
causeVO.setWhereClauseParam(0, causeNumber);
causeVO.setWhereClauseParam(1, groupId);
causeVO.executeQuery();

// второе выполнение
CausesVOImpl causeVO = getAm().getCausesVO1();
if (!causeVO.isPreparedForExecution()){
    causeVO.setMaxFetchSize(999);
}
causeVO.executeEmptyRowSet();
// causeVO.setWhereClause(null);
// causeVO.setWhereClauseParams(null);
causeVO.setWhereClause("GROUP_ID = :1");
causeVO.setWhereClauseParam(0, getEntityId());
try{
    causeVO.executeQuery();
}catch (Exception e){
    System.out.println(e.getMessage());
}
В приведенном примере ошибка возникнет при втором вызове executeQuery(). Как видим, setWhereClause и setWhereClauseParam не перекрывают ранее заданные значения.
Для устранения ошибки достаточно принудительно очистить ограничение (раскомментировать строки во втором выполнении)

08.06.2014

Отчет из данных ViewObject

Мы можем выдать пользователю отчет сразу по нажатию кнопки, без создания конкаррента. Такое удобно, например, при выгрузке в Excel результатов с поисковой формы.

Ключевые моменты.
1) Формирование XMLNode c данными из VO. Первый параметр - глубина структуры XML.
XMLNode xmlNode = (XMLNode)reportVO.writeXML(4, XMLInterface.XML_OPT_ALL_ROWS);
2) Добавление доп.параметров в XML (параметры шапки)
XMLNode xmlNode = (XMLNode)am.invokeMethod("fillWithVODataset");
XMLDocument doc = xmlNode.getDocument();
Element elem = doc.createElement("ReportInfo");
Element elemPeriod = doc.createElement("SomeParameter");
Text startPeriodVal = doc.createTextNode((String)someBean.getValue(pageContext));
elemPeriod.appendChild(startPeriodVal);
elem.appendChild(elemPeriod);
XMLNode firstChild = (XMLNode)xmlNode.getFirstChild();
// xmlNode пуст, если VO был пуст
if (firstChild != null){
  XMLNode parent = (XMLNode)firstChild.getParentNode();
  parent.appendChild(elem); 
}else{
  // добавляем элемент в корень
  xmlNode.appendChild(elem);
}
3) Натягиваем данные на существующий шаблон XDO
select lb.file_data
from xdo_templates_b tmp,
        xdo_lobs lb
where tmp.template_code = :1
   and tmp.application_short_name = :2
   and lb.application_short_name = tmp.application_short_name
   and lb.lob_code = tmp.data_source_code
   and lb.lob_type = 'TEMPLATE'
   and sysdate between tmp.start_date and nvl(tmp.end_date,sysdate)
4) Формируем выходной поток с помощью класса oracle.apps.xdo.template.FOProcessor

Шаблон для нашего отчета будет иметь особенности. Имя группы будет составлено как Имя_VO || 'Row'. Например, если VO отчета назывался ReportVO, то группа будет названа ReportVORow. Имена элементов будут совпадать с именами атрибутов VO. Отдельная группа - вручную добавленные параметры шапки:
<dataStructure>
  <group name="reportVORow" source="reportVORow">
   <element name="ATTR1" value="ATTR1"/>
   <element name="ATTR2" value="ATTR2"/>
   ...
  <group name="ReportInfo" source="ReportInfo">
   <element name="SomeParameter" value="SomeParameter"/>
   ...

29.05.2014

Разработка кастомной формы в Oracle Forms

Цель: создать кастомную форму "Расширенная история записи" и включить в меню Tools (Сервис). Будет показывать расширенную информацию из HR по текущей записи - Ф.И.О., департамент, должность и т.д.

Для работы нам понадобится ПО Oracle Developer Suite 10. Сразу оговорюсь, что эта IDE в теории позволяет запускать формы на выполнение через Java-апплет в браузере. На практике это сделать так и не удалось (были перепробованы JInitiator 1.3.22, 18, 26) с изменением содержимого formsweb.cfg. Так что этот вопрос остается открытым.

Этап 1: модификация формсового меню. Скачиваем $AU_TOP/resource/RU/FNDMENU.mmb и открываем в Oracle Developer Suite 10:

Добавляем новый пункт в самый конец списка.

Прописываем блок с вызываемым триггером в окне, открывшемся из контекстного меню PL/SQL Editor. Позже мы создадим этот триггер.
:global.menu_to_appcore := 'ADVANCED_RECORD_HISTORY';
execute_trigger('menu_to_appcore');
Сохраняем файл, заливаем на сервер:
frmcmp_batch module=FNDMENU.mmb userid=apps/apps output_file=$AU_TOP/resource/RU/FNDMENU.mmx compile_all=special module_type=menu
 ...
 - Inserting menu FNDMENU.
 - Inserting menu FILE.
 - Inserting menu EDIT.
 - Inserting menu VIEW.
 - Inserting menu FOLDER.
 - Inserting menu SPECIAL.
 - Inserting menu SPECIAL_B.
 - Inserting menu SPECIAL_C.
 - Inserting menu HELP.
 - Inserting menu QUERY.
 - Inserting menu DIAGNOSTICS.
 - Inserting menu TRACE_MENU.
 - Inserting menu LOGGING_MENU.
 - Inserting menu PROPERTIES_MENU.
 - Inserting menu CUSTOM_CODE_MENU.
 - Inserting menu CLEAR.
 - Inserting menu DUPLICATE.
 - Inserting menu PREFERENCES.
 - Inserting menu RECORD.
 - Ready.
После перезахода в Forms увидим наш пункт меню:

Если политика партии запрещает правку FNDMENU.mmb (как в нашем случае), то придется добавлять пункт меню динамически. Для этого:
 - добавляем код в обработчик EVENT библиотеки CUSTOM.pll. Эта библиотека как раз и была создана для кастомизаций. Приведенный ниже код будет вызывать обработчик нашей кастомной библиотеки при любом событии Forms. Там мы будем накладывать ограничения.
XXFND999.event(event_name);
 - создаем кастомную библиотеку XXFND999.pll c обработчиком EVENT. Обработчик выполняет двойную функцию: создает пункт меню один раз и вызывает форму при активации одноименного триггера.
procedure event(p_event_name in varchar2)
  is
          l_item_label      CONSTANT varchar2(100) := 'Расширенная история записи';
          l_event_name           varchar2(100);
   l_tgt_fn_app     CONSTANT varchar2(200) := 'XXMY';
   l_tgt_fn_name   CONSTANT varchar2(300) := 'XXFND999_HISTORY_RECORD_ADV';
   l_form_name     CONSTANT varchar2(300) := l_tgt_fn_name;
   l_frm_path        varchar2(5000);
   param_id          paramlist; 
   l_item_feature   varchar2(150);
   l_created_by     item;
   l_tmp     varchar2(150);
   l_block       varchar2(150);
  begin
   -- отменяем отработку функционала внутри формы
   if name_in('SYSTEM.CURRENT_FORM') = l_form_name then return; end if;
   l_block := name_in('SYSTEM.CURRENT_BLOCK');
   -- проверка на существование блока
   if length(l_block) = 0 then return; end if;
       -- событие при создании экземпляра формы
       if p_event_name = 'WHEN-NEW-BLOCK-INSTANCE' then
          l_event_name  := xx_form_custom.create_special(l_item_label,'LINE');
       end if;
       /* кастомный метод для получения первой
           незанятой ячейки SPECIAL */ 
       l_event_name  := xx_form_custom.get_special(l_item_label);
       -- включение видимости и активация нового пункта
       xx_form_custom.activate_special(l_item_label);
       -- сохранение пункта меню
       xx_form_custom.save_menu();
       -- если вызвали нашу форму
       if (p_event_name = l_event_name)then
        l_frm_path := fnd_navigate.formpath(l_tgt_fn_app, l_tgt_fn_name);
        -- проверяем наличие поля CREATED_BY в таблице с текущей записью
        l_created_by := find_item(name_in('SYSTEM.CURRENT_BLOCK')||'.CREATED_BY');
        if not id_null(l_created_by) then
           -- проверяем наличие созданного списка параметров
           param_id := get_parameter_list('FND_FORM_ARGS');
           if (not id_null(param_id))
           then
             destroy_parameter_list(param_id);
           end if;
          -- создаем список параметров и добавляем информацию из служебных полей
          param_id := create_parameter_list('FND_FORM_ARGS');
          add_parameter( param_id, 'CREATED_BY', TEXT_PARAMETER, name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.CREATED_BY'));
          add_parameter( param_id, 'LAST_UPDATED_BY', TEXT_PARAMETER, name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.LAST_UPDATED_BY'));
          add_parameter( param_id, 'CREATION_DATE', TEXT_PARAMETER,name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.CREATION_DATE'));
          add_parameter( param_id, 'LAST_UPDATE_DATE', TEXT_PARAMETER, name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.LAST_UPDATE_DATE'));
          -- открываем форму
           open_form ( formmodule_name => l_frm_path
               , activate_mode   => ACTIVATE
               , session_mode    => SESSION
               , data_mode       => NO_SHARE_LIBRARY_DATA
               , paramlist_id    => param_id
               );
       end if;
   end if;
  end event;
Этап 2: создание формы. Создаем Data Block, создаем триггер WHEN-NEW-BLOCK-INSTANCE и item USER_INFO, которому будем присваивать строку с данными о пользователе, разделенными через перевод строки. Создаем канву, бросаем туда item USER_INFO, ставим в свойствах Multiline=yes, Length=32000. Создаем несколько параметров, идентичных прописанным в кастомной библиотеке: CREATED_BY, LAST_UPDATED_BY, CREATION_DATE, LAST_UPDATE_DATE. Начинаем править код PL/SQL созданного блока данных и прописываем туда такое:
declare 
  l_create_id          varchar2(50) := :PARAMETER.CREATED_BY;
  l_update_id         varchar2(50) := :PARAMETER.LAST_UPDATED_BY;
  l_create_date          varchar2(50) := :PARAMETER.CREATION_DATE;
  l_update_date          varchar2(50) := :PARAMETER.LAST_UPDATE_DATE;
begin
  :XXFND361_TEST_V.CREATED_BY_FULLNAME := XXFND361_PKG.get_record_history(p_create_id => l_create_id,
                                              p_update_id => l_update_id,
                                              p_create_date => l_create_date,
                                              p_update_date => l_update_date
                                              );      
XXFND999_PKG - обычный PL/SQL пакет:
 function get_record_history(p_create_id in varchar2,
                              p_update_id in varchar2,
                              p_create_date in varchar2,
                              p_update_date in varchar2
                             ) return varchar2
  is
        l_result_line          varchar2(32000);

        l_create_person        varchar2(150);
        l_create_org_id        varchar2(150);
        l_create_job           varchar2(150);
        l_create_contact       varchar2(150);
       
        l_update_person        varchar2(150);
        l_update_org_id        varchar2(150);
        l_update_job           varchar2(150);
        l_update_contact       varchar2(150); 
        l_is_person            varchar2(1);
       
        l_create_organization_id number;
        l_update_organization_id number;
       
        l_create_id          varchar2(150) := p_create_id;
        l_update_id         varchar2(150) := p_update_id;
        l_create_date          varchar2(150) := p_create_date;
        l_update_date          varchar2(150) := p_update_date;       
begin
 if l_create_id is null then
   return '';
 end if; 
    select case when user_det.full_name is null then
             fnu.user_name
       else
             decode(regexp_substr(upper(user_det.full_name),'I'), null, user_det.full_name, fnu.user_name) || ' ' ||
       user_det.employee_number end,
       decode(user_det.full_name, null, 'N', 'Y') into l_create_person, l_is_person
    from FND_USER fnu,
           per_all_people_f    user_det
    where fnu.employee_id = user_det.person_id(+)
      and trunc(sysdate) between user_det.effective_start_date(+) and user_det.effective_end_date(+)
      and fnu.user_id = to_number(l_create_id);
   --
   if l_is_person = 'Y' then
    
   begin 
   select asg.organization_id into l_create_organization_id
    from per_all_people_f          user_det,
      per_all_assignments_f     asg,
      FND_USER                  fnu
    where user_det.person_id = asg.person_id
     and fnu.employee_id = user_det.person_id
     and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date
     and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date
     and fnu.user_id = to_number(l_create_id);
   exception
     when NO_DATA_FOUND then
       null;
   end;
 
   --
  
   begin 
   select j.name into l_create_job
    from per_all_people_f      user_det,
      per_all_assignments_f     asg,
      FND_USER                  fnu,
      per_jobs                  j
   where user_det.person_id = asg.person_id
     and fnu.employee_id = user_det.person_id
     and asg.job_id = j.job_id
     and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date
     and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date
     and trunc(sysdate) >= j.date_from and j.date_to is null
     and fnu.user_id = to_number(l_create_id)
     and rownum = 1;
   exception
     when NO_DATA_FOUND then
       null;
   end;        
   --
   begin
   select coalesce(user_det.email_address, fnu.Email_Address, ' ') || ' ' ||
      user_det.work_telephone
     into l_create_contact
    from per_all_people_f          user_det,
      per_all_assignments_f     asg,
      FND_USER                  fnu
   where user_det.person_id = asg.person_id
     and fnu.employee_id = user_det.person_id
    and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date
    and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date
    and fnu.user_id = to_number(l_create_id);
   exception
     when NO_DATA_FOUND then
       null;
   end;      
   end if;
   --------------------------
    l_is_person   := '';
   --------------------------
   select case when user_det.full_name is null then
             fnu.user_name
       else
             decode(regexp_substr(upper(user_det.full_name),'I'), null, user_det.full_name, fnu.user_name) || ' ' ||
       user_det.employee_number end,
       decode(user_det.full_name, null, 'N', 'Y') into l_update_person, l_is_person
    from FND_USER fnu,
         per_all_people_f    user_det
    where fnu.employee_id = user_det.person_id(+)
      and trunc(sysdate) between user_det.effective_start_date(+) and user_det.effective_end_date(+)
      and fnu.user_id = to_number(l_update_id);
     
   if l_is_person = 'Y' then

   begin
   select asg.organization_id into l_update_organization_id
    from per_all_people_f          user_det,
      per_all_assignments_f     asg,
      FND_USER                  fnu
   where user_det.person_id = asg.person_id
     and fnu.employee_id = user_det.person_id
     and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date
     and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date
     and fnu.user_id = to_number(l_update_id);
   exception
     when NO_DATA_FOUND then
       null;
   end;      
   --
   begin
   select j.name into l_update_job
     from per_all_people_f      user_det,
      per_all_assignments_f     asg,
      FND_USER                  fnu,
      per_jobs                  j
 where user_det.person_id = asg.person_id
   and fnu.employee_id = user_det.person_id
   and asg.job_id = j.job_id
   and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date
   and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date
   and trunc(sysdate) >= j.date_from and j.date_to is null
   and fnu.user_id = to_number(l_update_id);
   exception
     when NO_DATA_FOUND then
       null;
   end;   
   --
   begin
   select coalesce(user_det.email_address, fnu.Email_Address, ' ') || ' ' ||
       user_det.work_telephone
    into l_update_contact
    from per_all_people_f          user_det,
      per_all_assignments_f     asg,
      FND_USER                  fnu
 where user_det.person_id = asg.person_id
   and fnu.employee_id = user_det.person_id
   and trunc(sysdate) between user_det.effective_start_date and user_det.effective_end_date
   and trunc(sysdate) between asg.effective_start_date and asg.effective_end_date
   and fnu.user_id = to_number(l_update_id);
   exception
     when NO_DATA_FOUND then
       null;
   end;    
  end if;
   ---------------
   select decode(l_create_person, null, '', 'Создал запись ' || l_create_person || chr(10)) into l_create_person from dual;
   select decode(l_create_org_id, null, '', 'Структурное подразделение ' || l_create_org_id || chr(10)) into l_create_org_id from dual;
   select decode(l_create_job, null, '', 'Должность ' || l_create_job || chr(10)) into l_create_job from dual;
   if length(trim(l_create_contact)) > 0 then
      l_create_contact := 'Контакты ' || l_create_contact || chr(10);
   else
      l_create_contact := '';
   end if;
   select decode(l_create_date, null, '', 'Дата создания записи ' || l_create_date || chr(10)) into l_create_date from dual;

   select decode(l_update_person, null, '', 'Изменил запись ' || l_update_person || chr(10)) into l_update_person from dual;
   select decode(l_update_org_id, null, '', 'Структурное подразделение ' || l_update_org_id || chr(10)) into l_update_org_id from dual;
   select decode(l_update_job, null, '', 'Должность ' || l_update_job || chr(10)) into l_update_job from dual;
   if length(trim(l_update_contact)) > 0 then
      l_update_contact := 'Контакты ' || l_update_contact || chr(10);
   else
      l_update_contact := '';
   end if;
   select decode(l_update_date, null, '', 'Дата изменения записи ' || l_update_date) into l_update_date from dual;
  
  l_result_line := l_create_person ||
  l_create_org_id ||
  l_create_job ||
  l_create_contact ||
  l_create_date || chr(10) ||
 
  l_update_person ||
  l_update_org_id ||
  l_update_job ||
  l_update_contact ||
  l_update_date;
  return l_result_line;
  exception
    when OTHERS then
      return 'Ошибка! p_create_id=' || l_create_id || ';' ||
      'l_update_id=' || l_update_id || ';' ||
      'l_create_date=' || l_create_date || ';' ||
      'l_update_date=' || l_update_date ||
      'error info: ' || sqlerrm;   
 end get_record_history;
Этап 3: создание и связывание функции, формы в Forms. После того, как всё это будет создано, установка производится следующим скриптом:
cp ./forms/XXFND999_HISTORY_RECORD_ADV.fmb $AU_TOP/forms/RU
cp ./forms/XXFND999_HISTORY_RECORD_ADV.fmb $AU_TOP/forms/US
cp ./resource/CUSTOM.pll $AU_TOP/resource/CUSTOM.pll
cp ./resource/XXFND999_CUSTOM.pll $AU_TOP/resource/XXFND999_CUSTOM.pll
cd $AU_TOP/forms/RU
frmcmp_batch.sh module=$AU_TOP/forms/RU/XXFND999_HISTORY_RECORD_ADV.fmb userid=$APPSID module_type=form compile_all=yes output_file=$XX_TOP/forms/RU/XXFND999_HISTORY_RECORD_ADV.fmx
cd $AU_TOP/forms/US
frmcmp_batch.sh module=$AU_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmb userid=$APPSID module_type=form compile_all=yes output_file=$XX_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmx
cd $AU_TOP/resource
frmcmp_batch.sh module=$AU_TOP/resource/XXFND999_CUSTOM.pll userid=$APPSID module_type=library compile_all=yes output_file=$AU_TOP/resource/XXFND999_CUSTOM.plx
frmcmp_batch.sh module=$AU_TOP/resource/CUSTOM.pll userid=$APPSID module_type=library compile_all=yes output_file=$AU_TOP/resource/CUSTOM.plx
Результат:

Грабли.

FRM-41106 Cannot create record without parent. Не соблюдено обращение к данным по модели BLOCK.ITEM. Если блок можем быть любым, то корректное получение значения из item такое:
name_in(name_in('SYSTEM.CURSOR_BLOCK')||'.CREATED_BY'));
FRM-10056: Incorrect module type stored in the file. Неправильный параметр в sh-скрипте установки расширения. Внимание:
frmcmp_batch.sh module=$AU_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmb userid=$APPSID module_type=form compile_all=yes output_file=$XX_TOP/forms/US/XXFND999_HISTORY_RECORD_ADV.fmx
FRM-91507: Internal Error: Unable to generate library. Ошибка в кастомной библиотеке

FRM-30085: Unable to adjust form for output. Форма слишком велика, либо элементы выходят за границы формы.

FRM-47023:No such parameter named G_QUERY_FIND exists in form. Хитрая ошибка, которая поначалу вводит в тупик, потому как G_QUERY_FIND - стандартный параметр, который есть всегда. Имеет смысл проверить тип данных параметров. Даже если передается дата, то тип данных должен быть Char. После этой манипуляции проблема ушла.

ORA-06502: PL/SQL: numeric or value error: character string buffer too small. По умолчанию размер поля вывода ограничен 30 символами. Эта ошибка появится, если переменная будет содержать строковое значение большей длины. Имеет смысл поставить 32 000.

23.04.2014

Загрузка и распарсивание файла xls с помощью Java-based concurrent

Цель: прочитать файл электронной таблицы Excel 95-2003 и загрузить результат в таблицу БД Oracle 11g. В нашем примере мы хотим прочитать данные из одной ячейки и положить их в таблицу create table HF_TMP (val VARCHAR2(50))
Алгоритм решения:
1. Загрузка файла через пользовательский интерфейс. Применяется messageFileUploadBean с типом данных BLOB. 

При нажатии кнопки Загрузить происходит вставка записи в таблицу загрузок вместе с бинарным файлов Excel в blob-поле, присваивание upload_id и запуск Java-based канкарента, в который этот upload_id передается.
CO:
Serializable[] aserializable = { uploadId };
Class[] aclass = { uploadId.getClass() };
String requestId = (String)am.invokeMethod("sendConcurrentRequest", aserializable, aclass);
am.getTransaction().commit();
AM:
public void sendConcurrentRequest(Number uploadId) throws RequestSubmissionException {
  ConcurrentRequest cpRequest = new ConcurrentRequest(this.getOADBTransaction().getJdbcConnection());
  Vector requestParameters = new Vector(1);
  requestParameters.add(uploadId.stringValue());
  int requestId = cpRequest.submitRequest(UploadHistFactBean.APPLICATION_SHORT_NAME, UploadHistFactBean.CONCURRENT_PROGRAM_NAME, null, null, false, requestParameters);
}
2. Работа канкарента
    // считываем BLOB
    public InputStream getFileForUploadId(){
        StringBuffer strBuf = new StringBuffer();
        InputStream dataStream = null;
        String lQuery = "select file_data from DATA_UPLOADS where request_id = :1";
        String res = "";
        try
        {
          OracleCallableStatement statement = (OracleCallableStatement)connection.prepareCall(lQuery);
          statement.setInt(1, requestId);
          ResultSet resultSet = statement.executeQuery();
          while (resultSet.next()) {                  
              BLOB blob = ((OracleResultSet)resultSet).getBLOB(1);
              dataStream = blob.getBinaryStream();
          }       
        } catch (SQLException e)
        {
          throw OAException.wrapperException(e);
        }
        return dataStream;
    }
    public void initialize(CpContext cpContext) throws SQLException,
                                                       XMLParseException,
                                                       SAXException,
                                                       IOException,
                                                       InstantiationException,
                                                       IllegalAccessException,
                                                       ClassNotFoundException,
                                                       NoSuchMethodException,
                                                       InvocationTargetException     {
        connection = cpContext.getJDBCConnection();
        requestId = cpContext.getReqDetails().getRequestId();
        logFile = cpContext.getLogFile();
        reqCompletion = cpContext.getReqCompletion();
        NameValueType parameter;
        while (cpParameters.hasMoreElements()) {
         parameter = cpParameters.nextParameter();
          if ("p_upload_id".equalsIgnoreCase(parametr.getName())) {
          this.uploadId = Integer.valueOf(parametr.getValue()).intValue();
        }
    }
    public void runProgram(CpContext cpContext) {
        try {
            initialize(cpContext);
            HashMap<String, String> inputParameters = new HashMap<String, String>();
            inputParameters.put(DataInput.PARAMETER_PREFIX + "1",                                   "oracle.apps.xxmy.test.bean.TemplateXML");
            InputStream file = getFileForUploadId();

            { логика по распарсиванию файла Excel }
            connection.commit();

            // Успешно завершим конкаррент
            Message normComplMessage = new Message("FND", "CONC-CP SUCCESSFUL TERMINATION");
            reqCompletion.setCompletion(ReqCompletion.NORMAL, normComplMessage.getMessageText(cpContext.getResourceStore()));
        } catch (Exception e) {
            // Если произошла ошибка, нужно выставить конкаренту ошибочный статус и выдать в лог стек ошибки
            reqCompletion.setCompletion(ReqCompletion.ERROR, e.getMessage());
            StringWriter errStringWriter = new StringWriter();
            PrintWriter errPrintWriter = new PrintWriter(errStringWriter, true);
            e.printStackTrace(errPrintWriter);
            logFile.writeln(errStringWriter.toString(), LogFile.ERROR);
        } finally {
            cpContext.releaseJDBCConnection();
        }
    }
3. Распарсивание XLS с помощью библиотеки POI. Шаблон oracle.apps.xxmy.test.bean.TemplateXML из шага 2 содержит набор параметров для вызова API вставки в таблицу с указанием данных о том, в каких ячейках брать значения. В нашем случае:
<?xml version="1.0" encoding='windows-1251'?>
<upload method="XXMY_TEST_PKG.upload_excel">
 <element name="test" type="single">
  <location src="sheet" sheet="Sheet1" row="0" col="0"/>
 </element>
</upload>
Пробегаясь по структуре данных и файлу данных, мы формируем вызов API:
begin XXMY_TEST_PKG.upload_excel(P_TEST => :1, P_REQUEST_ID => :2, P_UPLOAD_ID => :3, P_UPLOAD_METHOD => :4); end;
Имея коллекции параметров и их значений, выполняем вызов:
    public void doDbUpload() throws SQLException {
        OracleCallableStatement statement = (OracleCallableStatement)connection.prepareCall(this.query);
        ArrayList<Integer> keyList = new ArrayList<Integer>();
        keyList.addAll(uploadParameters.keySet());
        Collections.sort(keyList);
        Iterator<Integer> parameterIterator = keyList.iterator();
        while (parameterIterator.hasNext()) {
            Integer parameterIndex = parameterIterator.next();
            UploadParameter parameter = this.uploadParameters.get(parameterIndex);
            statement.setString(parameterIndex.intValue(), parameter.getValue());
        }
        statement.execute();
        statement.close();
    }
Всё. Данные из файла отправляются в таблицу.

18.02.2014

WebADI: правка LDT вручную

Функционал интегратора OeBS, прямо скажем, не совершенен.
Глюки, с которыми сталкивался я:
  • быстрее и проще создать интегратор с нуля, чем править уже имеющийся. При удалении объектов интегратора не удаляются внутренние связи, и если удалить, например, один контент и создать новый под другим именем, мэппинг будет упорно ссылаться на старый;
  • если в интеграторе нет ошибок, это ещё не значит, что он будет корректно выполнять свою задачу. Это касается, например, добавления новых колонок в шаблон и интерфейсную таблицу. Колонки созданы, изменения в интерфейсе, мэппинге и контенте проведены, шаблон выгружается и загружается нормально, но данные просто не попадают в таблицу. Никаких ошибок, никакого результата;
  • при создании разметки полей документа двигать их по одному, пока не добьешься нужного порядка - занятие неэффективное, но другого инструмента BNE не предоставляет. Почему бы не выводить окошко с номером сразу для всех колонок? Здесь глюк был в том, что если спускать колонку сверху вниз, то на середине пути она замирает и не реагирует на нажатия кнопок "вверх", "вниз".
Итак, когда у нас есть большой и сложный интегратор, а нам нужно туда добавить несколько колонок, можно поправить LDT вручную. Программа действий:
1. Добавление атрибутов в интерфейс
BEGIN BNE_INTERFACE_COLS "2" //увеличивать это число
      OWNER = "ANONYMOUS"
      OBJECT_VERSION_NUMBER = "2"
      INTERFACE_COL_TYPE = "1"
      INTERFACE_COL_NAME = "LOAD_CODE"
      ENABLED_FLAG = "Y"
      REQUIRED_FLAG = "N"
      DISPLAY_FLAG = "Y"
      READ_ONLY_FLAG = "N"
      NOT_NULL_FLAG = "N"
      SUMMARY_FLAG = "N"
      MAPPING_ENABLED_FLAG = "Y"
      DATA_TYPE = "1"
      FIELD_SIZE = "22"
      DEFAULT_TYPE = "PARAMETER"
      DEFAULT_VALUE = "XXIP361_CODE"
      VAL_COMPONENT = "" ""
      DISPLAY_ORDER = "2" //увеличивать это число
      PROMPT_LEFT = "Код загрузки"
      PROMPT_ABOVE = "LOAD_CODE"
      LAST_UPDATE_DATE = "2013/05/24"
      VAL_QUERY = "" ""
      EXPANDED_SQL = "" ""
    END BNE_INTERFACE_COLS 
2. Добавление атрибутов в контент
BEGIN BNE_CONTENT_COLS "3" //увеличивать это число
      OBJECT_VERSION_NUMBER = "1"
      OWNER = "ANONYMOUS"
      COL_NAME = "PROJECT_CODE"
      USER_NAME = "PROJECT_CODE"
      LAST_UPDATE_DATE = "2013/05/24"
      READ_ONLY_FLAG = "N"
    END BNE_CONTENT_COLS
3. Правка таблицы BNE_STORED_SQL. Запрос апдейтим напрямую, добавив новые поля интерфейсной таблицы.
4. Добавление атрибутов в мэппинг BNE_MAPPING_LINES (внимательно!)
BEGIN BNE_MAPPING_LINES "2"  //увеличиваем это число
      INTERFACE_ASN = "XXMY"
      INTERFACE_CODE = "XXMY_LOAD_X_INTF1"
      INTERFACE_SEQ_NUM = "12"  //ID атрибута интерфейса
      OWNER = "ANONYMOUS"
      DECODE_FLAG = "N"
      OBJECT_VERSION_NUMBER = "1"
      CONTENT_ASN = "XXRT"
      CONTENT_CODE = "XXMY_LOAD_CNT2"
      CONTENT_SEQ_NUM = "2"    //ID атрибута контента
      LAST_UPDATE_DATE = "2013/05/24"
    END BNE_MAPPING_LINES
5. Добавление атрибутов в LAYOUT (внимательно!)
BEGIN BNE_LAYOUT_COLS "520"  //увеличиваем число с шагом в 10
        OWNER = "APANIN"
        OBJECT_VERSION_NUMBER = "1"
        INTERFACE_ASN = "XXMY"
        INTERFACE_CODE = "XXMY_LOAD_X_INTF1"
        INTERFACE_SEQ_NUM = "45" //ID атрибута интерфейса
        DISPLAY_WIDTH = "15"
        LAST_UPDATE_DATE = "2014/02/13"
        READ_ONLY_FLAG = "N"
      END BNE_LAYOUT_COLS
После этих манипуляций интегратор должен работать с новыми колонками.
Порядок столбцов в формируемом документе excel можно узнать запросом:
with col_order as
 (select intr.interface_col_name, col.sequence_num
    from BNE_LAYOUTS_B        lay,
         BNE_LAYOUT_BLOCKS_B  blo,
         BNE_LAYOUT_COLS      col,
         BNE_INTERFACE_COLS_B intr
   where lay.layout_code = blo.layout_code
     and blo.layout_code = col.layout_code
     and blo.block_id = col.block_id
     and intr.sequence_num = col.interface_seq_num
     and intr.interface_code = 'XXMY_LOAD_X_INTF1'
     and lay.layout_code = 'XXMY_CORRECT_LOAD'
     and blo.block_id = 2  //номер блока (1 - шапка документа)
   order by col.sequence_num)
select co.interface_col_name, co.sequence_num from col_order co;
Для изменения порядка делаем апдейт таблицы BNE_LAYOUT_COLS.

25.01.2014

Особенность функции PIVOT


Для облегчения запросов предварительно выполним
alter session set NLS_DATE_LANGUAGE = RUSSIAN
Положим, в текущем месяце мы сделали 5 и 8 скворечников, а в следующем сделаем -5 и -8.
select pk,
         initcap(to_char(pivot_key, 'MONTH')) pivot_key,
         sum(value) value
    from (select 1 pk, sysdate pivot_key, 5 value from dual
          union
          select 1, sysdate, 8 from dual
          union
          select 1, sysdate + 31, -5 from dual
          union
          select 1, sysdate + 31, -8 from dual)
   group by pk, initcap(to_char(pivot_key, 'MONTH'))
Развернем выборку, задав месяцы как колонки:
with a as
 (select pk,
         initcap(to_char(pivot_key, 'MONTH')) pivot_key,
         sum(value) value
    from (select 1 pk, sysdate pivot_key, 5 value from dual
          union
          select 1, sysdate, 8 from dual
          union
          select 1, sysdate + 31, -5 from dual
          union
          select 1, sysdate + 31, -8 from dual)
   group by pk, initcap(to_char(pivot_key, 'MONTH'))
 )
select pk, "'Январь'_MON" Jan, 
           "'Февраль'_MON" Feb
  from (select *
          from a pivot(sum(value) mon 
          FOR pivot_key IN('Январь',
                           'Февраль')
       )
);
PK        JAN        FEB
---------- ---------- ----------
         1   
Как видим, результат неверный. Обозначим месяц по-другому.
with a as
 (select pk,
         to_char(pivot_key, 'MM') pivot_key,
         sum(value) value
    from (select 1 pk, sysdate pivot_key, 5 value from dual
          union
          select 1, sysdate, 8 from dual
          union
          select 1, sysdate + 31, -5 from dual
          union
          select 1, sysdate + 31, -8 from dual)
   group by pk, to_char(pivot_key, 'MM')
 )
select pk, "'01'_MON" Jan, 
           "'02'_MON" Feb
  from (select *
          from a pivot(sum(value) mon 
          FOR pivot_key IN('01',
                           '02')
       )
);
PK        JAN        FEB
---------- ---------- ----------
         1         13        -13 
Непонятно, в чем причина ошибки, но точно не в кириллических названиях.
Вывод: для опорного поля в функции PIVOT использовать максимально простые, желательно циферные, псевдонимы.

14.01.2014

Описательные гибкие поля (DF) и OAF

Descriptive Flex Fields (описательные ГП) - инструмент в среде OeBS для универсализации использования одних и тех же полей таблиц для хранения свойств разных сущностей. Обычно это набор полей ATTRIBUTE1..ATTRIBUTE15 varchar2(150) для хранения свойств и поле ATTRIBUTE_CATEGORY для хранения контекста. В зависимости от контекста одно и то же поле может иметь разный бизнес-смысл.