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