logo

4 февр. 2013 г.

BI Publisher 11g: отправка zip-результатов по email

Привет всем!

Сегодня хочу рассказать о небольшой доработке BI Publisher 11g, которая позволит вам
отсылать по email результаты отчетов в виде zip-архивов.

1. Настройка bursting
В данном посте я исхожу из допущения, что вся работа с рассылаемыми результатами отчетов ведется через механизм bursting.

В панели администрирования BI Publisher выберем пункт Scheduler Configuration


Удостоверимся, что используемое соединение со схемой БД, используемой для хранения информации о запущенных процессах, рабочее.



Далее найдем какой-либо существующий отчет, для которого настроим механизм bursting ("дробления")


Отредактируем модель данных выбранного отчета


Создадим новое правило, описывающее "дробление". Правило будем задавать SQL-запросом к существующей БД Oracle (через заранее созданный JDBC/JNDI источник данных)

select
200402 KEY,
'Standard' TEMPLATE,
'RTF' TEMPLATE_FORMAT,
'en-US' LOCALE,
'PDF' OUTPUT_FORMAT,
'EMAIL' DEL_CHANNEL,
'TEST_REP#ZIP#' OUTPUT_NAME,
'sw_mail@mail.ru' PARAMETER1,
'' PARAMETER2,
'bipublisher@oracle.com' PARAMETER3,
'SUBJ: Bursting Test' PARAMETER4,
'BODY: Bursting Test'  PARAMETER5,
 'true' PARAMETER6,
'reply_to@mycompany.com' PARAMETER7
from dual

где
Parameter1: Email address
Parameter2: cc
Parameter3: From
Parameter4: Subject
Parameter5: Message body
Parameter6: Attachment value ("true" or "false"). If your output format is PDF, you must set this parameter to "true" to attach the PDF to the e-mail.
Parameter7: Reply-To
Parameter8: Bcc
(Parameters 9-10 are not used) 
Подробнее о параметрах инструкции "дробления" можно узнать в документации по BI Publisher 11g.

/* Ключ "дробления" в данном примере указан равным 200402, так как в отчете большая часть данных приведена именно для этого значения. В ваших отчетах ключ, естественно, будет другим. Кроме того, если как таковая разбивка результата отчета не несколько файлов вам не нужна, но нужна отправка результата отчета по email с архивированием - то ничего вам не мешает определить фиктивный ключ разбиения в структуре модели данных и "разбивать" отчет по нему */


Сохраним изменения в модели данных и вернемся в режим редактирования самого отчета, где отредактируем его свойства. А именно - укажем, что отчет должен "дробиться" с помощью только что созданного определения дробления.


Сохраним изменения и вернемся в каталожное представление отчетов.
Запустим наш отчет на выполнение в режиме планирования.


Оставим все значения планировщика по умолчанию.


Если все было сделано верно, то спустя какое-то время вам придет на почтовый адрес (определенный в сценарии дробления) письмо с заданной темой, телом и вложением - результатом отчета.
/* Само собой, ваш сервер BI Publisher должен содержать корректную настройку smtp-сервера */


Проблема же в том, что иногда бывают ситуации, когда отправляемые вложения-результата отчетов могут быть ОЧЕНЬ большими. Логичным решением в данном случае является архивирование.
Но, к сожалению, интерфейс BI Publisher не дает нам возможности настроить архивирование.
/* Правда, если вы работаете с BI Publisher как с набором java-API, то варианты возможны... */

2. Шеф, все пропало!


Как говорил мой хороший знакомый: "Безвыходных ситуаций не бывает".

Все что нам потребуется - это немного доработать код стандартного класса поставки BI Publisher - oracle.xdo.service.delivery.impl.DeliveryServiceImpl.
А именно - добавить в самое начало метода
public DeliveryStatus deliverToEmail(InputStream pDocument, String pFilename, String pContentType, String pFrom, String pTo, String pCC, String pBCC, 
            String pReplyTo, String pSubject, String pMessage, String pEmailServerName, Properties pProps)
        throws DeliveryException
следующий код:
if (pFilename.toUpperCase().indexOf("#ZIP#") != -1)
{
  Logger.getLogger("oracle.xdo.server.delivery").finest("");
  try
  {
    pFilename = pFilename.replace("#ZIP#", "").replace("#zip#", "");
                    
        java.io.File tmpZipFile = oracle.xdo.servlet.TemporaryStorage.getFile();
        java.io.OutputStream tmpZipOut = new java.io.BufferedOutputStream(new java.io.FileOutputStream(tmpZipFile));
                    
        org.apache.tools.zip.ZipOutputStream zout = new org.apache.tools.zip.ZipOutputStream(tmpZipOut);
        zout.setEncoding("CP866");
        zout.putNextEntry(new org.apache.tools.zip.ZipEntry(pFilename));
                                                
        int length = -1;
        byte buffer[] = new byte[8192];
        while((length = pDocument.read(buffer)) != -1) 
        {
          zout.write(buffer, 0, length);
        }
                    
        zout.closeEntry();
        zout.flush();
        zout.close();
                    
        tmpZipOut.flush();
        tmpZipOut.close();
        tmpZipOut = null;
                    
        pDocument.close();
        pDocument = null;
        pDocument = new java.io.BufferedInputStream(new java.io.FileInputStream(tmpZipFile));
                                        
        pFilename = pFilename.concat(".zip");
        pContentType = "application/zip";
  }
    catch(Exception e)
    {
      e.printStackTrace();
    }
}  

