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


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