[Git][java-team/kdgcommons-java][upstream] New upstream version 1.0.17

Andreas Tille (@tille) gitlab at salsa.debian.org
Sun Feb 2 08:10:03 GMT 2025



Andreas Tille pushed to branch upstream at Debian Java Maintainers / kdgcommons-java


Commits:
2ca9bfea by Andreas Tille at 2025-02-02T09:04:56+01:00
New upstream version 1.0.17
- - - - -


23 changed files:

- pom.xml
- src/main/java/net/sf/kdgcommons/collections/CollectionUtil.java
- src/main/java/net/sf/kdgcommons/collections/HashMultimap.java
- src/main/java/net/sf/kdgcommons/io/IOUtil.java
- src/main/java/net/sf/kdgcommons/lang/ClassUtil.java
- src/main/java/net/sf/kdgcommons/lang/StringUtil.java
- src/main/java/net/sf/kdgcommons/sql/JDBCUtil.java
- src/main/java/net/sf/kdgcommons/test/NumericAsserts.java
- + src/main/java/net/sf/kdgcommons/test/SelfMock.java
- src/main/java/net/sf/kdgcommons/test/StringAsserts.java
- src/main/java/net/sf/kdgcommons/util/Counters.java
- src/main/java/net/sf/kdgcommons/util/ReadThroughCache.java
- src/site/changes.xml
- src/site/findbugs-filter.xml
- + src/test/java/net/sf/kdgcommons/alt/TestSelfMockAlt.java
- + src/test/java/net/sf/kdgcommons/alt/package.html
- src/test/java/net/sf/kdgcommons/collections/TestCollectionUtil.java
- src/test/java/net/sf/kdgcommons/lang/TestClassUtil.java
- src/test/java/net/sf/kdgcommons/sql/TestJDBCUtil.java
- src/test/java/net/sf/kdgcommons/test/TestNumericAsserts.java
- + src/test/java/net/sf/kdgcommons/test/TestSelfMock.java
- src/test/java/net/sf/kdgcommons/test/TestStringAsserts.java
- src/test/java/net/sf/kdgcommons/util/TestCounters.java


Changes:

=====================================
pom.xml
=====================================
@@ -2,15 +2,9 @@
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
-    <parent>
-        <groupId>org.sonatype.oss</groupId>
-        <artifactId>oss-parent</artifactId>
-        <version>7</version>
-    </parent>
-
     <groupId>net.sf.kdgcommons</groupId>
     <artifactId>kdgcommons</artifactId>
-    <version>1.0.15</version>
+    <version>1.0.17</version>
 
     <name>KDG Commons</name>
     <packaging>jar</packaging>
@@ -32,6 +26,13 @@
     </licenses>
 
 
+    <scm>
+        <connection>scm:svn:svn://svn.code.sf.net/p/kdgcommons/code</connection>
+        <developerConnection>scm:svn:svn+ssh://kdgregory@svn.code.sf.ner/p/kdgcommons/code</developerConnection>
+        <url>http://kdgcommons.svn.sourceforge.net/viewvc/kdgcommons/</url>
+    </scm>
+
+
     <developers>
         <developer>
             <id>kdgregory</id>
@@ -54,6 +55,13 @@
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+
+        <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
+        <maven-javadoc-plugin.version>3.0.1</maven-javadoc-plugin.version>
+        <maven-site-plugin.version>3.7.1</maven-site-plugin.version>
+        <maven-changes-plugin.version>2.11</maven-changes-plugin.version>
+        <findbugs-plugin.version>3.0.5</findbugs-plugin.version>
+        <cobertura-plugin.version>2.7</cobertura-plugin.version>
     </properties>
 
 
@@ -62,32 +70,51 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
+                <version>${maven-compiler-plugin.version}</version>
                 <configuration>
                     <source>1.5</source>
                     <target>1.5</target>
                     <compilerArgument>-g</compilerArgument>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>${maven-javadoc-plugin.version}</version>
+                <configuration>
+                    <doclint>none</doclint>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>${maven-site-plugin.version}</version>
+            </plugin>
         </plugins>
     </build>
 
+
     <reporting>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
+                <version>${maven-javadoc-plugin.version}</version>
                 <configuration>
                     <bottom>
                         <a
                         href="http://sourceforge.net/projects/kdgcommons/">
                         <img
                         src="http://sflogo.sourceforge.net/sflogo.php?group_id=234884&type=3">
-                        </a> </bottom>
+                        </a>
+                    </bottom>
+                    <doclint>none</doclint>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.codehaus.mojo</groupId>
                 <artifactId>cobertura-maven-plugin</artifactId>
+                <version>${cobertura-plugin.version}</version>
                 <configuration>
                     <instrumentation>
                         <excludes>
@@ -107,7 +134,7 @@
             <plugin>
                 <groupId>org.codehaus.mojo</groupId>
                 <artifactId>findbugs-maven-plugin</artifactId>
-                <version>2.4.0</version>
+                <version>${findbugs-plugin.version}</version>
                 <configuration>
                     <excludeFilterFile>src/site/findbugs-filter.xml</excludeFilterFile>
                 </configuration>
@@ -115,7 +142,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-changes-plugin</artifactId>
-                <version>2.3</version>
+                <version>${maven-changes-plugin.version}</version>
                 <configuration>
                     <xmlPath>${basedir}/src/site/changes.xml</xmlPath>
                 </configuration>
@@ -142,13 +169,6 @@
     </dependencies>
 
 
-    <scm>
-        <connection>scm:svn:svn://svn.code.sf.net/p/kdgcommons/code</connection>
-        <developerConnection>scm:svn:svn+ssh://kdgregory@svn.code.sf.ner/p/kdgcommons/code</developerConnection>
-        <url>http://kdgcommons.svn.sourceforge.net/viewvc/kdgcommons/</url>
-    </scm>
-
-
     <distributionManagement>
         <repository>
             <id>build</id>


=====================================
src/main/java/net/sf/kdgcommons/collections/CollectionUtil.java
=====================================
@@ -18,9 +18,11 @@ import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -211,10 +213,14 @@ public class CollectionUtil
 
     /**
      *  Returns the last element of the passed list, <code>null</code> if
-     *  the list is empty or null.
+     *  the list is empty or null. Uses an indexed get unless the list is
+     *  a subclass of <code>java.util.LinkedList</code>
      */
     public static <T> T last(List<T> list) {
-        return isNotEmpty(list) ? list.get(list.size() - 1) : null;
+        if (isEmpty(list))              return null;
+
+        if (list instanceof LinkedList<?>) return ((LinkedList<T>)list).getLast();
+        return list.get(list.size() - 1);
     }
 
 
@@ -443,6 +449,28 @@ public class CollectionUtil
     }
 
 
+    /**
+     *  Returns <code>true</code> if the passed map is either <code>null</code>
+     *  or has size 0.
+     */
+    public static boolean isEmpty(Map<?,?> m)
+    {
+        return (m == null)
+             ? true
+             : (m.size() == 0);
+    }
+
+
+    /**
+     *  Returns <code>true</code> if the passed map is not <code>null</code>
+     *  and has size > 0.
+     */
+    public static boolean isNotEmpty(Map<?,?> m)
+    {
+        return (m != null) && (m.size() > 0);
+    }
+
+
     /**
      *  Compares two collections of <code>Comparable</code> elements. The two collections are
      *  iterated, and the first not-equal <code>compareTo()</code> result is returned. If the
@@ -763,6 +791,70 @@ public class CollectionUtil
     }
 
 
+    /**
+     *  Partitions the passed iterable into N sublists, each of which has
+     *  at most <code>maxSize</code> elements.
+     */
+    public static <T> List<List<T>> partition(Iterable<T> source, int maxSize)
+    {
+        if (source == null) return Collections.emptyList();
+
+        List<List<T>> result = new ArrayList<List<T>>();
+        List<T> sublist = new ArrayList<T>(maxSize);
+        int count = 0;
+        for (T item : source)
+        {
+            sublist.add(item);
+            count++;
+            if (count >= maxSize)
+            {
+                result.add(sublist);
+                sublist = new ArrayList<T>(maxSize);
+                count = 0;
+            }
+        }
+        if (sublist.size() > 0)
+        {
+            result.add(sublist);
+        }
+        return result;
+    }
+
+
+    /**
+     *  Returns a map that contains all keys in the specified collection.
+     *  <p>
+     *  The returned map is a HashMap; see variant for choosing map type.
+     */
+    public static <K,V> Map<K,V> submap(Map<K,V> src, Collection<K> keys)
+    {
+        return submap(src, keys, new HashMap<K,V>());
+    }
+
+
+    /**
+     *  Extracts all mappings from the source map that correspond to the passed
+     *  keys, and stores them in the destination map. Returns the destination
+     *  map as a convenience.
+     */
+    public static <K,V> Map<K,V> submap(Map<K,V> src, Collection<K> keys, Map<K,V> dest)
+    {
+        if ((src == null) || (keys == null) || (dest == null))
+        {
+            return dest;
+        }
+
+        for (K key : keys)
+        {
+            if (src.containsKey(key))
+            {
+                dest.put(key, src.get(key));
+            }
+        }
+        return dest;
+    }
+
+
 //----------------------------------------------------------------------------
 //  Supporting Objects
 //----------------------------------------------------------------------------