3. Собственно, хакерство
По-моему, несложно понять что делает приведенный выше код.
Вопрос в том, как его добавить в начало метода java-класса.
/* Представим, что мы честные и пушистые, и нам претит сама мысль декомпилировать указанный класс и подменить его, закинув измененную копию в папку c:\Middleware\user_projects\domains\bifoundation_domain\servers\AdminServer\tmp\_WL_user\bipublisher_11.1.1\6uc731\APP-INF\classes */

Воспользуемся тонкой настройкой java-программ с помощью механизма Instrumentation и javaassist.
Я не буду глубоко вдаваться в подробности - суть моего поста не в этом.
Скажу лишь, что мы с помощью механизма Instrumentation определим новый javaagent-класс, который будет "слушать" загрузку в JVM ВСЕХ классов. И если среди них окажется интересующий нас DeliveryServiceImpl, то его байт-код будет трансформирован таким образом, чтобы в методе deliverToEmail содержал в самом начале НАШ код.

Для этого нам потребуется создать 2 новых класса:
- класс-агент
package ru.servplus.common;

import java.lang.instrument.Instrumentation;

public class PatchClassAgent {

 static private Instrumentation inst = null;
  
    public static void agentmain(String agentArgs, Instrumentation instrumentation)
    {
        premain(agentArgs, instrumentation);
    } 
 
 public static void premain(String agentArgs, Instrumentation instrumentation){
  inst = instrumentation;
  inst.addTransformer(new XdoDeliveryServiceTransformer());
 }

}
и класс-трансформатор ;)
package ru.servplus.common;

import java.lang.instrument.*;
import java.security.ProtectionDomain;
import java.io.*;
import javassist.*;
import java.util.HashSet;
import java.util.Set;


public class XdoDeliveryServiceTransformer implements ClassFileTransformer {

    private ClassPool classPool = ClassPool.getDefault();
    private Set<ClassLoader> loaders = new HashSet<ClassLoader>(); 
 
 private static String PATCHED_CLASS_NAME = "oracle/xdo/service/delivery/impl/DeliveryServiceImpl";  
 private static String PATCHED_METHOD_NAME = "deliverToEmail";
 private static String PATCHED_METHOD_BODY = 
 "{\n" + 
 "      if ($2.toUpperCase().indexOf(\"#ZIP#\") != -1)\n" + 
 "      {\n" +  
 "        try\n" + 
 "        {\n" +  
 "          $2 = $2.replace(\"#ZIP#\", \"\").replace(\"#zip#\", \"\");\n" + 
 "\n" + 
 "              java.io.File tmpZipFile = oracle.xdo.servlet.TemporaryStorage.getFile();\n" + 
 "              java.io.OutputStream tmpZipOut = new java.io.BufferedOutputStream(new java.io.FileOutputStream(tmpZipFile));\n" + 
 "\n" + 
 "              org.apache.tools.zip.ZipOutputStream zout = new org.apache.tools.zip.ZipOutputStream(tmpZipOut);\n" + 
 "              zout.setEncoding(\"CP866\");\n" +
 "              zout.putNextEntry(new org.apache.tools.zip.ZipEntry($2));\n" + 
 "\n" + 
 "              int length = -1;\n" + 
 "              byte buffer[] = new byte[8192];\n" + 
 "              while((length = $1.read(buffer)) != -1)\n" + 
 "              {\n" + 
 "                zout.write(buffer, 0, length);\n" + 
 "              }\n" + 
 "\n" + 
 "              zout.closeEntry();\n" + 
 "              zout.flush();\n" + 
 "              zout.close();\n" + 
 "\n" + 
 "              tmpZipOut.flush();\n" + 
 "              tmpZipOut.close();\n" + 
 "              tmpZipOut = null;\n" + 
 "\n" + 
 "              $1.close();\n" + 
 "              $1 = null;\n" + 
 "              $1 = new java.io.BufferedInputStream(new java.io.FileInputStream(tmpZipFile));\n" + 
 "\n" + 
 "              $2 = $2.concat(\".zip\");\n" + 
 "              $3 = \"application/zip\";\n" + 
 "        }\n" + 
 "          catch(Exception e)\n" + 
 "          {\n" + 
 "              e.printStackTrace();\n" + 
 "          }\n" + 
 "      }\n" + 
 "}";


    private synchronized final void addClassLoader(ClassLoader loader) {
        if (!loaders.contains(loader)) {         
                loaders.add(loader);
                ClassPath cp = new LoaderClassPath(loader);
                classPool.appendClassPath(cp);
        }
    }

