1
2
3
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
32
33
34
35
36 @SuppressWarnings("PMD.TooManyMethods")
37 final class MkGrizzlyAdapter extends HttpHandler {
38
39
40
41
42 private static final String ENCODING = "UTF-8";
43
44
45
46
47 private final transient Queue<QueryWithAnswer> queue =
48 new ConcurrentLinkedQueue<>();
49
50
51
52
53 private final transient Queue<Conditional> conditionals =
54 new ConcurrentLinkedQueue<>();
55
56
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
79
80
81
82
83
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
94
95
96 public MkQuery take() {
97 return this.queue.remove().que;
98 }
99
100
101
102
103
104
105
106
107 public MkQuery take(final Matcher<MkAnswer> matcher) {
108 return this.takeMatching(matcher).next();
109 }
110
111
112
113
114
115
116
117
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
130
131
132 public int queries() {
133 return this.queue.size();
134 }
135
136
137
138
139
140
141
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
154
155
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
251
252
253
254 @EqualsAndHashCode(of = {"answr", "condition"})
255 private static final class Conditional {
256
257
258
259 private final transient MkAnswer answr;
260
261
262
263
264 private final transient Matcher<MkQuery> condition;
265
266
267
268
269 private final transient AtomicInteger count;
270
271
272
273
274
275
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
288
289
290 public MkAnswer answer() {
291 return this.answr;
292 }
293
294
295
296
297
298
299 public boolean matches(final MkQuery query) {
300 return this.condition.matches(query);
301 }
302
303
304
305
306
307 public int decrement() {
308 return this.count.decrementAndGet();
309 }
310
311
312
313
314
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
329
330
331
332 @EqualsAndHashCode(of = {"answr", "que"})
333 private static final class QueryWithAnswer {
334
335
336
337 private final transient MkAnswer answr;
338
339
340
341
342 private final transient MkQuery que;
343
344
345
346
347
348
349 QueryWithAnswer(final MkQuery qry, final MkAnswer ans) {
350 this.answr = ans;
351 this.que = qry;
352 }
353
354
355
356
357
358 public MkQuery query() {
359 return this.que;
360 }
361
362
363
364
365
366 public MkAnswer answer() {
367 return this.answr;
368 }
369 }
370
371
372
373
374
375
376 @RequiredArgsConstructor
377 private static final class MkQueryIterator implements Iterator<MkQuery> {
378
379
380
381
382 private final Queue<MkQuery> results = new LinkedList<>();
383
384
385
386
387 private final Iterator<QueryWithAnswer> iter;
388
389
390
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 }