Logo Oracle Deutschland   Application Express Community
ZIP-Archive ein- und auspacken ... mit Application Express

Der Umgang mit ZIP-Archiven ist etwas ganz alltägliches ... und so ist der Wunsch naheliegend, auch in Application Express-Anwendungen mit ZIP-Archiven umgehen zu können. So ist es denkbar, ein ZIP-Archiv aus mehreren Dokumenten (BLOBs) zu erstellen und via APEX_MAIL als Email zu versenden. Eine andere Anwendung wäre der umgekehrte Weg: Der Anwender lädt ein ZIP-Archiv über eine Formularseite hoch, die Anwendung packt es automatisch aus und speichert die unkomprimierten Dateien als BLOBs in eine Tabelle ab.

Ein erster Ansatz zur Lösung dieser Anforderung wäre das PL/SQL-Paket UTL_COMPRESS. Allerdings kann dies nur einen einzigen BLOB komprimieren oder dekomprimieren, ist also mit dem Unix gzip/gunzip vergleichbar. Ein ZIP-Archiv mit mehreren Dateien und der zugehörigen "Verzeichnisstruktur" kann es nicht bearbeiten. Allein mit PL/SQL lässt sich dies also nicht lösen ...

... allerdings gibt es seit Oracle8i eine vollständige Java-Umgebung in der Datenbank; und Java kann seit Version 1.1 mit ZIP-Archiven umgehen. Der heutige Tipp basiert im wesentlichen auf den Klassen des Java-Pakets java.util.zip und dem Blog Posting "Zip It" von Joel Kallman. Joel's Code haben wir an dieser Stelle ein wenig erweitert, so dass ZIP-Archive nicht nur erstellt, sondern auch "ausgepackt" werden können ...

Damit Sie im APEX auch ZIP-Archive mit Umlauten verwenden können, müssen wir noch einige zusätzliche Vorbereitungen erledigen - Diese Variante des Tipps unterstützt auch ZIP-Archive mit Umlauten vollständig.

1. OpenSource Implementierung für ZIP-Algorithmus inkl. Erweiterung für Umlaute einspielen

Der Grund für die fehlende Unterstützung von Umlauten in der Standard-Implementierung liegt in einem schon recht lange existierenden Bug in den Java ZIP-Klassen von Sun: Diese setzen voraus, dass Dateinamen in ZIP-Archiven in Unicode (UTF-8) kodiert werden. Das ist bei Werkzeugen wie WinZIP aber nicht der Fall; diese kodieren in der Codepage 850. Wenn in Dateinamen keine Umlaute vorhanden sind, ist dies völlig unproblematisch - ist dies jedoch der Fall, so wird eine Fehlermeldung wegen ungültiger Zeichen ausgelöst; das ZIP-Archiv kann nicht ausgepackt werden.

Aber nun zur Lösung: Es gibt eine OpenSource Implementierung für den ZIP-Algorithmus, die als Basis für unsere Problemlösung dienen kann ... Laden Sie daher als erstes die jazzlib herunter - nehmen Sie die Binärvariante der Version 0.07: jazzlib-binary-0.07.zip .

Spielen Sie dieses Archiv nun in die Datenbank ein. Führen Sie von der Kommandozeile aus folgenden Befehl aus:

loadjava -u[apex-parsing-schema]/[passwort] -o -r -v jazzlib-binary-0.07.zip

Sie sollten in etwa folgende Ausgabe sehen:

arguments: '-user' 'ziptest/***' '-o' '-r' '-v' '/mnt/hgfs/C/jazzlib-binary-0.07.zip'
creating : class net/sf/jazzlib/Adler32
loading  : class net/sf/jazzlib/Adler32
created  : CREATE$JAVA$LOB$TABLE
:
:
Classes Loaded: 30
Resources Loaded: 3
Sources Loaded: 0
Published Interfaces: 0
Classes generated: 0
Classes skipped: 0
Synonyms Created: 0
Errors: 0

Spielen Sie nun (ebenfalls mit dem Kommando loadjava) die Java-Klassen ZipInputStream.class (Quellcode) und ZipOutputStream.class (Quellcode) ein:

loadjava -u[apex-parsing-schema]/[passwort] -o -r -v ZipInputStream.class ZipOutputStream.class

2. Wrapper Klassen und PL/SQL-Package installieren

Spielen Sie nun das folgende SQL-Skript (Download als Skript) in das Parsing Schema Ihrer Application Express-Anwendung ein.

drop type zip_entry_ct
/
drop type zip_entry_t
/

create type zip_entry_t as object(
  file_path       varchar2(4000),
  file_name       varchar2(4000),
  is_dir          char(1),
  file_size       number,
  compressed_size number,
  content         blob
)
/
sho err

