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