Link to home
Start Free TrialLog in
Avatar of PaulCaswell
PaulCaswellFlag for United Kingdom of Great Britain and Northern Ireland

asked on

Truly invisible zip file access.

All,

Has anyone seen a package that will allow me to do stuff like:

MyFile f = new MyFile ("zipFile.zip\\zippedFileName.ext");
MyFile g = new MyFile("normalFileName.ext");

I have a bunch of working, tested code that works with files and paths and I would like to add zip file handling with the minimum of fuss.

I can see and understand the zip file interface in java.util.zip and it will access zip files and create streams to the zipped contents but this would impact the clarity of the code significantly if I have to detect that I am working with a zip file in many places in the code.

I realise that if I work solely with streams and pass those around everywhere I'll get just about the same effect but each process needs to handle the file in a different way (check length, count records, parse data etc) so a stream is not so useful.

Besides, it would be quite a tidy solution if I could do most I/O using a file path in the above form.

Paul
Avatar of Webstorm
Webstorm

Hi Paul,

I can't find such package.
You can create your own :

public class MyFile
{
    File f=null;
    ZipFile zip=null;
    ZipEntry entry=null;

    public MyFile(String path)
    // to quickly find separator between zipfile and zipentry, use | instead of \ or / for example
    {
          int i=path.indexOf('|'); //  generally | cannot be in file path
          if (i<0) f=new File(path);
          else
          {
               zip=new ZipFile(path.substring(0,i));
               entry=zip.getEntry(path.substring(i+1));
          }
    }

    public boolean exists()
    { return (f!=null && f.exists()) || entry!=null; }

    public String getName()
    { return (f!=null)?f.getName():(entry!=null)?entry.getName():null; }

    public long getSize()
    { return (f!=null)?f.length():(entry!=null)?entry.getSize():-1L; }

    public long getTime()
    { return (f!=null)?f.lastModified():(entry!=null)?entry.getTime():-1L; }

    public long isDirectory()
    { return (f!=null)?f.isDirectory():(entry!=null)?entry.isDirectory():false; }

    public InputStream getInputStream()
    { return (f!=null)? new FileInputStream(f) : (entry!=null)?entry.getInputStream() : null; }
}
Avatar of PaulCaswell

ASKER

Thanks Webstorm,

I've done it this way. As a C expert and a C++-able programmer I suspect I have done things in a less-than-best way but if you could review/comment for my learning experience I would appreciate it.

First the ZFile clas which is equivalent to the File class so I can implement 'exists':

public class ZFile extends File {
    // The path to the zip file itself.
    String fileName = null;
    // The file inside the zip file.
    String zipEntry = null;
    // Pattern to look for .zip in a path.
    static private Pattern zipSplitter = Pattern.compile(".zip"+Pattern.quote(File.separator), Pattern.CASE_INSENSITIVE);


    // Just one creator to stop the ZFile() constructor from neing created.
    public ZFile(String pathname) {
      super(pathname);
      // Decode my path.
      decodePath (getAbsolutePath());
    }

    /**
     * exists
     *
     * @return boolean
     */
    public boolean exists() {
      boolean exists = true;
      // Normal paths pass straight through.
      if (!super.exists()) {
          // Currently not there.
          exists = false;
          // Decode the path.
          decodePath( getAbsolutePath() );
          // Did it decode correctly?
          if (fileName != null) {
            // Does the zip file exist?
            File zipFile = new File(fileName);
            if (zipFile.exists()) {
                // If we get here the zip file exists, does it contain the file specified?
                ZipFile zip = null;
                try {
                  // Open the zip file.
                  zip = new ZipFile(zipFile);
                  // Is this part in there?
                  ZipEntry entry = zip.getEntry(zipEntry);
                  if (entry != null) {
                      // Entry is in the file.
                      exists = true;

                  }
                  // We only want existence, not the file.
                  zip.close();
                  // Here I know that the zip file exists and the first entry specified in the full path also exists. My work is done.
                  // Check the rest of the parts. (later).
                  //for ( ;i < parts.length - 1; i++ ) {
                  //entry.isDirectory();
                  //}
                } catch (IOException ex) {
                  // Not there! No problem. exists is still false.
                }
            }
          }
      }
      return exists;
    }

    /**
     * decodePath
     */
    private void decodePath(String p) {
      // Could there be a zip file in the path?
      fileName = fileName(p);
      // And the entry.
      if ( p.length() >= fileName.length() + File.separator.length() ) {
          zipEntry = p.substring(fileName.length()+File.separator.length());
      } else {
          zipEntry = "";
      }
    }

    /**
     * toString
     *
     * @return String
     */
    public String toString() {
      return fileName;
    }

    /**
     * fileName
     *
     * @param fileName String
     * @return String
     */
    public static String fileName(String fileName) {
      Matcher match = zipSplitter.matcher(fileName);
      // By default its the whole file name.
      String name = fileName.substring(0);
      // Must be there and not at the end.
      if ( match.find() ) {
          // Its in there! Has it got an entry name too?
          name = fileName.substring(0,match.start());
          // Add the .zip but with the correct case so pull it out of the file name.
          int pos = match.end() - File.separator.length() - ".zip".length();
          name += fileName.substring(pos, pos + ".zip".length());
      }
      return name;
    }

