Unzip file via java.util.zip package from .NET code

This week I had to modify a legacy java application that does quite a bit of unpacking of various zip files. Actually the file extensions are .EAR, .WAR, .JAR; those are all zip files. The target application will run under JRE 1.6.0_12.

I had to find a way to code the logic in .NET, but still I need a number of files unzipped (then modify various manifests and descriptors and zip into .EARs again).

Unfortunately, .NET/C# libraries do not have access to a direct equivalent of java.util.zip.* package. I considered few options, including new deflation namespace in .NET 3.5, a number of zlib-based options, use of J# namespace, and using interop to native code which in turn will JNDI to java.util.zip, purchasing a zip command line license, etc.

Finally, I ended up with the simplest solution that works great for my needs; the needs/constraints being:

-          Performance hit of 100 milliseconds per unzip operation is fine

-          Need 100% same behavior as the one that Sun’s java has

The solution is:

-          Write small console java application, which will implement the needed unzip operations. My small java application takes

o   <zip file name> <output directory name> [<zip entry name>]

-          Somehow build this application

-          Use this application from the .NET code. I set a small function for that.

The following 1) briefly describes the small java application; this one is actually doing the zip/unzip operations, 2) describes how to build it and 3) shows the .NET part of the code.

The java console application which actually unzips

First, we define needed imports, with the zip-related ones:

import java.io.File;

import java.io.FileOutputStream;

import java.io.InputStream;

import java.io.IOException;

import java.util.Enumeration;

import java.util.zip.ZipEntry;

import java.util.zip.ZipFile;

import java.util.zip.ZipInputStream;

 

Next, need few variables:

        File fileToUnpack = null;

        ZipFile zip = null;

        String targetDirName = null;

        String entryName = null;        // will stay null if unzip whole file

 

Then we retrieve command-line arguments, validate them (like the zip/ear/war/jar file exists), and open the zip container. We are still in Sun’s java world.

        try

        {

            zip = new ZipFile(fileToUnpack);

        }

        catch (java.util.zip.ZipException e)

        {

            log("Cannot open zip file: " + e.toString());

            return;

        }

        catch (java.io.IOException e)

        {

            log("Cannot open zip file: " + e.toString());

            return;

        }

 

In case the command line requested unzipping the whole file, the code path is below. Note few things like normalizing the output path…; if you miss this, you end up with the directory which is only partially functional. Also note the handling of the isDirectory case. The “streamToFile” code will follow.

        if (entryName == null) // unpack whole file

        {

            Enumeration en = zip.entries();

            while(en.hasMoreElements())

            {

                ZipEntry entry = (ZipEntry)en.nextElement();

                if (entry.isDirectory())

                {

                    log("directory ignored: " +

                        entry.toString());

                    continue;

                }

                InputStream stm;

                try {stm = zip.getInputStream(entry);}

                catch (java.io.IOException e)

                {

                    log ("Cannot get input stream for '" + entry.toString() + "' in zip file '" + fileToUnpack.getAbsolutePath());

                    return;

                }

                String targetFilename = targetDirName + "/" + entry.toString();

 

                // create dir if needed

               

                int lastSlash = targetFilename.lastIndexOf("/");

                if (lastSlash > 0)

                {

                    String dirName = targetFilename.substring(0, lastSlash);

                    new File(dirName).mkdirs();

                }

 

                // Copy zip entry to output directory

               

                targetFilename = targetFilename.replace('/','\\');

                streamToFile(stm, targetFilename);

            }

        }

 

Now the function that actually copied a zip entry to the file.

    private static void streamToFile(InputStream stm, String targetFilename)

    {

        FileOutputStream stmOut = null;

        byte[] buffer = new byte[copyBufferSize];

        int bytecount;

       

        try {stmOut = new FileOutputStream(new File(targetFilename));}

        catch (java.io.IOException e)

        {

            log("Error opening output file '" + targetFilename + "': " + e.toString());

            System.exit(101);

        }

       

        try

        {

            while ((bytecount = stm.read(buffer)) > 0)

            {

                stmOut.write(buffer, 0, bytecount);

            }

            stmOut.close();

        }

        catch (java.io.IOException e)

        {

            log("Error writing output file '" + targetFilename + "': " + e.toString());

            System.exit(101);

        }

    }

We are almost done with java part, but not completely. The by far the most difficult part remains, at least for me: building the executable. I chose to package the result into the file that I named “main.jar”.

There is a build tool which is similar to MSBUILD. While I did not find many MSBUILD features in ant (like automatic resolution of dependencies between projects), it’s very likely I don’t know enough about ant. Anyhow, the ant works great for me.

Building java application

<?xml version="1.0"?>

 

