001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.bct.assertions;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.list;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.Shorts.eq;
023import static org.apache.juneau.commons.utils.ThrowableUtils.*;
024
025import java.io.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.juneau.commons.function.*;
030
031
032/**
033 * Used for fluent assertion calls against throwables.
034 *
035 * <h5 class='section'>Test Methods:</h5>
036 * <p>
037 * <ul class='javatree'>
038 *    <li class='jc'>{@link FluentObjectAssertion}
039 *    <ul class='javatreec'>
040 *       <li class='jm'>{@link FluentObjectAssertion#isExists() isExists()}
041 *       <li class='jm'>{@link FluentObjectAssertion#is(Object) is(Object)}
042 *       <li class='jm'>{@link FluentObjectAssertion#is(Predicate) is(Predicate)}
043 *       <li class='jm'>{@link FluentObjectAssertion#isNot(Object) isNot(Object)}
044 *       <li class='jm'>{@link FluentObjectAssertion#isAny(Object...) isAny(Object...)}
045 *       <li class='jm'>{@link FluentObjectAssertion#isNotAny(Object...) isNotAny(Object...)}
046 *       <li class='jm'>{@link FluentObjectAssertion#isNull() isNull()}
047 *       <li class='jm'>{@link FluentObjectAssertion#isNotNull() isNotNull()}
048 *       <li class='jm'>{@link FluentObjectAssertion#isString(String) isString(String)}
049 *       <li class='jm'>{@link FluentObjectAssertion#isSame(Object) isSame(Object)}
050 *       <li class='jm'>{@link FluentObjectAssertion#isType(Class) isType(Class)}
051 *       <li class='jm'>{@link FluentObjectAssertion#isExactType(Class) isExactType(Class)}
052 *    </ul>
053 * </ul>
054 *
055 * <h5 class='section'>Transform Methods:</h5>
056 * <p>
057 * <ul class='javatree'>
058 *    <li class='jc'>{@link FluentThrowableAssertion}
059 *    <ul class='javatreec'>
060 *       <li class='jm'>{@link FluentThrowableAssertion#asMessage() asMessage()}
061 *       <li class='jm'>{@link FluentThrowableAssertion#asMessages() asMessages()}
062 *       <li class='jm'>{@link FluentThrowableAssertion#asLocalizedMessage() asLocalizedMessage()}
063 *       <li class='jm'>{@link FluentThrowableAssertion#asLocalizedMessages() asLocalizedMessages()}
064 *       <li class='jm'>{@link FluentThrowableAssertion#asStackTrace() asStackTrace()}
065 *       <li class='jm'>{@link FluentThrowableAssertion#asCausedBy() asCausedBy()}
066 *       <li class='jm'>{@link FluentThrowableAssertion#asCausedBy(Class) asCausedBy(Class)}
067 *       <li class='jm'>{@link FluentThrowableAssertion#asFind(Class) asFind(Class)}
068 *    </ul>
069 *    <li class='jc'>{@link FluentObjectAssertion}
070 *    <ul class='javatreec'>
071 *       <li class='jm'>{@link FluentObjectAssertion#asString() asString()}
072 *       <li class='jm'>{@link FluentObjectAssertion#asString(ThrowingFunction) asString(ThrowingFunction)}
073 *       <li class='jm'>{@link FluentObjectAssertion#asTransformed(UnaryOperator) asTransformed(UnaryOperator)}
074 *       <li class='jm'>{@link FluentObjectAssertion#asAny() asAny()}
075 * </ul>
076 * </ul>
077 *
078 * <h5 class='section'>Configuration Methods:</h5>
079 * <p>
080 * <ul class='javatree'>
081 *    <li class='jc'>{@link Assertion}
082 *    <ul class='javatreec'>
083 *       <li class='jm'>{@link Assertion#setMsg(String, Object...) setMsg(String, Object...)}
084 *       <li class='jm'>{@link Assertion#setOut(PrintStream) setOut(PrintStream)}
085 *       <li class='jm'>{@link Assertion#setSilent() setSilent()}
086 *       <li class='jm'>{@link Assertion#setStdOut() setStdOut()}
087 *       <li class='jm'>{@link Assertion#setThrowable(Class) setThrowable(Class)}
088*  </ul>
089 * </ul>
090 *
091 * <h5 class='section'>See Also:</h5><ul>
092 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauEcosystemOverview">Juneau Ecosystem</a>
093 * </ul>
094 *
095 * @param <T> The throwable type.
096 * @param <R> The return type.
097 */
098@SuppressWarnings({
099   "java:S115", // Constants use UPPER_snakeCase convention
100   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
101})
102public class FluentThrowableAssertion<T extends Throwable,R> extends FluentObjectAssertion<T,R> {
103
104   private static final Messages MESSAGES = Messages.of(FluentThrowableAssertion.class, "Messages");
105   private static final String MSG_exceptionWasNotExpectedType = MESSAGES.getString("exceptionWasNotExpectedType");
106   private static final String MSG_exceptionWasNotThrown = MESSAGES.getString("exceptionWasNotThrown");
107   private static final String MSG_causedByExceptionNotExpectedType = MESSAGES.getString("causedByExceptionNotExpectedType");
108
109   /**
110    * Chained constructor.
111    *
112    * <p>
113    * Used when transforming one assertion into another so that the assertion config can be used by the new assertion.
114    *
115    * @param creator
116    *    The assertion that created this assertion.
117    *    <br>Should be <jk>null</jk> if this is the top-level assertion.
118    * @param value
119    *    The object being tested.
120    *    <br>Can be <jk>null</jk> (assertions such as {@code isNull()} then operate on the absence of a value).
121    * @param returns
122    *    The object to return after a test method is called.
123    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
124    * used on the same assertion.
125    */
126   public FluentThrowableAssertion(Assertion creator, T value, R returns) {
127      super(creator, value, returns);
128   }
129
130   /**
131    * Constructor.
132    *
133    * @param value
134    *    The object being tested.
135    *    <br>Can be <jk>null</jk> (assertions such as {@code isNull()} then operate on the absence of a value).
136    * @param returns
137    *    The object to return after a test method is called.
138    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
139    * used on the same assertion.
140    */
141   public FluentThrowableAssertion(T value, R returns) {
142      this(null, value, returns);
143   }
144
145   /**
146    * Returns an assertion against the caused-by throwable.
147    *
148    * <h5 class='section'>Example:</h5>
149    * <p class='bjava'>
150    *    <jc>// Asserts that the specified method throws an exception whose
151    *    // caused-by message contains 'foobar'. </jc>
152    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
153    *       .asCausedBy()
154    *       .asMessage()
155    *       .isPattern(<js>"foobar"</js>);
156    * </p>
157    *
158    * @return An assertion against the caused-by.  Never <jk>null</jk>.
159    */
160   public FluentThrowableAssertion<Throwable,R> asCausedBy() {
161      return asCausedBy(Throwable.class);
162   }
163
164   /**
165    * Returns an assertion against the caused-by throwable.
166    *
167    * <h5 class='section'>Example:</h5>
168    * <p class='bjava'>
169    *    <jc>// Asserts that the specified method throws an exception whose
170    *    // caused-by message contains 'foobar'. </jc>
171    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
172    *       .asCausedBy(RuntimeException.<jk>class</jk>)
173    *       .asMessage()
174    *       .isPattern(<js>"foobar"</js>);
175    * </p>
176    *
177    * @param <X> The throwable type.
178    * @param type The expected exception type.
179    * @return An assertion against the caused-by.  Never <jk>null</jk>.
180    */
181   public <X extends Throwable> FluentThrowableAssertion<X,R> asCausedBy(Class<X> type) {
182      var t = map(Throwable::getCause).orElse(null);
183      if (t == null || type.isInstance(t))
184         return new FluentThrowableAssertion<>(this, type.cast(t), returns());
185      throw error(MSG_causedByExceptionNotExpectedType, cn(type), cn(t.getClass()));
186   }
187
188   /**
189    * Returns an assertion against the throwable localized message.
190    *
191    * <h5 class='section'>Example:</h5>
192    * <p class='bjava'>
193    *    <jc>// Asserts that the specified method throws an exception with a
194    *    // caused-by RuntimeException containing 'foobar'</jc>
195    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
196    *       .findCausedBy(RuntimeException.<jk>class</jk>)
197    *       .isExists()
198    *       .asMessage()
199    *       .isPattern(<js>"foobar"</js>);
200    * </p>
201    *
202    * @param <X> The throwable type.
203    * @param throwableClass The class type to search for in the caused-by chain.
204    * @return An assertion against the caused-by throwable.  Never <jk>null</jk>.
205    */
206   public <X extends Throwable> FluentThrowableAssertion<X,R> asFind(Class<X> throwableClass) {
207      Throwable t = orElse(null);
208      while (nn(t)) {
209         if (throwableClass.isInstance(t))
210            return new FluentThrowableAssertion<>(this, throwableClass.cast(t), returns());
211         t = t.getCause();
212      }
213      return new FluentThrowableAssertion<>(this, (X)null, returns());
214   }
215
216   /**
217    * Returns an assertion against the throwable localized message.
218    *
219    * <h5 class='section'>Example:</h5>
220    * <p class='bjava'>
221    *    <jc>// Asserts that the specified method throws an exception with
222    *    // 'foobar' somewhere in the localized messages. </jc>
223    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
224    *       .asLocalizedMessage()
225    *       .isPattern(<js>".*foobar.*"</js>);
226    * </p>
227    *
228    * @return An assertion against the throwable localized message.  Never <jk>null</jk>.
229    */
230   public FluentStringAssertion<R> asLocalizedMessage() {
231      return new FluentStringAssertion<>(this, map(Throwable::getLocalizedMessage).orElse(null), returns());
232   }
233
234   /**
235    * Returns an assertion against the throwable message and all caused-by messages.
236    *
237    * <h5 class='section'>Example:</h5>
238    * <p class='bjava'>
239    *    <jc>// Asserts that the specified method throws an exception with
240    *    // 'foobar' somewhere in the messages. </jc>
241    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
242    *       .asLocalizedMessages()
243    *       .isPattern(<js>".*foobar.*"</js>);
244    * </p>
245    *
246    * @return An assertion against the throwable message.  Never <jk>null</jk>.
247    */
248   public FluentListAssertion<String,R> asLocalizedMessages() {
249      List<String> l = null;
250      Throwable t = orElse(null);
251      if (nn(t)) {
252         if (t.getCause() == null)
253            l = singletonList(t.getMessage());
254         else {
255            l = list();
256            while (nn(t)) {
257               l.add(localizedMessage(t));
258               t = t.getCause();
259            }
260         }
261      }
262      return new FluentListAssertion<>(this, l, returns());
263   }
264
265   /**
266    * Returns an assertion against the throwable message.
267    *
268    * <h5 class='section'>Example:</h5>
269    * <p class='bjava'>
270    *    <jc>// Asserts that the specified method throws an exception
271    * // with 'foobar' somewhere in the messages. </jc>
272    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
273    *       .asMessage()
274    *       .isPattern(<js>".*foobar.*"</js>);
275    * </p>
276    *
277    * @return An assertion against the throwable message.  Never <jk>null</jk>.
278    */
279   public FluentStringAssertion<R> asMessage() {
280      return new FluentStringAssertion<>(this, map(Throwable::getMessage).orElse(null), returns());
281   }
282
283   /**
284    * Returns an assertion against the throwable message and all caused-by messages.
285    *
286    * <h5 class='section'>Example:</h5>
287    * <p class='bjava'>
288    *    <jc>// Asserts that the specified method throws an exception with
289    *    // 'foobar' somewhere in the messages. </jc>
290    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
291    *       .asMessages()
292    *       .isPattern(<js>".*foobar.*"</js>);
293    * </p>
294    *
295    * @return An assertion against the throwable message.  Never <jk>null</jk>.
296    */
297   public FluentListAssertion<String,R> asMessages() {
298      List<String> l = null;
299      Throwable t = orElse(null);
300      if (nn(t)) {
301         if (t.getCause() == null)
302            l = singletonList(t.getMessage());
303         else {
304            l = list();
305            while (nn(t)) {
306               l.add(t.getMessage());
307               t = t.getCause();
308            }
309         }
310      }
311      return new FluentListAssertion<>(this, l, returns());
312   }
313
314   /**
315    * Returns an assertion against the throwable localized message.
316    *
317    * <h5 class='section'>Example:</h5>
318    * <p class='bjava'>
319    *    <jc>// Asserts that the specified method throws an exception with
320    *    // 'foobar' somewhere in the stack trace. </jc>
321    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
322    *       .asStackTrace()
323    *       .isPattern(<js>"foobar"</js>);
324    * </p>
325    *
326    * @return An assertion against the throwable stacktrace.  Never <jk>null</jk>.
327    */
328   public FluentStringListAssertion<R> asStackTrace() {
329      return new FluentStringListAssertion<>(this, valueIsNull() ? null : l(getStackTrace(value())), returns());
330   }
331
332   @Override /* Overridden from FluentObjectAssertion */
333   public FluentThrowableAssertion<T,R> asTransformed(UnaryOperator<T> function) {
334      return new FluentThrowableAssertion<>(this, function.apply(orElse(null)), returns());
335   }
336
337   /**
338    * Asserts that this throwable is exactly the specified type.
339    *
340    * <h5 class='section'>Example:</h5>
341    * <p class='bjava'>
342    *    <jc>// Asserts that the specified method throws a RuntimeException. </jc>
343    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
344    *       .isExactType(RuntimeException.<jk>class</jk>);
345    * </p>
346    *
347    * @param type The type.
348    * @return The fluent return object.
349    */
350   @Override
351   public R isExactType(Class<?> type) {
352      reqnn("type", type);
353      if (type != value().getClass())
354         throw error(MSG_exceptionWasNotExpectedType, cn(type), cn(value()));
355      return returns();
356   }
357
358   /**
359    * Asserts that this throwable exists.
360    *
361    * <h5 class='section'>Example:</h5>
362    * <p class='bjava'>
363    *    <jc>// Asserts that the specified method throws any exception.</jc>
364    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar()).isExists();
365    * </p>
366    *
367    * @return The fluent return object.
368    */
369   @Override
370   public R isExists() {
371      if (valueIsNull())
372         throw error(MSG_exceptionWasNotThrown);
373      return returns();
374   }
375
376   /**
377    * Asserts that this throwable is of the specified type.
378    *
379    * <h5 class='section'>Example:</h5>
380    * <p class='bjava'>
381    *    <jc>// Asserts that the specified method throws a RuntimeException. </jc>
382    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
383    *       .isType(RuntimeException.<jk>class</jk>);
384    * </p>
385    *
386    * @param parent The type.
387    * @return The fluent return object.
388    */
389   @Override
390   public R isType(Class<?> parent) {
391      reqnn("parent", parent);
392      if (! parent.isInstance(value()))
393         throw error(MSG_exceptionWasNotExpectedType, cn(parent), cn(value()));
394      return returns();
395   }
396
397   @Override /* Overridden from Assertion */
398   public FluentThrowableAssertion<T,R> setMsg(String msg, Object...args) {
399      super.setMsg(msg, args);
400      return this;
401   }
402
403   @Override /* Overridden from Assertion */
404   public FluentThrowableAssertion<T,R> setOut(PrintStream value) {
405      super.setOut(value);
406      return this;
407   }
408
409   @Override /* Overridden from Assertion */
410   public FluentThrowableAssertion<T,R> setSilent() {
411      super.setSilent();
412      return this;
413   }
414
415   @Override /* Overridden from Assertion */
416   public FluentThrowableAssertion<T,R> setStdOut() {
417      super.setStdOut();
418      return this;
419   }
420
421   @Override /* Overridden from Assertion */
422   public FluentThrowableAssertion<T,R> setThrowable(Class<? extends RuntimeException> value) {
423      super.setThrowable(value);
424      return this;
425   }
426
427   @Override
428   protected boolean equals(Object o1, Object o2) {
429      if (o1 instanceof Throwable o1t && o2 instanceof Throwable o2t)
430         return eq(o1t, o2t, (x, y) -> eq(x.getClass(), y.getClass()) && eq(x.getMessage(), y.getMessage()));
431      return super.equals(o1, o2);
432   }
433}