View Javadoc

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     /* (non-Javadoc)
124      * @see org.apache.james.mpt.ProtocolScript#CL(java.lang.String)
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 }