logo

21 окт. 2013 г.

BI Publisher 11g: выгрузка BLOB из БД

Всем привет!
Сегодня расскажу о небольшой доработке Oracle BI Publisher,
которая позволит выгружать сохраненные в БД файлы в виде BLOB.




Решение заключается в следующем:
1) BLOB-файлы выгружаются из БД в BIPublisher обычным SQL-запросом. BIP умеет их распознавать и преобразует в строку на этапе генерации XML с данными.
2) В БД создается PL/SQL-функция по преобразованию BLOB-файла в CLOB с помощью base64-преобразования. (Это необходимо, так как java-обработчик BLOB-файлов внутри BIPublisher делает корректное преобразование в строку лишь для файлов-картинок).
3) В развернутом веб-приложении BIPublisher создается дополнительный фильтр сервлетов, который перехватывает OutputStream HTTP-ответа и в момент закрытия потока переписывает его содержимое.

Далее подробнее:

Созданные Java классы (файл oracle\xdo\servlet\XxBlobExportFilter.java):
package oracle.xdo.servlet;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import javax.activation.MimetypesFileTypeMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import oracle.xdo.common.encoding.Base64Util;
import oracle.xdo.servlet.resources.ResourceServlet;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class XxBlobExportFilter implements Filter {
    protected FilterConfig config;
    
    public void init(FilterConfig config) throws ServletException {
        this.config = config;
    }
    
    public void destroy() {
    }
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws ServletException, IOException {
        if (request instanceof HttpServletRequest) {
            String reportFormat = request.getParameter("_xf");
            //wrap response only for XML-report request
            if(reportFormat != null && reportFormat.equals("xml")) {
                XxBlobExportResponseWrapper wrappedResponse = new XxBlobExportResponseWrapper((HttpServletRequest) request,
                (HttpServletResponse) response);
                chain.doFilter(request, wrappedResponse);
                wrappedResponse.finishResponse();
                return;
            }
        }
        
        chain.doFilter(request, response);
    }
    
}

class XxBlobExportResponseStream extends ServletOutputStream {
    protected ByteArrayOutputStream fakeOutput = null;
    protected boolean closed = false;
    protected HttpServletRequest request = null;
    protected HttpServletResponse response = null;
    protected ServletOutputStream origOutput = null;
    
    public XxBlobExportResponseStream(HttpServletRequest request, HttpServletResponse response) throws IOException {
        super();
        closed = false;
        this.request = request;
        this.response = response;
        origOutput = response.getOutputStream();
        fakeOutput = new ByteArrayOutputStream();
    }
    