create type zip_entry_ct as table of zip_entry_t
/
sho err

create or replace java source named "JavaZipCode" as
import net.sf.jazzlib.ZipEntry;
import net.sf.jazzlib.ZipOutputStream;
import net.sf.jazzlib.ZipInputStream;
import java.util.Vector;
import java.io.OutputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.DriverManager;
import java.sql.ResultSet;
import oracle.jdbc.OracleDriver;
import oracle.jdbc.OracleResultSet;
import oracle.sql.BLOB;
import oracle.sql.STRUCT;
import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;
import oracle.sql.StructDescriptor;

public class zip {
    static Connection con = null;
    static  BLOB zipLob = null;    
    static  InputStream lobIs    = null;
    static  ZipInputStream zipIs = null;
    static  ZipEntry zipFile = null;
    static boolean bFileIsOpen = false;

    static {
      try {
        con = DriverManager.getConnection("jdbc:default:connection");
      } catch (Exception e) {
        e.printStackTrace(System.out);
      }
    }

    public static void open(BLOB inLob, String encoding) throws Exception {
      if (bFileIsOpen) {
        throw new Exception ("ZIP File already open - call close() first");
      }
      zipLob = inLob;
      lobIs = inLob.getBinaryStream();
      zipIs = new ZipInputStream(lobIs, encoding);
      bFileIsOpen = true;
    }
  
    public static int getCurrentHandle() throws Exception {
      return (bFileIsOpen?1:0);
    }

    public static int next() throws Exception {
      zipFile = zipIs.getNextEntry();
      return (zipFile == null?0:1);
    }

    private static String getFileName (String sEntryName) {
      int iLastSlashPos = sEntryName.lastIndexOf("/");
      if (iLastSlashPos != -1) {
        return sEntryName.substring(iLastSlashPos + 1);
      } else {
        return sEntryName;
      }
    }
 
    public static STRUCT getEntry() throws Exception {
      if (!bFileIsOpen) {
        throw new Exception ("ZIP File is not open - call open() first");
      }
      StructDescriptor sDescr = 
        StructDescriptor.createDescriptor("ZIP_ENTRY_T", con);
      BLOB blobEntryContent = 
        BLOB.createTemporary(con, true, BLOB.DURATION_SESSION);
      OutputStream lobOs = blobEntryContent.setBinaryStream(0L);
      int iChunkSize = blobEntryContent.getChunkSize();
      byte[] b = new byte[iChunkSize];

      int iBytesRead = 0;

      STRUCT oraZipEntry = null;
      Object[] oZipEntry = null;

      if (zipFile != null) {
       oZipEntry = new Object[6];
       oZipEntry[0] = zipFile.getName();
       oZipEntry[1] = getFileName(zipFile.getName());
       oZipEntry[2] = (zipFile.isDirectory()?"Y":"N");
       oZipEntry[3] = new java.math.BigDecimal(zipFile.getSize());
       oZipEntry[4] = new java.math.BigDecimal(zipFile.getCompressedSize());
       while ( (iBytesRead = zipIs.read(b, 0, iChunkSize)) != -1) {
        lobOs.write(b, 0, iBytesRead);
       }
       lobOs.flush();
       lobOs.close();
       oZipEntry[5] = blobEntryContent;
       oraZipEntry = new STRUCT(sDescr, con, oZipEntry);
      } else {
        throw new Exception ("End of zip file reached");
      }
      return oraZipEntry;
    }

    public static void close() throws Exception{
     lobIs.close();
     zipIs.close();
     lobIs = null;
     zipIs = null;
     bFileIsOpen = false;
    }
     

    public static ARRAY list (BLOB inLob, String encoding) throws Exception {
      InputStream lobIs = inLob.getBinaryStream();
      ZipInputStream zipIs = new ZipInputStream(lobIs, encoding);
      ZipEntry zipFile = null;
      boolean bEndOfArchive = false;

      ArrayDescriptor aDescr = ArrayDescriptor.createDescriptor("ZIP_ENTRY_CT", con);
      StructDescriptor sDescr = StructDescriptor.createDescriptor("ZIP_ENTRY_T", con);

      Object[] oZipEntry = new Object[6];
      STRUCT oraZipEntry = null;
      Vector vZipEntries = new Vector();

      while (!bEndOfArchive) {
        zipFile = zipIs.getNextEntry();
        if (zipFile != null) {
         oZipEntry[0] = zipFile.getName();
         oZipEntry[1] = getFileName(zipFile.getName());
         oZipEntry[2] = (zipFile.isDirectory()?"Y":"N");
         oZipEntry[3] = new java.math.BigDecimal(zipFile.getSize());
         oZipEntry[4] = new java.math.BigDecimal(zipFile.getCompressedSize());
         oZipEntry[5] = null;
         oraZipEntry = new STRUCT(sDescr, con, oZipEntry);
         vZipEntries.add(oraZipEntry);
        } else {
         bEndOfArchive = true;
        }
      }            
      lobIs.close();
      return new ARRAY(aDescr, con, vZipEntries.toArray());
    }