<project name="ops-tool" default="all">

 

  <target name="set">

    <property environment="env"/>

    <property name="flavor" value="retail"/>

  </target>

 

 

  <target name="init" depends="set">

    <path id="classpath">

    </path>

  </target>

 

  <target name="compile" depends="init">

    <mkdir dir="classes"/>

    <javac srcdir="." destdir="classes"

           classpathref="classpath">

      <classpath>

        <pathelement location="${classpath}"/>

      </classpath>

    </javac>

  </target>

 

  <target name="jar" depends="compile">

    <delete dir="lib" quiet="yes"/>

    <mkdir  dir="lib"/>

    <jar

      destfile="lib/main.jar"

      basedir="classes"

      manifest="MANIFEST.MF"

      filesonly="yes">

    </jar>

  </target>

 

  <target name="all" depends="jar"/>

 

  <target name="clean" depends="init">

    <delete includeEmptyDirs="yes" quiet="yes">

      <fileset dir="${lib}"/>

      <fileset dir="${classes}"/>

    </delete>

  </target>

 

</project>

Testing java application

I grabbed few different ZIP, JAR, EAR files and run the program from the command line test script, which I named run.cmd:

m:\jdk1.6.0_12\bin\java -jar lib\main.jar utils.zip.Main %*

 

For example, I run like:

run unittest3\lib\EXML.jar unittest5

Now .NET part

This is by far the easiest of course. The following is simple testing code, with the procedure to actually call the java unzip as a part of it (what I am saying, this is not the final code; this works great).

Declaration is:

namespace J2eePack

{

    using System;

    using System.Collections.Generic;

    using System.Diagnostics;

    using System.IO;

    using System.Linq;

    using System.Reflection;

    using System.Text;

 

    class Program

    {

        private const string jzip = @"…";  // this is the location of lib\main.jar

        . . .

Now, let’s call couple of un-zipping functions. Only the bold part is interesting:

            private const string testEar = @"I:\lm\service\se\lm8\private\service-next\private\ops\test\JZIP\unittest\deployable.ear";

            private const string exmlJar = @"I:\lm\service\se\lm8\private\service-next\private\ops\test\J2eePack\J2eePack\unpack2\lib\EXML.jar";

            private const string exmlOut = @"I:\lm\service\se\lm8\private\service-next\private\ops\test\J2eePack\J2eePack\unpack2\lib\EXML";

            private const string targetSubDir = "unpack2";

 

            // Target location

 

            string current = Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;

            current = Path.GetDirectoryName(current);

            current = Path.GetDirectoryName(current);

            current = Path.GetDirectoryName(current);

            string targetDir = Path.Combine(current, targetSubDir);

           

            // Unpack

 

            Unzip(testEar, targetDir);

            Unzip(exmlJar, exmlOut);

            // test result

 

            string[] files = Directory.GetFiles(targetDir, "*", SearchOption.AllDirectories);

            foreach (string file in files)

            {

                Console.WriteLine(file);

            }

 

The actual code that is doing all the work (by calling the java console program) is:

        /// <summary>

        /// Build and execute %java_home%\bin\java.exe -jar [loc]\lib\main.jar utils.zip.Main  [file to unzip] [targetDir];

        /// </summary>

        /// <param name="jar">Full or relative path to the file to unzip</param>

        /// <param name="targetDir">The directory to which the zip file will be unzipped to. The directory will be created if missing.</param>

        /// <returns>true if no erros detected.</returns>

        /// <remarks>Note that the example code below allows only 5 seconds for the callee to complete. Modify this to meet the

        /// production requirements. Also, note that the below example does not check the exit code of the java proccess.

        /// Please add a line when building the production code.

        /// </remarks>

        private static bool Unzip(string jar, string targetDir)

        {

            // java exec

 

            string javaHome = Environment.GetEnvironmentVariable("java_home");

            if (javaHome == null)

            {

                Console.WriteLine("cannot obtain java_home");

                // javaHome = @"M:\jdk1.6.0_12";

                Environment.ExitCode = 101;

                return false;

            }

            string java = Path.Combine(javaHome, @"bin\java.exe");

 

            // arguments

 

            string args = " -jar " + jzip + @"\lib\main.jar utils.zip.Main " + jar + " " + targetDir;

            Console.WriteLine(args);

 

            // shell out

 

            Process shell = new Process();

            shell.StartInfo.FileName = java;

            shell.StartInfo.Arguments = args;

            shell.StartInfo.UseShellExecute = false;

            shell.Start();

            bool ok = shell.WaitForExit(5000);

            if (!ok)

            {

                Console.WriteLine("Error");

                return false;

            }

 

            return true;

        }