pátek 25. února 2011

Expanze proměnných – neobjevujte kolo

Možná jste už někdy řešili problém, jak nahradit proměnné jejich hodnotou třeba v konfiguračním souboru. Například ze vstupu

<app-config baseDir="${projectDirectory}">
 <timeout>${timeout}</timeout>
</app-config>
chceme ve výsledku dostat
<app-config baseDir="C:\Projects\MyApp">
 <timeout>30</timeout>
</app-config>

Asi vás napadne využít String.replaceAll(String, String) nebo v lepším případě přímo dvojice Pattern/Matcher a capturing groups. Proč ale znovu vymýšlet kolo, když knihovna Apache Commons Lang (která by mimochodem měla být standardní součástí každého netriviálního projektu) nabízí pro daný problém přesně ty třídy, které potřebujeme. Výchozím bodem je třída StrSubstitutor, která obsahuje i statické metody pro zjednodušení v nejpoužívanějších případech:

Map<String, Object> properties = new HashMap<String, Object>();
properties.put("animal", "dog");
properties.put("legs", new Integer(4));

System.out.println(
    StrSubstitutor.replace("The ${animal} has ${legs} legs.", properties));
//prints: The dog has 4 legs. 

StrSubstitutor zvládá nahrazovat i Java property (standardní i ty zadané jako argumenty JVM)

System.out.println(
    StrSubstitutor.replaceSystemProperties("Your OS is '${os.name}'"));
//prints: Your OS is 'Windows XP'

A nejsme vázáni ani zdrojem hodnot našich proměnných. Například, chceme-li expandovat na proměnné prostředí a property mají syntaxi proměnných ve Windows, není to vůbec složité. Stačí vlastní implementace abstraktní třídy StrLookup:

//Anonymous child of StrLookup class, which
//maps property names to environment variables in the system
public static final StrLookup ENV_VAR_LOOKUP = new StrLookup() {
    @Override public String lookup(final String key) {
        return System.getenv(key);
    }
};

//expands windows-like properties to system environment variables (e.g. %PATH%)
public static String expandBatch(final String textToExpand) {
    final StrSubstitutor batchSubstitutor = new StrSubstitutor(
        ENV_VAR_LOOKUP, "%", "%", StrSubstitutor.DEFAULT_ESCAPE);
    return batchSubstitutor.replace(textToExpand);
}

//used then
System.out.println(
    expandBatch("Windows is installed in '%WINDIR%'."));
//prints: Windows is installed in 'C:\WINNT'.

A připomenutí na závěr – neobjevujte znovu kolo – pro začátek si projděte API ke Commons Lang a nebojte se tuto knihovnu používat.

sobota 5. února 2011

Zipujeme efektivně

Jedna ze základních vlastností Javy je práce se ZIP archívy, ať už jsou to knihovny tříd a spustitelné JARy, webové aplikace (war), nebo třeba JEE bumbrlíčci (ear). Není tedy divu, že i přímo v základním API je implementována práce s těmito archívy. Slouží k tomu třídy v balíku java.util.zip a nejzajímavější z nich jsou ZipOutputStream a ZipInputStream.

Příkladem budiž vytvoření zipu:

//Vytvorime Zip
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("javlog.zip"));
//V Zipu chceme mit jeden textovy soubor
zos.putNextEntry(new ZipEntry("priznani.txt"));
//naplnime obsah textoveho souboru
zos.write("Máme rádi Javu!".getBytes());
//zavrem entry (priznani.txt)
zos.closeEntry();
//zavrem stream
zos.close();
a jeho rozbalení:
ZipInputStream zis = new ZipInputStream(new FileInputStream("javlog.zip"));
ZipEntry zipEntry;
//budem predpokladat, ze v ZIPu mame jen textove soubory a tak je vypisem do konzole
while ((zipEntry = zis.getNextEntry()) != null) {
    System.out.println("Soubor: " + zipEntry.getName());
    BufferedReader br = new BufferedReader(new InputStreamReader(zis));
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    System.out.println();
    zis.closeEntry();
}
zis.close();

Jak je vidět, není tenhle přístup úplně přímočarý. Tím pádem je i větší prostor pro zavlečení nějaké chybky. Víte například, jak rozlišíte při vytváření ZIPu prázdný soubor od prázdného adresáře?