    public static BLOB zip(String query, String encoding) throws Exception {
        PreparedStatement pstmt = con.prepareStatement(query);
        BLOB result = zip(pstmt.executeQuery(), encoding);
        pstmt.close();
        return result;
    }


    public static BLOB zip(ResultSet   rset, String encoding) throws Exception {
        BLOB zipLob = BLOB.createTemporary(con, true, BLOB.DURATION_SESSION);
  
        OutputStream os = zipLob.setBinaryStream(1);
        ZipOutputStream zos = new ZipOutputStream(os, encoding);

        BLOB src = null;
        String filename = null;

        int chunksize = zipLob.getChunkSize();
        while (rset.next()) {
            filename = rset.getString( 1 );
            src = ((OracleResultSet)rset).getBLOB( 2);

            ZipEntry entry = new ZipEntry(filename);
            if (src != null) {
              entry.setSize(src.length());
            } else {
              entry.setSize(0);
            }
            zos.putNextEntry(entry);
            if (src != null) {
              long len = src.length();
              long offset = 1;
              byte[] buffer;
              while (offset < len) {
                 buffer = src.getBytes(offset, chunksize);
                 if (buffer == null) 
                   break;
 
                 zos.write(buffer, 0, buffer.length);  
                 offset += buffer.length;
              }
            }
            zos.closeEntry();
        }
        zos.close();
        rset.close();
        return zipLob;
    }
}
/

alter java source "JavaZipCode" compile
/
sho err

create or replace package zip is
  type cursor_t is ref cursor;

  function zip(p_cursor in cursor_t, p_encoding in varchar2) return BLOB;
  function zip(p_query in varchar2, p_encoding in varchar2) return BLOB;
  function list(p_zipfile in blob, p_encoding in varchar2) return ZIP_ENTRY_CT;
  procedure open(p_zipfile in blob, p_encoding in varchar2);
  function next return number;
  function get_Entry return ZIP_ENTRY_T;
  function is_open return number;
  procedure close;
end zip;
/
sho err

create or replace package body zip is
  function zip(p_cursor in cursor_t, p_encoding in varchar2) return BLOB
  as language java name 'zip.zip(java.sql.ResultSet, java.lang.String) return oracle.sql.BLOB';

  function zip(p_query in varchar2, p_encoding in varchar2) return BLOB
  as language java name 'zip.zip(java.lang.String, java.lang.String) return oracle.sql.BLOB';

  function list(p_zipfile in blob, p_encoding in varchar2) return ZIP_ENTRY_CT
  as language java name 'zip.list(oracle.sql.BLOB, java.lang.String) return oracle.sql.ARRAY';

  procedure open(p_zipfile in blob, p_encoding in varchar2)
  as language java name 'zip.open(oracle.sql.BLOB, java.lang.String)';
 
  function next return number
  as language java name 'zip.next() return int';
 
  function get_Entry return ZIP_ENTRY_T
  as language java name 'zip.getEntry() return oracle.sql.STRUCT';

  procedure close
  as language java name 'zip.close()';

  function is_open return number
  as language java name 'zip.getCurrentHandle() return int';
end zip;
/
sho err

3. Package-Beschreibung

Anschließend steht Ihnen das PL/SQL-Paket ZIP zur Verfügung:

PROCEDURE OPEN(P_ZIPFILE BLOB, P_ENCODING VARCHAR2) Öffnet das im BLOB enthaltene ZIP-Archiv
PROCEDURE IS_OPEN
RETURN NUMBER
Gibt "1" zurück, wenn ein ZIP-Archiv geöffnet ist, ansonsten "0"
FUNCTION NEXT
RETURN NUMBER
Navigiert zur nächsten Datei im ZIP-Archiv und gibt "1" zurück. Wenn das Ende des ZIP-Archivs erreicht ist, wird "0" zurückgegeben.
FUNCTION GET_ENTRY
RETURN ZIP_ENTRY_T
Ruft die Datei aus dem ZIP-Archiv als Datentyp ZIP_ENTRY_T ab. Die Datei selbst ist darin als Attribut CONTENT vom Typ BLOB verfügbar.
PROCEDURE CLOSE Schließt das gerade geöffnete ZIP-Archiv
FUNCTION LIST(P_ZIPFILE BLOB, P_ENCODING VARCHAR2)
RETURN ZIP_ENTRY_CT
Diese Table-Function listet die Inhalte des ZIP-Archivs in P_ZIPFILE auf; das ZIP-Archiv muss hierfür nicht geöffnet werden.
FUNCTION ZIP(P_QUERY VARCHAR2, P_ENCODING VARCHAR2)
RETURN BLOB
Erstellt ein ZIP-Archiv; die in P_QUERY übergebene SQL-Abfrage muss eine VARCHAR2- (Dateiname) und eine BLOB-Spalte (Inhalt) selektieren.
FUNCTION ZIP(P_CURSOR REF CURSOR, P_ENCODING VARCHAR2)
RETURN BLOB
Erstellt ein ZIP-Archiv und nimmt einen Cursor entgegen; ansonsten funktioniert sie genauso wie die vorhergehende Funktion ZIP.

