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: 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 }