    /**
     * zip
     *
     * @return boolean
     */
    public boolean zip() {
      return zipEntry.length() > 0;
    }

}

Then the ZReader which is like a FileReader but multiplexes between a normal FileReader and a Zip Stream:

public class ZReader extends FileReader {
    // My ZFile.
    ZFile zFile = null;
    // My ZipFile.
    ZipFile zipFile = null;
    // The entry in the zip file.
    ZipEntry zipEntry = null;
    // My ZipFileInputStream.
    // Remember that the file accessed by the 'this' methods will read the zip file itself, not the entry in the file.
    // All access of the entry must be redirected through this object.
    InputStreamReader z = null;

    public ZReader(String fileName) throws FileNotFoundException {
      // Use the static method in ZFile to grab the true file name on the way past.
      super(ZFile.fileName(fileName));
      /*
       * Hopefully, so long as my FileReader doppleganger found the file, the zip file is now open.
       * I can now present that stream to a ZipFile for opening the entity.
       */
      // Grab my file. It decides the file name and the entry name from the path name.
      zFile = new ZFile(fileName);
      // Is it a reference to an entry in a zip file?
      if ( zFile.zipEntry.length() > 0 ) {
          // Build my ZipFile object.
          try {
            // Use me as a stream.
            //z = new ZipInputStream(new InputStream(this));
            zipFile = new ZipFile(zFile.fileName);
            // Grab the entry.
            zipEntry = zipFile.getEntry(zFile.zipEntry);
            // Create the stream to that entry.
            z = new InputStreamReader(zipFile.getInputStream(zipEntry));
          } catch (IOException ex) {
            // Invalid file structure?
            throw new FileNotFoundException(ex.toString());
          }
      }
    }

    // Redirect the read to z if its a zip file.
    public int read(char b[], int off, int len) throws IOException {
      if ( z != null ) {
          return z.read(b, off, len);
      } else {
          return super.read(b, off, len);
      }
    }

    // Redirect the read to z if its a zip file.
    public int read() throws IOException {
      if ( z != null ) {
          return z.read();
      } else {
          return super.read();
      }
    }
    /**
     * Tell whether this stream is ready to be read.  An InputStreamReader is
     * ready if its input buffer is not empty, or if bytes are available to be
     * read from the underlying byte stream.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public boolean ready() throws IOException {
      if ( z != null ) {
          return z.ready();
      } else {
          return super.ready();
      }
    }

    /**
     * Close the stream.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public void close() throws IOException {
      // Normal closure but should only close the entryStream.
      super.close();
      // Were we a zip file?
      if ( z != null ) {
          // Close the stream.
          z.close();
          z = null;
          // Close the ZipFile. Probably not a good idea.
          zipFile.close();
          zipFile = null;
      }
    }
}

The beauty of this setup is that all I need to do is change:

            // Open it.
            BufferedReader f = new BufferedReader(new FileReader(path + FileNames[STATUS_FILE]));

to:

            // Open it.
            BufferedReader f = new BufferedReader(new ZReader(path + FileNames[STATUS_FILE]));

and whether its a normal file or an entity in a zip file it still works.

Also:

          File f = new File(path + FileNames[i]);
          if (!f.exists()) {

becomes:

          ZFile f = new ZFile(path + FileNames[i]);
          if (!f.exists()) {

Any thoughts/enhancements/leaks?

Paul
ASKER CERTIFIED SOLUTION
Avatar of Webstorm
Webstorm

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
>>both will references the same String, String are immutable, so it's useless trying to clone them.
I think this came from my C/C++ experience. In C++ if you make a pointer to an object passed as a parameter you are likely to crash later. I guess this does not apply in Java, is that what 'immutable' means? That if I reference an object and it later changes, my copy does not?

>>... isnt explicitly closed ...
I thought about closure. Essentially I have two streams to the same file. As you can see from the comments:

    public void close() throws IOException {
      // Normal closure but should only close the entryStream.
      super.close();
      // Were we a zip file?
      if ( z != null ) {
          // Close the stream.
          z.close();
          z = null;
          // Close the ZipFile. Probably not a good idea.
          zipFile.close();
          zipFile = null;
      }
    }

First I invoke super.close which should close the entity stream. I then close the entity stream myself, then the file stream. My concern is that if the close is propagated throughout the whole object chain then the close of the file stream should close any entity streams open to it. Closing the entity stream first assumes that when the file is closed the entity stream object is resilient to multiple closure. Is this a safe assumption?

Sorry to wander a bit here. I'll try to get around to an 'Accept' in a little while. If you feel I am asking too many questions just say. Thanks for all your help. :-)

Paul
immutable means cannot be modified.
For example, if you want to append a string to another :
  String a = "first",b;
  b = a + " and second";    // variable a still contain "first", variable b reference a new String containing "first and second"
  a = a + " string";   //  variable a reference a new string containing "first string"
  // the original string "first" is still in memory but isn't referenced anymore, so will be garbage-collected.

Each operation on String objects (+, substring(), replace() ...) creates a new String. The original one is never modified.

>>I thought about closure.
I meant in my implementation.