Author: chatellier Date: 2009-05-01 16:01:39 +0000 (Fri, 01 May 2009) New Revision: 1458 Added: lutinutil/trunk/src/main/java/org/codelutin/util/Tbz2Util.java lutinutil/trunk/src/test/java/org/codelutin/util/Tbz2UtilTest.java lutinutil/trunk/src/test/resources/compress/ lutinutil/trunk/src/test/resources/compress/i18n.tbz2 Modified: lutinutil/trunk/pom.xml Log: Add tar/bzip2 compression util class Modified: lutinutil/trunk/pom.xml =================================================================== --- lutinutil/trunk/pom.xml 2009-05-01 16:01:12 UTC (rev 1457) +++ lutinutil/trunk/pom.xml 2009-05-01 16:01:39 UTC (rev 1458) @@ -10,7 +10,7 @@ <parent> <groupId>org.codelutin</groupId> <artifactId>lutinproject</artifactId> - <version>3.4</version> + <version>3.5</version> </parent> <artifactId>lutinutil</artifactId> @@ -46,6 +46,13 @@ <version>1.8.0</version> <scope>compile</scope> </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-compress</artifactId> + <version>20090428</version> + <scope>compile</scope> + </dependency> </dependencies> Added: lutinutil/trunk/src/main/java/org/codelutin/util/Tbz2Util.java =================================================================== --- lutinutil/trunk/src/main/java/org/codelutin/util/Tbz2Util.java (rev 0) +++ lutinutil/trunk/src/main/java/org/codelutin/util/Tbz2Util.java 2009-05-01 16:01:39 UTC (rev 1458) @@ -0,0 +1,328 @@ +/* *##% Lutin utilities library + * Copyright (C) 2009 CodeLutin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. ##%* */ + +package org.codelutin.util; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.ArchiveOutputStream; +import org.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorInputStream; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.logging.LogFactory; + +/** + * Utility class for tar.bzip2 compression and md5 creation. + * + * @author chatellier + * @version $Revision: 1456 $ + * + * Last update: $Date: 2009-04-30 14:26:16 +0200 (jeu. 30 avril 2009) $ + * by : $Author: chatellier $ + */ +public class Tbz2Util { + + /** Class logger. */ + private static org.apache.commons.logging.Log log = LogFactory + .getLog(Tbz2Util.class); + + /** Le séparateur de fichier en local. */ + private static final String LOCAL_SEP = File.separator; + + private static final String LOCAL_SEP_PATTERN = "\\".equals(LOCAL_SEP) ? LOCAL_SEP + + LOCAL_SEP + : LOCAL_SEP; + + /** Le séparateur zip. */ + private static final String TBZ2_SEP = "/"; + + private static final String TBZ2_SEP_PATTERN = "/"; + + /** Accept all file pattern. */ + protected static FileFilter ALL_FILE_FILTER = new FileFilter() { + public boolean accept(File pathname) { + return true; + } + }; + + /** + * Uncompress tbz2 file in targetDir. + * + * @param tbz2File the tbz2 source file + * @param targetDir the destination directory + * @return return last entry name + * @throws IOException if any problem while uncompressing + */ + public static String uncompress(File tbz2File, File targetDir) + throws IOException { + String result = uncompressAndRename(tbz2File, targetDir, null, null); + return result; + } + + /** + * Uncompress tbz2 file in targetDir, and rename uncompressed file if + * necessary. If renameFrom or renameTo is null no renaming is done + * <p/> + * file in tbz2 use / to separate directory and not begin with / + * each directory ended with / + * + * @param tbz2File the tbz2 source file + * @param targetDir the destination directory + * @param renameFrom pattern to permit rename file before uncompress it + * @param renameTo new name for file if renameFrom is applicable to it + * you can use $1, $2, ... if you have '(' ')' in renameFrom + * @return return last entry name + * @throws IOException if any problem while uncompressing + */ + public static String uncompressAndRename(File tbz2File, File targetDir, + String renameFrom, String renameTo) throws IOException { + String result = null; + try { + CompressorInputStream cis = new CompressorStreamFactory() + .createCompressorInputStream("bzip2", new FileInputStream( + tbz2File)); + ArchiveInputStream in = new ArchiveStreamFactory() + .createArchiveInputStream("tar", cis); + TarArchiveEntry entry; + while ((entry = (TarArchiveEntry) in.getNextEntry()) != null) { + String name = entry.getName(); + if (renameFrom != null && renameTo != null) { + name = name.replaceAll(renameFrom, renameTo); + if (log.isDebugEnabled()) { + log.debug("rename " + entry.getName() + " -> " + name); + } + } + result = name; + File target = new File(targetDir, name); + if (entry.isDirectory()) { + target.mkdirs(); + } else { + target.getParentFile().mkdirs(); + OutputStream out = new FileOutputStream(target); + try { + IOUtils.copy(in, out); + } finally { + out.close(); + } + } + } + in.close(); + } catch (CompressorException e) { + throw new IOException("Compression exception (not a bzip2 file ?)", + e); + } catch (ArchiveException e) { + throw new IOException("Archive exception (not a tar file ?)", e); + } + return result; + } + + /** + * Compress 'includes' files in tbz2 file. If file in includes is directory + * only the directory is put in tbz2 file, not the file contained in directory + * + * @param tbz2File the destination tbz2 file + * @param root for all file in includes that is in this directory, then we + * remove this directory in tar entry name (aka -C for tar), can be null; + * @param includes the files to include in tbz2File + * @throws IOException if any problem while compressing + */ + public static void compressFiles(File tbz2File, File root, + Collection<File> includes) throws IOException { + compressFiles(tbz2File, root, includes, false); + } + + /** + * Compress 'includes' files in tbz2. If file in includes is directory + * only the directory is put in tbz2, not the file contained in directory + * + * @param tbz2File the destination tbz2 file + * @param root for all file in includes that is in this directory, then we + * remove this directory in tar entry name (aka -C for tar), can be null; + * @param includes the files to include in tbz2File + * @param createMD5 also create a MD5 file (tbz2 name + .md5). MD5 file is created after tbz2File. + * @throws IOException if any problem while compressing + */ + public static void compressFiles(File tbz2File, File root, + Collection<File> includes, boolean createMD5) throws IOException { + OutputStream tbz2os = new FileOutputStream(tbz2File); + + try { + OutputStream cos = new CompressorStreamFactory().createCompressorOutputStream("bzip2", tbz2os); + + // if md5 creation flag + if (createMD5) { + cos = new MD5OutputStream(cos); + } + + ArchiveOutputStream os = new ArchiveStreamFactory() + .createArchiveOutputStream("tar", cos); + + for (File file : includes) { + String entryName = toArchiveEntryName(root, file); + + // Création d'une nouvelle entrée dans le zip + TarArchiveEntry entry = new TarArchiveEntry(entryName); + entry.setSize(file.length()); + entry.setModTime(file.lastModified()); + os.putArchiveEntry(entry); + + if (file.isFile() && file.canRead()) { + InputStream is = new FileInputStream(file); + try { + IOUtils.copy(is, os); + } finally { + is.close(); + } + } + os.closeArchiveEntry(); + } + os.finish(); + os.close(); + tbz2os.close(); + + // if md5 creation flag + if (createMD5) { + String md5hash = MD5.asHex(((MD5OutputStream) cos).hash()); + File md5File = new File(tbz2File.getAbsoluteFile() + ".md5"); + FileUtil.writeString(md5File, md5hash); + } + + cos.close(); + } catch (ArchiveException e) { + throw new IOException("Can't archive as tar", e); + } catch (CompressorException e) { + throw new IOException("Can't compress as bzip2", e); + } + } + + /** + * If fileOrDirectory is directory Compress recursively all file in this + * directory, else if is just file compress one file. + * <p/> + * Entry result name in tbz2 start at fileOrDirectory. + * example: if we compress /etc/apache, entry will be apache/http.conf, ... + * + * @param tbz2File the source tbz2 file + * @param fileOrDirectory the file or directory to compress + * @throws IOException if any problem while compressing + */ + public static void compress(File tbz2File, File fileOrDirectory) + throws IOException { + compress(tbz2File, fileOrDirectory, null, false); + } + + /** + * If fileOrDirectory is directory Compress recursively all file in this + * directory, else if is just file compress one file. + * <p/> + * Entry result name in tbz2 start at fileOrDirectory. + * example: if we compress /etc/apache, entry will be apache/http.conf, ... + * + * @param tbz2File the source tbz2 file + * @param fileOrDirectory the file or directory to compress + * @param filter used to accept file, if null, all file is accepted + * @throws IOException if any problem while compressing + */ + public static void compress(File tbz2File, File fileOrDirectory, + FileFilter filter) throws IOException { + compress(tbz2File, fileOrDirectory, filter, false); + } + + /** + * If fileOrDirectory is directory Compress recursively all file in this + * directory, else if is just file compress one file. + * <p/> + * Entry result name in tbz2 start at fileOrDirectory. + * example: if we compress /etc/apache, entry will be apache/http.conf, ... + * + * @param tbz2File the source tbz2 file + * @param fileOrDirectory the file or directory to compress + * @param filter used to accept file, if null, all file is accepted + * @param createMD5 also create a MD5 file (zip name + .md5). MD5 file is created after zip. + * @throws IOException if any problem while compressing + */ + public static void compress(File tbz2File, File fileOrDirectory, + FileFilter filter, boolean createMD5) throws IOException { + FileFilter localFilter = filter; + if (localFilter == null) { + localFilter = ALL_FILE_FILTER; + } + List<File> files = new ArrayList<File>(); + if (fileOrDirectory.isDirectory()) { + files = FileUtil.getFilteredElements(fileOrDirectory, localFilter, + true); + } else if (localFilter.accept(fileOrDirectory)) { + files.add(fileOrDirectory); + } + + compressFiles(tbz2File, fileOrDirectory.getParentFile(), files, + createMD5); + } + + /** + * <li> supprime le root du fichier + * <li> Converti les '\' en '/' car les archives entry utilise des '/' + * <li> ajoute un '/' a la fin pour les repertoires + * <li> supprime le premier '/' si la chaine commence par un '/' + * + * @param root the root directory + * @param file the file to treate + * @return the archive entry name corresponding to the given <code>file</code> + * from <code>root</code> dir. + */ + protected static String toArchiveEntryName(File root, File file) { + String result = file.getPath(); + + if (root != null) { + String rootPath = root.getPath(); + if (result.startsWith(rootPath)) { + result = result.substring(rootPath.length()); + } + } + + result = result.replace('\\', '/'); + if (file.isDirectory()) { + result += '/'; + } + while (result.startsWith("/")) { + result = result.substring(1); + } + return result; + } + + protected static String convertToLocalEntryName(String txt) { + String s = txt.replaceAll(TBZ2_SEP_PATTERN, LOCAL_SEP_PATTERN); + if (s.endsWith(TBZ2_SEP)) { + s = s.substring(0, s.length() - 1); + } + return s; + } +} Added: lutinutil/trunk/src/test/java/org/codelutin/util/Tbz2UtilTest.java =================================================================== --- lutinutil/trunk/src/test/java/org/codelutin/util/Tbz2UtilTest.java (rev 0) +++ lutinutil/trunk/src/test/java/org/codelutin/util/Tbz2UtilTest.java 2009-05-01 16:01:39 UTC (rev 1458) @@ -0,0 +1,153 @@ +/* *##% Lutin utilities library + * Copyright (C) 2009 CodeLutin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. ##%* */ + +package org.codelutin.util; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.LogFactory; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tbz2UtilTest.java + * + * @author chatellier + * @version $Revision$ + * + * Last update: $Date$ + * by : $Author$ + */ +public class Tbz2UtilTest { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + private static org.apache.commons.logging.Log log = LogFactory + .getLog(Tbz2UtilTest.class); + + public static File I18N_DIR = new File("src" + File.separator + "test" + + File.separator + "resources" + File.separator + "i18n"); + + /** + * Test de decompression d'une archive. + * + * @throws IOException + */ + @Test + public void testUncompress() throws IOException { + + File tbz2File = new File("src" + File.separator + "test" + + File.separator + "resources" + File.separator + "compress", + "i18n.tbz2"); + + Assert.assertTrue(tbz2File.isFile()); + + File uctbz2 = FileUtil.createTempDirectory("testUncompressTbz2", ""); + log.info("uncompress tbz2 " + tbz2File + " in " + uctbz2); + + Tbz2Util.uncompress(tbz2File, uctbz2); + + List<File> dest = FileUtil.getFilteredElements(uctbz2, null, true); + + Assert.assertEquals(13, dest.size()); + + // remove created temp dirs : + FileUtil.deleteRecursively(uctbz2); + } + + /* + * Test method for 'org.codelutin.util.ZipUtil.compress(File, File, FileFilter)' + */ + @Test + public void testCompress() throws IOException { + + File tbz2File = File.createTempFile("testCompressTbz2-", ".tbz2"); + //tbz2File.deleteOnExit(); + log.info("Compress " + I18N_DIR + " in tbz2 file = " + tbz2File); + + Tbz2Util.compress(tbz2File, I18N_DIR); + + Assert.assertTrue(tbz2File.exists()); + Assert.assertTrue(tbz2File.length() > 0); + } + + @Test + public void testCompressFilter() throws IOException { + + File tbz2File = File.createTempFile("testCompressTbz2Filter-", ".tbz2"); + tbz2File.deleteOnExit(); + log.info("Compress " + I18N_DIR + " in tbz2 file = " + tbz2File); + + FileFilter filter = new FileFilter() { + public boolean accept(File pathname) { + boolean result; + result = !pathname.getPath().contains("fr"); + return result; + } + + }; + + Tbz2Util.compress(tbz2File, I18N_DIR, filter); + + Assert.assertTrue(tbz2File.exists()); + Assert.assertTrue(tbz2File.length() > 0); + } + + /** + * Compress a single file. + * + * @throws IOException + */ + @Test + public void testCompressFile() throws IOException { + File dir = new File(I18N_DIR, "dummy-fr.properties"); + File tbz2File = File.createTempFile("testCompressTbz2File-", ".tbz2"); + tbz2File.deleteOnExit(); + log.info("Compress " + dir + " in tbz2 file = " + tbz2File); + + Tbz2Util.compress(tbz2File, dir); + + Assert.assertTrue(tbz2File.exists()); + Assert.assertTrue(tbz2File.length() > 0); + } + + /** + * Compress a directory and check md5 sum. + * @throws IOException + */ + @Test + public void testCompressMD5() throws IOException { + File tbz2File = File.createTempFile("testCompressTbz2Md2", ".tbz2"); + tbz2File.deleteOnExit(); + log.info("Compress " + I18N_DIR + " in tbz2 file = " + tbz2File); + + Tbz2Util.compress(tbz2File, I18N_DIR, null, true); + + Assert.assertTrue(tbz2File.exists()); + Assert.assertTrue(tbz2File.length() > 0); + File md5File = new File(tbz2File.getAbsoluteFile() + ".md5"); + md5File.deleteOnExit(); + Assert.assertTrue(md5File.exists()); + String md5sum = FileUtil.readAsString(md5File); + Assert.assertEquals("3ca9b3afa73537942580f9412849cc24", md5sum); + } + +} Property changes on: lutinutil/trunk/src/test/java/org/codelutin/util/Tbz2UtilTest.java ___________________________________________________________________ Name: svn:keywords + Author Date Revision Added: lutinutil/trunk/src/test/resources/compress/i18n.tbz2 =================================================================== (Binary files differ) Property changes on: lutinutil/trunk/src/test/resources/compress/i18n.tbz2 ___________________________________________________________________ Name: svn:mime-type + application/octet-stream