Merge pull request #470 from iluwatar/Promise

Implements #403 Promise pattern
This commit is contained in:
Ilkka Seppälä 2016-09-02 20:47:21 +03:00 committed by GitHub
commit 678a06e7f8
11 changed files with 1096 additions and 0 deletions

View File

@ -129,6 +129,7 @@
<module>hexagonal</module>
<module>abstract-document</module>
<module>aggregator-microservices</module>
<module>promise</module>
<module>page-object</module>
</modules>

46
promise/README.md Normal file
View File

@ -0,0 +1,46 @@
---
layout: pattern
title: Promise
folder: promise
permalink: /patterns/promise/
categories: Concurrency
tags:
- Java
- Functional
- Reactive
- Difficulty-Intermediate
---
## Also known as
CompletableFuture
## Intent
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. Promises are a way to write async code that still appears as though it is executing
in a synchronous way.
![alt text](./etc/promise.png "Promise")
## Applicability
Promise pattern is applicable in concurrent programming when some work needs to be done asynchronously
and:
* code maintainablity and readability suffers due to callback hell.
* you need to compose promises and need better error handling for asynchronous tasks.
* you want to use functional style of programming.
## Real world examples
* [java.util.concurrent.CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html)
* [Guava ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained)
## Related Patterns
* Async Method Invocation
* Callback
## Credits
* [You are missing the point to Promises](https://gist.github.com/domenic/3889970)
* [Functional style callbacks using CompleteableFuture](https://www.infoq.com/articles/Functional-Style-Callbacks-Using-CompletableFuture)

BIN
promise/etc/promise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

111
promise/etc/promise.ucls Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.1.10" icons="true" automaticImage="PNG" always-add-relationships="false"
generalizations="true" realizations="true" associations="true" dependencies="false" nesting-relationships="true"
router="FAN">
<class id="1" language="java" name="com.iluwatar.promise.Promise" project="promise"
file="/promise/src/main/java/com/iluwatar/promise/Promise.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="524" y="541"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="false" static="true"/>
<operations public="true" package="false" protected="true" private="false" static="true"/>
</display>
</class>
<interface id="2" language="java" name="java.util.concurrent.Future" project="async-method-invocation"
file="/usr/lib/java/jdk1.8.0_45/jre/lib/rt.jar" binary="true" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="527" y="94"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</interface>
<class id="3" language="java" name="com.iluwatar.promise.PromiseSupport" project="promise"
file="/promise/src/main/java/com/iluwatar/promise/PromiseSupport.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="524" y="313"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="false" protected="true" private="false" static="true"/>
<operations public="true" package="false" protected="true" private="true" static="true"/>
</display>
</class>
<interface id="4" language="java" name="java.util.concurrent.Executor" project="async-method-invocation"
file="/usr/lib/java/jdk1.8.0_45/jre/lib/rt.jar" binary="true" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="798" y="541"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</interface>
<interface id="5" language="java" name="java.util.concurrent.Callable" project="async-method-invocation"
file="/usr/lib/java/jdk1.8.0_45/jre/lib/rt.jar" binary="true" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="847" y="345"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</interface>
<interface id="6" language="java" name="java.util.function.Consumer" project="async-method-invocation"
file="/usr/lib/java/jdk1.8.0_45/jre/lib/rt.jar" binary="true" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="158" y="336"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="false" static="true"/>
</display>
</interface>
<interface id="7" language="java" name="java.util.function.Function" project="async-method-invocation"
file="/usr/lib/java/jdk1.8.0_45/jre/lib/rt.jar" binary="true" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="166" y="546"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="false" static="false"/>
</display>
</interface>
<class id="8" language="java" name="com.iluwatar.promise.App" project="promise"
file="/promise/src/main/java/com/iluwatar/promise/App.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="801" y="189"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="false" static="true"/>
<operations public="true" package="true" protected="true" private="false" static="true"/>
</display>
</class>
<dependency id="9">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="5"/>
</dependency>
<dependency id="10">
<end type="SOURCE" refId="8"/>
<end type="TARGET" refId="1"/>
</dependency>
<dependency id="11">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="6"/>
</dependency>
<generalization id="12">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="3"/>
</generalization>
<dependency id="13">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="4"/>
</dependency>
<dependency id="14">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="7"/>
</dependency>
<realization id="15">
<end type="SOURCE" refId="3"/>
<end type="TARGET" refId="2"/>
</realization>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<association-display labels="true" multiplicity="true"/>
</class-diagram>

47
promise/pom.xml Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0"?>
<!--
The MIT License
Copyright (c) 2014 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.13.0-SNAPSHOT</version>
</parent>
<artifactId>promise</artifactId>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,176 @@
/**
* The MIT License
* Copyright (c) 2014 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.promise;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
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.
*
* <p>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.
*
* <p>Promises provide a few advantages over callback objects:
* <ul>
* <li> Functional composition and error handling
* <li> Prevents callback hell and provides callback aggregation
* </ul>
*
* <p>
* In this application the usage of promise is demonstrated with two examples:
* <ul>
* <li>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.
* <li>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.
* </ul>
*
* @see CompletableFuture
*/
public class App {
private static final String DEFAULT_URL = "https://raw.githubusercontent.com/iluwatar/java-design-patterns/Promise/promise/README.md";
private final ExecutorService executor;
private final CountDownLatch stopLatch;
private App() {
executor = Executors.newFixedThreadPool(2);
stopLatch = new CountDownLatch(2);
}
/**
* Program entry point
* @param args arguments
* @throws InterruptedException if main thread is interrupted.
* @throws ExecutionException if an execution error occurs.
*/
public static void main(String[] args) throws InterruptedException, ExecutionException {
App app = new App();
try {
app.promiseUsage();
} finally {
app.stop();
}
}
private void promiseUsage() {
calculateLineCount();
calculateLowestFrequencyChar();
}
/*
* Calculate the lowest frequency character and when that promise is fulfilled,
* consume the result in a Consumer<Character>
*/
private void calculateLowestFrequencyChar() {
lowestFrequencyChar()
.thenAccept(
charFrequency -> {
System.out.println("Char with lowest frequency is: " + charFrequency);
taskCompleted();
}
);
}
/*
* Calculate the line count and when that promise is fulfilled, consume the result
* in a Consumer<Integer>
*/
private void calculateLineCount() {
countLines()
.thenAccept(
count -> {
System.out.println("Line count is: " + count);
taskCompleted();
}
);
}
/*
* 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<Character> 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<Map<Character, Integer>> 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<Integer> 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<String> download(String urlString) {
Promise<String> downloadPromise = new Promise<String>()
.fulfillInAsync(
() -> {
return Utility.downloadFile(urlString);
}, executor)
.onError(
throwable -> {
throwable.printStackTrace();
taskCompleted();
}
);
return downloadPromise;
}
private void stop() throws InterruptedException {
stopLatch.await();
executor.shutdownNow();
}
private void taskCompleted() {
stopLatch.countDown();
}
}

View File

@ -0,0 +1,193 @@
/**
* The MIT License
* Copyright (c) 2014 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.promise;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* 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.
*
* @param <T> type of result.
*/
public class Promise<T> extends PromiseSupport<T> {
private Runnable fulfillmentAction;
private Consumer<? super Throwable> exceptionHandler;
/**
* Creates a promise that will be fulfilled in future.
*/
public Promise() {
}
/**
* Fulfills the promise with the provided value.
* @param value the fulfilled value that can be accessed using {@link #get()}.
*/
@Override
public void fulfill(T value) {
super.fulfill(value);
postFulfillment();
}
/**
* Fulfills the promise with exception due to error in execution.
* @param exception the exception will be wrapped in {@link ExecutionException}
* when accessing the value using {@link #get()}.
*/
@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;
}
fulfillmentAction.run();
}
/**
* Executes the task using the executor in other thread and fulfills the promise returned
* once the task completes either successfully or with an exception.
*
* @param task the task that will provide the value to fulfill the promise.
* @param executor the executor in which the task should be run.
* @return a promise that represents the result of running the task provided.
*/
public Promise<T> fulfillInAsync(final Callable<T> task, Executor executor) {
executor.execute(() -> {
try {
fulfill(task.call());
} catch (Exception ex) {
fulfillExceptionally(ex);
}
});
return this;
}
/**
* Returns a new promise that, when this promise is fulfilled normally, is fulfilled with
* result of this promise as argument to the action provided.
* @param action action to be executed.
* @return a new promise.
*/
public Promise<Void> thenAccept(Consumer<? super T> action) {
Promise<Void> 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<T> onError(Consumer<? super Throwable> exceptionHandler) {
this.exceptionHandler = exceptionHandler;
return this;
}
/**
* Returns a new promise that, when this promise is fulfilled normally, is fulfilled with
* result of this promise as argument to the function provided.
* @param func function to be executed.
* @return a new promise.
*/
public <V> Promise<V> thenApply(Function<? super T, V> func) {
Promise<V> dest = new Promise<>();
fulfillmentAction = new TransformAction<V>(this, dest, func);
return dest;
}
/**
* Accesses the value from source promise and calls the consumer, then fulfills the
* destination promise.
*/
private class ConsumeAction implements Runnable {
private final Promise<T> src;
private final Promise<Void> dest;
private final Consumer<? super T> action;
private ConsumeAction(Promise<T> src, Promise<Void> dest, Consumer<? super T> action) {
this.src = src;
this.dest = dest;
this.action = action;
}
@Override
public void run() {
try {
action.accept(src.get());
dest.fulfill(null);
} catch (Throwable throwable) {
dest.fulfillExceptionally((Exception) throwable.getCause());
}
}
}
/**
* Accesses the value from source promise, then fulfills the destination promise using the
* transformed value. The source value is transformed using the transformation function.
*/
private class TransformAction<V> implements Runnable {
private final Promise<T> src;
private final Promise<V> dest;
private final Function<? super T, V> func;
private TransformAction(Promise<T> src, Promise<V> dest, Function<? super T, V> func) {
this.src = src;
this.dest = dest;
this.func = func;
}
@Override
public void run() {
try {
dest.fulfill(func.apply(src.get()));
} catch (Throwable throwable) {
dest.fulfillExceptionally((Exception) throwable.getCause());
}
}
}
}

View File

@ -0,0 +1,119 @@
/**
* The MIT License
* Copyright (c) 2014 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.promise;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* A really simplified implementation of future that allows completing it successfully with a value
* or exceptionally with an exception.
*/
class PromiseSupport<T> implements Future<T> {
private static final int RUNNING = 1;
private static final int FAILED = 2;
private static final int COMPLETED = 3;
private final Object lock;
private volatile int state = RUNNING;
private T value;
private Exception exception;
PromiseSupport() {
this.lock = new Object();
}
void fulfill(T value) {
this.value = value;
this.state = COMPLETED;
synchronized (lock) {
lock.notifyAll();
}
}
void fulfillExceptionally(Exception exception) {
this.exception = exception;
this.state = FAILED;
synchronized (lock) {
lock.notifyAll();
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return state > RUNNING;
}
@Override
public T get() throws InterruptedException, ExecutionException {
if (state == COMPLETED) {
return value;
} else if (state == FAILED) {
throw new ExecutionException(exception);
} else {
synchronized (lock) {
lock.wait();
if (state == COMPLETED) {
return value;
} else {
throw new ExecutionException(exception);
}
}
}
}
@Override
public T get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (state == COMPLETED) {
return value;
} else if (state == FAILED) {
throw new ExecutionException(exception);
} else {
synchronized (lock) {
lock.wait(unit.toMillis(timeout));
if (state == COMPLETED) {
return value;
} else if (state == FAILED) {
throw new ExecutionException(exception);
} else {
throw new TimeoutException();
}
}
}
}
}

View File

@ -0,0 +1,101 @@
package com.iluwatar.promise;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
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<Character, Integer> characterFrequency(String fileLocation) {
Map<Character, Integer> characterToFrequency = new HashMap<>();
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)) {
characterToFrequency.put(c, 1);
} else {
characterToFrequency.put(c, characterToFrequency.get(c) + 1);
}
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
return characterToFrequency;
}
/**
* @return the character with lowest frequency if it exists, {@code Optional.empty()} otherwise.
*/
public static Character lowestFrequencyChar(Map<Character, Integer> charFrequency) {
Character lowestFrequencyChar = null;
Iterator<Entry<Character, Integer>> iterator = charFrequency.entrySet().iterator();
Entry<Character, Integer> entry = iterator.next();
int minFrequency = entry.getValue();
lowestFrequencyChar = entry.getKey();
while (iterator.hasNext()) {
entry = iterator.next();
if (entry.getValue() < minFrequency) {
minFrequency = entry.getValue();
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)) {
while (bufferedReader.readLine() != null) {
lineCount++;
}
} catch (IOException ex) {
ex.printStackTrace();
}
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());
BufferedReader bufferedReader = new BufferedReader(reader);
FileWriter writer = new FileWriter(file)) {
for (String line; (line = bufferedReader.readLine()) != null; ) {
writer.write(line);
writer.write("\n");
}
System.out.println("File downloaded at: " + file.getAbsolutePath());
return file.getAbsolutePath();
} catch (IOException ex) {
throw ex;
}
}
}

