1 /****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one *
3 * or more contributor license agreements. See the NOTICE file *
4 * distributed with this work for additional information *
5 * regarding copyright ownership. The ASF licenses this file *
6 * to you under the Apache License, Version 2.0 (the *
7 * "License"); you may not use this file except in compliance *
8 * with the License. You may obtain a copy of the License at *
9 * *
10 * http://www.apache.org/licenses/LICENSE-2.0 *
11 * *
12 * Unless required by applicable law or agreed to in writing, *
13 * software distributed under the License is distributed on an *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15 * KIND, either express or implied. See the License for the *
16 * specific language governing permissions and limitations *
17 * under the License. *
18 ****************************************************************/
19
20 package org.apache.james.mpt;
21
22 import java.io.IOException;
23 import java.util.ArrayList;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.regex.Pattern;
27
28
29 /**
30 * A protocol session which can be run against a reader and writer, which checks
31 * the server response against the expected values. TODO make ProtocolSession
32 * itself be a permissible ProtocolElement, so that we can nest and reuse
33 * sessions.
34 *
35 * @author Darrell DeBoer <darrell@apache.org>
36 *
37 * @version $Revision: 776401 $
38 */
39 public class ProtocolSession implements ProtocolInteractor {
40 private boolean continued = false;
41
42 private boolean continuationExpected = false;
43
44 private int maxSessionNumber;
45
46 protected List<ProtocolElement> testElements = new ArrayList<ProtocolElement>();
47
48 private Iterator elementsIterator;
49
50 private Session[] sessions;
51
52 private ProtocolElement nextTest;
53
54 private boolean continueAfterFailure = false;
55
56 public final boolean isContinueAfterFailure() {
57 return continueAfterFailure;
58 }
59
60 public final void setContinueAfterFailure(boolean continueAfterFailure) {
61 this.continueAfterFailure = continueAfterFailure;
62 }
63
64 /**
65 * Returns the number of sessions required to run this ProtocolSession. If
66 * the number of readers and writers provided is less than this number, an
67 * exception will occur when running the tests.
68 */
69 public int getSessionCount() {
70 return maxSessionNumber + 1;
71 }
72
73 /**
74 * Executes the ProtocolSession in real time against the readers and writers
75 * supplied, writing client requests and reading server responses in the
76 * order that they appear in the test elements. The index of a reader/writer
77 * in the array corresponds to the number of the session. If an exception
78 * occurs, no more test elements are executed.
79 *
80 * @param sessions not null
81 */
82 public void runSessions(Session[] sessions) throws Exception {
83 this.sessions = sessions;
84 elementsIterator = testElements.iterator();
85 while (elementsIterator.hasNext()) {
86 Object obj = elementsIterator.next();
87 if (obj instanceof ProtocolElement) {
88 ProtocolElement test = (ProtocolElement) obj;
89 test.testProtocol(sessions, continueAfterFailure);
90 }
91 }
92 }
93
94 public void doContinue() {
95 try {
96 if (continuationExpected) {
97 continued = true;
98 while (elementsIterator.hasNext()) {
99 Object obj = elementsIterator.next();
100 if (obj instanceof ProtocolElement) {
101 nextTest = (ProtocolElement) obj;
102
103 if (!nextTest.isClient()) {
104 break;
105 }
106 nextTest.testProtocol(sessions, continueAfterFailure);
107 }
108 }
109 if (!elementsIterator.hasNext()) {
110 nextTest = null;
111 }
112 } else {
113 throw new RuntimeException("Unexpected continuation");
114 }
115 } catch (Exception e) {
116 throw new RuntimeException(e);
117 }
118 }
119
120 /**
121 * @see org.apache.james.mpt.ProtocolInteractor#CL(java.lang.String)
122 */
123 public void CL(String clientLine) {
124 testElements.add(new ClientRequest(clientLine));
125 }
126
127 /**
128 * @see org.apache.james.mpt.ProtocolInteractor#SL(java.lang.String, java.lang.String)
129 */
130 public void SL(String serverLine, String location) {
131 testElements.add(new ServerResponse(serverLine, location));
132 }
133
134 /**
135 * @see org.apache.james.mpt.ProtocolInteractor#SUB(java.util.List, java.lang.String)
136 */
137 public void SUB(List<String> serverLines, String location) {
138 testElements
139 .add(new ServerUnorderedBlockResponse(serverLines, location));
140 }
141
142 /**
143 * @see org.apache.james.mpt.ProtocolInteractor#CL(int, java.lang.String)
144 */
145 public void CL(int sessionNumber, String clientLine) {
146 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
147 testElements.add(new ClientRequest(sessionNumber, clientLine));
148 }
149
150 /**
151 * @see org.apache.james.mpt.ProtocolInteractor#CONT(int)
152 */
153 public void CONT(int sessionNumber) throws Exception {
154 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
155 testElements.add(new ContinuationElement(sessionNumber));
156 }
157
158 /**
159 * @see org.apache.james.mpt.ProtocolInteractor#SL(int, java.lang.String, java.lang.String, java.lang.String)
160 */
161 public void SL(int sessionNumber, String serverLine, String location,
162 String lastClientMessage) {
163 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
164 testElements.add(new ServerResponse(sessionNumber, serverLine,
165 location, lastClientMessage));
166 }
167
168 /**
169 * @see org.apache.james.mpt.ProtocolInteractor#SUB(int, java.util.List, java.lang.String, java.lang.String)
170 */
171 public void SUB(int sessionNumber, List<String> serverLines, String location,
172 String lastClientMessage) {
173 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
174 testElements.add(new ServerUnorderedBlockResponse(sessionNumber,
175 serverLines, location, lastClientMessage));
176 }
177
178 /**
179 * A client request, which write the specified message to a Writer.
180 */
181 private class ClientRequest implements ProtocolElement {
182 private int sessionNumber;
183
184 private String message;
185
186 /**
187 * Initialises the ClientRequest with the supplied message.
188 */
189 public ClientRequest(String message) {
190 this(-1, message);
191 }
192
193 /**
194 * Initialises the ClientRequest, with a message and session number.
195 *
196 * @param sessionNumber
197 * @param message
198 */
199 public ClientRequest(int sessionNumber, String message) {
200 this.sessionNumber = sessionNumber;
201 this.message = message;
202 }
203
204 /**
205 * Writes the request message to the PrintWriters. If the sessionNumber ==
206 * -1, the request is written to *all* supplied writers, otherwise, only
207 * the writer for this session is writted to.
208 *
209 * @throws Exception
210 */
211 public void testProtocol(Session[] sessions,
212 boolean continueAfterFailure) throws Exception {
213 if (sessionNumber < 0) {
214 for (int i = 0; i < sessions.length; i++) {
215 Session session = sessions[i];
216 writeMessage(session);
217 }
218 } else {
219 Session session = sessions[sessionNumber];
220 writeMessage(session);
221 }
222 }
223
224 private void writeMessage(Session session) throws Exception {
225 session.writeLine(message);
226 }
227
228 public boolean isClient() {
229 return true;
230 }
231
232 /**
233 * Constructs a <code>String</code> with all attributes
234 * in name = value format.
235 *
236 * @return a <code>String</code> representation
237 * of this object.
238 */
239 public String toString()
240 {
241 final String TAB = " ";
242
243 String retValue = "ClientRequest ( "
244 + "sessionNumber = " + this.sessionNumber + TAB
245 + "message = " + this.message + TAB
246 + " )";
247
248 return retValue;
249 }
250
251
252 }
253
254 /**
255 * Represents a single-line server response, which reads a line from a
256 * reader, and compares it with the defined regular expression definition of
257 * this line.
258 */
259 private class ServerResponse implements ProtocolElement {
260 private String lastClientMessage;
261
262 private int sessionNumber;
263
264 private String expectedLine;
265
266 protected String location;
267
268 /**
269 * Sets up a server response.
270 *
271 * @param expectedPattern
272 * A Perl regular expression pattern used to test the line
273 * recieved.
274 * @param location
275 * A descriptive value to use in error messages.
276 */
277 public ServerResponse(String expectedPattern, String location) {
278 this(-1, expectedPattern, location, null);
279 }
280
281 /**
282 * Sets up a server response.
283 *
284 * @param sessionNumber
285 * The number of session for a multi-session test
286 * @param expectedPattern
287 * A Perl regular expression pattern used to test the line
288 * recieved.
289 * @param location
290 * A descriptive value to use in error messages.
291 */
292 public ServerResponse(int sessionNumber, String expectedPattern,
293 String location, String lastClientMessage) {
294 this.sessionNumber = sessionNumber;
295 this.expectedLine = expectedPattern;
296 this.location = location;
297 this.lastClientMessage = lastClientMessage;
298 }
299
300 /**
301 * Reads a line from the supplied reader, and tests that it matches the
302 * expected regular expression. If the sessionNumber == -1, then all
303 * readers are tested, otherwise, only the reader for this session is
304 * tested.
305 *
306 * @param out
307 * Is ignored.
308 * @param in
309 * The server response is read from here.
310 * @throws InvalidServerResponseException
311 * If the actual server response didn't match the regular
312 * expression expected.
313 */
314 public void testProtocol(Session[] sessions,
315 boolean continueAfterFailure) throws Exception {
316 if (sessionNumber < 0) {
317 for (int i = 0; i < sessions.length; i++) {
318 Session session = sessions[i];
319 checkResponse(session, continueAfterFailure);
320 }
321 } else {
322 Session session = sessions[sessionNumber];
323 checkResponse(session, continueAfterFailure);
324 }
325 }
326
327 protected void checkResponse(Session session,
328 boolean continueAfterFailure) throws Exception {
329 String testLine = readLine(session);
330 if (!match(expectedLine, testLine)) {
331 String errMsg = "\nLocation: " + location + "\nLastClientMsg: "
332 + lastClientMessage + "\nExpected: '" + expectedLine
333 + "'\nActual : '" + testLine + "'";
334 if (continueAfterFailure) {
335 System.out.println(errMsg);
336 } else {
337 throw new InvalidServerResponseException(errMsg);
338 }
339 }
340 }
341
342 /**
343 * A convenience method which returns true if the actual string matches
344 * the expected regular expression.
345 *
346 * @param expected
347 * The regular expression used for matching.
348 * @param actual
349 * The actual message to match.
350 * @return <code>true</code> if the actual matches the expected.
351 */
352 protected boolean match(String expected, String actual) {
353 final boolean result = Pattern.matches(expected, actual);
354 return result;
355 }
356
357 /**
358 * Grabs a line from the server and throws an error message if it
359 * doesn't work out
360 *
361 * @return String of the line from the server
362 */
363 protected String readLine(Session session) throws Exception {
364 try {
365 return session.readLine();
366 } catch (IOException e) {
367 String errMsg = "\nLocation: " + location + "\nExpected: "
368 + expectedLine + "\nReason: Server Timeout.";
369 throw new InvalidServerResponseException(errMsg);
370 }
371 }
372
373 public boolean isClient() {
374 return false;
375 }
376
377 /**
378 * Constructs a <code>String</code> with all attributes
379 * in name = value format.
380 *
381 * @return a <code>String</code> representation
382 * of this object.
383 */
384 public String toString()
385 {
386 final String TAB = " ";
387
388 String result = "ServerResponse ( "
389 + "lastClientMessage = " + this.lastClientMessage + TAB
390 + "sessionNumber = " + this.sessionNumber + TAB
391 + "expectedLine = " + this.expectedLine + TAB
392 + "location = " + this.location + TAB
393 + " )";
394
395 return result;
396 }
397
398
399 }
400
401 /**
402 * Represents a set of lines which must be recieved from the server, in a
403 * non-specified order.
404 */
405 private class ServerUnorderedBlockResponse extends ServerResponse {
406 private List<String> expectedLines = new ArrayList<String>();
407
408 /**
409 * Sets up a ServerUnorderedBlockResponse with the list of expected
410 * lines.
411 *
412 * @param expectedLines
413 * A list containing a reqular expression for each expected
414 * line.
415 * @param location
416 * A descriptive location string for error messages.
417 */
418 public ServerUnorderedBlockResponse(List<String> expectedLines, String location) {
419 this(-1, expectedLines, location, null);
420 }
421
422 /**
423 * Sets up a ServerUnorderedBlockResponse with the list of expected
424 * lines.
425 *
426 * @param sessionNumber
427 * The number of the session to expect this block, for a
428 * multi-session test.
429 * @param expectedLines
430 * A list containing a reqular expression for each expected
431 * line.
432 * @param location
433 * A descriptive location string for error messages.
434 */
435 public ServerUnorderedBlockResponse(int sessionNumber,
436 List<String> expectedLines, String location, String lastClientMessage) {
437 super(sessionNumber, "<Unordered Block>", location,
438 lastClientMessage);
439 this.expectedLines = expectedLines;
440 }
441
442 /**
443 * Reads lines from the server response and matches them against the
444 * list of expected regular expressions. Each regular expression in the
445 * expected list must be matched by only one server response line.
446 *
447 * @param reader
448 * Server responses are read from here.
449 * @throws InvalidServerResponseException
450 * If a line is encountered which doesn't match one of the
451 * expected lines.
452 */
453 protected void checkResponse(Session session,
454 boolean continueAfterFailure) throws Exception {
455 List<String> testLines = new ArrayList<String>(expectedLines);
456 while (testLines.size() > 0) {
457 String actualLine = readLine(session);
458
459 boolean foundMatch = false;
460 for (int i = 0; i < testLines.size(); i++) {
461 String expected = (String) testLines.get(i);
462 if (match(expected, actualLine)) {
463 foundMatch = true;
464 testLines.remove(expected);
465 break;
466 }
467 }
468
469 if (!foundMatch) {
470 StringBuffer errMsg = new StringBuffer().append(
471 "\nLocation: ").append(location).append(
472 "\nExpected one of: ");
473 Iterator iter = expectedLines.iterator();
474 while (iter.hasNext()) {
475 errMsg.append("\n ");
476 errMsg.append(iter.next());
477 }
478 errMsg.append("\nActual: ").append(actualLine);
479 if (continueAfterFailure) {
480 System.out.println(errMsg.toString());
481 } else {
482 throw new InvalidServerResponseException(errMsg
483 .toString());
484 }
485 }
486 }
487 }
488
489 /**
490 * Constructs a <code>String</code> with all attributes
491 * in name = value format.
492 *
493 * @return a <code>String</code> representation
494 * of this object.
495 */
496 public String toString()
497 {
498 final String TAB = " ";
499
500 String result = "ServerUnorderedBlockResponse ( "
501 + "expectedLines = " + this.expectedLines + TAB
502 + " )";
503
504 return result;
505 }
506
507
508 }
509
510 private class ContinuationElement implements ProtocolElement {
511
512 private final int sessionNumber;
513
514 public ContinuationElement(final int sessionNumber) throws Exception {
515 this.sessionNumber = sessionNumber < 0 ? 0 : sessionNumber;
516 }
517
518 public void testProtocol(Session[] sessions,
519 boolean continueAfterFailure) throws Exception {
520 Session session = sessions[sessionNumber];
521 continuationExpected = true;
522 continued = false;
523 String testLine = session.readLine();
524 if (!"+".equals(testLine) || !continued) {
525 final String message = "Expected continuation";
526 if (continueAfterFailure) {
527 System.out.print(message);
528 } else {
529 throw new InvalidServerResponseException(message);
530 }
531 }
532 continuationExpected = false;
533 continued = false;
534
535 if (nextTest != null) {
536 nextTest.testProtocol(sessions, continueAfterFailure);
537 }
538 }
539
540 public boolean isClient() {
541 return false;
542 }
543
544 /**
545 * Constructs a <code>String</code> with all attributes
546 * in name = value format.
547 *
548 * @return a <code>String</code> representation
549 * of this object.
550 */
551 public String toString()
552 {
553 final String TAB = " ";
554
555 String result = "ContinuationElement ( "
556 + "sessionNumber = " + this.sessionNumber + TAB
557 + " )";
558
559 return result;
560 }
561
562
563 }
564
565 /**
566 * Represents a generic protocol element, which may write requests to the
567 * server, read responses from the server, or both. Implementations should
568 * test the server response against an expected response, and throw an
569 * exception on mismatch.
570 */
571 private interface ProtocolElement {
572 /**
573 * Executes the ProtocolElement against the supplied session.
574 *
575 * @param continueAfterFailure true when the execution should continue,
576 * false otherwise
577 * @throws Exception
578 */
579 void testProtocol(Session[] sessions,
580 boolean continueAfterFailure) throws Exception;
581
582 boolean isClient();
583 }
584
585 /**
586 * Constructs a <code>String</code> with all attributes
587 * in name = value format.
588 *
589 * @return a <code>String</code> representation
590 * of this object.
591 */
592 public String toString()
593 {
594 final String TAB = " ";
595
596 String result = "ProtocolSession ( "
597 + "continued = " + this.continued + TAB
598 + "continuationExpected = " + this.continuationExpected + TAB
599 + "maxSessionNumber = " + this.maxSessionNumber + TAB
600 + "testElements = " + this.testElements + TAB
601 + "elementsIterator = " + this.elementsIterator + TAB
602 + "sessions = " + this.sessions + TAB
603 + "nextTest = " + this.nextTest + TAB
604 + "continueAfterFailure = " + this.continueAfterFailure + TAB
605 + " )";
606
607 return result;
608 }
609
610
611 }