LANG-740: Implementation of a Memomizer (closes #203)

changes suggested in https://github.com/apache/commons-lang/pull/80:
- tabs to spaces
- use @Override
- remove unused variables
- other minimal clean-ups


Project: http://git-wip-us.apache.org/repos/asf/commons-lang/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-lang/commit/9f89fd46
Tree: http://git-wip-us.apache.org/repos/asf/commons-lang/tree/9f89fd46
Diff: http://git-wip-us.apache.org/repos/asf/commons-lang/diff/9f89fd46

Branch: refs/heads/master
Commit: 9f89fd4626bbb4e34a905835e397bcffbad59307
Parents: c0c7112
Author: pascalschumacher <pascalschumac...@gmx.net>
Authored: Fri Oct 28 21:55:22 2016 +0200
Committer: pascalschumacher <pascalschumac...@gmx.net>
Committed: Sun Nov 13 17:51:00 2016 +0100

----------------------------------------------------------------------
 src/changes/changes.xml                         |   1 +
 .../commons/lang3/concurrent/Computable.java    |  26 ++-
 .../commons/lang3/concurrent/Memoizer.java      | 228 +++++++++++--------
 .../commons/lang3/concurrent/MemoizerTest.java  | 202 ++++++++--------
 4 files changed, 252 insertions(+), 205 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/changes/changes.xml
----------------------------------------------------------------------
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index d0d86fc..ecfa481 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -55,6 +55,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action issue="LANG-1070" type="fix" dev="pschumacher" due-to="Paul 
Pogonyshev">ArrayUtils#add confusing example in javadoc</action>
     <action issue="LANG-1271" type="fix" dev="pschumacher" due-to="Pierre 
Templier">StringUtils#isAnyEmpty and #isAnyBlank should return false for an 
empty array</action>
     <action issue="LANG-1155" type="fix" dev="pschumacher" due-to="Saif Asif, 
Thiago Andrade">Add StringUtils#unwrap</action>
+    <action issue="LANG-740" type="add" dev="pschumacher" due-to="James 
Sawle">Implementation of a Memomizer</action>
     <action issue="LANG-1258" type="add" dev="pschumacher" due-to="IG, 
Grzegorz Rożniecki">Add ArrayUtils#toStringArray method</action>
     <action issue="LANG-1160" type="add" dev="kinow">StringUtils#abbreviate 
should support 'custom ellipses' parameter</action>
     <action issue="LANG-1270" type="add" dev="pschumacher" due-to="Pierre 
Templier">Add StringUtils#isAnyNotEmpty and #isAnyNotBlank</action>

http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/main/java/org/apache/commons/lang3/concurrent/Computable.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/lang3/concurrent/Computable.java 
b/src/main/java/org/apache/commons/lang3/concurrent/Computable.java
index 7934330..d9e5468 100644
--- a/src/main/java/org/apache/commons/lang3/concurrent/Computable.java
+++ b/src/main/java/org/apache/commons/lang3/concurrent/Computable.java
@@ -21,17 +21,21 @@ package org.apache.commons.lang3.concurrent;
  * <p/>
  * <p>This interface allows for wrapping a calculation into a class so that it 
maybe passed around an application.</p>
  *
- * @param <A> the type of the input to the calculation
- * @param <V> the type of the output of the calculation
+ * @param <I> the type of the input to the calculation
+ * @param <O> the type of the output of the calculation
+ * 
+ * @since 3.6
  */
-public interface Computable<A, V> {
+public interface Computable<I, O> {
 
-       /**
-        * This method carries out the given operation with the provided 
argument.
-        *
-        * @param arg the argument for the calculation
-        * @return the result of the calculation
-        * @throws InterruptedException thrown if the calculation is interrupted
-        */
-       V compute(final A arg) throws InterruptedException;
+    /**
+     * This method carries out the given operation with the provided argument.
+     *
+     * @param arg
+     *            the argument for the calculation
+     * @return the result of the calculation
+     * @throws InterruptedException
+     *             thrown if the calculation is interrupted
+     */
+    O compute(final I arg) throws InterruptedException;
 }

http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java 
b/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java
index 5682b0d..d0fe406 100644
--- a/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java
+++ b/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java
@@ -25,109 +25,141 @@ import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
 
 /**
- * <p>Definition of an interface for a wrapper around a calculation that takes 
a single parameter and returns a result.
- * The results for the calculation will be cached for future requests.</p>
- * <p/>
- * <p>This is not a fully functional cache, there is no way of limiting or 
removing results once they have been generated.
- * However, it is possible to get the implementation to regenerate the result 
for a given parameter, if an error was
- * thrown during the previous calculation, by setting the option during the 
construction of the class. If this is not
- * set the class will return the cached exception.</p>
- * <p/>
- * <p>Thanks should go to Brian Goetz, Tim Peierls and the members of JCP 
JSR-166 Expert Group for coming up with the
- * original implementation of the class. It was also published within Java 
Concurreny in Practice as a sample.</p>
+ * <p>
+ * Definition of an interface for a wrapper around a calculation that takes a
+ * single parameter and returns a result. The results for the calculation will
+ * be cached for future requests.
+ * </p>
+ * <p>
+ * This is not a fully functional cache, there is no way of limiting or 
removing
+ * results once they have been generated. However, it is possible to get the
+ * implementation to regenerate the result for a given parameter, if an error
+ * was thrown during the previous calculation, by setting the option during the
+ * construction of the class. If this is not set the class will return the
+ * cached exception.
+ * </p>
+ * <p>
+ * Thanks should go to Brian Goetz, Tim Peierls and the members of JCP JSR-166
+ * Expert Group for coming up with the original implementation of the class. It
+ * was also published within Java Concurrency in Practice as a sample.
+ * </p>
  *
- * @param <A> the type of the input to the calculation
- * @param <V> the type of the output of the calculation
+ * @param <I>
+ *            the type of the input to the calculation
+ * @param <O>
+ *            the type of the output of the calculation
+ * 
+ * @since 3.6
  */
-public class Memoizer<A, V> implements Computable<A, V> {
-       private final ConcurrentMap<A, Future<V>> cache
-                       = new ConcurrentHashMap<A, Future<V>>();
-       private final Computable<A, V> c;
-       private final boolean recalculate;
+public class Memoizer<I, O> implements Computable<I, O> {
 
-       /**
-        * <p>Constructs a Memoizer for the provided Computable calculation.</p>
-        * <p/>
-        * <p>If a calculation is thrown an exception for any reason, this 
exception will be cached and returned for
-        * all future calls with the provided parameter.</p>
-        *
-        * @param c the computation whose results should be memorized
-        */
-       public Memoizer(Computable<A, V> c) {
-               this(c, false);
-       }
+    private final ConcurrentMap<I, Future<O>> cache = new 
ConcurrentHashMap<>();
+    private final Computable<I, O> computable;
+    private final boolean recalculate;
 
-       /**
-        * <p>Constructs a Memoizer for the provided Computable calculation, 
with the option of whether a Computation
-        * that experiences an error should recalculate on subsequent calls or 
return the same cached exception.</p>
-        *
-        * @param c           the computation whose results should be memorized
-        * @param recalculate determines whether the computation should be 
recalculated on subsequent calls if the previous
-        *                    call failed
-        */
-       public Memoizer(Computable<A, V> c, boolean recalculate) {
-               this.c = c;
-               this.recalculate = recalculate;
-       }
+    /**
+     * <p>
+     * Constructs a Memoizer for the provided Computable calculation.
+     * </p>
+     * <p>
+     * If a calculation is thrown an exception for any reason, this exception
+     * will be cached and returned for all future calls with the provided
+     * parameter.
+     * </p>
+     *
+     * @param computable
+     *            the computation whose results should be memorized
+     */
+    public Memoizer(final Computable<I, O> computable) {
+        this(computable, false);
+    }
 
-       /**
-        * <p>This method will return the result of the calculation and cache 
it, if it has not previously been calculated.</p>
-        * <p/>
-        * <p>This cache will also cache exceptions that occur during the 
computation if the {@code recalculate} parameter is
-        * the constructor was set to {@code false}, or not set. Otherwise, if 
an exception happened on the previous
-        * calculation, the method will attempt again to generate a value.</p>
-        *
-        * @param arg the argument for the calculation
-        * @return the result of the calculation
-        * @throws InterruptedException  thrown if the calculation is 
interrupted
-        * @throws IllegalStateException a wrapper around any checked exception 
that occurs during the computation of the result
-        */
-       public V compute(final A arg) throws InterruptedException, 
IllegalStateException {
-               while (true) {
-                       Future<V> f = cache.get(arg);
-                       if (f == null) {
-                               Callable<V> eval = new Callable<V>() {
-                                       public V call() throws 
InterruptedException {
-                                               return c.compute(arg);
-                                       }
-                               };
-                               FutureTask<V> ft = new FutureTask<V>(eval);
-                               f = cache.putIfAbsent(arg, ft);
-                               if (f == null) {
-                                       f = ft;
-                                       ft.run();
-                               }
-                       }
-                       try {
-                               return f.get();
-                       }
-                       catch (CancellationException e) {
-                               cache.remove(arg, f);
-                       }
-                       catch (ExecutionException e) {
-                               if (recalculate) {
-                                       cache.remove(arg, f);
-                               }
+    /**
+     * <p>
+     * Constructs a Memoizer for the provided Computable calculation, with the
+     * option of whether a Computation that experiences an error should
+     * recalculate on subsequent calls or return the same cached exception.
+     * </p>
+     *
+     * @param computable
+     *            the computation whose results should be memorized
+     * @param recalculate
+     *            determines whether the computation should be recalculated on
+     *            subsequent calls if the previous call failed
+     */
+    public Memoizer(final Computable<I, O> computable, final boolean 
recalculate) {
+        this.computable = computable;
+        this.recalculate = recalculate;
+    }
 
-                               throw launderException(e.getCause());
-                       }
-               }
-       }
+    /**
+     * <p>
+     * This method will return the result of the calculation and cache it, if 
it
+     * has not previously been calculated.
+     * </p>
+     * <p>
+     * This cache will also cache exceptions that occur during the computation
+     * if the {@code recalculate} parameter is the constructor was set to
+     * {@code false}, or not set. Otherwise, if an exception happened on the
+     * previous calculation, the method will attempt again to generate a value.
+     * </p>
+     *
+     * @param arg
+     *            the argument for the calculation
+     * @return the result of the calculation
+     * @throws InterruptedException
+     *             thrown if the calculation is interrupted
+     */
+    @Override
+    public O compute(final I arg) throws InterruptedException {
+        while (true) {
+            Future<O> future = cache.get(arg);
+            if (future == null) {
+                Callable<O> eval = new Callable<O>() {
 
-       /**
-        * <p>This method launders a Throwable to either a RuntimeException, 
Error or any other Exception wrapped
-        * in an IllegalStateException.</p>
-        *
-        * @param t the throwable to laundered
-        * @return a RuntimeException, Error or an IllegalStateException
-        */
-       private RuntimeException launderException(Throwable t) {
-               if (t instanceof RuntimeException) {
-                       return (RuntimeException) t;
-               } else if (t instanceof Error) {
-                       throw (Error) t;
-               } else {
-                       throw new IllegalStateException("Unchecked exception", 
t);
-               }
-       }
+                    @Override
+                    public O call() throws InterruptedException {
+                        return computable.compute(arg);
+                    }
+                };
+                FutureTask<O> futureTask = new FutureTask<>(eval);
+                future = cache.putIfAbsent(arg, futureTask);
+                if (future == null) {
+                    future = futureTask;
+                    futureTask.run();
+                }
+            }
+            try {
+                return future.get();
+            } catch (CancellationException e) {
+                cache.remove(arg, future);
+            } catch (ExecutionException e) {
+                if (recalculate) {
+                    cache.remove(arg, future);
+                }
+
+                throw launderException(e.getCause());
+            }
+        }
+    }
+
+    /**
+     * <p>
+     * This method launders a Throwable to either a RuntimeException, Error or
+     * any other Exception wrapped in an IllegalStateException.
+     * </p>
+     *
+     * @param throwable
+     *            the throwable to laundered
+     * @return a RuntimeException, Error or an IllegalStateException
+     */
+    private RuntimeException launderException(final Throwable throwable) {
+        if (throwable instanceof RuntimeException) {
+            return (RuntimeException) throwable;
+        } else if (throwable instanceof Error) {
+            throw (Error) throwable;
+        } else {
+            throw new IllegalStateException("Unchecked exception", throwable);
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java 
b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java
index aa765f8..ae511c9 100644
--- a/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java
+++ b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java
@@ -1,3 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.lang3.concurrent;
 
 import org.easymock.EasyMockRunner;
@@ -13,100 +29,94 @@ import static org.junit.Assert.fail;
 @RunWith(EasyMockRunner.class)
 public class MemoizerTest {
 
-       @Mock
-       private Computable<Integer, Integer> computable;
-
-       @Test
-       public void testOnlyCallComputableOnceIfDoesNotThrowException() throws 
Exception {
-               Integer input = 1;
-               Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, 
Integer>(computable);
-               expect(computable.compute(input)).andReturn(input);
-               replay(computable);
-
-               assertEquals("Should call computable first time", input, 
memoizer.compute(input));
-               assertEquals("Should not call the computable the second time", 
input, memoizer.compute(input));
-       }
-
-       @Test(expected = IllegalStateException.class)
-       public void testDefaultBehaviourNotToRecalculateExecutionExceptions() 
throws Exception {
-               Integer input = 1;
-               Integer answer = 3;
-               Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, 
Integer>(computable);
-               InterruptedException interruptedException = new 
InterruptedException();
-               
expect(computable.compute(input)).andThrow(interruptedException);
-               replay(computable);
-
-               try {
-                       memoizer.compute(input);
-                       fail();
-               }
-               catch (Throwable ex) {
-                       //Should always be thrown the first time
-               }
-
-               memoizer.compute(input);
-       }
-
-       @Test(expected = IllegalStateException.class)
-       public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
-               Integer input = 1;
-               Integer answer = 3;
-               Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, 
Integer>(computable, false);
-               InterruptedException interruptedException = new 
InterruptedException();
-               
expect(computable.compute(input)).andThrow(interruptedException);
-               replay(computable);
-
-               try {
-                       memoizer.compute(input);
-                       fail();
-               }
-               catch (Throwable ex) {
-                       //Should always be thrown the first time
-               }
-
-               memoizer.compute(input);
-       }
-
-       @Test
-       public void testDoesRecalculateWhenSetToTrue() throws Exception {
-               Integer input = 1;
-               Integer answer = 3;
-               Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, 
Integer>(computable, true);
-               InterruptedException interruptedException = new 
InterruptedException();
-               
expect(computable.compute(input)).andThrow(interruptedException).andReturn(answer);
-               replay(computable);
-
-               try {
-                       memoizer.compute(input);
-                       fail();
-               }
-               catch (Throwable ex) {
-                       //Should always be thrown the first time
-               }
-
-               assertEquals(answer, memoizer.compute(input));
-       }
-
-
-       @Test(expected = RuntimeException.class)
-       public void testWhenComputableThrowsRuntimeException() throws Exception 
{
-               Integer input = 1;
-               Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, 
Integer>(computable);
-               RuntimeException runtimeException = new RuntimeException("Some 
runtime exception");
-               expect(computable.compute(input)).andThrow(runtimeException);
-               replay(computable);
-
-               memoizer.compute(input);
-       }
-
-       @Test(expected = Error.class)
-       public void testWhenComputableThrowsError() throws Exception {
-               Integer input = 1;
-               Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, 
Integer>(computable);
-               Error error = new Error();
-               expect(computable.compute(input)).andThrow(error);
-               replay(computable);
-
-               memoizer.compute(input);
-       }
+    @Mock
+    private Computable<Integer, Integer> computable;
+
+    @Test
+    public void testOnlyCallComputableOnceIfDoesNotThrowException() throws 
Exception {
+        Integer input = 1;
+        Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+        expect(computable.compute(input)).andReturn(input);
+        replay(computable);
+
+        assertEquals("Should call computable first time", input, 
memoizer.compute(input));
+        assertEquals("Should not call the computable the second time", input, 
memoizer.compute(input));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testDefaultBehaviourNotToRecalculateExecutionExceptions() 
throws Exception {
+        Integer input = 1;
+        Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+        InterruptedException interruptedException = new InterruptedException();
+        expect(computable.compute(input)).andThrow(interruptedException);
+        replay(computable);
+
+        try {
+            memoizer.compute(input);
+            fail("Expected Throwable to be thrown!");
+        } catch (Throwable expected) {
+            // Should always be thrown the first time
+        }
+
+        memoizer.compute(input);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
+        Integer input = 1;
+        Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable, 
false);
+        InterruptedException interruptedException = new InterruptedException();
+        expect(computable.compute(input)).andThrow(interruptedException);
+        replay(computable);
+
+        try {
+            memoizer.compute(input);
+            fail("Expected Throwable to be thrown!");
+        } catch (Throwable expected) {
+            // Should always be thrown the first time
+        }
+
+        memoizer.compute(input);
+    }
+
+    @Test
+    public void testDoesRecalculateWhenSetToTrue() throws Exception {
+        Integer input = 1;
+        Integer answer = 3;
+        Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable, true);
+        InterruptedException interruptedException = new InterruptedException();
+        
expect(computable.compute(input)).andThrow(interruptedException).andReturn(answer);
+        replay(computable);
+
+        try {
+            memoizer.compute(input);
+            fail("Expected Throwable to be thrown!");
+        } catch (Throwable expected) {
+            // Should always be thrown the first time
+        }
+
+        assertEquals(answer, memoizer.compute(input));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testWhenComputableThrowsRuntimeException() throws Exception {
+        Integer input = 1;
+        Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+        RuntimeException runtimeException = new RuntimeException("Some runtime 
exception");
+        expect(computable.compute(input)).andThrow(runtimeException);
+        replay(computable);
+
+        memoizer.compute(input);
+    }
+
+    @Test(expected = Error.class)
+    public void testWhenComputableThrowsError() throws Exception {
+        Integer input = 1;
+        Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
+        Error error = new Error();
+        expect(computable.compute(input)).andThrow(error);
+        replay(computable);
+
+        memoizer.compute(input);
+    }
 }

Reply via email to