    //main logic here
    public void close() throws IOException {
        if (closed) {
            throw new IOException("This output stream has already been closed");
        }
        byte[] bytes = fakeOutput.toByteArray();
        InputStream is = new ByteArrayInputStream(bytes);
        
        boolean needBlobExport = false;
        
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = null;
        Document doc = null;
        
        try {
            dBuilder = dbFactory.newDocumentBuilder();
            doc = dBuilder.parse(is);
            //check BLOB export necessity
            needBlobExport = "TRUE".equalsIgnoreCase(doc.getElementsByTagName("G_BLOB_EXPORT").item(0).getTextContent());
            } catch (Exception e) {
            //none
        }
        
        if (needBlobExport) {
            
            NodeList blobList = doc.getElementsByTagName("G_BLOB");
            Element blobElement;
            NodeList blobDataList;
            NodeList blobNameList;
            String blobData = null;
            String blobName = null;
            String mimeType = null;
            
            byte[] blobBytes = null;
            
            //if G_BLOB tags not found
            if (blobList.getLength() == 0) {
                blobBytes = "G_BLOB not found".getBytes();
                blobName = "Error.txt";
            } else
            //standard - only one BLOB inside XML
            if (blobList.getLength() == 1) {
                Node blobNode = blobList.item(0);
                if (blobNode.getNodeType() == Node.ELEMENT_NODE) {
                    
                    blobElement = (Element) blobNode;
                    blobDataList = blobElement.getElementsByTagName("BLOB_DATA");
                    blobNameList = blobElement.getElementsByTagName("BLOB_NAME");
                    
                    //check existence of BLOB_DATA/BLOB_NAME tags
                    if (blobDataList.getLength() > 0 && blobNameList.getLength() > 0) {
                        blobData =     blobDataList.item(0).getTextContent();
                        blobName =     blobNameList.item(0).getTextContent();
                        blobBytes = Base64Util.decode(blobData); //get bytes from string/varchar2
                        } else {
                        blobName = "Error.txt";
                        if (blobDataList.getLength() == 0)
                        blobBytes = "BLOB_DATA not found".getBytes();
                        else
                        blobBytes = "BLOB_NAME not found".getBytes();
                    }
                }
            }
            //zipping several BLOBs
            else {
                ByteArrayOutputStream zipBAOS = new ByteArrayOutputStream();
                org.apache.tools.zip.ZipOutputStream zipOutput = new org.apache.tools.zip.ZipOutputStream(zipBAOS);
                zipOutput.setEncoding("CP866"); //cyrillic fileNames support
                
                for (int i = 0; i < blobList.getLength(); i++) {
                    Node blobNode = blobList.item(i);
                    if (blobNode.getNodeType() == Node.ELEMENT_NODE) {
                        
                        blobElement = (Element) blobNode;
                        blobDataList = blobElement.getElementsByTagName("BLOB_DATA");
                        blobNameList = blobElement.getElementsByTagName("BLOB_NAME");
                        
                        //check existence of BLOB_DATA/BLOB_NAME tags
                        if (blobDataList.getLength() > 0 && blobNameList.getLength() > 0) {
                            blobData =     blobDataList.item(0).getTextContent();
                            blobName =     blobNameList.item(0).getTextContent();
                            blobBytes = Base64Util.decode(blobData); //get bytes from string/varchar2
                            } else {
                            blobName = "Error" + i + ".txt";
                            if (blobDataList.getLength() == 0)
                            blobBytes = "BLOB_DATA not found".getBytes();
                            else
                            blobBytes = "BLOB_NAME not found".getBytes();
                        }
                        
                        //write each BLOB as new zip entry
                        zipOutput.putNextEntry(new org.apache.tools.zip.ZipEntry(blobName));
                        zipOutput.write(blobBytes, 0, blobBytes.length);
                        zipOutput.closeEntry();
                    }
                }
                
                zipOutput.flush();
                zipOutput.close();
                
                zipBAOS.flush();
                zipBAOS.close();
                
                blobBytes = zipBAOS.toByteArray();
                blobName = "Files.zip";
            }
            
            MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
            mimeType = mimeTypesMap.getContentType(blobName); //get MIME type from BLOB fileName
            
            blobName = ResourceServlet.getEncodedFileName(request, blobName); //encode BLOB fileName (including Firefox content-disposition exception)
            
            response.setHeader("Content-Length", Integer.toString(blobBytes.length));
            response.setHeader("Content-Type", mimeType);
            response.setHeader("Content-Disposition", (new StringBuilder()).append("attachment; filename=\"").append(blobName).append("\"").toString());
            origOutput.write(blobBytes); //write BLOB bytes to real response outputStream
            } else {
            origOutput.write(bytes);
        }
        
        origOutput.flush();
        origOutput.close();
        closed = true;
    }
    
    public void flush() throws IOException {
        if (closed) {
            throw new IOException("Cannot flush a closed output stream");
        }
        fakeOutput.flush();
    }
    
    public void write(int b) throws IOException {
        if (closed) {
            throw new IOException("Cannot write to a closed output stream");
        }
        fakeOutput.write((byte)b);
    }
    
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }
    
    public void write(byte b[], int off, int len) throws IOException {
        if (closed) {
            throw new IOException("Cannot write to a closed output stream");
        }
        fakeOutput.write(b, off, len);
    }
    
    public boolean closed() {
        return (this.closed);
    }
    
    public void reset() {
    }
}

class XxBlobExportResponseWrapper extends HttpServletResponseWrapper {
    protected HttpServletRequest origRequest = null;
    protected HttpServletResponse origResponse = null;
    protected ServletOutputStream stream = null;
    protected PrintWriter writer = null;
    
    public XxBlobExportResponseWrapper(HttpServletRequest request, HttpServletResponse response) {
        super(response);
        origRequest = request;
        origResponse = response;
    }
    
    public ServletOutputStream createOutputStream() throws IOException {
        return (new XxBlobExportResponseStream(origRequest, origResponse)); //it's main purpose of wrapper
    }
    
    public void finishResponse() {
        try {
            if (writer != null) {
                writer.close();
                } else {
                if (stream != null) {
                    stream.close();
                }
            }
    } catch (IOException e) {}
    }
    
