diff --git a/promise/src/main/java/com/iluwatar/promise/App.java b/promise/src/main/java/com/iluwatar/promise/App.java
index 1315f0927..2b2ae78b4 100644
--- a/promise/src/main/java/com/iluwatar/promise/App.java
+++ b/promise/src/main/java/com/iluwatar/promise/App.java
@@ -29,35 +29,45 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
- *
- *
The Promise object is used for asynchronous computations. A Promise represents an operation that
- * hasn't completed yet, but is expected in the future.
- *
- *
A Promise represents a proxy for a value not necessarily known when the promise is created. It
- * allows you to associate dependent promises to an asynchronous action's eventual success value or
- * failure reason. This lets asynchronous methods return values like synchronous methods: instead of the final
- * value, the asynchronous method returns a promise of having a value at some point in the future.
- *
+ *
+ * The Promise object is used for asynchronous computations. A Promise represents an operation
+ * that hasn't completed yet, but is expected in the future.
+ *
+ *
A Promise represents a proxy for a value not necessarily known when the promise is created. It
+ * allows you to associate dependent promises to an asynchronous action's eventual success value or
+ * failure reason. This lets asynchronous methods return values like synchronous methods: instead
+ * of the final value, the asynchronous method returns a promise of having a value at some point
+ * in the future.
+ *
*
Promises provide a few advantages over callback objects:
*
* Functional composition and error handling
* Prevents callback hell and provides callback aggregation
*
- *
+ *
*
+ * In this application the usage of promise is demonstrated with two examples:
+ *
+ * Count Lines: In this example a file is downloaded and its line count is calculated.
+ * The calculated line count is then consumed and printed on console.
+ * Lowest Character Frequency: In this example a file is downloaded and its lowest frequency
+ * character is found and printed on console. This happens via a chain of promises, we start with
+ * a file download promise, then a promise of character frequency, then a promise of lowest frequency
+ * character which is finally consumed and result is printed on console.
+ *
*
* @see CompletableFuture
*/
public class App {
- private static final String URL = "https://raw.githubusercontent.com/iluwatar/java-design-patterns/Promise/promise/README.md";
+ private static final String DEFAULT_URL = "https://raw.githubusercontent.com/iluwatar/java-design-patterns/Promise/promise/README.md";
private ExecutorService executor;
- private CountDownLatch canStop = new CountDownLatch(2);
-
+ private CountDownLatch stopLatch = new CountDownLatch(2);
+
private App() {
executor = Executors.newFixedThreadPool(2);
}
-
+
/**
* Program entry point
* @param args arguments
@@ -67,28 +77,25 @@ public class App {
public static void main(String[] args) throws InterruptedException, ExecutionException {
App app = new App();
try {
- app.run();
+ app.promiseUsage();
} finally {
app.stop();
}
}
- private void run() throws InterruptedException, ExecutionException {
- promiseUsage();
+ private void promiseUsage() {
+ calculateLineCount();
+
+ calculateLowestFrequencyChar();
}
- private void promiseUsage() {
-
- countLines()
- .then(
- count -> {
- System.out.println("Line count is: " + count);
- taskCompleted();
- }
- );
-
- lowestCharFrequency()
- .then(
+ /*
+ * Calculate the lowest frequency character and when that promise is fulfilled,
+ * consume the result in a Consumer
+ */
+ private void calculateLowestFrequencyChar() {
+ lowestFrequencyChar()
+ .thenAccept(
charFrequency -> {
System.out.println("Char with lowest frequency is: " + charFrequency);
taskCompleted();
@@ -96,49 +103,73 @@ public class App {
);
}
- private Promise lowestCharFrequency() {
- return characterFrequency()
- .then(
- charFrequency -> {
- return Utility.lowestFrequencyChar(charFrequency).orElse(null);
- }
- );
- }
-
- private Promise> characterFrequency() {
- return download(URL)
- .then(
- fileLocation -> {
- return Utility.characterFrequency(fileLocation);
+ /*
+ * Calculate the line count and when that promise is fulfilled, consume the result
+ * in a Consumer
+ */
+ private void calculateLineCount() {
+ countLines()
+ .thenAccept(
+ count -> {
+ System.out.println("Line count is: " + count);
+ taskCompleted();
}
);
}
- private Promise countLines() {
- return download(URL)
- .then(
- fileLocation -> {
- return Utility.countLines(fileLocation);
- }
- );
+ /*
+ * Calculate the character frequency of a file and when that promise is fulfilled,
+ * then promise to apply function to calculate lowest character frequency.
+ */
+ private Promise lowestFrequencyChar() {
+ return characterFrequency()
+ .thenApply(Utility::lowestFrequencyChar);
}
+ /*
+ * Download the file at DEFAULT_URL and when that promise is fulfilled,
+ * then promise to apply function to calculate character frequency.
+ */
+ private Promise> characterFrequency() {
+ return download(DEFAULT_URL)
+ .thenApply(Utility::characterFrequency);
+ }
+
+ /*
+ * Download the file at DEFAULT_URL and when that promise is fulfilled,
+ * then promise to apply function to count lines in that file.
+ */
+ private Promise countLines() {
+ return download(DEFAULT_URL)
+ .thenApply(Utility::countLines);
+ }
+
+ /*
+ * Return a promise to provide the local absolute path of the file downloaded in background.
+ * This is an async method and does not wait until the file is downloaded.
+ */
private Promise download(String urlString) {
Promise downloadPromise = new Promise()
.fulfillInAsync(
() -> {
return Utility.downloadFile(urlString);
- }, executor);
-
+ }, executor)
+ .onError(
+ throwable -> {
+ throwable.printStackTrace();
+ taskCompleted();
+ }
+ );
+
return downloadPromise;
}
private void stop() throws InterruptedException {
- canStop.await();
+ stopLatch.await();
executor.shutdownNow();
}
-
+
private void taskCompleted() {
- canStop.countDown();
+ stopLatch.countDown();
}
}
diff --git a/promise/src/main/java/com/iluwatar/promise/Promise.java b/promise/src/main/java/com/iluwatar/promise/Promise.java
index 7d8a97e84..870e1556d 100644
--- a/promise/src/main/java/com/iluwatar/promise/Promise.java
+++ b/promise/src/main/java/com/iluwatar/promise/Promise.java
@@ -36,6 +36,7 @@ import java.util.function.Function;
public class Promise extends PromiseSupport {
private Runnable fulfillmentAction;
+ private Consumer super Throwable> exceptionHandler;
/**
* Creates a promise that will be fulfilled in future.
@@ -61,9 +62,17 @@ public class Promise extends PromiseSupport {
@Override
public void fulfillExceptionally(Exception exception) {
super.fulfillExceptionally(exception);
+ handleException(exception);
postFulfillment();
}
+ private void handleException(Exception exception) {
+ if (exceptionHandler == null) {
+ return;
+ }
+ exceptionHandler.accept(exception);
+ }
+
private void postFulfillment() {
if (fulfillmentAction == null) {
return;
@@ -83,8 +92,8 @@ public class Promise extends PromiseSupport {
executor.execute(() -> {
try {
fulfill(task.call());
- } catch (Exception e) {
- fulfillExceptionally(e);
+ } catch (Exception ex) {
+ fulfillExceptionally(ex);
}
});
return this;
@@ -96,11 +105,22 @@ public class Promise extends PromiseSupport {
* @param action action to be executed.
* @return a new promise.
*/
- public Promise then(Consumer super T> action) {
+ public Promise thenAccept(Consumer super T> action) {
Promise dest = new Promise<>();
fulfillmentAction = new ConsumeAction(this, dest, action);
return dest;
}
+
+ /**
+ * Set the exception handler on this promise.
+ * @param exceptionHandler a consumer that will handle the exception occurred while fulfilling
+ * the promise.
+ * @return this
+ */
+ public Promise onError(Consumer super Throwable> exceptionHandler) {
+ this.exceptionHandler = exceptionHandler;
+ return this;
+ }
/**
* Returns a new promise that, when this promise is fulfilled normally, is fulfilled with
@@ -108,7 +128,7 @@ public class Promise extends PromiseSupport {
* @param func function to be executed.
* @return a new promise.
*/
- public Promise then(Function super T, V> func) {
+ public Promise thenApply(Function super T, V> func) {
Promise dest = new Promise<>();
fulfillmentAction = new TransformAction(this, dest, func);
return dest;
@@ -135,8 +155,8 @@ public class Promise extends PromiseSupport {
try {
action.accept(src.get());
dest.fulfill(null);
- } catch (Throwable e) {
- dest.fulfillExceptionally((Exception) e.getCause());
+ } catch (Throwable throwable) {
+ dest.fulfillExceptionally((Exception) throwable.getCause());
}
}
}
@@ -162,8 +182,8 @@ public class Promise extends PromiseSupport {
try {
V result = func.apply(src.get());
dest.fulfill(result);
- } catch (Throwable e) {
- dest.fulfillExceptionally((Exception) e.getCause());
+ } catch (Throwable throwable) {
+ dest.fulfillExceptionally((Exception) throwable.getCause());
}
}
}
diff --git a/promise/src/main/java/com/iluwatar/promise/Utility.java b/promise/src/main/java/com/iluwatar/promise/Utility.java
index 2cfad46d0..8d5be2538 100644
--- a/promise/src/main/java/com/iluwatar/promise/Utility.java
+++ b/promise/src/main/java/com/iluwatar/promise/Utility.java
@@ -12,15 +12,19 @@ import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
-import java.util.Optional;
import java.util.Map.Entry;
public class Utility {
+ /**
+ * Calculates character frequency of the file provided.
+ * @param fileLocation location of the file.
+ * @return a map of character to its frequency, an empty map if file does not exist.
+ */
public static Map characterFrequency(String fileLocation) {
Map characterToFrequency = new HashMap<>();
- try (Reader reader = new FileReader(fileLocation);
- BufferedReader bufferedReader = new BufferedReader(reader);) {
+ try (Reader reader = new FileReader(fileLocation);
+ BufferedReader bufferedReader = new BufferedReader(reader)) {
for (String line; (line = bufferedReader.readLine()) != null;) {
for (char c : line.toCharArray()) {
if (!characterToFrequency.containsKey(c)) {
@@ -35,33 +39,35 @@ public class Utility {
}
return characterToFrequency;
}
-
- public static Optional lowestFrequencyChar(Map charFrequency) {
- Optional lowestFrequencyChar = Optional.empty();
- if (charFrequency.isEmpty()) {
- return lowestFrequencyChar;
- }
-
+
+ /**
+ * @return the character with lowest frequency if it exists, {@code Optional.empty()} otherwise.
+ */
+ public static Character lowestFrequencyChar(Map charFrequency) {
+ Character lowestFrequencyChar = null;
Iterator> iterator = charFrequency.entrySet().iterator();
Entry entry = iterator.next();
int minFrequency = entry.getValue();
- lowestFrequencyChar = Optional.of(entry.getKey());
-
+ lowestFrequencyChar = entry.getKey();
+
while (iterator.hasNext()) {
entry = iterator.next();
if (entry.getValue() < minFrequency) {
minFrequency = entry.getValue();
- lowestFrequencyChar = Optional.of(entry.getKey());
+ lowestFrequencyChar = entry.getKey();
}
}
-
+
return lowestFrequencyChar;
}
-
+
+ /**
+ * @return number of lines in the file at provided location. 0 if file does not exist.
+ */
public static Integer countLines(String fileLocation) {
int lineCount = 0;
- try (Reader reader = new FileReader(fileLocation);
- BufferedReader bufferedReader = new BufferedReader(reader);) {
+ try (Reader reader = new FileReader(fileLocation);
+ BufferedReader bufferedReader = new BufferedReader(reader)) {
while (bufferedReader.readLine() != null) {
lineCount++;
}
@@ -71,11 +77,15 @@ public class Utility {
return lineCount;
}
+ /**
+ * Downloads the contents from the given urlString, and stores it in a temporary directory.
+ * @return the absolute path of the file downloaded.
+ */
public static String downloadFile(String urlString) throws MalformedURLException, IOException {
System.out.println("Downloading contents from url: " + urlString);
URL url = new URL(urlString);
File file = File.createTempFile("promise_pattern", null);
- try (Reader reader = new InputStreamReader(url.openStream());
+ try (Reader reader = new InputStreamReader(url.openStream());
BufferedReader bufferedReader = new BufferedReader(reader);
FileWriter writer = new FileWriter(file)) {
for (String line; (line = bufferedReader.readLine()) != null; ) {
diff --git a/promise/src/test/java/com/iluwatar/promise/PromiseTest.java b/promise/src/test/java/com/iluwatar/promise/PromiseTest.java
index de0ecb6d7..45c4c1d36 100644
--- a/promise/src/test/java/com/iluwatar/promise/PromiseTest.java
+++ b/promise/src/test/java/com/iluwatar/promise/PromiseTest.java
@@ -26,6 +26,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -40,7 +43,6 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
-
/**
* Tests Promise class.
*/
@@ -73,7 +75,8 @@ public class PromiseTest {
testWaitingSomeTimeForPromiseToBeFulfilled();
}
- private void testWaitingForeverForPromiseToBeFulfilled() throws InterruptedException, TimeoutException {
+ private void testWaitingForeverForPromiseToBeFulfilled()
+ throws InterruptedException, TimeoutException {
Promise promise = new Promise<>();
promise.fulfillInAsync(new Callable() {
@@ -134,7 +137,7 @@ public class PromiseTest {
throws InterruptedException, ExecutionException {
Promise dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
- .then(value -> {
+ .thenAccept(value -> {
assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, value);
});
@@ -149,17 +152,18 @@ public class PromiseTest {
throws InterruptedException, ExecutionException, TimeoutException {
Promise dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
- .then(new Consumer() {
+ .thenAccept(new Consumer() {
@Override
- public void accept(Integer t) {
+ public void accept(Integer value) {
throw new RuntimeException("Barf!");
}
});
try {
dependentPromise.get();
- fail("Fetching dependent promise should result in exception if the action threw an exception");
+ fail("Fetching dependent promise should result in exception "
+ + "if the action threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
@@ -167,7 +171,8 @@ public class PromiseTest {
try {
dependentPromise.get(1000, TimeUnit.SECONDS);
- fail("Fetching dependent promise should result in exception if the action threw an exception");
+ fail("Fetching dependent promise should result in exception "
+ + "if the action threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
@@ -179,7 +184,7 @@ public class PromiseTest {
throws InterruptedException, ExecutionException {
Promise dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
- .then(value -> {
+ .thenApply(value -> {
assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, value);
return String.valueOf(value);
});
@@ -195,17 +200,18 @@ public class PromiseTest {
throws InterruptedException, ExecutionException, TimeoutException {
Promise dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
- .then(new Function() {
+ .thenApply(new Function() {
@Override
- public String apply(Integer t) {
+ public String apply(Integer value) {
throw new RuntimeException("Barf!");
}
});
try {
dependentPromise.get();
- fail("Fetching dependent promise should result in exception if the function threw an exception");
+ fail("Fetching dependent promise should result in exception "
+ + "if the function threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
@@ -213,7 +219,8 @@ public class PromiseTest {
try {
dependentPromise.get(1000, TimeUnit.SECONDS);
- fail("Fetching dependent promise should result in exception if the function threw an exception");
+ fail("Fetching dependent promise should result in exception "
+ + "if the function threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
@@ -228,6 +235,19 @@ public class PromiseTest {
promise.get(1000, TimeUnit.SECONDS);
}
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void exceptionHandlerIsCalledWhenPromiseIsFulfilledExceptionally() {
+ Promise promise = new Promise<>();
+ Consumer exceptionHandler = mock(Consumer.class);
+ promise.onError(exceptionHandler);
+
+ Exception exception = new Exception("barf!");
+ promise.fulfillExceptionally(exception);
+
+ verify(exceptionHandler).accept(eq(exception));
+ }
private static class NumberCrunchingTask implements Callable {
@@ -236,7 +256,7 @@ public class PromiseTest {
@Override
public Integer call() throws Exception {
// Do number crunching
- Thread.sleep(1000);
+ Thread.sleep(100);
return CRUNCHED_NUMBER;
}
}