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.assertions;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.commons.utils.AssertionUtils.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
024
025import java.io.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.juneau.cp.*;
030import org.apache.juneau.serializer.*;
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#isJson(String) isJson(String)}
050 *       <li class='jm'>{@link FluentObjectAssertion#isSame(Object) isSame(Object)}
051 *       <li class='jm'>{@link FluentObjectAssertion#isSameJsonAs(Object) isSameJsonAs(Object)}
052 *       <li class='jm'>{@link FluentObjectAssertion#isSameSortedJsonAs(Object) isSameSortedJsonAs(Object)}
053 *       <li class='jm'>{@link FluentObjectAssertion#isSameSerializedAs(Object, WriterSerializer) isSameSerializedAs(Object, WriterSerializer)}
054 *       <li class='jm'>{@link FluentObjectAssertion#isType(Class) isType(Class)}
055 *       <li class='jm'>{@link FluentObjectAssertion#isExactType(Class) isExactType(Class)}
056 *    </ul>
057 * </ul>
058 *
059 * <h5 class='section'>Transform Methods:</h5>
060 * <p>
061 * <ul class='javatree'>
062 *    <li class='jc'>{@link FluentThrowableAssertion}
063 *    <ul class='javatreec'>
064 *       <li class='jm'>{@link FluentThrowableAssertion#asMessage() asMessage()}
065 *       <li class='jm'>{@link FluentThrowableAssertion#asMessages() asMessages()}
066 *       <li class='jm'>{@link FluentThrowableAssertion#asLocalizedMessage() asLocalizedMessage()}
067 *       <li class='jm'>{@link FluentThrowableAssertion#asLocalizedMessages() asLocalizedMessages()}
068 *       <li class='jm'>{@link FluentThrowableAssertion#asStackTrace() asStackTrace()}
069 *       <li class='jm'>{@link FluentThrowableAssertion#asCausedBy() asCausedBy()}
070 *       <li class='jm'>{@link FluentThrowableAssertion#asCausedBy(Class) asCausedBy(Class)}
071 *       <li class='jm'>{@link FluentThrowableAssertion#asFind(Class) asFind(Class)}
072 *    </ul>
073 *    <li class='jc'>{@link FluentObjectAssertion}
074 *    <ul class='javatreec'>
075 *       <li class='jm'>{@link FluentObjectAssertion#asString() asString()}
076 *       <li class='jm'>{@link FluentObjectAssertion#asString(WriterSerializer) asString(WriterSerializer)}
077 *       <li class='jm'>{@link FluentObjectAssertion#asString(Function) asString(Function)}
078 *       <li class='jm'>{@link FluentObjectAssertion#asJson() asJson()}
079 *       <li class='jm'>{@link FluentObjectAssertion#asJsonSorted() asJsonSorted()}
080 *       <li class='jm'>{@link FluentObjectAssertion#asTransformed(Function) asApplied(Function)}
081 *       <li class='jm'>{@link FluentObjectAssertion#asAny() asAny()}
082 * </ul>
083 * </ul>
084 *
085 * <h5 class='section'>Configuration Methods:</h5>
086 * <p>
087 * <ul class='javatree'>
088 *    <li class='jc'>{@link Assertion}
089 *    <ul class='javatreec'>
090 *       <li class='jm'>{@link Assertion#setMsg(String, Object...) setMsg(String, Object...)}
091 *       <li class='jm'>{@link Assertion#setOut(PrintStream) setOut(PrintStream)}
092 *       <li class='jm'>{@link Assertion#setSilent() setSilent()}
093 *       <li class='jm'>{@link Assertion#setStdOut() setStdOut()}
094 *       <li class='jm'>{@link Assertion#setThrowable(Class) setThrowable(Class)}
095*  </ul>
096 * </ul>
097 *
098 * <h5 class='section'>See Also:</h5><ul>
099 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauEcosystemOverview">Juneau Ecosystem Overview</a>
100 * </ul>
101 *
102 * @param <T> The throwable type.
103 * @param <R> The return type.
104 */
105public class FluentThrowableAssertion<T extends Throwable,R> extends FluentObjectAssertion<T,R> {
106
107   // @formatter:off
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   // @formatter:on
114
115   /**
116    * Chained constructor.
117    *
118    * <p>
119    * Used when transforming one assertion into another so that the assertion config can be used by the new assertion.
120    *
121    * @param creator
122    *    The assertion that created this assertion.
123    *    <br>Should be <jk>null</jk> if this is the top-level assertion.
124    * @param value
125    *    The object being tested.
126    *    <br>Can be <jk>null</jk>.
127    * @param returns
128    *    The object to return after a test method is called.
129    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
130    * used on the same assertion.
131    */
132   public FluentThrowableAssertion(Assertion creator, T value, R returns) {
133      super(creator, value, returns);
134   }
135
136   /**
137    * Constructor.
138    *
139    * @param value
140    *    The object being tested.
141    *    <br>Can be <jk>null</jk>.
142    * @param returns
143    *    The object to return after a test method is called.
144    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
145    * used on the same assertion.
146    */
147   public FluentThrowableAssertion(T value, R returns) {
148      this(null, value, returns);
149   }
150
151   /**
152    * Returns an assertion against the caused-by throwable.
153    *
154    * <h5 class='section'>Example:</h5>
155    * <p class='bjava'>
156    *    <jc>// Asserts that the specified method throws an exception whose
157    *    // caused-by message contains 'foobar'. </jc>
158    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
159    *       .asCausedBy()
160    *       .asMessage()
161    *       .isPattern(<js>"foobar"</js>);
162    * </p>
163    *
164    * @return An assertion against the caused-by.  Never <jk>null</jk>.
165    */
166   public FluentThrowableAssertion<Throwable,R> asCausedBy() {
167      return asCausedBy(Throwable.class);
168   }
169
170   /**
171    * Returns an assertion against the caused-by throwable.
172    *
173    * <h5 class='section'>Example:</h5>
174    * <p class='bjava'>
175    *    <jc>// Asserts that the specified method throws an exception whose
176    *    // caused-by message contains 'foobar'. </jc>
177    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
178    *       .asCausedBy(RuntimeException.<jk>class</jk>)
179    *       .asMessage()
180    *       .isPattern(<js>"foobar"</js>);
181    * </p>
182    *
183    * @param <X> The throwable type.
184    * @param type The expected exception type.
185    * @return An assertion against the caused-by.  Never <jk>null</jk>.
186    */
187   public <X extends Throwable> FluentThrowableAssertion<X,R> asCausedBy(Class<X> type) {
188      var t = map(Throwable::getCause).orElse(null);
189      if (t == null || type.isInstance(t))
190         return new FluentThrowableAssertion<>(this, type.cast(t), returns());
191      throw error(MSG_causedByExceptionNotExpectedType, cn(type), cn(t.getClass()));
192   }
193
194   /**
195    * Returns an assertion against the throwable localized message.
196    *
197    * <h5 class='section'>Example:</h5>
198    * <p class='bjava'>
199    *    <jc>// Asserts that the specified method throws an exception with a
200    *    // caused-by RuntimeException containing 'foobar'</jc>
201    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
202    *       .findCausedBy(RuntimeException.<jk>class</jk>)
203    *       .isExists()
204    *       .asMessage()
205    *       .isPattern(<js>"foobar"</js>);
206    * </p>
207    *
208    * @param <X> The throwable type.
209    * @param throwableClass The class type to search for in the caused-by chain.
210    * @return An assertion against the caused-by throwable.  Never <jk>null</jk>.
211    */
212   public <X extends Throwable> FluentThrowableAssertion<X,R> asFind(Class<X> throwableClass) {
213      Throwable t = orElse(null);
214      while (nn(t)) {
215         if (throwableClass.isInstance(t))
216            return new FluentThrowableAssertion<>(this, throwableClass.cast(t), returns());
217         t = t.getCause();
218      }
219      return new FluentThrowableAssertion<>(this, (X)null, returns());
220   }
221
222   /**
223    * Returns an assertion against the throwable localized message.
224    *
225    * <h5 class='section'>Example:</h5>
226    * <p class='bjava'>
227    *    <jc>// Asserts that the specified method throws an exception with
228    *    // 'foobar' somewhere in the localized messages. </jc>
229    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
230    *       .asLocalizedMessage()
231    *       .isPattern(<js>".*foobar.*"</js>);
232    * </p>
233    *
234    * @return An assertion against the throwable localized message.  Never <jk>null</jk>.
235    */
236   public FluentStringAssertion<R> asLocalizedMessage() {
237      return new FluentStringAssertion<>(this, map(Throwable::getLocalizedMessage).orElse(null), returns());
238   }
239
240   /**
241    * Returns an assertion against the throwable message and all caused-by messages.
242    *
243    * <h5 class='section'>Example:</h5>
244    * <p class='bjava'>
245    *    <jc>// Asserts that the specified method throws an exception with
246    *    // 'foobar' somewhere in the messages. </jc>
247    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
248    *       .asLocalizedMessages()
249    *       .isPattern(<js>".*foobar.*"</js>);
250    * </p>
251    *
252    * @return An assertion against the throwable message.  Never <jk>null</jk>.
253    */
254   public FluentListAssertion<String,R> asLocalizedMessages() {
255      var l = (List<String>)null;
256      Throwable t = orElse(null);
257      if (nn(t)) {
258         if (t.getCause() == null)
259            l = singletonList(t.getMessage());
260         else {
261            l = list();
262            while (nn(t)) {
263               l.add(lm(t));
264               t = t.getCause();
265            }
266         }
267      }
268      return new FluentListAssertion<>(this, l, returns());
269   }
270
271   /**
272    * Returns an assertion against the throwable message.
273    *
274    * <h5 class='section'>Example:</h5>
275    * <p class='bjava'>
276    *    <jc>// Asserts that the specified method throws an exception
277    * // with 'foobar' somewhere in the messages. </jc>
278    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
279    *       .asMessage()
280    *       .isPattern(<js>".*foobar.*"</js>);
281    * </p>
282    *
283    * @return An assertion against the throwable message.  Never <jk>null</jk>.
284    */
285   public FluentStringAssertion<R> asMessage() {
286      return new FluentStringAssertion<>(this, map(Throwable::getMessage).orElse(null), returns());
287   }
288
289   /**
290    * Returns an assertion against the throwable message and all caused-by messages.
291    *
292    * <h5 class='section'>Example:</h5>
293    * <p class='bjava'>
294    *    <jc>// Asserts that the specified method throws an exception with
295    *    // 'foobar' somewhere in the messages. </jc>
296    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
297    *       .asMessages()
298    *       .isPattern(<js>".*foobar.*"</js>);
299    * </p>
300    *
301    * @return An assertion against the throwable message.  Never <jk>null</jk>.
302    */
303   public FluentListAssertion<String,R> asMessages() {
304      var l = (List<String>)null;
305      Throwable t = orElse(null);
306      if (nn(t)) {
307         if (t.getCause() == null)
308            l = singletonList(t.getMessage());
309         else {
310            l = list();
311            while (nn(t)) {
312               l.add(t.getMessage());
313               t = t.getCause();
314            }
315         }
316      }
317      return new FluentListAssertion<>(this, l, returns());
318   }
319
320   /**
321    * Returns an assertion against the throwable localized message.
322    *
323    * <h5 class='section'>Example:</h5>
324    * <p class='bjava'>
325    *    <jc>// Asserts that the specified method throws an exception with
326    *    // 'foobar' somewhere in the stack trace. </jc>
327    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
328    *       .asStackTrace()
329    *       .isPattern(<js>"foobar"</js>);
330    * </p>
331    *
332    * @return An assertion against the throwable stacktrace.  Never <jk>null</jk>.
333    */
334   public FluentStringListAssertion<R> asStackTrace() {
335      return new FluentStringListAssertion<>(this, valueIsNull() ? null : l(getStackTrace(value())), returns());
336   }
337
338   @Override /* Overridden from FluentObjectAssertion */
339   public FluentThrowableAssertion<T,R> asTransformed(Function<T,T> function) { // NOSONAR - Intentional.
340      return new FluentThrowableAssertion<>(this, function.apply(orElse(null)), returns());
341   }
342
343   /**
344    * Asserts that this throwable is exactly the specified type.
345    *
346    * <h5 class='section'>Example:</h5>
347    * <p class='bjava'>
348    *    <jc>// Asserts that the specified method throws a RuntimeException. </jc>
349    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
350    *       .isExactType(RuntimeException.<jk>class</jk>);
351    * </p>
352    *
353    * @param type The type.
354    * @return The fluent return object.
355    */
356   @Override
357   public R isExactType(Class<?> type) {
358      assertArgNotNull("type", type);
359      if (type != value().getClass())
360         throw error(MSG_exceptionWasNotExpectedType, cn(type), cn(value()));
361      return returns();
362   }
363
364   /**
365    * Asserts that this throwable exists.
366    *
367    * <h5 class='section'>Example:</h5>
368    * <p class='bjava'>
369    *    <jc>// Asserts that the specified method throws any exception.</jc>
370    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar()).isExists();
371    * </p>
372    *
373    * @return The fluent return object.
374    */
375   @Override
376   public R isExists() {
377      if (valueIsNull())
378         throw error(MSG_exceptionWasNotThrown);
379      return returns();
380   }
381
382   /**
383    * Asserts that this throwable is of the specified type.
384    *
385    * <h5 class='section'>Example:</h5>
386    * <p class='bjava'>
387    *    <jc>// Asserts that the specified method throws a RuntimeException. </jc>
388    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
389    *       .isType(RuntimeException.<jk>class</jk>);
390    * </p>
391    *
392    * @param parent The type.
393    * @return The fluent return object.
394    */
395   @Override
396   public R isType(Class<?> parent) {
397      assertArgNotNull("parent", parent);
398      if (! parent.isInstance(value()))
399         throw error(MSG_exceptionWasNotExpectedType, cn(parent), cn(value()));
400      return returns();
401   }
402
403   @Override /* Overridden from Assertion */
404   public FluentThrowableAssertion<T,R> setMsg(String msg, Object...args) {
405      super.setMsg(msg, args);
406      return this;
407   }
408
409   @Override /* Overridden from Assertion */
410   public FluentThrowableAssertion<T,R> setOut(PrintStream value) {
411      super.setOut(value);
412      return this;
413   }
414
415   @Override /* Overridden from Assertion */
416   public FluentThrowableAssertion<T,R> setSilent() {
417      super.setSilent();
418      return this;
419   }
420
421   @Override /* Overridden from Assertion */
422   public FluentThrowableAssertion<T,R> setStdOut() {
423      super.setStdOut();
424      return this;
425   }
426
427   @Override /* Overridden from Assertion */
428   public FluentThrowableAssertion<T,R> setThrowable(Class<? extends java.lang.RuntimeException> value) {
429      super.setThrowable(value);
430      return this;
431   }
432
433   @Override
434   protected boolean equals(Object o1, Object o2) {
435      if (o1 instanceof Throwable o1t && o2 instanceof Throwable o2t)
436         return eq(o1t, o2t, (x, y) -> eq(x.getClass(), y.getClass()) && eq(x.getMessage(), y.getMessage()));
437      return super.equals(o1, o2);
438   }
439}