How do you write a generic filter that can be used while iterating a data file?
On the way to listing the Jar manifest file I decided to hone my skills with Generics.
Jar metadata
A Java jar file is just a compressed zip compatible file. However, it also follows the Jar Specification and in particular the concept of a manifest file, MANIFEST.MF. Surprisingly the standard “jar” tool does not have support for listing or querying that file. Other tools do, such as “bnd”. And, Ant, while it has two tasks that can create or update a manifest file, does not have any access tasks.
Why would you want to access a manifest file? Because in many cases it contains important information, like application version.
To access the information on your own the JDK provides the java.util.jar package. An example is:
new JarFile(TEST_JAR_PATH).getManifest().getMainAttributes().getValue("Implementation-Version")
Iteration Filters
It is surprising that though navigating a data structure is so common, a generic filter interface is not found in the JDK. There probably is, but I just don’t see it today. Something like the java.io.FileFilter#accept(File pathname).
Without ready made filters, things get more complicated, unlike in languages where you can use “closures”. Here is a filter interface I created for this example. In the example we created various filter implementations, like CollectingFilter and MatchingFilter.
Not a biggie, I just wanted to experiment with not duplicating loops in each method that do the same thing except for the “accept”.
Listing 2 Generic Filter
/** * Generic filter support base type. * * @author jbetancourt * * @param <T> * type argument to accept method * @param <R> * type of result */ public interface Filter<T, R> { /** * * Evaluate acceptance of Object. * * @param obj * object to use for evaluation * @param args * zero or more Object arguments * @return true if accepted * @throws Exception */ public boolean accept(final T obj, Object... args) throws Exception; /** * setter * * @param result */ public void setResult(final R result); /** * getter * * @return the result */ public R getResult(); }
In listing 3 below, the implementation. Also available as a Gist.
Implementation. Click to expand/** * */ package com.octodecillion.utils; import java.io.IOException; import java.util.Iterator; import java.util.Map.Entry; import java.util.jar.Attributes; import java.util.jar.JarFile; /** * Utility to access Jar manifest information. * * @author jbetancourt * */ public class JarInfo { /** * Get value of a main attribute in jar manifest. * * @param filePath * of jar file * @param attributeName * of main attribute * @return value of attribute * @throws IOException */ public static String getValue(final String filePath, final String attributeName) throws IOException { return new JarFile(filePath).getManifest().getMainAttributes() .getValue(attributeName); } /** * * Find an attribute whose key matches a regular expression. * * @param filePath * @param regex * should match the full key string * @return matched attribute entry * @throws IOException */ public static Entry<Object, Object> find(final String filePath, final String regex) throws IOException { MatchingFilter filter = new MatchingFilter(regex); scan(filePath, filter); return filter.getResult(); } /** * Get the manifest as a String. * * The pecularities of the manifest format are not maintained, such as the * 72 character line width. * * @param filePath * of jar file * @return string representation of MANIFEST.MF * @throws IOException */ public static String getManifest(final String filePath) throws IOException { CollectingFilter filter = new CollectingFilter(); scan(filePath, filter); return filter.getResult().toString(); } /** * Iterate thru the main attributes. * * For each entry an accept method is invoked. * * @param filePath * @param filter * @throws IOException */ private static <T> void scan(final String filePath, final Filter<Entry<Object, Object>, T> filter) throws IOException { Attributes attributes = new JarFile(filePath).getManifest() .getMainAttributes(); Iterator<Entry<Object, Object>> iterator = attributes.entrySet() .iterator(); while (iterator.hasNext()) { Entry<Object, Object> entry = iterator.next(); try { boolean result = filter.accept(entry); if (result) { break; } } catch (Exception e) { throw new IOException(e); } } } /** * Filter that collects entries into a String buffer. * * @author jbetancourt * */ private static class CollectingFilter implements Filter<Entry<Object, Object>, StringBuffer> { private StringBuffer result; public CollectingFilter() { setResult(new StringBuffer()); } @Override public boolean accept(final Entry<Object, Object> entry, final Object... args) throws Exception { getResult().append(entry + "\n"); return false; } @Override public void setResult(final StringBuffer result) { this.result = result; } @Override public StringBuffer getResult() { return result; } } /** * Filter that accepts entry whose key matches a regex. * * @author jbetancourt * */ private static class MatchingFilter implements Filter<Entry<Object, Object>, Entry<Object, Object>> { private final String regex; private Entry<Object, Object> result; public MatchingFilter(final String regex) { this.regex = regex; } @Override public boolean accept(final Entry<Object, Object> entry, final Object... args) throws Exception { boolean flag = false; if (entry.getKey().toString().matches(regex)) { setResult(entry); flag = true; } return flag; } @Override public void setResult(final Entry<Object, Object> result) { this.result = result; } @Override public Entry<Object, Object> getResult() { return result; } } /** * Generic filter support base type. * * @author jbetancourt * * @param <T> * type argument to accept method * @param <R> * type of result */ private static interface Filter<T, R> { /** * * Evaluate acceptance of Object. * * @param obj * object to use for evaluation * @param args * zero or more Object arguments * @return true if accepted * @throws Exception */ public boolean accept(final T obj, Object... args) throws Exception; /** * setter * * @param result */ public void setResult(final R result); /** * getter * * @return the result */ public R getResult(); } }
The JUnit tests.
Listing 4. JUnit tests
package com.octodecillion.utils; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Map.Entry; import org.junit.Test; /** * * Test the {@link JarInfo} class. * * @author jbetancourt * */ public class JarInfoTest { /** * @throws IOException */ @Test public void test() throws IOException { String actual = JarInfo.getValue(TEST_JAR_PATH, "Implementation-Version"); assertThat(actual, is(equalTo("2.0.5"))); } /** * @throws IOException */ @SuppressWarnings("boxing") @Test public void shouldGetManifest() throws IOException { String actual = JarInfo.getManifest(TEST_JAR_PATH); assertThat(actual.length(), is(equalTo(8600))); assertTrue(actual.contains("Export-Package")); assertTrue(actual.contains("Bundle-RequiredExecutionEnvironment")); } /** * @throws IOException */ @Test public void should_get_match() throws IOException { String regex = "(?i)^.*?impl.*-version"; Entry<Object, Object> actual = JarInfo.find(TEST_JAR_PATH, regex); assertThat(actual.toString(), is(equalTo("Implementation-Version=2.0.5"))); } private static final String TEST_JAR_PATH = "/java/groovy/embeddable/groovy-all-2.0.5.jar"; }
Summary
Shown was an example of Java Generics use. I also attempted to show how to use a filter. However, what should have been very easy was made very complex by using generics.
Further Reading
- http://docs.oracle.com/javase/7/docs/technotes/guides/jar/index.html
- http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html
- http://stackoverflow.com/questions/4702928/java-generics-example
- http://docs.oracle.com/javase/tutorial/java/generics/
- JarURLConnection