 @Override
 public byte[] transform(ClassLoader loader, String className,
   Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
   byte[] classfileBuffer) throws IllegalClassFormatException {

  byte [] transformed = null;  
 
  if( PATCHED_CLASS_NAME.equals(className) ) {
   System.out.println("Trying to transform " + className);
   
   if (!loaders.contains(loader)) {
                addClassLoader(loader);
   }   

   CtClass cl = null;
   try {
    cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
    CtMethod method = cl.getDeclaredMethod(PATCHED_METHOD_NAME);
    method.insertBefore(PATCHED_METHOD_BODY);
    transformed = cl.toBytecode();
    cl.detach();
   } catch (Throwable e) {
    System.err.println("Failed to transform " + className);
    e.printStackTrace();
   }
 
  }
  
  return transformed;
 }
}

Из кода видно, что класс-агент добавляет в стек трансформаторов новый класс, задача которого - в случае загрузки в JVM класса oracle/xdo/service/delivery/impl/DeliveryServiceImpl добавить в начало метода deliverToEmail
новый код.

Вызов класс-агента мы обеспечим правкой параметров инициализации JVM для сервера WLS (bi_server1 или AdminServer).
Для этого внесем изменения в файл setDomainEnv.cmd (setDomainEnv.sh для *nix )


Суть действий в том,

а) чтобы в CLASSPATH нужного сервера WLS были добавлены пути до jar-файлов javaassist.jar, servplus.jar и ant.jar (из стандартной поставки Apache Ant).
/* Первый jar-архив содержит классы проекта javassist, второй - классы PatchClassAgent и XdoDeliveryServiceTransformer, третий - классы, необходимые для корректной работы с кириллицей в архивах.
Также, крайне важно содержание файла MANIFEST.MF архива servplus.jar
Manifest-Version: 1.0
Premain-Class: ru.servplus.common.PatchClassAgent
Agent-Class: ru.servplus.common.PatchClassAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
где определяется класс-агент.
*/

б) была добавлена опция
set JAVA_OPTIONS=%JAVA_OPTIONS% -javaagent:c:/Middleware/wlserver_10.3/server/lib/servplus.jar
где "-javaagent" - опция JVM, указывающая путь до jar-файла к классом-агентом (ну, а какой класс внутри jar является собственно агентом определяется на основе свойства Agent-Class файла MANIFEST.MF)
/* важно, чтобы слеш в пути до jar-файла был "прямой" */

в) не забудьте скопировать jar-файлы javaassist.jar, servplus.jar и ant.jar в директорию {Middleware}\wlserver_10.3\server\lib

г) если вы запускаете ваши WLS сервера с помощью сервисов Windows (как описано на gerardnico.com), то следует пересоздать сервисы после правки файла setDomainEnv.cmd (либо внести соответствующие изменения непосредственно в параметры сервиса)


4. Эпилог
Опять же, если все проделано верно, то после запуска запланированного отчета BI Publisher, в названии которого присутствует #ZIP#, получателям отчета придет email с вложением в виде ZIP-архива.



6 комментариев:

  1. Павел Иванов5 февраля 2013 г., 10:52

    Очень полезная статья, спасибо!
    Настроили архивацию исходящих файлов при отправке отчетов по E-mail. Работает.

    Единственный нюанс - внутри архива не распознается кодировка при русскоязычном названии файла.
    При этом русскоязычное название архива и кириллица внутри самого отчета распознаются штатно.

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

    ОтветитьУдалить
  2. Добрый день, Павел!
    Внес изменения по тексту статьи. Суть их в том, что:
    1) используется ant.jar с классами org.apache.tools.zip.*
    org.apache.tools.zip.ZipOutputStream zout = new org.apache.tools.zip.ZipOutputStream(tmpZipOut);
    zout.setEncoding("CP866");
    zout.putNextEntry(new org.apache.tools.zip.ZipEntry(pFilename));
    также выставлена явно кодировка CP866 для потока записи в архив.

    2) сам ant.jar добавлен в каталог WL_HOME/server/lib
    3) в CLASSPATH серверов WLS добавлен путь до ant.jar

    Проделанные изменения позволят вам получать zip-архивы с файлами с кириллическими названиями.

    ОтветитьУдалить
    Ответы
    1. Павел Иванов5 февраля 2013 г., 19:16

      Большое спасибо!
      Все отработало штатно.
      Теперь кириллица распознается на всех уровнях архивирования Отчета.

      Удалить
  3. А к 10g (10.1.3.4) есть способ архивацию вложений прикрутить?

    ОтветитьУдалить
  4. Максим, подозреваю, что можно аналогично описанному прикрутить архивацию и к 10g.
    Но надо проверять.

    ОтветитьУдалить
  5. Здравствуйте. Извините за вопрос не по теме статьи. Как настроить шедулер BI Publishera так, чтобы готовые отчеты он мог положить на шару, просто в указанную папку?

    ОтветитьУдалить