Commits

Kevin A. Archie  committed d1c3bef

added spreadsheet lookup fn; lots of cleanup for DicomBrowser 1.7.0

  • Participants
  • Parent commits d6e6ff0
  • Tags v2.3.0

Comments (0)

Files changed (25)

   <groupId>org.nrg</groupId>
   <artifactId>DicomEdit</artifactId>
   <packaging>jar</packaging>
-  <version>2.2.1</version>
+  <version>2.3.0</version>
   <name>DicomEdit</name>
   <description>Language for modifying DICOM metadata</description>
   <url>http://nrg.wustl.edu</url>
       <url>http://maven.xnat.org/libs-release</url>
     </repository>
   </repositories>
+  <distributionManagement>
+    <repository>
+      <id>XNAT Server</id>
+      <name>XNAT Maven Library Release</name>
+      <url>http://maven.xnat.org/libs-release-local</url>
+    </repository>
+  </distributionManagement>
   <dependencies>
     <dependency>
       <groupId>junit</groupId>
     	<artifactId>java-uuid-generator</artifactId>
     	<version>3.1.3</version>
     </dependency>
+    <dependency>
+    	<groupId>org.apache.poi</groupId>
+    	<artifactId>poi</artifactId>
+    	<version>3.8</version>
+    	<type>jar</type>
+    	<scope>compile</scope>
+    </dependency>
+    <dependency>
+    	<groupId>org.apache.poi</groupId>
+    	<artifactId>poi-ooxml</artifactId>
+    	<version>3.8</version>
+    	<type>jar</type>
+    	<scope>compile</scope>
+    </dependency>
   </dependencies>
 </project>

