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