=====================================
src/main/java/net/sf/kdgcommons/collections/HashMultimap.java
=====================================
@@ -60,7 +60,7 @@ implements Serializable
     /**
      *  Controls the handling of equal key-value pairs.
      */
-    enum Behavior { LIST, SET }
+    public enum Behavior { LIST, SET }
 
 //----------------------------------------------------------------------------
 //  Instance variables and Constructors


=====================================
src/main/java/net/sf/kdgcommons/io/IOUtil.java
=====================================
@@ -215,7 +215,7 @@ public class IOUtil
 
 
     /**
-     *  Repeatedly calls <code>skip()/code> on the underlying stream, until either the
+     *  Repeatedly calls <code>skip()</code> on the underlying stream, until either the
      *  desired number of bytes have been read or EOF is reached. Returns the number
      *  of bytes actually skipped.
      *


=====================================
src/main/java/net/sf/kdgcommons/lang/ClassUtil.java
=====================================
@@ -15,6 +15,7 @@
 package net.sf.kdgcommons.lang;
 
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
@@ -309,6 +310,38 @@ public class ClassUtil
     }
 
 
+    /**
+     *  Looks for the specified field in the class or its superclasses, returning
+     *  its value if it exists. Throws <code>NoSuchFieldException</code> if unable
+     *  to find the field in the class hierarchy, or if an exception occurred while
+     *  retrieving its value.
+     *  <p>
+     *  Note: does not verify that actual field value matches <code>expectedClass</code>
+     *  (this gets tricky when dealing with primitive wrapper classes).
+     */
+    public static <T> T getFieldValue(Object obj, String fieldName, Class<T> expectedClass)
+    throws NoSuchFieldException
+    {
+        for (Class<?> objKlass = obj.getClass() ; objKlass != null ; objKlass = objKlass.getSuperclass())
+        {
+            Field field = null;
+            try
+            {
+                field = objKlass.getDeclaredField(fieldName);
+                field.setAccessible(true);
+                return (T)field.get(obj);
+            }
+            catch (Exception ignored)
+            {
+                // most likely a NoSuchFieldException, but could be a security exception
+                // from setAccessible(); in either case we'll just try superclass
+            }
+        }
+
+        throw new NoSuchFieldException("unable to retrieve field " + fieldName + " from object of class " + obj.getClass().getName());
+    }
+
+
 //----------------------------------------------------------------------------
 //  Internals
 //----------------------------------------------------------------------------