File src/main/java/org/nrg/dcm/edit/AbstractIndexedLabelFunction.java

 public abstract class AbstractIndexedLabelFunction implements ScriptFunction {
     protected abstract boolean isAvailable(String label) throws ScriptEvaluationException;
     
-    private final Value getFormat(final List<Value> values)
+    private final Value getFormat(final List<? extends Value> values)
     throws ScriptEvaluationException {
 	try {
 	    return values.get(0);
     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
 	final Value format = getFormat(args);
 	return new Value() {
 	    private NumberFormat buildFormatter(final int len) {

File src/main/java/org/nrg/dcm/edit/AbstractOperation.java

 public abstract class AbstractOperation implements Operation {
     private final String name;
 
-    AbstractOperation(final String name) {
+    protected AbstractOperation(final String name) {
         this.name = name;
     }
 

File src/main/java/org/nrg/dcm/edit/IntegerValue.java

 /**
- * Copyright (c) 2010,2011 Washington University
+ * Copyright (c) 2010-2012 Washington University
  */
 package org.nrg.dcm.edit;
 
         sval = value;
         val = Integer.parseInt(value);
     }
+    
+    public IntegerValue(final int value) {
+        sval = String.valueOf(value);
+        val = value;
+    }
 
     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.Value#getTags()

File src/main/java/org/nrg/dcm/edit/MessageFormatValue.java

     private final SortedSet<Integer> tags;
     private final Set<Variable> variables;
 
-    public MessageFormatValue(final Value format, final Iterable<Value> values) {
+    public MessageFormatValue(final Value format, final Iterable<? extends Value> values) {
         this.format = format;
         this.values = Lists.newArrayList(values);
 
         this.variables = Collections.unmodifiableSet(vars);
     }
 
-    public MessageFormatValue(final String format, final Iterable<Value> values) {
+    public MessageFormatValue(final String format, final Iterable<? extends Value> values) {
         this(new ConstantValue(format), values);
     }
 

File src/main/java/org/nrg/dcm/edit/ScriptApplicator.java

 import org.nrg.dcm.edit.fn.Lowercase;
 import org.nrg.dcm.edit.fn.Match;
 import org.nrg.dcm.edit.fn.Replace;
+import org.nrg.dcm.edit.fn.SpreadsheetLookup;
 import org.nrg.dcm.edit.fn.Substring;
 import org.nrg.dcm.edit.fn.Uppercase;
 import org.nrg.dcm.edit.fn.UrlEncode;
                     astParser.setFunction(Match.name, new Match());
                     astParser.setFunction(UrlEncode.name, new UrlEncode());
                     astParser.setFunction(HashUID.name, new HashUID());
+                    astParser.setFunction(SpreadsheetLookup.name, new SpreadsheetLookup());
                     for (final Map.Entry<String,ScriptFunction> me : functions.entrySet()) {
                         logger.trace("adding function {}", me);
                         astParser.setFunction(me.getKey(), me.getValue());

File src/main/java/org/nrg/dcm/edit/ScriptFunction.java

  *
  */
 public interface ScriptFunction {
-    Value apply(List<Value> args) throws ScriptEvaluationException;
+    Value apply(List<? extends Value> args) throws ScriptEvaluationException;
 }

File src/main/java/org/nrg/dcm/edit/fn/Format.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         return new MessageFormatValue(args.get(0), args.subList(1, args.size()));
     }
 }

File src/main/java/org/nrg/dcm/edit/fn/GetURL.java

 public class GetURL implements ScriptFunction {
     public static final String name = "getURL";
 
-    public Value apply(List<Value> args) throws ScriptEvaluationException {
+    public Value apply(List<? extends Value> args) throws ScriptEvaluationException {
         final Value url = (Value) args.get(0);
         return new Value() {
             public Set<Variable> getVariables() { return Collections.emptySet(); }

File src/main/java/org/nrg/dcm/edit/fn/HashUID.java

 import org.nrg.dcm.edit.Variable;
 
 import com.fasterxml.uuid.Generators;
-import com.google.common.base.Strings;
 
 /**
  * Creates a one-way hash UID by first creating a Version 5 UUID (SHA-1 hash) from the provided string,
     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         if (args.isEmpty()) {
             throw new ScriptEvaluationException("usage: hashUID[string-to-hash {, optional algorithm-name} ]");
         }

File src/main/java/org/nrg/dcm/edit/fn/Lowercase.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         final Value v;
         try {
             v = args.get(0);

File src/main/java/org/nrg/dcm/edit/fn/Match.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(List<Value> args) throws ScriptEvaluationException {
+    public Value apply(List<? extends Value> args) throws ScriptEvaluationException {
         final Value value = args.get(0);
         final Value pattern = args.get(1);
         final int group = ((IntegerValue)args.get(2)).getValue();

File src/main/java/org/nrg/dcm/edit/fn/Replace.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         final Value original, pre, post;
         try {
             original = args.get(0);

File src/main/java/org/nrg/dcm/edit/fn/SpreadsheetLookup.java

+/**
+ * Copyright (c) 2012 Washington University
+ */
+package org.nrg.dcm.edit.fn;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.dcm4che2.data.DicomObject;
+import org.nrg.dcm.edit.IntegerValue;
+import org.nrg.dcm.edit.ScriptEvaluationException;
+import org.nrg.dcm.edit.ScriptFunction;
+import org.nrg.dcm.edit.Value;
+import org.nrg.dcm.edit.Variable;
+import org.nrg.io.MapCachedFileProduct;
+import org.nrg.io.MapCachedFileProduct.CacheEntry;
+import org.nrg.io.XSSFWorkbookFileProduct;
+import org.nrg.util.CheckedExceptionFunction;
+import org.nrg.util.NamedFileFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+/**
+ * @author Kevin A. Archie <karchie@wustl.edu>
+ *
+ */
+public final class SpreadsheetLookup implements ScriptFunction {
+    public static final String name = "spreadsheet";
+
+    private final Logger logger = LoggerFactory.getLogger(SpreadsheetLookup.class);
+    private final CheckedExceptionFunction<String,Workbook,? extends Exception> workbookProvider;
+    
+    public SpreadsheetLookup(CheckedExceptionFunction<String,Workbook,? extends Exception> workbookProvider) {
+        this.workbookProvider = workbookProvider;
+    }
+        
+    public SpreadsheetLookup() {
+        this(new NamedFileFunction<Workbook,IOException>(
+                new MapCachedFileProduct<Workbook,IOException>(
+                        new HashMap<File,CacheEntry<Workbook>>(),
+                        new XSSFWorkbookFileProduct())));
+    }
+    
+    private String getCellAsString(final Cell cell) {
+        switch (cell.getCellType()) {
+        case Cell.CELL_TYPE_STRING:
+            return cell.getStringCellValue(); 
+
+        case Cell.CELL_TYPE_NUMERIC:
+            if (DateUtil.isCellDateFormatted(cell)) {
+                return cell.getDateCellValue().toString();
+            } else {
+                return String.valueOf(cell.getNumericCellValue());
+            }
+
+        default:
+            return null;
+        }    
+    }
+
+    private String apply(final Sheet sheet, final String key,
+            final int keycol, final int valcol)
+    throws IOException {
+        for (final Row row : sheet) {
+            final String keyval = getCellAsString(row.getCell(keycol));
+            logger.trace("checking key {} against {}", keyval, key);
+            if (key.equals(keyval)) {
+                final String v = getCellAsString(row.getCell(valcol));
+                logger.trace("matched; {} -> {}", key, v);
+                return v;
+            }
+        }
+        return null;
+    }
+
+    private int getColumnIndex(final Value value, final Sheet sheet) {
+        if (value instanceof IntegerValue) {
+            return ((IntegerValue)value).getValue();
+        } else {
+            // TODO: look up column by name?
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
+     */
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
+        if (args.size() < 2) {
+            throw new ScriptEvaluationException("usage: " + name +
+                    "[filename, key, {, key-index {, val-index}}]");
+        }
+        final Value vFilename = args.get(0);
+        final Value vKey = args.get(1);
+        final Value vKeyCol = args.size() > 2 ? args.get(2) : new IntegerValue(0);
+        final Value vValCol = args.size() > 3 ? args.get(3) : new IntegerValue(1);
+
+        return new Value() {
+            /*
+             * (non-Javadoc)
+             * @see org.nrg.dcm.edit.Value#on(java.util.Map)
+             */
+            public String on(Map<Integer,String> m) throws ScriptEvaluationException {
+                final Workbook workbook;
+                try {
+                    workbook = workbookProvider.apply(vFilename.on(m));
+                } catch (Throwable t) {
+                    throw new ScriptEvaluationException(t);
+                }
+                final Sheet sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
+                try {
+                    return apply(sheet, vKey.on(m),
+                            getColumnIndex(vKeyCol, sheet),
+                            getColumnIndex(vValCol, sheet));
+                } catch (IOException e) {
+                    throw new ScriptEvaluationException(e);
+                }
+            }
+
+            /*
+             * (non-Javadoc)
+             * @see org.nrg.dcm.edit.Value#on(org.dcm4che2.data.DicomObject)
+             */
+            public String on(DicomObject o) throws ScriptEvaluationException {
+                final Workbook workbook;
+                try {
+                    workbook = workbookProvider.apply(vFilename.on(o));
+                } catch (Throwable t) {
+                    throw new ScriptEvaluationException(t);
+                }
+                final Sheet sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
+                try {
+                    return apply(sheet, vKey.on(o),
+                            getColumnIndex(vKeyCol, sheet),
+                            getColumnIndex(vValCol, sheet));
+
+                } catch (IOException e) {
+                    throw new ScriptEvaluationException(e);
+                }
+            }
+
+            /*
+             * (non-Javadoc)
+             * @see org.nrg.dcm.edit.Value#getVariables()
+             */
+            public Set<Variable> getVariables() {
+                final Set<Variable> vars = Sets.newLinkedHashSet();
+                vars.addAll(vFilename.getVariables());
+                vars.addAll(vKey.getVariables());
+                return vars;
+            }
+
+            /*
+             * (non-Javadoc)
+             * @see org.nrg.dcm.edit.Value#getTags()
+             */
+            public SortedSet<Integer> getTags() {
+                final SortedSet<Integer> tags = Sets.newTreeSet();
+                tags.addAll(vFilename.getTags());
+                tags.addAll(vKey.getTags());
+                return tags;
+            }
+        };
+    }
+}

File src/main/java/org/nrg/dcm/edit/fn/Substring.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         final Value s = args.get(0);
         final int start;
         final int end;

File src/main/java/org/nrg/dcm/edit/fn/Uppercase.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         final Value v;
         try {
             v = args.get(0);

File src/main/java/org/nrg/dcm/edit/fn/UrlEncode.java

     /* (non-Javadoc)
      * @see org.nrg.dcm.edit.ScriptFunction#apply(java.util.List)
      */
-    public Value apply(final List<Value> args) throws ScriptEvaluationException {
+    public Value apply(final List<? extends Value> args) throws ScriptEvaluationException {
         final Value v;
         try {
             v = args.get(0);

File src/main/java/org/nrg/dcm/io/BatchExporter.java

         }
 
         objectExporter.close();
+        if (null != pm) {
+            pm.close();
+        }
 
         logger.debug("Export completed");
         if (!failures.isEmpty()) {
             logger.debug("Failures: {}", failures);
         }
         isPending = false;
+        
     }
 }

File src/main/java/org/nrg/io/MapCachedFileProduct.java

+/**
+ * Copyright (c) 2012 Washington University
+ */
+package org.nrg.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+import org.nrg.util.CheckedExceptionFunction;
+
+/**
+ * @author Kevin A. Archie <karchie@wustl.edu>
+ *
+ */
+public class MapCachedFileProduct<T,E extends IOException> implements CheckedExceptionFunction<File,T,E> {
+    private final Map<File,CacheEntry<T>> cache;
+    private final CheckedExceptionFunction<File,T,E> product;
+    
+    public MapCachedFileProduct(final Map<File,CacheEntry<T>> m, final CheckedExceptionFunction<File,T,E> product) {
+        this.cache = m;
+        this.product = product;
+    }
+    
+    public T apply(File f) throws E {
+        final CacheEntry<T> ce = cache.get(f);
+        if (null == ce) {
+            cache.put(f, new CacheEntry<T>(product.apply(f), f));
+        } else synchronized (cache) {
+            final long lm = f.lastModified();
+            if (lm > ce.lastModified) {
+                cache.put(f, new CacheEntry<T>(product.apply(f), f));
+            }
+        }
+        return cache.get(f).t;
+    }
+    
+    public static final class CacheEntry<T> {
+        private final T t;
+        private final long lastModified;
+        
+        private CacheEntry(T t, File f) {
+            this.t = t;
+            this.lastModified = f.lastModified();
+        }
+    }
+}

File src/main/java/org/nrg/io/XSSFWorkbookFileProduct.java

+/**
+ * Copyright (c) 2012 Washington University
+ */
+package org.nrg.io;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.nrg.util.CheckedExceptionFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * @author Kevin A. Archie <karchie@wustl.edu>
+ *
+ */
+public final class XSSFWorkbookFileProduct implements CheckedExceptionFunction<File,Workbook,IOException> {
+    private final Logger logger = LoggerFactory.getLogger(XSSFWorkbookFileProduct.class);
+
+    public Workbook apply(final File name) throws IOException {
+        IOException ioexception = null;
+        final InputStream in = new FileInputStream(name);
+        try {
+            return new XSSFWorkbook(in);
+        } finally {
+            try {
+                in.close();
+            } catch (IOException e) {
+                if (null == ioexception) {
+                    throw e;
+                } else {
+                    logger.error("unable to close " + name, e);
+                    throw ioexception;
+                }
+            }
+        }  
+    }
+}

File src/main/java/org/nrg/util/CheckedExceptionFunction.java

+/**
+ * Copyright (c) 2012 Washington University
+ */
+package org.nrg.util;
+
+/**
+ * @author Kevin A. Archie <karchie@wustl.edu>
+ *
+ */
+public interface CheckedExceptionFunction<K,V,E extends Exception> {
+    V apply(K k) throws E;
+}

File src/main/java/org/nrg/util/NamedFileFunction.java

+/**
+ * Copyright (c) 2012 Washington University
+ */
+package org.nrg.util;
+
+import java.io.File;
+
+/**
+ * @author Kevin A. Archie <karchie@wustl.edu>
+ *
+ */
+public final class NamedFileFunction<V,E extends Exception> implements
+        CheckedExceptionFunction<String,V, E> {
+    private final CheckedExceptionFunction<File,V,E> f;
+    
+    public NamedFileFunction(final CheckedExceptionFunction<File,V,E> f) {
+        this.f = f;
+    }
+    
+    public V apply(final String name) throws E {
+        return f.apply(new File(name));
+    }
+}

File src/test/java/org/nrg/dcm/edit/ScriptApplicatorTest.java

 /**
- * Copyright (c) 2009-2011 Washington University
+ * Copyright (c) 2009-2012 Washington University
  */
 package org.nrg.dcm.edit;
 
 import java.util.Collections;
 import java.util.Set;
 
+import junit.framework.TestCase;
+
+import org.dcm4che2.data.BasicDicomObject;
 import org.dcm4che2.data.DicomObject;
 import org.dcm4che2.data.Tag;
 import org.dcm4che2.util.UIDUtils;
 import com.google.common.base.Function;
 import com.google.common.collect.Sets;
 
-import junit.framework.TestCase;
-
 /**
  * The test scripts here are based on the script language documentation at:
  * http://nrg.wustl.edu/projects/DICOM/AnonScript.jsp
     private static final String S_USE_FORMAT = "(0008,103e) := format[\"{0}_{1}\", (0008,103e), \"extended\"]\n";
 
     private static final String S_USE_REPLACE = "(0008,103e) := replace[(0008,103e), \"_\", \":\"]\n";
-    
+
     private static final String S_USE_HASHUID = "(0020,000d) := hashUID[(0020,000d)]\n";
 
     private static final String S_DESCRIPTION = "describe foo \"Description\"\nbar := foo\n";
 
     private static final String S_HIDDEN = "foo := \"bar\"\ndescribe foo hidden\n";
 
+    private static final String S_LOOKUP = "(0008,1030) := spreadsheet[\"src/test/resources/excel-spreadsheet.xlsx\", \"B\"]\n";
+
     private ByteArrayInputStream bytes(final String s) {
         return new ByteArrayInputStream(s.getBytes());
     }
         final ScriptApplicator s_use_replace = new ScriptApplicator(bytes(S_USE_REPLACE));
         final DicomObject do4_use_replace = s_use_replace.apply(f4);
         assertEquals("t1:mpr:1mm:p2:pos50", do4_use_replace.getString(Tag.SeriesDescription));
-        
+
         final ScriptApplicator s_use_hashUID = new ScriptApplicator(bytes(S_USE_HASHUID));
         final DicomObject do4_use_hashUID = s_use_hashUID.apply(f4);
         assertTrue(UIDUtils.isValidUID(do4_use_hashUID.getString(Tag.StudyInstanceUID)));
         final Variable f2 = a2.getVariable("foo");
         assertFalse(f2.isHidden());
     }
+
+    public void testLookup() throws Exception {
+        final DicomObject o = new BasicDicomObject();
+        final ScriptApplicator applicator = new ScriptApplicator(bytes(S_LOOKUP));
+        applicator.apply(null, o);
+        assertEquals("Badger", o.getString(Tag.StudyDescription));
+    }
 }

File src/test/java/org/nrg/dcm/edit/fn/SpreadsheetLookupTest.java

+/**
+ * Copyright (c) 2012 Washington University
+ */
+package org.nrg.dcm.edit.fn;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.nrg.dcm.edit.ConstantValue;
+import org.nrg.dcm.edit.IntegerValue;
+import org.nrg.dcm.edit.ScriptEvaluationException;
+import org.nrg.dcm.edit.Value;
+
+import junit.framework.TestCase;
+
+/**
+ * @author Kevin A. Archie <karchie@wustl.edu>
+ *
+ */
+public class SpreadsheetLookupTest extends TestCase {
+
+    /**
+     * Test method for {@link org.nrg.dcm.edit.fn.SpreadsheetLookup#apply(java.util.List)}.
+     */
+    public void testApply() throws ScriptEvaluationException {
+        final SpreadsheetLookup fn = new SpreadsheetLookup();
+        final ConstantValue ss = new ConstantValue("src/test/resources/excel-spreadsheet.xlsx");
+        final ConstantValue a = new ConstantValue("A");
+
+        final Value a1 = fn.apply(Arrays.asList(ss, a));
+        assertEquals("Aardvark", a1.on(Collections.<Integer,String>emptyMap()));
+    }
+
+    public void testApplyWithKeyColumn() throws ScriptEvaluationException {
+        final SpreadsheetLookup fn = new SpreadsheetLookup();
+        final Value ss = new ConstantValue("src/test/resources/excel-spreadsheet.xlsx");
+        final Value a = new ConstantValue("Apple");
+
+        final Value a1 = fn.apply(Arrays.asList(ss, a, new IntegerValue(2)));
+        assertEquals("Aardvark", a1.on(Collections.<Integer,String>emptyMap())); 
+    }
+
+    public void testApplyWithKeyValueColumns() throws ScriptEvaluationException {
+        final SpreadsheetLookup fn = new SpreadsheetLookup();
+        final Value ss = new ConstantValue("src/test/resources/excel-spreadsheet.xlsx");
+        final Value a = new ConstantValue("Apple");
+
+        final Value a1 = fn.apply(Arrays.asList(ss, a, new IntegerValue(2), new IntegerValue(0)));
+        assertEquals("A", a1.on(Collections.<Integer,String>emptyMap())); 
+        
+        final Value a2 = fn.apply(Arrays.asList(ss, a, new IntegerValue(2), new IntegerValue(2)));
+        assertEquals("Apple", a2.on(Collections.<Integer,String>emptyMap()));
+    }
+}

File src/test/resources/excel-spreadsheet.xlsx

Binary file added.