4. erster Test

Nun wagen wir einen ersten Test: Wir wollen eine Application Express-Seite erstellen, auf welcher ein ZIP-Archiv hochgeladen werden kann. Beim Hochladen soll es automatisch ausgepackt und die Dateien in die Tabelle MEINE_DOKUMENTE übernommen werden. Erstellen Sie also zunächst die Tabelle MEINE_DOKUMENTE.

create table MEINE_DOKUMENTE(
  ID            number(10),
  NAME          varchar2(4000),
  DOKUMENT      blob
)
/

create sequence SEQ_MEINE_DOKUMENTE
/

Erzeugen Sie danach eine Application Express-Anwendung und darin eine Seite mit einem Bericht auf die Tabelle. Nehmen Sie für den Bericht die folgende SQL-Abfrage:

select 
  id, name, dokument, dbms_lob.getlength(dokument) groesse 
from meine_dokumente

Fügen Sie anschließend ein Element vom Typ Datei durchsuchen mit Namen P1_DATEI und eine Schaltfläche hinzu. Die Seite sollte dann in etwa wie in Abbildung 1 aussehen ...

ZIP-Archive in APEX hochladen: Die Anwendungsseite

Abbildung 1: ZIP-Archive in APEX hochladen: Die Anwendungsseite

Wenn Sie nun eine Datei auswählen und auf die Schaltfläche klicken, passiert noch nichts - schließlich haben Sie noch keinen PL/SQL-Prozeß zum Auspacken des Archivs erstellt - dies geschieht nun: Erstellen Sie einen Prozeß, der beim Weiterleiten der Seite ausgelöst wird ( onSubmit) und hinterlegen Sie folgenden PL/SQL-Code:

declare
  upload_file blob;
  upload_mime varchar2(32767);

  zipentry zip_entry_t;
  v_next  pls_integer := 1;
begin
  -- Hochgeladene Datei aus Tabelle WWV_FLOW_FILES
  -- herausnehmen und in Variable "UPLOAD_FILE" ablegen.
  select 
    mime_type, blob_content
  into 
    upload_mime, upload_file
  from wwv_flow_files
  where name = :P1_DATEI;   

  -- ZIP-Archiv öffnen
  zip.open(upload_file, 'cp850');

  -- Schleife, solange noch Einträge im ZIP-Archiv vorhanden sind.
  while v_next = 1 loop

   -- Zum nächsten Eintrag navigieren
   v_next := zip.next;

   -- Eintrag vorhanden?
   if v_next = 1 then 
 
    -- Dann Eintragsdaten übernehmen ...
    zipentry := zip.get_entry;

    -- ... und in Tabelle MEINE_DOKUMENTE kopieren
    insert into MEINE_DOKUMENTE (id, name, dokument)
    values (seq_meine_dokumente.nextval, zipentry.file_name, zipentry.content);

    -- Temporären LOB freigeben
    if zipentry.content is not null then 
     dbms_lob.freetemporary(zipentry.content);
    end if;
   end if;
  end loop;

  -- und schließlich: ZIP-Archiv schließen
  zip.close;

  delete from wwv_flow_files where name = :P1_DATEI;
exception 
  when NO_DATA_FOUND then null;
end;

Probieren Sie es nun einmal aus - Wählen Sie ein ZIP-Archiv als Datei aus und klicken Sie auf die Schaltfläche. Anschließend sollte die Seite in etwa wie folgt aussehen ...

Das Ergebnis: Anwendungsseite nach dem Hochladen und Auspacken des ZIP-Archivs

Abbildung 2: Das Ergebnis: Anwendungsseite nach dem Hochladen und Auspacken des ZIP-Archivs

Nun können Sie weiteren Code zum Umgang mit den ausgepackten Dateien hinzufügen. Gerade wenn es darum geht, mehrere Dateien auf einmal hochzuladen oder bereitzustellen, kann dieses Paket sehr hilfreich sein.

Zurück zur Community-Seite