=====================================
src/main/java/net/sf/kdgcommons/lang/StringUtil.java
=====================================
@@ -362,8 +362,10 @@ public class StringUtil
 
 
     /**
-     *  Generates a random string consisting of characters from the passed
-     *  string.
+     *  Generates a (non-cryptographicaly-) random string consisting of characters
+     *  from the passed string. Useful for generating bogus string fields.
+     *  <p>
+     *  Warning: not threadsafe; uses a shared instance of <code>java.util.Random</code>.
      *
      *  @param  chars       Defines the set of characters used to create the
      *                      returned string.
@@ -381,8 +383,10 @@ public class StringUtil
 
 
     /**
-     *  Generates a string containing random ASCII alphabetic characters
-     *  (A-Za-z).
+     *  Generates a string containing (non-cryptographicaly-) random ASCII alphabetic
+     *  characters (A-Za-z). Useful for generating bogus string fields.
+     *  <p>
+     *  Warning: not threadsafe; uses a shared instance of <code>java.util.Random</code>.
      *
      *  @param  minLength   Minimum length of the returned string.
      *  @param  maxLength   Maximum length of the returned string.


=====================================
src/main/java/net/sf/kdgcommons/sql/JDBCUtil.java
=====================================
@@ -14,10 +14,11 @@
 
 package net.sf.kdgcommons.sql;
 
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
+import java.sql.*;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
 
 
 /**
@@ -25,6 +26,102 @@ import java.sql.Statement;
  */
 public class JDBCUtil
 {
+    /**
+     *  Executes a query and returns the results, ensuring that the created statement
+     *  and resultset are closed.
+     *
+     *  @param  args    Parameters for the query. May be empty.
+     *
+     *  @return A list of maps, where each entry in the list represents a row from the
+     *          results, and the keys in the map represent the column names.
+     */
+    public static List<Map<String,Object>> executeQuery(Connection cxt, String sql, Object... args)
+    throws SQLException
+    {
+        PreparedStatement stmt = null;
+        ResultSet rslt = null;
+        try
+        {
+            stmt = prepare(cxt, sql, args);
+            rslt = stmt.executeQuery();
+            return retrieve(rslt);
+        }
+        finally
+        {
+            closeQuietly(stmt);
+            closeQuietly(rslt);
+        }
+    }
+
+
+    /**
+     *  Executes an update and returns the results, ensuring that the created statement is closed.
+     *
+     *  @param  args    Parameters for the query. May be empty.
+     *
+     *  @return The number of rows updated by this statement.
+     */
+    public static int executeUpdate(Connection cxt, String sql, Object... args)
+    throws SQLException
+    {
+        PreparedStatement stmt = null;
+        try
+        {
+            stmt = prepare(cxt, sql, args);
+            return stmt.executeUpdate();
+        }
+        finally
+        {
+            closeQuietly(stmt);
+        }
+    }
+
+
+    /**
+     *  Creates a <code>PreparedStatement</code> from the provided connection.
+     *
+     *  @param  args    Parameters for the query. May be empty.
+     */
+    public static PreparedStatement prepare(Connection cxt, String sql, Object... args)
+    throws SQLException
+    {
+        PreparedStatement stmt = cxt.prepareStatement(sql);
+        for (int ii = 0 ; ii < args.length ; ii++)
+        {
+            stmt.setObject(ii + 1, args[ii]);
+        }
+        return stmt;
+    }
+
+
+    /**
+     *  Iterates through the passed <code>ResultSet</code>, converting each row into a
+     *  <code>Map</code>, where keys are the column names as retrieved from metadata,
+     *  and values are the result of calling <code>getObject()</code>.
+     *  <p>
+     *  Caller is responsible for closing the <code>ResultSet</code>.
+     */
+    public static List<Map<String,Object>> retrieve(ResultSet rslt)
+    throws SQLException
+    {
+        // we use a LinkedList because it has simpler memory allocation characteristics
+        List<Map<String,Object>> results = new LinkedList<Map<String,Object>>();
+
+        ResultSetMetaData meta = rslt.getMetaData();
+        while (rslt.next())
+        {
+            Map<String,Object> row = new HashMap<String,Object>();
+            for (int ii = 1 ; ii <= meta.getColumnCount() ; ii++)
+            {
+                row.put(meta.getColumnName(ii), rslt.getObject(ii));
+            }
+            results.add(row);
+        }
+
+        return results;
+    }
+
+
     /**
      *  Closes the passed <code>Connection</code> ignoring exceptions. This is usually
      *  called in a <code>finally</code> block, and throwing an exception there would


=====================================
src/main/java/net/sf/kdgcommons/test/NumericAsserts.java
=====================================
@@ -23,15 +23,147 @@ import junit.framework.Assert;
 public class NumericAsserts
 {
     /**
-     *  Asserts that the actual value is within the expected, plus/minus
-     *  the specified percentage.
+     *  Asserts that the actual value is within the expected, plus/minus the
+     *  specified percentage (useful for probabilistic testing).
      */
     public static void assertApproximate(int expected, int actual, int deltaPercent)
+    {
+        assertApproximate(null, expected, actual, deltaPercent);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within the expected, plus/minus the
+     *  specified percentage (useful for probabilistic testing). On failure,
+     *  prepends the supplied message (if any) to a description of the failure.
+     */
+    public static void assertApproximate(String message, int expected, int actual, int deltaPercent)
     {
         int delta = (int)(((long)expected * deltaPercent) / 100);
-        int loBound = expected - delta;
-        Assert.assertTrue("expected >= " + loBound + ", was " + actual, actual >= loBound);
-        int hiBound = expected + delta;
-        Assert.assertTrue("expected <= " + hiBound + ", was " + actual, actual <= hiBound);
+        assertInRange(message, expected - delta, expected + delta, actual);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within the expected, plus/minus the
+     *  specified percentage (useful for probabilistic testing).
+     */
+    public static void assertApproximate(long expected, long actual, int deltaPercent)
+    {
+        assertApproximate(null, expected, actual, deltaPercent);
+    }
+
+    /**
+     *  Asserts that the actual value is within the expected, plus/minus the
+     *  specified percentage (useful for probabilistic testing). On failure,
+     *  prepends the supplied message (if any) to a description of the failure.
+     */
+    public static void assertApproximate(String message, long expected, long actual, int deltaPercent)
+    {
+        // to avoid overflow, we swap the divide and multiply depending on the size of
+        // the value -- assumption is that range of error is minimal compared to delta
+        long delta = (expected > Integer.MAX_VALUE * 100)
+                   ? (expected / 100) * deltaPercent
+                   : (expected * deltaPercent) / 100;
+        assertInRange(message, expected - delta, expected + delta, actual);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within the expected, plus/minus the
+     *  specified percentage (useful for probabilistic testing).
+     */
+    public static void assertApproximate(double expected, double actual, double deltaPercent)
+    {
+        assertApproximate(null, expected, actual, deltaPercent);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within the expected, plus/minus the
+     *  specified percentage (useful for probabilistic testing). On failure,
+     *  prepends the supplied message (if any) to a description of the failure.
+     */
+    public static void assertApproximate(String message, double expected, double actual, double deltaPercent)
+    {
+        double delta = (expected * deltaPercent) / 100;
+        assertInRange(message, expected - delta, expected + delta, actual);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within an arbitrary range +/- the expected value.
+     */
+    public static void assertInRange(int expectedLow, int expectedHigh, int actual)
+    {
+        assertInRange(null, expectedLow, expectedHigh, actual);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within an arbitrary range +/- the expected value.
+     *  On failure, prepends the supplied message (if any) to a description of the failure.
+     */
+    public static void assertInRange(String message, int expectedLow, int expectedHigh, int actual)
+    {
+        if ((actual < expectedLow) || (actual > expectedHigh))
+        {
+            String baseMessage = "value not in expected range: was " + actual + ", expected between " + expectedLow + " and " + expectedHigh;
+            String actualMessage = (message != null)
+                                 ? message + ": " + baseMessage
+                                 : baseMessage;
+            Assert.fail(actualMessage);
+        }
+    }
+
+
+    /**
+     *  Asserts that the actual value is within an arbitrary range +/- the expected value.
+     */
+    public static void assertInRange(long expectedLow, long expectedHigh, long actual)
+    {
+        assertInRange(null, expectedLow, expectedHigh, actual);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within an arbitrary range +/- the expected value.
+     *  On failure, prepends the supplied message (if any) to a description of the failure.
+     */
+    public static void assertInRange(String message, long expectedLow, long expectedHigh, long actual)
+    {
+        if ((actual < expectedLow) || (actual > expectedHigh))
+        {
+            String baseMessage = "value not in expected range: was " + actual + ", expected between " + expectedLow + " and " + expectedHigh;
+            String actualMessage = (message != null)
+                                 ? message + ": " + baseMessage
+                                 : baseMessage;
+            Assert.fail(actualMessage);
+        }
+    }
+
+
+    /**
+     *  Asserts that the actual value is within an arbitrary range +/- the expected value.
+     */
+    public static void assertInRange(double expectedLow, double expectedHigh, double actual)
+    {
+        assertInRange(null, expectedLow, expectedHigh, actual);
+    }
+
+
+    /**
+     *  Asserts that the actual value is within an arbitrary range +/- the expected value.
+     */
+    public static void assertInRange(String message, double expectedLow, double expectedHigh, double actual)
+    {
+        if ((actual < expectedLow) || (actual > expectedHigh))
+        {
+            String baseMessage = "value not in expected range: was " + actual + ", expected between " + expectedLow + " and " + expectedHigh;
+            String actualMessage = (message != null)
+                                 ? message + ": " + baseMessage
+                                 : baseMessage;
+            Assert.fail(actualMessage);
+        }
     }
 }


=====================================
src/main/java/net/sf/kdgcommons/test/SelfMock.java
=====================================
@@ -0,0 +1,199 @@
+// Copyright Keith D Gregory
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.sf.kdgcommons.test;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.sf.kdgcommons.util.Counters;
+
+
+/**
+ *  A reflection-based proxy that invokes requests on itself. This class is
+ *  intended to be subclassed, with the subclass implementing the methods
+ *  that are to be mocked.
+ *  <p>
+ *  To use, construct with the interface to be mocked (only one allowed, due to
+ *  Java parameterization), then call {@link #getInstance} to create the proxy
+ *  instance.
+ *  <p>
+ *  By default, all invocations are counted and the invocation arguments retained.
+ *  See {@link #getInvocationCount} and {@link #getInvocationArgument} for more
+ *  information.
+ */
+public abstract class SelfMock<MockedType>
+implements InvocationHandler
+{
+    private Class<MockedType> klass;
+
+    private Counters<String> invocationCounts = new Counters<String>();
+    private ConcurrentHashMap<String,ArrayList<Object[]>> invocationArgs = new ConcurrentHashMap<String,ArrayList<Object[]>>();
+
+
+    public SelfMock(Class<MockedType> klass)
+    {
+        this.klass = klass;
+    }
+
+//----------------------------------------------------------------------------
+//  Public API
+//----------------------------------------------------------------------------
+
+    /**
+     *  Returns a proxy instance that passes invocations to this mock. Multiple
+     *  calls will return different proxy instances.
+     */
+    public MockedType getInstance()
+    {
+        return klass.cast(
+                Proxy.newProxyInstance(
+                    this.getClass().getClassLoader(),
+                    new Class[] {klass},
+                    this));
+    }
+
+
+    /**
+     *  Returns the number of times the named function was invoked. This does
+     *  not differentiate between overloaded methods: all methods with the same
+     *  name is counted together.
+     */
+    public int getInvocationCount(String methodName)
+    {
+        return invocationCounts.getInt(methodName);
+    }
+
+
+    /**
+     *  Returns the arguments passed to a particular invocation of the named method
+     *  (using zero-based counting). This is primarily useful when dealing with
+     *  overloaded methods, where you might not know what types the arguments are.
+     *  If you know the arguments, {@link #getInvocationArgAs} is a better choice.
+     *  <p>
+     *  Note: this method returns the actual argument array that was passed to the
+     *  invocation handler. Don't modify it unless you want to invalidate your tests.
+     *
+     *  @throws IndexOutOfBoundsException if accessing a call that was never made.
+     */
+    public Object[] getInvocationArgs(String methodName, int invocationIndex)
+    {
+        return invocationHistoryFor(methodName).get(invocationIndex);
+    }
+
+
+    /**
+     *  Returns a specific invocation argument, cast to a particular type. Both
+     *  indexes are zero-based.
+     */
+    public <T> T getInvocationArg(String methodName, int invocationIndex, int argumentIndex, Class<T> argType)
+    {
+        return argType.cast(getInvocationArgs(methodName, invocationIndex)[argumentIndex]);
+    }
+
+
+    /**
+     *  Returns the arguments passed to the most recent invocation of the named
+     *  method.This is primarily useful when dealing with overloaded methods,
+     *  where you might not know what types the arguments are. If you know the
+     *  arguments, {@link #getInvocationArgAs} is a better choice.
+     *  <p>
+     *  Note: this method returns the actual argument array that was passed to the
+     *  invocation handler. Don't modify it unless you want to invalidate your tests.
+     */
+    public Object[] getMostRecentInvocationArgs(String methodName)
+    {
+        int index = getInvocationCount(methodName) - 1;
+        return (index < 0)
+             ? null
+             : getInvocationArgs(methodName, index);
+    }
+
+
+    /**
+     *  The a specific argument from the most recent invocation, cast to a particular
+     *  type.
+     */
+    public <T> T getMostRecentInvocationArg(String methodName, int argumentIndex, Class<T> argType)
+    {
+        Object[] args = getMostRecentInvocationArgs(methodName);
+        return (args == null)
+             ? null
+             : argType.cast(args[argumentIndex]);
+    }
+
+//----------------------------------------------------------------------------
+//  Internals
+//----------------------------------------------------------------------------
+
+    public Object invoke(Object proxy, Method method, Object[] args)
+    throws Throwable
+    {
+        String methodName = method.getName();
+        invocationCounts.increment(methodName);
+
+        synchronized (this)
+        {
+            invocationHistoryFor(methodName).add(args);
+        }
+
+        try
+        {
+            Method selfMethod = getClass().getMethod(methodName, method.getParameterTypes());
+            selfMethod.setAccessible(true);
+            return selfMethod.invoke(this, args);
+        }
+        catch (NoSuchMethodException ex)
+        {
+            throw new UnsupportedOperationException("mock does not implement method: " + methodName
+                                                    + "(" + Arrays.asList(method.getParameterTypes()) + ")");
+        }
+        catch (SecurityException ex)
+        {
+            throw new RuntimeException("security exception when invoking: " + methodName, ex);
+        }
+        catch (IllegalAccessException ex)
+        {
+            throw new RuntimeException("illegal access exception when invoking: " + methodName, ex);
+        }
+        catch (InvocationTargetException ex)
+        {
+            // this is an exception thrown by the mock instance, which is probably intentional
+            throw ex.getCause();
+        }
+    }
+
+
+    /**
+     *  Returns the list of historical arguments for the named method, creating it
+     *  if necessary. May be called without synchronization, although changes to the
+     *  returned array should be synchronized.
+     */
+    private ArrayList<Object[]> invocationHistoryFor(String methodName)
+    {
+        ArrayList<Object[]> history = invocationArgs.get(methodName);
+        if (history == null)
+        {
+            // this could be invoked concurrently, and someone else could create the list
+            invocationArgs.putIfAbsent(methodName, new ArrayList<Object[]>());
+            history = invocationArgs.get(methodName);
+        }
+        return history;
+    }
+}


=====================================
src/main/java/net/sf/kdgcommons/test/StringAsserts.java
=====================================
@@ -27,6 +27,30 @@ import junit.framework.Assert;
  */
 public class StringAsserts
 {
+    /**
+     *  Asserts that the passed string is not empty or null.
+     */
+    public static void assertNotEmpty(String value)
+    {
+        assertNotEmpty(null, value);
+    }
+
+
+    /**
+     *  Asserts that the passed string is not empty or null. On failure, reports
+     *  the specified message along with a descriptive error message.
+     */
+    public static void assertNotEmpty(String message, String value)
+    {
+        String baseMessage = (message == null)
+                           ? "expected not-empty"
+                           : message + ": expected not-empty";
+
+        if (value == null)          Assert.fail(baseMessage + ", was null");
+        if (value.length() == 0)    Assert.fail(baseMessage);
+    }
+
+
     /**
      *  Asserts that a given string contains N instances of a substring.
      *
@@ -58,7 +82,7 @@ public class StringAsserts
         int actual = 0;
         for (int idx = str.indexOf(sub) ; (idx >= 0) && (idx < str.length()) ; )
         {
-            actual += (idx >= 0) ? 1 : 0;
+            actual++;
             idx = str.indexOf(sub, idx + 1);
         }
         Assert.assertEquals(message + ": count(" + sub + ")",


=====================================
src/main/java/net/sf/kdgcommons/util/Counters.java
=====================================
@@ -49,6 +49,32 @@ implements Map<K,Long>, Iterable<Map.Entry<K,Long>>
 {
     private ConcurrentHashMap<K,AtomicLong> _map = new ConcurrentHashMap<K,AtomicLong>();
 
+//----------------------------------------------------------------------------
+//  Object overrides
+//----------------------------------------------------------------------------
+
+    /**
+     *  Outputs all counters in the format "[ NAME: VALUE, ...]".
+     */
+    @Override
+    public String toString()
+    {
+        StringBuilder sb = new StringBuilder(16384);
+        sb.append("[");
+        for (Map.Entry<K,AtomicLong> entry : _map.entrySet())
+        {
+            if (sb.length() > 1)
+                sb.append(", ");
+
+            sb.append(String.valueOf(entry.getKey()))
+              .append(": ")
+              .append(entry.getValue().longValue());
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+
 
 //----------------------------------------------------------------------------
 //  Implementation of Map
@@ -166,13 +192,17 @@ implements Map<K,Long>, Iterable<Map.Entry<K,Long>>
 
 
     /**
-     *  Returns the current keys in the map. This operation is performed directly on the
-     *  underlying map, so does not involve a performance penalty (other than what the
-     *  map incurs).
+     *  Returns the current keys in the map. Note that this represents a point-in-time
+     *  view of the map, so if you are concurrently adding or removing counters it may
+     *  be missing keys or contain keys that are no longer in the map.
      */
     public Set<K> keySet()
     {
-        return _map.keySet();
+        // in Java 8, ConcurrentHashMap broke binary compatibility for keySet(), returning
+        // a different concrete class; this cast forces the compiler to use an invokeinterface
+        // rather than invokevirtual, so the bytecode remains compatible
+
+        return ((Map<K,AtomicLong>)_map).keySet();
     }
 
 
@@ -208,7 +238,6 @@ implements Map<K,Long>, Iterable<Map.Entry<K,Long>>
         return entries;
     }
 
-
 //----------------------------------------------------------------------------
 //  Additional Public Methods
 //----------------------------------------------------------------------------
@@ -230,7 +259,7 @@ implements Map<K,Long>, Iterable<Map.Entry<K,Long>>
 
 
     /**
-     *  Retrieves the value of the specified key as a primitive. If there is no
+     *  Retrieves the value of the specified key as a primitive long. If there is no
      *  mapping for the key, returns 0.
      */
     public long getLong(K key)
@@ -240,6 +269,19 @@ implements Map<K,Long>, Iterable<Map.Entry<K,Long>>
     }
 
 
+    /**
+     *  Retrieves the value of the specified key as a primitive int. If there is no
+     *  mapping for the key, returns 0. This is only valid if you know that your
+     *  counters will remain in integer range (but often works better with other
+     *  variables in your code).
+     */
+    public int getInt(K key)
+    {
+        AtomicLong mapping = _map.get(key);
+        return (mapping == null) ? 0 : (int)mapping.get();
+    }
+
+
     /**
      *  Sets the mapping to the specified value. This is equivalent to calling
      *  {@link #put}, with the same caveats regarding concurent access.


=====================================
src/main/java/net/sf/kdgcommons/util/ReadThroughCache.java
=====================================
@@ -149,7 +149,7 @@ public class ReadThroughCache<K,V>
 
              @Override
              protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
-                return size() > size;
+                return this.size() > size;
              }
         };
     }


=====================================
src/site/changes.xml
=====================================
@@ -4,7 +4,61 @@
 	</properties>
 
     <body>
-        <release version="1.0.15" date="TBD"
+        <release version="1.0.17" date="2019-08-03"
+            description="TBD">
+            <action dev='kdgregory' type='add'>
+                Counters: add toString()
+            </action>
+            <action dev='kdgregory' type='add'>
+                JDBCUtil: add functions to support simple SQL operations
+            </action>
+            <action dev='kdgregory' type='update'>
+                CollectionUtil.last(): optimization for linked lists
+            </action>
+            <action dev='kdgregory' type='update'>
+                Counters: work-around signature change in ConcurrentHashMap
+                (was producing an invalid artifact when compiling with JDK 8)
+            </action>
+            <action dev='kdgregory' type='update'>
+                HashMultimap: Behavior enum was not public
+            </action>
+            <action dev='kdgregory' type='update'>
+                SelfMock: retain invocation arguments
+            </action>
+        </release>
+
+        <release version="1.0.16" date="2018-07-22"
+            description="grab-bag of additions">
+            <action dev='kdgregory' type='add'>
+                ClassUtil.getFieldValue(): retrieves a field's value from wherever it's defined in
+                the class hierarchy
+            </action>
+            <action dev='kdgregory' type='add'>
+                CollectionUtil.isEmpty(), isNotEmpty(): now accept Maps
+            </action>
+            <action dev='kdgregory' type='add'>
+                CollectionUtil.partition(): breaks a passed collection into a list of max-size lists
+            </action>
+            <action dev='kdgregory' type='add'>
+                CollectionUtil.submap(): extracts mappings for a set of keys.
+            </action>
+            <action dev='kdgregory' type='add'>
+                NumericAsserts: add assertApproximate() for long and double values
+            </action>
+            <action dev='kdgregory' type='add'>
+                NumericAsserts.assertInRange(): allows caller to assert that the actual value is
+                within an arbitrary range +/- of the expected value
+            </action>
+            <action dev='kdgregory' type='add'>
+                StringAsserts.assertNotEmpty()
+            </action>
+            <action dev='kdgregory' type='add'>
+                SelfMock: a reflection-based mock object that invokes methods on itself; used to mock
+                a portion of an interface without a long if-else chain in the invocation handler
+            </action>
+        </release>
+
+        <release version="1.0.15" date="2017-01-22"
             description="A variety of new functions, across utils">
             <action dev='kdgregory' type='add'>
                 BufferUtil.toArray: returns the entire contents of a ByteBuffer as an array


=====================================
src/site/findbugs-filter.xml
=====================================
@@ -47,4 +47,11 @@
         <!-- all paths are covered; FindBugs is being paranoid -->
     </Match>
 
+    <Match>
+        <Class name='net.sf.kdgcommons.lang.ObjectUtil' />
+        <Method name='equals' />
+        <Bug pattern='RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE' />
+        <!-- intentional: as written I consider the conditions more obvious -->
+    </Match>
+
 </FindBugsFilter>


=====================================
src/test/java/net/sf/kdgcommons/alt/TestSelfMockAlt.java
=====================================
@@ -0,0 +1,40 @@
+// Copyright Keith D Gregory
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.sf.kdgcommons.alt;
+
+import junit.framework.TestCase;
+
+import net.sf.kdgcommons.test.SelfMock;
+
+
+public class TestSelfMockAlt extends TestCase
+{
+    // there was a bug where methods in anonymous classes in a different package
+    // (ie, not net.sf.kdgcommons.test) were unaccessible and needed to be set
+    // accessible to be invoked
+    public void testAnonymousImplementationClassInDifferentPackage()
+    {
+        CharSequence instance = new SelfMock<CharSequence>(CharSequence.class)
+        {
+            @SuppressWarnings("unused")
+            public int length()
+            {
+                return 123;
+            }
+        }.getInstance();
+
+        assertEquals(123, instance.length());
+    }
+}


=====================================
src/test/java/net/sf/kdgcommons/alt/package.html
=====================================
@@ -0,0 +1,4 @@
+<body>
+This package exists because some testcases need to run in a different package
+than the classes under test to verify protection problems.
+</body>


=====================================
src/test/java/net/sf/kdgcommons/collections/TestCollectionUtil.java
=====================================
@@ -24,6 +24,7 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -197,18 +198,40 @@ public class TestCollectionUtil extends TestCase
         List<String> l4 = null;
         assertEquals("first(), list is null",   null, CollectionUtil.first(l4));
         assertEquals("last(),  list is null",   null, CollectionUtil.last(l4));
+
+        // this test is just here for coverage; we don't verify behavior
+        List<String> l5 = new LinkedList<String>(Arrays.asList("foo", "bar", "baz"));
+        assertEquals("first(), LinkedList",     "foo", CollectionUtil.first(l5));
+        assertEquals("last(),  LinkedList",     "baz", CollectionUtil.last(l5));
     }
 
 
     public void testIsEmpty() throws Exception
     {
-        assertTrue(CollectionUtil.isEmpty(null));
-        assertTrue(CollectionUtil.isEmpty(Arrays.asList()));
-        assertFalse(CollectionUtil.isEmpty(Arrays.asList("foo")));
+        List<String> list1 = null;
+        assertTrue(CollectionUtil.isEmpty(list1));
+        assertFalse(CollectionUtil.isNotEmpty(list1));
+
+        List<String> list2 = new ArrayList<String>();
+        assertTrue(CollectionUtil.isEmpty(list2));
+        assertFalse(CollectionUtil.isNotEmpty(list2));
+
+        List<String> list3 = Arrays.asList("foo");
+        assertFalse(CollectionUtil.isEmpty(list3));
+        assertTrue(CollectionUtil.isNotEmpty(list3));
 
-        assertFalse(CollectionUtil.isNotEmpty(null));
-        assertFalse(CollectionUtil.isNotEmpty(Arrays.asList()));
-        assertTrue(CollectionUtil.isNotEmpty(Arrays.asList("foo")));
+        Map<String,String> map1 = null;
+        assertTrue(CollectionUtil.isEmpty(map1));
+        assertFalse(CollectionUtil.isNotEmpty(map1));
+
+        Map<String,String> map2 = new HashMap<String,String>();
+        assertTrue(CollectionUtil.isEmpty(map2));
+        assertFalse(CollectionUtil.isNotEmpty(map2));
+
+        Map<String,String> map3 = new HashMap<String,String>();
+        map3.put("foo", "bar");
+        assertFalse(CollectionUtil.isEmpty(list3));
+        assertTrue(CollectionUtil.isNotEmpty(map3));
     }
 
 
@@ -856,4 +879,56 @@ public class TestCollectionUtil extends TestCase
         }
     }
 
+
+    public void testPartition() throws Exception
+    {
+        assertEquals(
+                "null source",
+                Collections.<Integer>emptyList(),
+                CollectionUtil.partition(null, 2));
+
+        assertEquals(
+                "empty source list",
+                Collections.<Integer>emptyList(),
+                CollectionUtil.partition(Collections.<Integer>emptyList(), 2));
+
+        assertEquals(
+                "non-empty source array",
+                Arrays.asList(Arrays.asList(1,2), Arrays.asList(3,4), Arrays.asList(5)),
+                CollectionUtil.partition(Arrays.asList(1,2,3,4,5), 2));
+    }
+
+
+    public void testSubmap() throws Exception
+    {
+        Map<Integer,String> source = new HashMap<Integer,String>();
+        source.put(1, "foo");
+        source.put(2, "bar");
+        source.put(3, "baz");
+
+        assertEquals("null source",
+                     Collections.emptyMap(),
+                     CollectionUtil.submap(null, Arrays.asList(1, 2, 3)));
+
+        assertEquals("null keylist",
+                     Collections.emptyMap(),
+                     CollectionUtil.submap(source, null));
+
+        assertEquals("null destination",
+                     null,
+                     CollectionUtil.submap(source, Arrays.asList(1,2), null));
+
+        Map<Integer,String> expected = new HashMap<Integer,String>();
+        expected.put(2, "bar");
+
+        assertEquals("normal operation",
+                     expected,
+                     CollectionUtil.submap(source, Arrays.asList(2)));
+
+        Map<Integer,String> dest = new TreeMap<Integer,String>();
+
+        assertSame("explicit destination",
+                     dest,
+                     CollectionUtil.submap(source, Arrays.asList(2), dest));
+    }
 }


=====================================
src/test/java/net/sf/kdgcommons/lang/TestClassUtil.java
=====================================
@@ -48,27 +48,70 @@ public class TestClassUtil extends TestCase
 
     public static class Parent
     {
-        public int foo()                        { return 1; }
+        protected int parentField;
+        protected int fieldToBeShadowed;
+
+        public Parent()
+        {}
+
+        public Parent(int fieldValue, int shadowValue)
+        {
+            parentField = fieldValue;
+            fieldToBeShadowed = shadowValue;
+        }
+
+        public int foo()
+        { return 1; }
 
         @Bar
-        public int bar()                        { return 2; }
+        public int bar()
+        { return 2; }
     }
 
 
     public static class Child
     extends Parent
     {
+        // leaving this private to verify that we can make it accessible
+        @SuppressWarnings("unused")
+        private int childField;
+
+        public Child()
+        {}
+
+        public Child(int parentValue, int shadowValue, int fieldValue)
+        {
+            super(parentValue, shadowValue);
+            childField = fieldValue;
+        }
+
         @Override
-        public int bar()                        { return 3; }
+        public int bar()
+        { return 3; }
 
         @Foo
-        public int baz()                        { return 3; }
+        public int baz()
+        { return 3; }
     }
 
 
     public static class Grandchild extends Child
     {
-        public int bar(int param)               { return 4; }
+        public int grandchildField;
+        public int fieldToBeShadowed;
+
+        public Grandchild()
+        {}
+
+        public Grandchild(int parentValue, int parentShadowValue, int childValue, int fieldValue, int shadowValue)
+        {
+            super(parentValue, parentShadowValue, childValue);
+            grandchildField = fieldValue;
+            fieldToBeShadowed = shadowValue;
+        }
+
+        public int bar(int param)
+        { return 4; }
     }
 
 
@@ -76,6 +119,7 @@ public class TestClassUtil extends TestCase
     {
         public    void foo(Object val)          { /* nothing here */ }
 
+        @SuppressWarnings("unused")
         private   void bar(Object val)          { /* nothing here */ }
 
         protected void baz(Object val)          { /* nothing here */ }
@@ -513,4 +557,34 @@ public class TestClassUtil extends TestCase
         assertEquals("override: parameters",    Arrays.asList(Object.class),
                                                 Arrays.asList(m3.getParameterTypes()));
     }
+
+
+    public void testGetFieldValue() throws Exception
+    {
+        Grandchild obj1 = new Grandchild(1, 2, 3, 4, 5);
+        assertEquals("parentField",         Integer.valueOf(1),     ClassUtil.getFieldValue(obj1, "parentField",     Integer.TYPE));
+        assertEquals("childField",          Integer.valueOf(3),     ClassUtil.getFieldValue(obj1, "childField",      Integer.TYPE));
+        assertEquals("grandchildField",     Integer.valueOf(4),     ClassUtil.getFieldValue(obj1, "grandchildField", Integer.TYPE));
+        assertEquals("fieldToBeShadowed",   Integer.valueOf(5),     ClassUtil.getFieldValue(obj1, "fieldToBeShadowed", Integer.TYPE));
+
+        Child obj2 = new Child(1, 2, 3);
+        assertEquals("parentField",         Integer.valueOf(1),     ClassUtil.getFieldValue(obj2, "parentField",     Integer.TYPE));
+        assertEquals("childField",          Integer.valueOf(3),     ClassUtil.getFieldValue(obj2, "childField",      Integer.TYPE));
+        assertEquals("fieldToBeShadowed",   Integer.valueOf(2),     ClassUtil.getFieldValue(obj2, "fieldToBeShadowed", Integer.TYPE));
+
+        try
+        {
+            ClassUtil.getFieldValue(obj2, "grandchildField", Integer.TYPE);
+            fail("did not throw when retrieving non-existent field");
+        }
+        catch (NoSuchFieldException ex)
+        {
+            String message = ex.getMessage();
+            assertTrue("exception message includes field name (was: \"" + message + "\")",
+                       message.contains("grandchildField"));
+            assertTrue("exception message includes class name (was: \"" + message + "\")",
+                       message.contains("Child"));
+        }
+    }
+
 }


=====================================
src/test/java/net/sf/kdgcommons/sql/TestJDBCUtil.java
=====================================
@@ -14,14 +14,18 @@
 
 package net.sf.kdgcommons.sql;
 
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
 import junit.framework.TestCase;
 
+import net.sf.kdgcommons.collections.CollectionUtil;
 import net.sf.kdgcommons.test.ExceptionMock;
+import net.sf.kdgcommons.test.SelfMock;
 import net.sf.kdgcommons.test.SimpleMock;
 
 
@@ -31,22 +35,247 @@ import net.sf.kdgcommons.test.SimpleMock;
  */
 public class TestJDBCUtil extends TestCase
 {
-
     public TestJDBCUtil(String testName)
     {
         super(testName);
     }
 
-
 //----------------------------------------------------------------------------
 //  Support Code
 //----------------------------------------------------------------------------
 
+    private final static List<? extends Map<Object,Object>> SAMPLE_DATA = Arrays.asList(
+        CollectionUtil.asMap("foo", "123",  "argle", Integer.valueOf(123)),
+        CollectionUtil.asMap("foo", "baz",  "argle", Integer.valueOf(456)),
+        CollectionUtil.asMap("foo", null,   "argle", Integer.valueOf(789))
+    );
+
+    private final static String[] SAMPLE_DATA_COLNAMES = new String[] { "foo", "argle" };
+
+
+    private static class MockConnection
+    extends SelfMock<Connection>
+    {
+        public boolean isOpen = true;
+        public String lastPrepareSql;
+        public MockPreparedStatement lastPrepareMock;
+
+        public MockConnection()
+        {
+            super(Connection.class);
+        }
+
+        @SuppressWarnings("unused")
+        public void close()
+        {
+            isOpen = false;
+        }
+
+        @SuppressWarnings("unused")
+        public PreparedStatement prepareStatement(String sql)
+        {
+            lastPrepareSql = sql;
+            lastPrepareMock = new MockPreparedStatement();
+            return lastPrepareMock.getInstance();
+        }
+    }
+
+
+    private static class MockPreparedStatement
+    extends SelfMock<PreparedStatement>
+    {
+        // these can be changed if necessary
+        public String[] queryColumnNames = SAMPLE_DATA_COLNAMES;
+        public List<? extends Map<Object,Object>> queryData = SAMPLE_DATA;
+        public int rowsUpdated = 1;
+
+        // note: first element will always be null; objects are stored
+        //       at the index specified in the call
+        public ArrayList<Object> parameters = new ArrayList<Object>();
+        public boolean isOpen = true;
+        public MockResultSet lastResultMock;
+
+        public MockPreparedStatement()
+        {
+            super(PreparedStatement.class);
+        }
+
+        @SuppressWarnings("unused")
+        public void close()
+        {
+            isOpen = false;
+        }
+
+        @SuppressWarnings("unused")
+        public int executeUpdate()
+        {
+            return rowsUpdated;
+        }
+
+        @SuppressWarnings("unused")
+        public ResultSet executeQuery()
+        {
+            lastResultMock = new MockResultSet(queryColumnNames, queryData);
+            return lastResultMock.getInstance();
+        }
+
+        @SuppressWarnings("unused")
+        public void setObject(int idx, Object obj)
+        {
+            if (parameters.size() > idx)
+                parameters.set(idx, obj);
+            else
+            {
+                while (parameters.size() < idx)
+                    parameters.add(null);
+                parameters.add(obj);
+            }
+        }
+    }
+
+
+    private static class MockResultSet
+    extends SelfMock<ResultSet>
+    {
+        private String[] columnNames;
+
+        public int getMetaDataInvocationCount;
+        public int nextInvocationCount;
+        public boolean isOpen = true;
+
+        private Iterator<? extends Map<Object,Object>> rowItx;
+        private Map<Object,Object> currentRow;
+
+        public MockResultSet(String[] columnNames, List<? extends Map<Object,Object>> data)
+        {
+            super(ResultSet.class);
+            this.columnNames = columnNames;
+            this.rowItx = data.iterator();
+        }
+
+        @SuppressWarnings("unused")
+        public void close()
+        {
+            isOpen = false;
+        }
+
+        @SuppressWarnings("unused")
+        public ResultSetMetaData getMetaData()
+        {
+            getMetaDataInvocationCount++;
+            return new MockResultSetMetaData(columnNames).getInstance();
+        }
+
+        @SuppressWarnings("unused")
+        public boolean next()
+        {
+            nextInvocationCount++;
+            if (rowItx.hasNext())
+            {
+                currentRow = rowItx.next();
+                return true;
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        @SuppressWarnings("unused")
+        public Object getObject(int idx)
+        {
+            return currentRow.get(columnNames[idx - 1]);
+        }
+    }
+
+
+    private static class MockResultSetMetaData
+    extends SelfMock<ResultSetMetaData>
+    {
+        private String[] columnNames;
+
+        public MockResultSetMetaData(String[] columnNames)
+        {
+            super(ResultSetMetaData.class);
+            this.columnNames = columnNames;
+        }
+
+        @SuppressWarnings("unused")
+        public int getColumnCount()
+        {
+            return columnNames.length;
+        }
+
+        @SuppressWarnings("unused")
+        public String getColumnName(int idx)
+        {
+            return columnNames[idx - 1];
+        }
+    }
+
 
 //----------------------------------------------------------------------------
 //  Test cases
 //----------------------------------------------------------------------------
 
+    public void testExecuteQuery() throws Exception
+    {
+        final String sql = "select * from foo where bar = ?";
+
+        MockConnection cxtMock = new MockConnection();
+        List<? extends Map<String,Object>> result = JDBCUtil.executeQuery(cxtMock.getInstance(), sql, "baz");
+
+        assertEquals("SQL",                 sql,                        cxtMock.lastPrepareSql);
+        assertEquals("parameters",          Arrays.asList(null, "baz"), cxtMock.lastPrepareMock.parameters);
+        assertEquals("results",             SAMPLE_DATA,                result);
+        assertTrue("connection still open",                             cxtMock.isOpen);
+        assertFalse("statment not open",                                cxtMock.lastPrepareMock.isOpen);
+        assertFalse("resultset not open",                               cxtMock.lastPrepareMock.lastResultMock.isOpen);
+    }
+
+
+    public void testExecuteUpdate() throws Exception
+    {
+        final String sql = "insert into foo values(?, ?)";
+
+        MockConnection cxtMock = new MockConnection();
+        int count = JDBCUtil.executeUpdate(cxtMock.getInstance(), sql, "bar", "baz");
+
+        assertEquals("SQL",                 sql,                                cxtMock.lastPrepareSql);
+        assertEquals("parameters",          Arrays.asList(null, "bar", "baz"),  cxtMock.lastPrepareMock.parameters);
+        assertEquals("results",             1,                                  count);
+        assertTrue("connection still open",                                     cxtMock.isOpen);
+        assertFalse("statment not open",                                        cxtMock.lastPrepareMock.isOpen);
+    }
+
+
+    public void testPrepare() throws Exception
+    {
+        final String sql = "select * from foo where bar = ?";
+
+        MockConnection cxtMock = new MockConnection();
+        PreparedStatement stmt = JDBCUtil.prepare(cxtMock.getInstance(), sql, "baz");
+
+        assertNotNull("created statement",                              stmt);
+        assertEquals("SQL",                 sql,                        cxtMock.lastPrepareSql);
+        assertEquals("parameters",          Arrays.asList(null, "baz"), cxtMock.lastPrepareMock.parameters);
+        assertTrue("statement open",                                    cxtMock.lastPrepareMock.isOpen);
+        assertTrue("connection still open",                             cxtMock.isOpen);
+    }
+
+
+    public void testRetrieve() throws Exception
+    {
+        MockResultSet mock = new MockResultSet(SAMPLE_DATA_COLNAMES, SAMPLE_DATA);
+        List<Map<String,Object>> result = JDBCUtil.retrieve(mock.getInstance());
+
+        assertEquals("result equivalent to source data",    SAMPLE_DATA,   result);
+        assertEquals("getMetaData() invocation count",      1,      mock.getMetaDataInvocationCount);
+        assertEquals("next() invocation count",             4,      mock.nextInvocationCount);
+        assertTrue("resultSet still open",                          mock.isOpen);
+    }
+
+
     public void testCloseQuietly()
     throws Exception
     {


=====================================
src/test/java/net/sf/kdgcommons/test/TestNumericAsserts.java
=====================================
@@ -26,26 +26,125 @@ public class TestNumericAsserts extends TestCase
         NumericAsserts.assertApproximate(100, 101, 1);
         NumericAsserts.assertApproximate(100,  99, 1);
 
-        AssertionFailedError last = null;
+        // note: we can't use fail() inside the try block (because it throws
+        //       AssertionFailedError) so must capture the exception and
+        //       assert on it afterward
+        AssertionFailedError lastAssertionResult;
+
+        try
+        {
+            NumericAsserts.assertApproximate("example", 100, 98, 1);
+            lastAssertionResult = null;
+        }
+        catch (AssertionFailedError ee)
+        {
+            lastAssertionResult = ee;
+        }
+        assertNotNull("did not assert for < delta %",   lastAssertionResult);
+        assertTrue("message contained passed title",    lastAssertionResult.getMessage().contains("example: "));
+        assertTrue("message identified original value", lastAssertionResult.getMessage().contains("98"));
+        assertTrue("message identified expected low",   lastAssertionResult.getMessage().contains("99"));
+        assertTrue("message identified expected high",  lastAssertionResult.getMessage().contains("101"));
+
+        try
+        {
+            NumericAsserts.assertApproximate("example", 100, 102, 1);
+            lastAssertionResult = null;
+        }
+        catch (AssertionFailedError ee)
+        {
+            lastAssertionResult = ee;
+        }
+        assertNotNull("did not assert for > delta %",   lastAssertionResult);
+        assertTrue("message contained passed title",    lastAssertionResult.getMessage().contains("example: "));
+        assertTrue("message identified original value", lastAssertionResult.getMessage().contains("102"));
+        assertTrue("message identified expected low",   lastAssertionResult.getMessage().contains("99"));
+        assertTrue("message identified expected high",  lastAssertionResult.getMessage().contains("101"));
+    }
+
+
+    public void testAssertApproximateLong() throws Exception
+    {
+        NumericAsserts.assertApproximate(100L, 100L, 0);
+        NumericAsserts.assertApproximate(100L, 101L, 1);
+        NumericAsserts.assertApproximate(100L,  99L, 1);
+
+        // note: we can't use fail() inside the try block (because it throws
+        //       AssertionFailedError) so must capture the exception and
+        //       assert on it afterward
+        AssertionFailedError lastAssertionResult;
+
         try
         {
-            NumericAsserts.assertApproximate(100, 98, 1);
+            NumericAsserts.assertApproximate("example", 100L, 98L, 1);
+            lastAssertionResult = null;
         }
         catch (AssertionFailedError ee)
         {
-            last = ee;
+            lastAssertionResult = ee;
         }
-        assertNotNull("did not assert for < delta %", last);
+        assertNotNull("did not assert for < delta %",   lastAssertionResult);
+        assertTrue("message contained passed title",    lastAssertionResult.getMessage().contains("example: "));
+        assertTrue("message identified original value", lastAssertionResult.getMessage().contains("98"));
+        assertTrue("message identified expected low",   lastAssertionResult.getMessage().contains("99"));
+        assertTrue("message identified expected high",  lastAssertionResult.getMessage().contains("101"));
 
         try
         {
-            NumericAsserts.assertApproximate(100, 102, 1);
+            NumericAsserts.assertApproximate("example", 100L, 102L, 1);
+            lastAssertionResult = null;
         }
         catch (AssertionFailedError ee)
         {
-            last = ee;
+            lastAssertionResult = ee;
         }
-        assertNotNull("did not assert for > delta %", last);
+        assertNotNull("did not assert for > delta %",   lastAssertionResult);
+        assertTrue("message contained passed title",    lastAssertionResult.getMessage().contains("example: "));
+        assertTrue("message identified original value", lastAssertionResult.getMessage().contains("102"));
+        assertTrue("message identified expected low",   lastAssertionResult.getMessage().contains("99"));
+        assertTrue("message identified expected high",  lastAssertionResult.getMessage().contains("101"));
     }
 
+
+    public void testAssertApproximateDouble() throws Exception
+    {
+        NumericAsserts.assertApproximate(100.0, 100.0, 0);
+        NumericAsserts.assertApproximate(100.0, 101.0, 1);
+        NumericAsserts.assertApproximate(100.0,  99.0, 1);
+
+        // note: we can't use fail() inside the try block (because it throws
+        //       AssertionFailedError) so must capture the exception and
+        //       assert on it afterward
+        AssertionFailedError lastAssertionResult;
+
+        try
+        {
+            NumericAsserts.assertApproximate("example", 100.0, 98.0, 1);
+            lastAssertionResult = null;
+        }
+        catch (AssertionFailedError ee)
+        {
+            lastAssertionResult = ee;
+        }
+        assertNotNull("did not assert for < delta %",   lastAssertionResult);
+        assertTrue("message contained passed title",    lastAssertionResult.getMessage().contains("example: "));
+        assertTrue("message identified original value", lastAssertionResult.getMessage().contains("98"));
+        assertTrue("message identified expected low",   lastAssertionResult.getMessage().contains("99"));
+        assertTrue("message identified expected high",  lastAssertionResult.getMessage().contains("101"));
+
+        try
+        {
+            NumericAsserts.assertApproximate("example", 100.0, 102.0, 1);
+            lastAssertionResult = null;
+        }
+        catch (AssertionFailedError ee)
+        {
+            lastAssertionResult = ee;
+        }
+        assertNotNull("did not assert for > delta %",   lastAssertionResult);
+        assertTrue("message contained passed title",    lastAssertionResult.getMessage().contains("example: "));
+        assertTrue("message identified original value", lastAssertionResult.getMessage().contains("102"));
+        assertTrue("message identified expected low",   lastAssertionResult.getMessage().contains("99"));
+        assertTrue("message identified expected high",  lastAssertionResult.getMessage().contains("101"));
+    }
 }


=====================================
src/test/java/net/sf/kdgcommons/test/TestSelfMock.java
=====================================
@@ -0,0 +1,113 @@
+// Copyright Keith D Gregory
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.sf.kdgcommons.test;
+
+import junit.framework.TestCase;
+
+
+public class TestSelfMock extends TestCase
+{
+    // note that we don't implement all methods of the interface
+    public static class TestMock
+    extends SelfMock<CharSequence>
+    {
+        public TestMock()
+        {
+            super(CharSequence.class);
+        }
+
+        // tests normal invocation -- can be called with different arguments
+        public char charAt(int index)
+        {
+            return (char)('0' + (index % 10));
+        }
+
+        // tests invocation exceptions
+        public int length()
+        {
+            throw new IllegalStateException("testing");
+        }
+
+    }
+
+
+    public void testNormalOperation() throws Exception
+    {
+        TestMock mock = new TestMock();
+        CharSequence instance = mock.getInstance();
+
+        assertEquals("count before invocations", 0, mock.getInvocationCount("charAt"));
+
+        // two invocations so that we can verify history
+
+        assertEquals('3', instance.charAt(3));
+        assertEquals('5', instance.charAt(15));
+
+        assertEquals("count after invocations",     2,                      mock.getInvocationCount("charAt"));
+        assertEquals("argument count, call 0",      1,                      mock.getInvocationArgs("charAt", 0).length);
+        assertEquals("argument value, call 0",      Integer.valueOf(3),     mock.getInvocationArgs("charAt", 0)[0]);
+        assertEquals("as-type value, call 0",       3,                      mock.getInvocationArg("charAt", 0, 0, Integer.class).intValue());
+        assertEquals("argument count, call 1",      1,                      mock.getInvocationArgs("charAt", 1).length);
+        assertEquals("argument value, call 1",      Integer.valueOf(15),    mock.getInvocationArgs("charAt", 1)[0]);
+        assertEquals("as-type value, call 1",       15,                     mock.getInvocationArg("charAt", 1, 0, Integer.class).intValue());
+
+        assertEquals("argument to most recent call", Integer.valueOf(15),   mock.getMostRecentInvocationArg("charAt", 0, Integer.class));
+    }
+
+
+    public void testExceptionInMock() throws Exception
+    {
+        TestMock mock = new TestMock();
+        CharSequence instance = mock.getInstance();
+
+        try
+        {
+            instance.length();
+            fail("successful invocation of method that was supposed to throw");
+        }
+        catch (IllegalStateException ex)
+        {
+            assertEquals("count incremented even though method throws", 1,      mock.getInvocationCount("length"));
+            assertEquals("history recorded even though method throws",  null,   mock.getInvocationArgs("length", 0));
+        }
+    }
+
+
+    public void testMissingMethod() throws Exception
+    {
+        TestMock mock = new TestMock();
+        CharSequence instance = mock.getInstance();
+
+        try
+        {
+            instance.subSequence(1, 10);
+            fail("successful invocation of method that doesn't exist");
+        }
+        catch (UnsupportedOperationException ex)
+        {
+            assertEquals("count incremented even though method doesn't exist", 1, mock.getInvocationCount("subSequence"));
+        }
+    }
+
+
+    public void testInvokeInheritedFunctions() throws Exception
+    {
+        TestMock mock = new TestMock() { /* nothing new here */ };
+        CharSequence instance = mock.getInstance();
+
+        assertEquals('0', instance.charAt(10));
+    }
+
+}


=====================================
src/test/java/net/sf/kdgcommons/test/TestStringAsserts.java
=====================================
@@ -20,6 +20,50 @@ import junit.framework.TestCase;
 
 public class TestStringAsserts extends TestCase
 {
+    public void testAsserNotEmpty() throws Exception
+    {
+        StringAsserts.assertNotEmpty("this succeeds");
+
+        AssertionFailedError fail1 = null;
+        try
+        {
+            StringAsserts.assertNotEmpty(null);
+        }
+        catch (AssertionFailedError ex)
+        {
+            fail1 = ex;
+            assertEquals("expected exception to indicate value was null", "expected not-empty, was null", ex.getMessage());
+        }
+        assertNotNull("null did not cause assertion failure", fail1);
+
+        AssertionFailedError fail2 = null;
+        try
+        {
+            StringAsserts.assertNotEmpty("");
+        }
+        catch (AssertionFailedError ex)
+        {
+            fail2 = ex;
+            assertEquals("expected exception to indicate value was empty", "expected not-empty", ex.getMessage());
+        }
+        assertNotNull("empty string did not cause assertion failure", fail2);
+
+        AssertionFailedError fail3 = null;
+        try
+        {
+            StringAsserts.assertNotEmpty("example", "");
+        }
+        catch (AssertionFailedError ex)
+        {
+            fail3 = ex;
+            assertEquals("expected exception to start with user message", "example: expected not-empty", ex.getMessage());
+        }
+        assertNotNull("null did not cause assertion failure", fail3);
+
+
+    }
+
+
     public void testAssertSubstringCount0() throws Exception
     {
         StringAsserts.assertSubstringCount("foo", "bar", 0);


=====================================
src/test/java/net/sf/kdgcommons/util/TestCounters.java
=====================================
@@ -37,21 +37,24 @@ extends TestCase
     {
         Counters<String> counters = new Counters<String>();
 
-        assertEquals("object get from new instance",        null,               counters.get("foo"));
-        assertEquals("primitive get from new instance",     0,                  counters.getLong("foo"));
+        assertEquals("object get from new instance",            null,               counters.get("foo"));
+        assertEquals("primitive long get from new instance",    0,                  counters.getLong("foo"));
+        assertEquals("primitive int get from new instance",     0,                  counters.getInt("foo"));
 
-        assertEquals("return from put, new mapping",        null,               counters.put("foo", Long.valueOf(12)));
+        assertEquals("return from put, new mapping",            null,               counters.put("foo", Long.valueOf(12)));
 
-        assertEquals("object get, existing mapping",        Long.valueOf(12),   counters.get("foo"));
-        assertEquals("primitive get, existing mapping",     12,                 counters.getLong("foo"));
+        assertEquals("object get, existing mapping",            Long.valueOf(12),   counters.get("foo"));
+        assertEquals("primitive long get, existing mapping",    12,                 counters.getLong("foo"));
+        assertEquals("primitive int get, existing mapping",     12,                 counters.getInt("foo"));
 
         counters.putLong("foo", 13);
-        assertEquals("primitive get after primitive put",   13,                 counters.getLong("foo"));
 
-        assertEquals("return value from remove()",          Long.valueOf(13),   counters.remove("foo"));
+        assertEquals("primitive get after primitive put",       13,                 counters.getLong("foo"));
 
-        assertEquals("object get after remove()",           null,               counters.get("foo"));
-        assertEquals("primitive get after remove()",        0,                  counters.getLong("foo"));
+        assertEquals("return value from remove()",              Long.valueOf(13),   counters.remove("foo"));
+
+        assertEquals("object get after remove()",               null,               counters.get("foo"));
+        assertEquals("primitive get after remove()",            0,                  counters.getLong("foo"));
     }
 
 
@@ -60,19 +63,23 @@ extends TestCase
         Counters<String> counters = new Counters<String>();
 
         assertEquals("increment creates counter",           1,                  counters.increment("foo"));
-        assertEquals("post-increment primitive get",        1,                  counters.getLong("foo"));
-        assertEquals("post-increment object get",           Long.valueOf(1),    counters.get("foo"));
+        assertEquals("post-create primitive long get",      1,                  counters.getLong("foo"));
+        assertEquals("post-create primitive int get",       1,                  counters.getInt("foo"));
+        assertEquals("post-create object get",              Long.valueOf(1),    counters.get("foo"));
 
-        assertEquals("increment of existing counte",        2,                  counters.increment("foo"));
-        assertEquals("post-increment primitive get",        2,                  counters.getLong("foo"));
+        assertEquals("increment of existing counter",       2,                  counters.increment("foo"));
+        assertEquals("post-increment primitive long get",   2,                  counters.getLong("foo"));
+        assertEquals("post-increment primitive int get",    2,                  counters.getInt("foo"));
         assertEquals("post-increment object get",           Long.valueOf(2),    counters.get("foo"));
 
         assertEquals("decrement creates counter",           -1,                 counters.decrement("bar"));
-        assertEquals("post-decrement primitive get",        -1,                 counters.getLong("bar"));
-        assertEquals("post-decrement object get",           Long.valueOf(-1),   counters.get("bar"));
+        assertEquals("post-create primitive long get",      -1,                 counters.getLong("bar"));
+        assertEquals("post-create primitive int get",       -1,                 counters.getInt("bar"));
+        assertEquals("post-create object get",              Long.valueOf(-1),   counters.get("bar"));
 
         assertEquals("decrement of existing counter",       -2,                 counters.decrement("bar"));
-        assertEquals("post-decrement primitive get",        -2,                 counters.getLong("bar"));
+        assertEquals("post-decrement primitive long get",   -2,                 counters.getLong("bar"));
+        assertEquals("post-decrement primitive int get",    -2,                 counters.getInt("bar"));
         assertEquals("post-decrement object get",           Long.valueOf(-2),   counters.get("bar"));
     }
 
@@ -129,6 +136,7 @@ extends TestCase
     }
 
 
+    @SuppressWarnings("unlikely-arg-type")
     public void testContains() throws Exception
     {
         Counters<String> counters = new Counters<String>();
@@ -220,4 +228,25 @@ extends TestCase
     }
 
 
+    public void testToString() throws Exception
+    {
+        Counters<String> counters = new Counters<String>();
+
+        assertEquals("empty counters", "[]", counters.toString());
+
+        counters.put("foo", 12L);
+        assertEquals("single element", "[foo: 12]", counters.toString());
+
+        counters.put("bar", 13L);
+
+        // note: since the map is hashed, the order of elements may change depending
+        //       on Java version, so we'll accept either order
+        String value = counters.toString();
+        boolean order1 = value.equals("[foo: 12, bar: 13]");
+        boolean order2 = value.equals("[bar: 13, foo: 12]");
+        assertTrue("multiple elements (was: " + value + ")", order1 || order2);
+    }
+
+
+
 }



View it on GitLab: https://salsa.debian.org/java-team/kdgcommons-java/-/commit/2ca9bfea7c63763440bede2d56a4c43bbf832619

-- 
View it on GitLab: https://salsa.debian.org/java-team/kdgcommons-java/-/commit/2ca9bfea7c63763440bede2d56a4c43bbf832619
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20250202/f5d794e6/attachment.htm>


More information about the pkg-java-commits mailing list