Сегодня расскажу о небольшой доработке 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).
Сохраняем отчет и можем работать с ним.
Комментариев нет:
Отправить комментарий