    public void flushBuffer() throws IOException {
        stream.flush();
    }
    
    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called!");
        }
        
        if (stream == null)
        stream = createOutputStream();
        return (stream);
    }
    
    public PrintWriter getWriter() throws IOException {
        if (writer != null) {
            return (writer);
        }
        
        if (stream != null) {
            throw new IllegalStateException("getOutputStream() has already been called!");
        }
        
        stream = createOutputStream();
        writer = new PrintWriter(new OutputStreamWriter(stream, "UTF-8"));
        return (writer);
    }
    
    public void setContentLength(int length) {}
}

Ссылка на архив с исходным кодом и скомпилированными классами

Включение созданного фильтра сервлетов в развернутое веб-приложение BIPublisher
1) Найдите ear-файл приложения в каталоге Middleware\Oracle_BI1\bifoundation\jee
2) Переименуйте xmlpserver.ear в xmlpserver.ear.original
3) Создайте новую папку xmlpserver.ear. Разархивируйте в нее содержимое файла xmlpserver.ear.original.



4) В каталоге xmlpserver.ear переименуйте файл xmlpserver.war в xmlpserver.war.original.
5) Создайте новую папку xmlpserver.war. Разархивируйте в нее содержимое файла xmlpserver.war.original.



6) Перейдите в каталог WEB-INF в папке xmlpserver.war.
7) Создайте новую папку classes и скопируйте в нее скомпилированные классы, включая их полный путь: oracle.xdo.servlet



8) Далее необходимо указать приложению xmlpserver, что все HTTP-запросы с URL "*.xdo" должны обрабатываться новым фильтром – XxBlobExportFilter. Для этого вносим изменения в файл xmlpserver.war/WEB-INF/web.xml
<filter>
  <filter-name>XxBlobExportFilter</filter-name>
  <filter-class>oracle.xdo.servlet.XxBlobExportFilter</filter-class>
</filter>  
<filter-mapping>
  <filter-name>XxBlobExportFilter</filter-name>
  <url-pattern>/servlet/xdo</url-pattern>
</filter-mapping>
<filter-mapping>
  <filter-name>XxBlobExportFilter</filter-name>
  <url-pattern>*.xdo</url-pattern>
</filter-mapping>



9) Также необходимо обновить инсталляцию приложения bipublisher (xmlpserver) в консоли WebLogic.





Указанные действия обеспечат обработку всех HTTP-запросов к веб-приложению xmlpserver новым фильтром.


Работы в БД

1) Создадим таблицу в БД, хранящую BLOB-файлы.
create table BLOB_TBL
(
  blob_data BLOB,
  blob_name VARCHAR2(150)
)

2) Создадим функцию по base54-преобразованию BLOB в CLOB
CREATE OR REPLACE FUNCTION base64encode(p_blob IN BLOB)
  RETURN CLOB
IS
  l_clob CLOB;
  l_step PLS_INTEGER := 12000; -- make sure you set a multiple of 3 not higher than 24573
BEGIN
  FOR i IN 0 .. TRUNC((DBMS_LOB.getlength(p_blob) - 1 )/l_step) LOOP
    l_clob := l_clob || UTL_RAW.cast_to_varchar2(UTL_ENCODE.base64_encode(DBMS_LOB.substr(p_blob, l_step, i * l_step + 1)));
  END LOOP;
  RETURN l_clob;
END;

Создание отчета в BIPublisher

1) Создадим новую модель данных с набором данных типа SQl Query
Альясы столбцов должны быть следующими: BLOB_DATA и BLOB_NAME (java-код фильтра будет искать именно эти теги в сформированном XML-файле).



2) Очевидно, что не всегда требуется включение фильтра. Поэтому для отчетов, выгружающих BLOB-файлы из БД, мы создадим признак с тегом G_BLOB_EXPORT. Значение TRUE будет являться сигналом для работы фильтра.




3) Также нам следует явно задать имя группы в формируемом XML с данными, которая будет содержать выгружаемые BLOB (по сути - CLOB) файлы. Для этого перейдите на вкладку Structure модели данных и задайте имя группы равным G_BLOB



4) Последний шаг – это создание шаблона разметки. Без него отчет создать не получится.
Но как таковой шаблон нам не нужен. Поэтому мы можем использовать в качестве него пустой RTF-файл.
Для загруженного шаблона снимаем возможность выгрузки во все форматы кроме Data (XML).
Сохраняем отчет и можем работать с ним.




Комментариев нет:

Отправить комментарий