001package com.identityworksllc.iiq.common;
002
003import java.util.NoSuchElementException;
004import java.util.function.Consumer;
005import java.util.function.Function;
006import java.util.function.Predicate;
007
008/**
009 * A functional "monad" that contains either a non-null value of the given type
010 * or an exception, but never both. The idea here is:
011 *
012 * objectList
013 *  .stream()
014 *  .map(obj -> Maybe.wrap(String.class, some::functionThatCanFail))
015 *  .filter(Maybe.fnHasValue())
016 *  .forEach(items);
017 *
018 * @param <T> The type that this object might contain
019 */
020public final class Maybe<T> {
021    /**
022     * A consumer extension that handles the Maybe concept. If the Maybe has a
023     * value, it will be passed to the wrapped Consumer, and if it does not,
024     * no action will be taken.
025     * @param <T> The type contained within the Maybe, maybe.
026     */
027    public static final class MaybeConsumer<T> implements Consumer<Maybe<T>> {
028        public static <T> MaybeConsumer<T> from(Consumer<T> wrappedConsumer) {
029            return new MaybeConsumer<>(wrappedConsumer);
030        }
031        private final Consumer<T> wrapped;
032
033        private MaybeConsumer(Consumer<T> wrappedConsumer) {
034            if (wrappedConsumer == null) {
035                throw new IllegalArgumentException("Consumer passed to MaybeConsumer must not be null");
036            }
037            this.wrapped = wrappedConsumer;
038        }
039
040        /**
041         * Performs this operation on the given argument.
042         *
043         * @param tMaybe the input argument
044         */
045        @Override
046        public void accept(Maybe<T> tMaybe) {
047            if (tMaybe.hasValue()) {
048                wrapped.accept(tMaybe.get());
049            }
050        }
051    }
052
053    /**
054     * Creates a Predicate that returns true if the Maybe object has an error
055     * @param <F> The arbitrary input type
056     * @return The predicate
057     */
058    public static <F> Predicate<Maybe<F>> fnHasError() {
059        return Maybe::hasError;
060    }
061
062    /**
063     * Creates a Predicate that returns true if the Maybe object has a value
064     * @param <F> The arbitrary input type
065     * @return The predicate
066     */
067    public static <F> Predicate<Maybe<F>> fnHasValue() {
068        return Maybe::hasValue;
069    }
070
071    /**
072     * Returns a Maybe object containing the given value
073     * @param value The value
074     * @param <A> The type of the value
075     * @return A Maybe object containing the given value
076     */
077    public static <A> Maybe<A> of(A value) {
078        return new Maybe<>(value, null);
079    }
080
081    /**
082     * Returns a Maybe that is either a value or an error, depending on the outcome of
083     * the supplier in question.
084     *
085     * @param aClass The class expected, used solely to distinguish this method from the other of() implementations
086     * @param supplier The supplier of a value to wrap in a Maybe
087     * @return A Maybe repesenting the outcome of the Supplier's action
088     * @param <A> The content type of the Maybe, if it has a value
089     */
090    public static <A> Maybe<A> of(Class<A> aClass, Functions.SupplierWithError<A> supplier) {
091        try {
092            return Maybe.of(supplier.getWithError());
093        } catch(Throwable t) {
094            return Maybe.of(t);
095        }
096    }
097
098    /**
099     * Returns a Maybe object containing an exception. The class parameter is only used for casting the output type.
100     * @param value The exception
101     * @param otherwiseExpectedType The parameterized type that we would be expecting if this was not an exception
102     * @param <A> The parameterized type that we would be expecting if this was not an exception
103     * @return a Maybe object containing the exception
104     */
105    public static <A> Maybe<A> of(Throwable value, Class<A> otherwiseExpectedType) {
106        return new Maybe<>(null, value);
107    }
108
109    /**
110     * Chains a Maybe object by passing along the Throwable from an existing Maybe into the next. This is for use with streams.
111     * @param value An existing Maybe in error state
112     * @param otherwiseExpectedType The parameterized type that we would be expecting if this was not an exception
113     * @param <A> The parameterized type that we would be expecting if this was not an exception
114     * @return a Maybe object containing the exception
115     */
116    public static <A> Maybe<A> of(Maybe<?> value, Class<A> otherwiseExpectedType) {
117        if (!value.hasError()) {
118            throw new IllegalArgumentException("The chained 'Maybe' implementation can only be used for errors");
119        }
120        return new Maybe<>(null, value.contents.getRight());
121    }
122
123    /**
124     * Chains a Maybe object by passing along the Throwable from an existing Maybe into the next. This is for use with streams.
125     * @param value An existing Maybe in error state
126     * @param <A> The parameterized type that we would be expecting if this was not an exception
127     * @return a Maybe object containing the exception
128     */
129    public static <A> Maybe<A> of(Maybe<?> value) {
130        if (!value.hasError()) {
131            throw new IllegalArgumentException("The chained 'Maybe' implementation can only be used for errors");
132        }
133        return new Maybe<>(null, value.contents.getRight());
134    }
135
136    /**
137     * Creates a new maybe from the given throwable with an inferred type by context
138     * @param e The throwable
139     * @param <R> The inferred type
140     * @return A chained Maybe
141     */
142    @SuppressWarnings("unchecked")
143    public static <R> Maybe<R> of(Throwable e) {
144        return (Maybe<R>)new Maybe<>(null, e);
145    }
146
147    /**
148     * Returns a function wrapping the input function. The wrapper will resolve the
149     * output of the input function by invoking {@link com.identityworksllc.iiq.common.Functions.FunctionWithError#applyWithError(I)}
150     * and wrapping the output (whether a value or an exception) in a Maybe.
151     *
152     * @param aClass The output class expected
153     * @param func The function that will be wrapped in a Maybe producer
154     * @return A Maybe repesenting the outcome of the Supplier's action
155     * @param <I> The input type to the function
156     * @param <O> The content type of the Maybe, assuming it was to have a value
157     */
158    public static <I, O> Function<I, Maybe<O>> wrap(Class<O> aClass, Functions.FunctionWithError<I, O> func) {
159        return (x) -> {
160            try {
161                return Maybe.of(func.applyWithError(x));
162            } catch (Throwable t) {
163                return Maybe.of(t);
164            }
165        };
166    }
167
168    /**
169     * The contents of the Maybe object, which contain either a T or a Throwable
170     */
171    private final Either<T, Throwable> contents;
172
173    @SuppressWarnings("unchecked")
174    private Maybe(T left, Throwable right) {
175        if (left != null) {
176            contents = (Either<T, Throwable>) Either.left(left);
177        } else {
178            contents = (Either<T, Throwable>) Either.right(right);
179        }
180    }
181
182    /**
183     * Gets the value or throws the exception wrapped in an ExecutionException
184     * @return The value if it exists
185     * @throws IllegalStateException If this Maybe was an exception instead
186     */
187    public T get() throws IllegalStateException {
188        if (contents.hasRight()) {
189            throw new IllegalStateException(contents.getRight());
190        }
191        return contents.getLeft();
192    }
193
194    /**
195     * Returns the error if there is one, or else throws a {@link java.util.NoSuchElementException}.
196     * @return The error if one exists
197     * @throws java.util.NoSuchElementException if the error doesn't exist
198     */
199    public Throwable getError() throws NoSuchElementException {
200        return contents.getRight();
201    }
202
203    /**
204     * If this Maybe has an error and not a value
205     * @return True if this Maybe has an error
206     */
207    public boolean hasError() {
208        return contents.hasRight();
209    }
210
211    /**
212     * If this Maybe has a value and not an error
213     * @return True if this Maybe does not have an error
214     */
215    public boolean hasValue() {
216        return contents.hasLeft();
217    }
218
219    /**
220     * Chains a Maybe object by invoking the given function on it.
221     *
222     * There are three possible outcomes:
223     *
224     * 1. This object already has an error ({@link Maybe#hasError()} returns true), in which case this method will return a new Maybe with that error.
225     *
226     * 2. Applying the function to this Maybe's value results in an exception, in which case this method will return a new Maybe with that exception.
227     *
228     * 3. Applying the function is successful and produces an object of type 'B', in which case this method returns a new Maybe containing that object.
229     *
230     * @param downstream The mapping function to apply to this Maybe
231     * @param <B> The output type
232     * @return a Maybe object containing the exception
233     */
234    public <B> Maybe<B> map(Functions.FunctionWithError<T, B> downstream) {
235        if (this.hasError()) {
236            return Maybe.of(this.getError());
237        }
238
239        T input = this.get();
240
241        try {
242            B output = downstream.applyWithError(input);
243            return Maybe.of(output);
244        } catch(Throwable e) {
245            return Maybe.of(e);
246        }
247    }
248}