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