View File

@ -0,0 +1,39 @@
/**
* The MIT License
* Copyright (c) 2014 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.promise;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
/**
*
* Application test.
*/
public class AppTest {
@Test
public void testApp() throws InterruptedException, ExecutionException {
App.main(null);
}
}

View File

@ -0,0 +1,263 @@
/**
* The MIT License
* Copyright (c) 2014 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.promise;
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;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
* Tests Promise class.
*/
public class PromiseTest {
private Executor executor;
private Promise<Integer> promise;
@Rule public ExpectedException exception = ExpectedException.none();
@Before
public void setUp() {
executor = Executors.newSingleThreadExecutor();
promise = new Promise<>();
}
@Test
public void promiseIsFulfilledWithTheResultantValueOfExecutingTheTask()
throws InterruptedException, ExecutionException {
promise.fulfillInAsync(new NumberCrunchingTask(), executor);
assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, promise.get());
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
@Test
public void promiseIsFulfilledWithAnExceptionIfTaskThrowsAnException()
throws InterruptedException, ExecutionException, TimeoutException {
testWaitingForeverForPromiseToBeFulfilled();
testWaitingSomeTimeForPromiseToBeFulfilled();
}
private void testWaitingForeverForPromiseToBeFulfilled()
throws InterruptedException, TimeoutException {
Promise<Integer> promise = new Promise<>();
promise.fulfillInAsync(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
throw new RuntimeException("Barf!");
}
}, executor);
try {
promise.get();
fail("Fetching promise should result in exception if the task threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
try {
promise.get(1000, TimeUnit.SECONDS);
fail("Fetching promise should result in exception if the task threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
}
private void testWaitingSomeTimeForPromiseToBeFulfilled()
throws InterruptedException, TimeoutException {
Promise<Integer> promise = new Promise<>();
promise.fulfillInAsync(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
throw new RuntimeException("Barf!");
}
}, executor);
try {
promise.get(1000, TimeUnit.SECONDS);
fail("Fetching promise should result in exception if the task threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
try {
promise.get();
fail("Fetching promise should result in exception if the task threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
}
@Test
public void dependentPromiseIsFulfilledAfterTheConsumerConsumesTheResultOfThisPromise()
throws InterruptedException, ExecutionException {
Promise<Void> dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
.thenAccept(value -> {
assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, value);
});
dependentPromise.get();
assertTrue(dependentPromise.isDone());
assertFalse(dependentPromise.isCancelled());
}
@Test
public void dependentPromiseIsFulfilledWithAnExceptionIfConsumerThrowsAnException()
throws InterruptedException, ExecutionException, TimeoutException {
Promise<Void> dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
.thenAccept(new Consumer<Integer>() {
@Override
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");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
try {
dependentPromise.get(1000, TimeUnit.SECONDS);
fail("Fetching dependent promise should result in exception "
+ "if the action threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
}
@Test
public void dependentPromiseIsFulfilledAfterTheFunctionTransformsTheResultOfThisPromise()
throws InterruptedException, ExecutionException {
Promise<String> dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
.thenApply(value -> {
assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, value);
return String.valueOf(value);
});
assertEquals(String.valueOf(NumberCrunchingTask.CRUNCHED_NUMBER), dependentPromise.get());
assertTrue(dependentPromise.isDone());
assertFalse(dependentPromise.isCancelled());
}
@Test
public void dependentPromiseIsFulfilledWithAnExceptionIfTheFunctionThrowsException()
throws InterruptedException, ExecutionException, TimeoutException {
Promise<String> dependentPromise = promise
.fulfillInAsync(new NumberCrunchingTask(), executor)
.thenApply(new Function<Integer, String>() {
@Override
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");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
try {
dependentPromise.get(1000, TimeUnit.SECONDS);
fail("Fetching dependent promise should result in exception "
+ "if the function threw an exception");
} catch (ExecutionException ex) {
assertTrue(promise.isDone());
assertFalse(promise.isCancelled());
}
}
@Test
public void fetchingAnAlreadyFulfilledPromiseReturnsTheFulfilledValueImmediately()
throws InterruptedException, ExecutionException, TimeoutException {
Promise<Integer> promise = new Promise<>();
promise.fulfill(NumberCrunchingTask.CRUNCHED_NUMBER);
promise.get(1000, TimeUnit.SECONDS);
}
@SuppressWarnings("unchecked")
@Test
public void exceptionHandlerIsCalledWhenPromiseIsFulfilledExceptionally() {
Promise<Object> promise = new Promise<>();
Consumer<Throwable> 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<Integer> {
private static final Integer CRUNCHED_NUMBER = Integer.MAX_VALUE;
@Override
public Integer call() throws Exception {
// Do number crunching
Thread.sleep(100);
return CRUNCHED_NUMBER;
}
}
}