Today, a colleague had a problem, that was very odd. He wrote a few lines of code, that did a following things:
- query some data source
- write the data to zip file
- place that zip on the ftp
That does not sound like a rocket since, does it? Still, he got himself a bit lost… because when he executed the code…out of 7 zip files written to ftp…there was always at least 1 that could not be unpacked! And it happened randomly! There was always some file broken…
Curious, what could it be? Let’s have a look at the code:
public void call() throws Exception { ResultSet rs = null; FTPClient ftpClient = null; Writer writer = null; PreparedStatement stmt = null; try ( Connection connection = initializeConnection()) { stmt = prepareData(connection); ftpClient = initFTPClient(); writer = createWriter(ftpClient); rs = stmt.executeQuery(); while (rs.next()) { writeData(rs, writer); } cleanUp(stmt, ftpClient, writer, rs); } finally { cleanUp(stmt, ftpClient, writer, rs); } } private Writer createWriter(FTPClient ftpClient) throws IOException { final String filename = "DataFeed" + "_" + new Date(); OutputStream os = ftpClient.storeFileStream(new File("somePath").getPath() + "/" + filename + ".zip"); ZipOutputStream zos = new ZipOutputStream(os); zos.putNextEntry(new ZipEntry(filename + ".txt")); return new BufferedWriter(new OutputStreamWriter(zos)); } private void writeData(ResultSet rs, Writer writer) throws SQLException, IOException { writer.write(convertToLine(rs)); } private void cleanUp(PreparedStatement s, FTPClient f, Writer w, ResultSet rs) throws SQLException, IOException { if (rs != null && !rs.isClosed()) { rs.close(); } if (s != null && !s.isClosed()) { s.close(); } if (w != null) { w.close(); } if (f != null && f.isConnected()) { f.disconnect(); } }
Have you already spotted the problem? It all seem to be quite right, isn’t it? The OutputStream is created, wrapped in the ZipOutputStream, and next in the BufferedWriter, etc. The most outer wrapper’s ‘close‘ method is called, which calls all other ‘close‘ methods of the wrapped objects…it should work! But it does not…. have a look at the following line of code..
private Writer createWriter(FTPClient ftpClient) throws IOException { final String filename = "DataFeed" + "_" + new Date(); OutputStream os = ftpClient.storeFileStream(new File("somePath").getPath() + "/" + filename + ".zip"); ZipOutputStream zos = new ZipOutputStream(os); zos.putNextEntry(new ZipEntry(filename + ".txt")); return new BufferedWriter(new OutputStreamWriter(zos)); }
What does it do? Well it appears, that we are doing something more than just writing to the OutputStream here. What is that ‘Entry‘ thing? Well, this marker is very important, as it turns out! Because we are working with ZipOutputStream, we are also working with its File system. What? A file system? Exactly! ‘Entry‘ tells the stream, that from now on, everything that is written to it should go under the file with that entry name. Therefore, in fact, we are writing to a file that is in the zip file system. And we need to tell the stream where the file ends! How would we do that? Of course, by closing it! 😉
ZipOutputStream zos = new ZipOutputStream(os); zos.putNextEntry(new ZipEntry(filename + ".txt")); zos.write(.....write something to the file with 'filename.txt'...); zos.closeEntry();
So what do you say? Wasn’t it a bit tricky? Well, for sure, it was not that obvious to us!
Conclusion
While working with the ZipOutputStream, always remember to close the files (Entries) in it!!!
Example:
public boolean zip() { // S3 does not see the folder objects // validateRootPath(); validateFiles(); List files = getFilesStream().collect(Collectors.toList()); String parentName = rootPath.getFileName().toString(); Path targetPath = Paths.get(configuration.getTarget(), parentName + ".zip"); info(log, "Creating the zip file"); try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); final ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { for (Path path : files) { String filePathInZip = path.getFileName().toString(); ZipEntry zipEntry = new ZipEntry(filePathInZip); zipOutputStream.putNextEntry(zipEntry); zipOutputStream.write(fileSystem.readAllBytes(path)); zipOutputStream.closeEntry(); } zipOutputStream.finish(); zipOutputStream.close(); fileSystem.write(targetPath, byteArrayOutputStream.toByteArray()); return true; } catch (IOException e) { e.printStackTrace(); return false; } }