Tento základní přístup se často hodí v případě, kdy obsah, který chcete do zipu vložit, nemáte ve file systému, ale např. ho generujete přímo ve webové aplikaci. Pak daný ZipOutputStream vytvoříte nad OutputStreamem servletu a naládujete do něj data přímo z paměti nebo z databáze.

To co vývojář aplikace často potřebuje, je možnost jednoduše zazipovat část souborového systému (složku a všechno co je v ní), a nebo ze ZIPu zase všechno někam rozbalit. Samozřejmě, že se dají na internetu najít knihovny, které tuto funkcionalitu poskytují, ale částo jsou kanón na vrabce (opravdu potřebujete aby knihovna uměla i bzip2, 7z, arj a cab?), nebo naopak příliš jednoduché a nezvládnou operaci typu – sbal mi část filesystému a k tomu přidej tenhle vygenerovaný (v paměti) PDF report.

Takže by se v těchto případech hodilo malé rozšíření Zip*Stream tříd. Aby na jednu stranu bylo možné jednoduše pracovat přímo se souborovým systémem a na druhou, aby nebyl programátor ochuzen o to, co mu ZipStreamy umožňují. Hodily by se nám tedy metody:

void ZipOutputStream.put(File fileOrFolderToZip};
void ZipInputStream.unzip(File folderWhereToStoreUnzippedFiles);

A protože originální Zip*Stream třídy nejsou finální, nic nám nebrání v rozšíření. Má implementace přidává ještě dodatečné parametry, aby bylo použití univerzálnější.

cz.cacek.javlog.zip.ZipOutputStream extends java.util.zip.ZipOutputStream:

/**
 * Adds file or directory (with its content) to this {@link ZipOutputStream}.
 * If the given {@link File} doesn't exist, it does nothing and returns.
 * The path of the given {@link File} in the result zip is determined only
 * from the file name
 * and from the provided basePath. It doesn't take the file path into consideration 
 * (but the folder structure under the given folder is preserved).
 * 
 * @param file
 *            {@link File} (normal or directory) to add to the stream
 *            (must not be null)
 * @param basePath
 *            base path (path prefix) for added {@link File} (may be null)
 * @param contentOnly
 *            determines if the folder name (in case the file
 *            parameter is directory) should be added to the zip path. If
 *            true is provided only the folder content is added and the
 *            folder itself is not included.
 * @throws IOException
 */
public void put(final File file, final String basePath, boolean contentOnly) throws IOException;
cz.cacek.javlog.zip.ZipInputStream extends java.util.zip.ZipInputStream:
/**
 * Unzips the stream to the given base folder. If the baseFolder parameter
 * is null, the default (working) directory is used instead. If the
 * baseFolder doesn't exist it is created.
 * 
 * @param baseFolder
 *            base folder to unpack (may be null).
 * @param allowAbsolutePath
 *            flag which says if handling of absolute paths is allowed in
 *            the zip. If the flag is
 *            true and ZipEntry contains absolute path, the baseFolder
 *            parameter is not used and File is created from the absolute
 *            path in the ZipEntry.
 * @throws IllegalArgumentException
 *             base folder can't be created or file denoted by baseFolder
 *             parameter already exists,
 *             but it's not a directory.
 * @throws IOException
 */
public void unzip(final File baseFolder, final boolean allowAbsolutePath) throws IllegalArgumentException, IOException;

A použití? Chceme například rozbalit war soubor do nějaké složky, upravit obsah a zase ho zabalit:

final File baseFolder = new File("temp-webapp"); //folder to unpack the zip content (will be created)
final File warFile = new File("myapp.war"); //file to unpack

//unpack ZIP
ZipInputStream zis = new ZipInputStream(new FileInputStream(warFile));
zis.unzip(baseFolder, false);
zis.close();

//TODO make some magic with files in the temp-webapp folder
new FileOutputStream(new File(baseFolder, "newEmptyFile.xxx")).close();

//repack the ZIP (WAR) file
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(warFile));
zos.put(baseFolder, null, true); //pack only the content of the folder - 3rd parameter is true
zos.close();

//TODO remove the baseFolder
//delete(baseFolder);

Toť vše, ani to nebolelo.

Upravené ZipStreamy připravené k použití najdete v tomto jaru (3 třídy + zdrojáky), nebo v zazipovaném Eclipse projektu i s JUnit testy.

A co používáte k práci s ZIP archívy vy? Podělte se v komentářích.