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}