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