--- /dev/null
+/*
+ * Copyright 2005-2010 the original author or authors.
+ *
+ * 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 org.wamblee.cache;
+
+/**
+ * Utility class to deal with recomputation of a certain value. The goal is to
+ * have only one thread at a time compute the value while other threads that
+ * simulateneously detect recomputation continue with the old value.
+ *
+ *
+ * @author Erik Brakkee
+ *
+ * @param <T>
+ */
+public class ComputedValue<T> {
+
+ /**
+ * Computation
+ *
+ * @param <T>
+ * Type of object to compute.
+ */
+ public static interface Computation<T> {
+ /**
+ * Checks whether the object is out of date. This will be invoked while
+ * holding the lock passed at construction of the compute guard.
+ *
+ * Any runtime exceptions thrown are passed back through the
+ * {@link ComputedValue#get()}.
+ *
+ * @return True iff recomputation is necessary.
+ */
+ boolean isOutOfDate();
+
+ /**
+ * Computes the object. This will be invoked while <em>not</em> holding
+ * the lock passed at construction of the compute guard. It is
+ * guaranteed that per ComputeGuard, no concurrent calls to compute()
+ * are done.
+ *
+ * Any runtime exceptions thrown are passed back through the
+ * {@link ComputedValue#get()}.
+ *
+ * @return Computed object.
+ */
+ T compute();
+ }
+
+ private Object lock;
+ private Computation<T> computedValue;
+
+ private Boolean busy;
+ private T value;
+
+ /**
+ * Constructs the compute guard
+ *
+ * @param aLock
+ * Lock to use during computation and to guard the value.
+ * @param aComputation
+ * Computation to use.
+ */
+ public ComputedValue(Object aLock, Computation aComputation) {
+ lock = aLock;
+ computedValue = aComputation;
+ busy = false;
+ value = null;
+ }
+
+ /**
+ * Triggers computation of the value (if no other thread is currently
+ * computing the value).
+ */
+ public void compute() {
+ synchronized (this) {
+ if (busy) {
+ return; // another thread is already taking care of it.
+ }
+ busy = true;
+ }
+ try {
+ T newvalue = computedValue.compute();
+ set(newvalue);
+ } finally {
+ synchronized (this) {
+ busy = false;
+ }
+ }
+ }
+
+ /**
+ * Gets the current value of the object, recomputing it if the object is out
+ * of date. This method ensures that only one thread at a time will do
+ * recomputations.
+ *
+ * @return Current value.
+ */
+ public T get() {
+ boolean mustCompute = false;
+ synchronized (lock) {
+ if (computedValue.isOutOfDate()) {
+ mustCompute = true;
+ }
+ }
+ if (mustCompute) {
+ compute();
+ }
+ synchronized (lock) {
+ return value;
+ }
+ }
+
+ /**
+ * Gets the currently cached value. No recomputation is performed.
+ * @return Cached value.
+ */
+ public T getCached() {
+ synchronized(lock) {
+ return value;
+ }
+
+ }
+
+ /**
+ * Sets the value explicitly.
+ * @param aValue value to set.
+ */
+ public void set(T aValue) {
+ synchronized(lock) {
+ value = aValue;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2005-2010 the original author or authors.
+ *
+ * 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 org.wamblee.cache;
+
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.wamblee.cache.ComputedValue.Computation;
+
+public class ComputedValueTest extends TestCase {
+
+ private Computation<Integer> computation;
+ private ComputedValue<Integer> guard;
+
+
+ @Override
+ protected void setUp() throws Exception {
+ computation = mock(Computation.class);
+ }
+
+ public void testComputeOutOfDate() {
+ initGuard();
+ }
+
+ private void initGuard() {
+ guard = new ComputedValue<Integer>(this, computation);
+
+ assertNull(guard.getCached());
+
+ when(computation.isOutOfDate()).thenReturn(true);
+ when(computation.compute()).thenReturn(10);
+
+ int value = guard.get();
+ assertEquals(10, value);
+ verify(computation).compute();
+ reset(computation);
+ }
+
+ public void testGetCached() {
+ initGuard();
+ assertEquals(10, (int)guard.getCached());
+ verifyNoMoreInteractions(computation);
+ }
+
+ public void testNoComputationWhenNotOutOfDate() {
+ initGuard();
+ when(computation.isOutOfDate()).thenReturn(false);
+ assertEquals(10, (int) guard.get());
+ verify(computation, never()).compute();
+ }
+
+ public void testOnlyOneConcurrentComputation() throws Exception {
+ initGuard();
+ final int computeTime = 500;
+ when(computation.isOutOfDate()).thenReturn(true);
+ when(computation.compute()).thenAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock aInvocation)
+ throws Throwable {
+ Thread.sleep(computeTime);
+ return 100;
+ }
+ });
+
+ final List<Integer> results = new ArrayList<Integer>();
+
+ Runnable task = new Runnable() {
+ @Override
+ public void run() {
+ int res = guard.get();
+ results.add(res);
+ }
+ };
+
+ // concurrent computation in two threads.
+ Thread t1 = new Thread(task);
+ Thread t2 = new Thread(task);
+ t1.start();
+ Thread.sleep(computeTime / 2);
+ // second task will return old value 10 because first one is still busy.
+ t2.start();
+ t1.join();
+ t2.join();
+ assertEquals(2, results.size());
+ assertEquals(10, (int) results.get(0));
+ assertEquals(100, (int) results.get(1));
+ verify(computation, times(2)).isOutOfDate();
+ }
+
+ public void testExceptionWhileComputing() {
+ initGuard();
+ when(computation.isOutOfDate()).thenReturn(true);
+ when(computation.compute()).thenThrow(new RuntimeException("xx"));
+ try {
+ guard.get();
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("xx", e.getMessage());
+ }
+ }
+
+ public void testExceptionWhileCheckingOutOfDate() {
+ initGuard();
+ when(computation.isOutOfDate()).thenThrow(new RuntimeException("xx"));
+ try {
+ guard.get();
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("xx", e.getMessage());
+ }
+ }
+}