View Javadoc
1   /*
2    * Copyright (c) 2011-2022, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.http.mock;
31  
32  import com.jcabi.log.Logger;
33  import java.io.OutputStreamWriter;
34  import java.io.PrintWriter;
35  import java.io.UnsupportedEncodingException;
36  import java.net.HttpURLConnection;
37  import java.util.Collection;
38  import java.util.Iterator;
39  import java.util.LinkedList;
40  import java.util.NoSuchElementException;
41  import java.util.Queue;
42  import java.util.concurrent.ConcurrentLinkedQueue;
43  import java.util.concurrent.atomic.AtomicInteger;
44  import lombok.EqualsAndHashCode;
45  import lombok.RequiredArgsConstructor;
46  import org.apache.http.HttpHeaders;
47  import org.glassfish.grizzly.http.server.HttpHandler;
48  import org.glassfish.grizzly.http.server.Request;
49  import org.glassfish.grizzly.http.server.Response;
50  import org.hamcrest.Matcher;
51  
52  /**
53   * Mocker of Java Servlet container.
54   *
55   * @since 0.10
56   * @checkstyle ClassDataAbstractionCouplingCheck (300 lines)
57   */
58  @SuppressWarnings("PMD.TooManyMethods")
59  final class MkGrizzlyAdapter extends HttpHandler {
60  
61      /**
62       * The encoding to use.
63       */
64      private static final String ENCODING = "UTF-8";
65  
66      /**
67       * Queries received.
68       */
69      private final transient Queue<QueryWithAnswer> queue =
70          new ConcurrentLinkedQueue<>();
71  
72      /**
73       * Answers to give conditionally.
74       */
75      private final transient Queue<Conditional> conditionals =
76          new ConcurrentLinkedQueue<>();
77  
78      // @checkstyle ExecutableStatementCount (55 lines)
79      @Override
80      @SuppressWarnings
81          (
82              {
83                  "PMD.AvoidCatchingThrowable",
84                  "PMD.AvoidInstantiatingObjectsInLoops",
85                  "rawtypes"
86              }
87          )
88      public void service(
89          final Request request,
90          final Response response
91      ) {
92          try {
93              final MkQuery query = new GrizzlyQuery(request);
94              final Iterator<Conditional> iter = this.conditionals.iterator();
95              boolean matched = false;
96              while (iter.hasNext()) {
97                  final Conditional cond = iter.next();
98                  if (cond.matches(query)) {
99                      matched = true;
100                     final MkAnswer answer = cond.answer();
101                     this.queue.add(new QueryWithAnswer(query, answer));
102                     for (final String name : answer.headers().keySet()) {
103                         // @checkstyle NestedForDepth (3 lines)
104                         for (final String value : answer.headers().get(name)) {
105                             response.addHeader(name, value);
106                         }
107                     }
108                     response.addHeader(
109                         HttpHeaders.SERVER,
110                         String.format(
111                             "%s query #%d, %d answer(s) left",
112                             this.getClass().getName(),
113                             this.queue.size(), this.conditionals.size()
114                         )
115                     );
116                     response.setStatus(answer.status());
117                     final byte[] body = answer.bodyBytes();
118                     response.createOutputStream().write(body);
119                     response.setContentLength(body.length);
120                     if (cond.decrement() == 0) {
121                         iter.remove();
122                     }
123                     break;
124                 }
125             }
126             if (!matched) {
127                 throw new NoSuchElementException("No matching answers found.");
128             }
129             // @checkstyle IllegalCatch (1 line)
130         } catch (final Throwable ex) {
131             MkGrizzlyAdapter.fail(response, ex);
132         }
133     }
134 
135     /**
136      * Give this answer on the next request(s) if they match the given condition
137      * a certain number of consecutive times.
138      * @param answer Next answer to give
139      * @param query The query that should be satisfied to return this answer
140      * @param count The number of times this answer can be returned for matching
141      *  requests
142      */
143     public void next(
144         final MkAnswer answer, final Matcher<MkQuery> query,
145         final int count
146     ) {
147         this.conditionals.add(new Conditional(answer, query, count));
148     }
149 
150     /**
151      * Get the oldest request received.
152      * @return Request received
153      */
154     public MkQuery take() {
155         return this.queue.remove().que;
156     }
157 
158     /**
159      * Get the oldest request received subject to the matching condition.
160      * ({@link java.util.NoSuchElementException} if no elements satisfy the
161      * condition).
162      * @param matcher The matcher specifying the condition
163      * @return Request received satisfying the matcher
164      */
165     public MkQuery take(final Matcher<MkAnswer> matcher) {
166         return this.takeMatching(matcher).next();
167     }
168 
169     /**
170      * Get the all requests received satisfying the given matcher.
171      * ({@link java.util.NoSuchElementException} if no elements satisfy the
172      * condition).
173      * @param matcher The matcher specifying the condition
174      * @return Collection of all requests satisfying the matcher, ordered from
175      *  oldest to newest.
176      */
177     public Collection<MkQuery> takeAll(final Matcher<MkAnswer> matcher) {
178         final Collection<MkQuery> results = new LinkedList<>();
179         final Iterator<MkQuery> iter = this.takeMatching(matcher);
180         while (iter.hasNext()) {
181             results.add(iter.next());
182         }
183         return results;
184     }
185 
186     /**
187      * Total number of available queue.
188      * @return Number of them
189      */
190     public int queries() {
191         return this.queue.size();
192     }
193 
194     /**
195      * Get the all requests received satisfying the given matcher.
196      * ({@link java.util.NoSuchElementException} if no elements satisfy the
197      * condition).
198      * @param matcher The matcher specifying the condition
199      * @return Iterator over all requests
200      */
201     private Iterator<MkQuery> takeMatching(final Matcher<MkAnswer> matcher) {
202         final Iterator<QueryWithAnswer> iter = this.queue.iterator();
203         final Iterator<MkQuery> result = new MkQueryIterator(iter, matcher);
204         if (!result.hasNext()) {
205             throw new NoSuchElementException("No matching results found");
206         }
207         return result;
208     }
209 
210     /**
211      * Notify this response about failure.
212      * @param response The response to notify
213      * @param failure The failure just happened
214      */
215     private static void fail(
216         final Response response,
217         final Throwable failure
218     ) {
219         response.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
220         final PrintWriter writer;
221         try {
222             writer = new PrintWriter(
223                 new OutputStreamWriter(
224                     response.createOutputStream(),
225                     MkGrizzlyAdapter.ENCODING
226                 )
227             );
228         } catch (final UnsupportedEncodingException ex) {
229             throw new IllegalStateException(ex);
230         }
231         try {
232             writer.print(Logger.format("%[exception]s", failure));
233         } finally {
234             writer.close();
235         }
236     }
237 
238     /**
239      * Answer with condition.
240      *
241      * @since 1.5
242      */
243     @EqualsAndHashCode(of = {"answr", "condition"})
244     private static final class Conditional {
245         /**
246          * The MkAnswer.
247          */
248         private final transient MkAnswer answr;
249 
250         /**
251          * Condition for this answer.
252          */
253         private final transient Matcher<MkQuery> condition;
254 
255         /**
256          * The number of times the answer is expected to appear.
257          */
258         private final transient AtomicInteger count;
259 
260         /**
261          * Ctor.
262          * @param ans The answer.
263          * @param matcher The matcher.
264          * @param times Number of times the answer should appear.
265          */
266         Conditional(
267             final MkAnswer ans, final Matcher<MkQuery> matcher,
268             final int times
269         ) {
270             this.answr = ans;
271             this.condition = matcher;
272             this.count = Conditional.positiveAtomic(times);
273         }
274 
275         /**
276          * Get the answer.
277          * @return The answer
278          */
279         public MkAnswer answer() {
280             return this.answr;
281         }
282 
283         /**
284          * Does the query match the answer?
285          * @param query The query to match
286          * @return True, if the query matches the condition
287          */
288         public boolean matches(final MkQuery query) {
289             return this.condition.matches(query);
290         }
291 
292         /**
293          * Decrement the count for this conditional.
294          * @return The updated count
295          */
296         public int decrement() {
297             return this.count.decrementAndGet();
298         }
299 
300         /**
301          * Check if positive and convert to atomic.
302          * @param num Number
303          * @return Positive atomic integer
304          */
305         private static AtomicInteger positiveAtomic(final int num) {
306             if (num < 1) {
307                 throw new IllegalArgumentException(
308                     "Answer must be returned at least once."
309                 );
310             }
311             return new AtomicInteger(num);
312         }
313 
314     }
315 
316     /**
317      * Query with answer.
318      *
319      * @since 1.5
320      */
321     @EqualsAndHashCode(of = {"answr", "que"})
322     private static final class QueryWithAnswer {
323         /**
324          * The answer.
325          */
326         private final transient MkAnswer answr;
327 
328         /**
329          * The query.
330          */
331         private final transient MkQuery que;
332 
333         /**
334          * Ctor.
335          * @param qry The query
336          * @param ans The answer
337          */
338         QueryWithAnswer(final MkQuery qry, final MkAnswer ans) {
339             this.answr = ans;
340             this.que = qry;
341         }
342 
343         /**
344          * Get the query.
345          * @return The query.
346          */
347         public MkQuery query() {
348             return this.que;
349         }
350 
351         /**
352          * Get the answer.
353          * @return Answer
354          */
355         public MkAnswer answer() {
356             return this.answr;
357         }
358     }
359 
360     /**
361      * Iterator over matching answers.
362      *
363      * @since 1.17.3
364      */
365     @RequiredArgsConstructor
366     private static final class MkQueryIterator implements Iterator<MkQuery> {
367 
368         /**
369          * Queue of results.
370          */
371         private final Queue<MkQuery> results = new LinkedList<>();
372 
373         /**
374          * Original iterator.
375          */
376         private final Iterator<QueryWithAnswer> iter;
377 
378         /**
379          * Matcher.
380          */
381         private final Matcher<MkAnswer> matcher;
382 
383         @Override
384         public boolean hasNext() {
385             while (this.iter.hasNext()) {
386                 final QueryWithAnswer candidate = this.iter.next();
387                 if (this.matcher.matches(candidate.answer())) {
388                     this.results.add(candidate.query());
389                     this.iter.remove();
390                     break;
391                 }
392             }
393             return !this.results.isEmpty();
394         }
395 
396         @Override
397         public MkQuery next() {
398             if (this.results.isEmpty()) {
399                 throw new NoSuchElementException();
400             }
401             return this.results.remove();
402         }
403 
404         @Override
405         public void remove() {
406             this.results.remove();
407         }
408     }
409 }