001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mpt.protocol;
021
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.TimeUnit;
029import java.util.regex.Pattern;
030
031import org.apache.james.mpt.api.ProtocolInteractor;
032import org.apache.james.mpt.api.Session;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import com.google.common.base.Stopwatch;
037
038/**
039 * A protocol session which can be run against a reader and writer, which checks
040 * the server response against the expected values. TODO make ProtocolSession
041 * itself be a permissible ProtocolElement, so that we can nest and reuse
042 * sessions.
043 * 
044 * @author Darrell DeBoer <darrell@apache.org>
045 */
046public class ProtocolSession implements ProtocolInteractor {
047
048    private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolSession.class);
049
050    private boolean continued = false;
051
052    private boolean continuationExpected = false;
053
054    private int maxSessionNumber;
055
056    protected List<ProtocolElement> testElements = new ArrayList<ProtocolElement>();
057
058    private Iterator<ProtocolElement> elementsIterator;
059
060    private Session[] sessions;
061
062    private ProtocolElement nextTest;
063
064    private boolean continueAfterFailure = false;
065
066    private Map<String, Stopwatch> timers = new HashMap<String, Stopwatch>();
067
068    public final boolean isContinueAfterFailure() {
069        return continueAfterFailure;
070    }
071
072    enum LolLevel {
073        Debug,
074        Info,
075        Warn,
076        Err
077    }
078
079    public final void setContinueAfterFailure(boolean continueAfterFailure) {
080        this.continueAfterFailure = continueAfterFailure;
081    }
082
083    /**
084     * Returns the number of sessions required to run this ProtocolSession. If
085     * the number of readers and writers provided is less than this number, an
086     * exception will occur when running the tests.
087     */
088    public int getSessionCount() {
089        return maxSessionNumber + 1;
090    }
091
092    /**
093     * Executes the ProtocolSession in real time against the readers and writers
094     * supplied, writing client requests and reading server responses in the
095     * order that they appear in the test elements. The index of a reader/writer
096     * in the array corresponds to the number of the session. If an exception
097     * occurs, no more test elements are executed.
098     * 
099     * @param out
100     *            The client requests are written to here.
101     * @param in
102     *            The server responses are read from here.
103     */
104    public void runSessions(Session[] sessions) throws Exception {
105        this.sessions = sessions;
106        elementsIterator = testElements.iterator();
107        while (elementsIterator.hasNext()) {
108            Object obj = elementsIterator.next();
109            if (obj instanceof ProtocolElement) {
110                ProtocolElement test = (ProtocolElement) obj;
111                test.testProtocol(sessions, continueAfterFailure);
112            }
113        }
114    }
115
116    public void doContinue() {
117        try {
118            if (continuationExpected) {
119                continued = true;
120                while (elementsIterator.hasNext()) {
121                    Object obj = elementsIterator.next();
122                    if (obj instanceof ProtocolElement) {
123                        nextTest = (ProtocolElement) obj;
124
125                        if (!nextTest.isClient()) {
126                            break;
127                        }
128                        nextTest.testProtocol(sessions, continueAfterFailure);
129                    }
130                }
131                if (!elementsIterator.hasNext()) {
132                    nextTest = null;
133                }
134            }
135            else {
136                throw new RuntimeException("Unexpected continuation");
137            }
138        }
139        catch (Exception e) {
140            throw new RuntimeException(e);
141        }
142    }
143
144    /**
145     * adds a new Client request line to the test elements
146     */
147    public void CL(String clientLine) {
148        testElements.add(new ClientRequest(clientLine));
149    }
150
151    /**
152     * adds a new Server Response line to the test elements, with the specified
153     * location.
154     */
155    public void SL(String serverLine, String location) {
156        testElements.add(new ServerResponse(serverLine, location));
157    }
158
159    /**
160     * adds a new Server Unordered Block to the test elements.
161     */
162    public void SUB(List<String> serverLines, String location) {
163        testElements.add(new ServerUnorderedBlockResponse(serverLines, location));
164    }
165
166    /**
167     * adds a new Client request line to the test elements
168     */
169    public void CL(int sessionNumber, String clientLine) {
170        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
171        testElements.add(new ClientRequest(sessionNumber, clientLine));
172    }
173
174    /**
175     * Adds a continuation. To allow one thread to be used for testing.
176     */
177    public void CONT(int sessionNumber) throws Exception {
178        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
179        testElements.add(new ContinuationElement(sessionNumber));
180    }
181
182    /**
183     * adds a new Server Response line to the test elements, with the specified
184     * location.
185     */
186    public void SL(int sessionNumber, String serverLine, String location, String lastClientMessage) {
187        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
188        testElements.add(new ServerResponse(sessionNumber, serverLine, location, lastClientMessage));
189    }
190
191    /**
192     * adds a new Server Unordered Block to the test elements.
193     */
194    public void SUB(int sessionNumber, List<String> serverLines, String location, String lastClientMessage) {
195        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
196        testElements.add(new ServerUnorderedBlockResponse(sessionNumber, serverLines, location, lastClientMessage));
197    }
198
199    /**
200     * adds a Wait condition
201     */
202    public void WAIT(int sessionNumber, long timeToWaitInMs) {
203        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
204        testElements.add(new WaitElement(timeToWaitInMs));
205    }
206
207    public void LOG(int sessionNumber, LolLevel level, String message) {
208        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
209        testElements.add(new LogElement(level, message));
210    }
211
212    public void REINIT(int sessionNumber) {
213        this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber);
214        testElements.add(new ReinitElement(sessionNumber));
215    }
216
217    public void TIMER(TimerCommand timerCommand, String timerName) {
218        testElements.add(new TimerElement(timerCommand, timerName));
219    }
220
221    /**
222     * A client request, which write the specified message to a Writer.
223     */
224    private static class ClientRequest implements ProtocolElement {
225        private final int sessionNumber;
226
227        private final String message;
228
229        /**
230         * Initialises the ClientRequest with the supplied message.
231         */
232        public ClientRequest(String message) {
233            this(-1, message);
234        }
235
236        /**
237         * Initialises the ClientRequest, with a message and session number.
238         * 
239         * @param sessionNumber
240         * @param message
241         */
242        public ClientRequest(int sessionNumber, String message) {
243            this.sessionNumber = sessionNumber;
244            this.message = message;
245        }
246
247        /**
248         * Writes the request message to the PrintWriters. If the sessionNumber
249         * == -1, the request is written to *all* supplied writers, otherwise,
250         * only the writer for this session is writted to.
251         * 
252         * @throws Exception
253         */
254        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
255            if (sessionNumber < 0) {
256                for (Session session : sessions) {
257                    writeMessage(session);
258                }
259            }
260            else {
261                Session session = sessions[sessionNumber];
262                writeMessage(session);
263            }
264        }
265
266        private void writeMessage(Session session) throws Exception {
267            session.writeLine(message);
268        }
269
270        public boolean isClient() {
271            return true;
272        }
273    }
274
275    /**
276     * Represents a single-line server response, which reads a line from a
277     * reader, and compares it with the defined regular expression definition of
278     * this line.
279     */
280    private class ServerResponse implements ProtocolElement {
281        private final String lastClientMessage;
282
283        private final int sessionNumber;
284
285        private final String expectedLine;
286
287        protected String location;
288
289        /**
290         * Sets up a server response.
291         * 
292         * @param expectedPattern
293         *            A Perl regular expression pattern used to test the line
294         *            recieved.
295         * @param location
296         *            A descriptive value to use in error messages.
297         */
298        public ServerResponse(String expectedPattern, String location) {
299            this(-1, expectedPattern, location, null);
300        }
301
302        /**
303         * Sets up a server response.
304         * 
305         * @param sessionNumber
306         *            The number of session for a multi-session test
307         * @param expectedPattern
308         *            A Perl regular expression pattern used to test the line
309         *            recieved.
310         * @param location
311         *            A descriptive value to use in error messages.
312         */
313        public ServerResponse(int sessionNumber, String expectedPattern, String location, String lastClientMessage) {
314            this.sessionNumber = sessionNumber;
315            this.expectedLine = expectedPattern;
316            this.location = location;
317            this.lastClientMessage = lastClientMessage;
318        }
319
320        /**
321         * Reads a line from the supplied reader, and tests that it matches the
322         * expected regular expression. If the sessionNumber == -1, then all
323         * readers are tested, otherwise, only the reader for this session is
324         * tested.
325         * 
326         * @param out
327         *            Is ignored.
328         * @param in
329         *            The server response is read from here.
330         * @throws InvalidServerResponseException
331         *             If the actual server response didn't match the regular
332         *             expression expected.
333         */
334        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
335            if (sessionNumber < 0) {
336                for (Session session : sessions) {
337                    checkResponse(session, continueAfterFailure);
338                }
339            }
340            else {
341                Session session = sessions[sessionNumber];
342                checkResponse(session, continueAfterFailure);
343            }
344        }
345
346        protected void checkResponse(Session session, boolean continueAfterFailure) throws Exception {
347            String testLine = readLine(session);
348            if (!match(expectedLine, testLine)) {
349                String errMsg = "\nLocation: " + location + "\nLastClientMsg: " + lastClientMessage + "\nExpected: '"
350                        + expectedLine + "'\nActual   : '" + testLine + "'";
351                if (continueAfterFailure) {
352                    System.out.println(errMsg);
353                }
354                else {
355                    throw new InvalidServerResponseException(errMsg);
356                }
357            }
358        }
359
360        /**
361         * A convenience method which returns true if the actual string matches
362         * the expected regular expression.
363         * 
364         * @param expected
365         *            The regular expression used for matching.
366         * @param actual
367         *            The actual message to match.
368         * @return <code>true</code> if the actual matches the expected.
369         */
370        protected boolean match(String expected, String actual) {
371            return Pattern.matches(expected, actual);
372        }
373
374        /**
375         * Grabs a line from the server and throws an error message if it
376         * doesn't work out
377         * 
378         * @return String of the line from the server
379         */
380        protected String readLine(Session session) throws Exception {
381            try {
382                return session.readLine();
383            }
384            catch (IOException e) {
385                String errMsg = "\nLocation: " + location + "\nExpected: " + expectedLine + "\nReason: Server Timeout.";
386                throw new InvalidServerResponseException(errMsg);
387            }
388        }
389
390        public boolean isClient() {
391            return false;
392        }
393    }
394
395    /**
396     * Represents a set of lines which must be recieved from the server, in a
397     * non-specified order.
398     */
399    private class ServerUnorderedBlockResponse extends ServerResponse {
400        private List<String> expectedLines = new ArrayList<String>();
401
402        /**
403         * Sets up a ServerUnorderedBlockResponse with the list of expected
404         * lines.
405         * 
406         * @param expectedLines
407         *            A list containing a reqular expression for each expected
408         *            line.
409         * @param location
410         *            A descriptive location string for error messages.
411         */
412        public ServerUnorderedBlockResponse(List<String> expectedLines, String location) {
413            this(-1, expectedLines, location, null);
414        }
415
416        /**
417         * Sets up a ServerUnorderedBlockResponse with the list of expected
418         * lines.
419         * 
420         * @param sessionNumber
421         *            The number of the session to expect this block, for a
422         *            multi-session test.
423         * @param expectedLines
424         *            A list containing a reqular expression for each expected
425         *            line.
426         * @param location
427         *            A descriptive location string for error messages.
428         */
429        public ServerUnorderedBlockResponse(int sessionNumber, List<String> expectedLines, String location,
430                String lastClientMessage) {
431            super(sessionNumber, "<Unordered Block>", location, lastClientMessage);
432            this.expectedLines = expectedLines;
433        }
434
435        /**
436         * Reads lines from the server response and matches them against the
437         * list of expected regular expressions. Each regular expression in the
438         * expected list must be matched by only one server response line.
439         * 
440         * @param reader
441         *            Server responses are read from here.
442         * @throws InvalidServerResponseException
443         *             If a line is encountered which doesn't match one of the
444         *             expected lines.
445         */
446        protected void checkResponse(Session session, boolean continueAfterFailure) throws Exception {
447            List<String> testLines = new ArrayList<String>(expectedLines);
448            while (testLines.size() > 0) {
449                String actualLine = readLine(session);
450
451                boolean foundMatch = false;
452                for (int i = 0; i < testLines.size(); i++) {
453                    String expected = (String) testLines.get(i);
454                    if (match(expected, actualLine)) {
455                        foundMatch = true;
456                        testLines.remove(expected);
457                        break;
458                    }
459                }
460
461                if (!foundMatch) {
462                    StringBuffer errMsg = new StringBuffer().append("\nLocation: ").append(location)
463                            .append("\nExpected one of: ");
464                    Iterator<String> iter = expectedLines.iterator();
465                    while (iter.hasNext()) {
466                        errMsg.append("\n    ");
467                        errMsg.append(iter.next());
468                    }
469                    errMsg.append("\nActual: ").append(actualLine);
470                    if (continueAfterFailure) {
471                        System.out.println(errMsg.toString());
472                    }
473                    else {
474                        throw new InvalidServerResponseException(errMsg.toString());
475                    }
476                }
477            }
478        }
479    }
480
481    private class ContinuationElement implements ProtocolElement {
482
483        private final int sessionNumber;
484
485        public ContinuationElement(int sessionNumber) throws Exception {
486            this.sessionNumber = Math.max(0, sessionNumber);
487        }
488
489        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
490            Session session = sessions[sessionNumber];
491            continuationExpected = true;
492            continued = false;
493            String testLine = session.readLine();
494            if (!"+".equals(testLine) || !continued) {
495                final String message = "Expected continuation";
496                if (continueAfterFailure) {
497                    System.out.print(message);
498                }
499                else {
500                    throw new InvalidServerResponseException(message);
501                }
502            }
503            continuationExpected = false;
504            continued = false;
505
506            if (nextTest != null) {
507                nextTest.testProtocol(sessions, continueAfterFailure);
508            }
509        }
510
511        public boolean isClient() {
512            return false;
513        }
514    }
515
516    private class ReinitElement implements ProtocolElement {
517
518        private final int sessionNumber;
519
520        public ReinitElement(int sessionNumber) {
521            this.sessionNumber = Math.max(0, sessionNumber);
522        }
523
524        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
525            Session session = sessions[sessionNumber];
526            session.restart();
527        }
528
529        public boolean isClient() {
530            return false;
531        }
532    }
533
534    protected enum TimerCommand {
535        START, PRINT, RESET;
536
537        public static TimerCommand from(String value) throws InvalidServerResponseException {
538            if (value.equalsIgnoreCase("start")) {
539                return START;
540            }
541            if (value.equalsIgnoreCase("print")) {
542                return PRINT;
543            }
544            if (value.equalsIgnoreCase("reset")) {
545                return RESET;
546            }
547            throw new InvalidServerResponseException("Invalid TIMER command '" + value + "'");
548        }
549    }
550
551    private class TimerElement implements ProtocolElement {
552
553        private TimerCommand timerCommand;
554        private String timerName;
555
556        public TimerElement(TimerCommand timerCommand, String timerName) {
557            this.timerCommand = timerCommand;
558            this.timerName = timerName;
559        }
560
561        @Override
562        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
563            switch(timerCommand) {
564            case START:
565                start();
566                break;
567            case PRINT:
568                print();
569                break;
570            case RESET:
571                reset();
572                break;
573            default:
574                throw new InvalidServerResponseException("Invalid TIMER command '" + timerCommand + "' for timer name: '" + timerName + "'");
575            }
576        }
577
578        private void start() {
579            timers.put(timerName, Stopwatch.createStarted());
580        }
581
582        private void print() throws InvalidServerResponseException {
583            Stopwatch stopwatch = timers.get(timerName);
584            if (stopwatch == null) {
585                throw new InvalidServerResponseException("TIMER '" + timerName + "' undefined");
586            }
587            LOGGER.info("Time spent in '" + timerName + "': " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + " ms");
588        }
589
590        private void reset() throws InvalidServerResponseException {
591            Stopwatch stopwatch = timers.get(timerName);
592            if (stopwatch == null) {
593                throw new InvalidServerResponseException("TIMER '" + timerName + "' undefined");
594            }
595            stopwatch.reset();
596            stopwatch.start();
597        }
598
599        @Override
600        public boolean isClient() {
601            return false;
602        }
603    }
604
605    /**
606     * Allow you to wait a given time at a given point of the test script
607     */
608    private class WaitElement implements ProtocolElement {
609
610        private final long timeToWaitInMs;
611
612        public WaitElement(long timeToWaitInMs) {
613            this.timeToWaitInMs = timeToWaitInMs;
614        }
615
616        @Override
617        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
618            Thread.sleep(timeToWaitInMs);
619        }
620
621        @Override
622        public boolean isClient() {
623            return false;
624        }
625    }
626
627    /**
628     * Allow you to wait a given time at a given point of the test script
629     */
630    private class LogElement implements ProtocolElement {
631
632        private final LolLevel level;
633        private final String message;
634
635        public LogElement(LolLevel level, String message) {
636            this.level = level;
637            this.message = message;
638        }
639
640        @Override
641        public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception {
642            switch (level) {
643            case Debug:
644                LOGGER.debug(message);
645                break;
646            case Info:
647                LOGGER.info(message);
648                break;
649            case Warn:
650                LOGGER.warn(message);
651                break;
652            case Err:
653                LOGGER.error(message);
654                break;
655            }
656        }
657
658        @Override
659        public boolean isClient() {
660            return false;
661        }
662    }
663
664    /**
665     * Represents a generic protocol element, which may write requests to the
666     * server, read responses from the server, or both. Implementations should
667     * test the server response against an expected response, and throw an
668     * exception on mismatch.
669     */
670    private interface ProtocolElement {
671        /**
672         * Executes the ProtocolElement against the supplied session.
673         * 
674         * @param continueAfterFailure
675         *            TODO
676         * @throws Exception
677         */
678        void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception;
679
680        boolean isClient();
681    }
682
683    /**
684     * An exception which is thrown when the actual response from a server is
685     * different from that expected.
686     */
687    @SuppressWarnings("serial")
688    public static class InvalidServerResponseException extends Exception {
689        public InvalidServerResponseException(String message) {
690            super(message);
691        }
692    }
693
694    /**
695     * Constructs a <code>String</code> with all attributes in name = value
696     * format.
697     * 
698     * @return a <code>String</code> representation of this object.
699     */
700    public String toString() {
701        final String TAB = " ";
702
703        String result = "ProtocolSession ( " + "continued = " + this.continued + TAB + "continuationExpected = "
704                + this.continuationExpected + TAB + "maxSessionNumber = " + this.maxSessionNumber + TAB
705                + "testElements = " + this.testElements + TAB + "elementsIterator = " + this.elementsIterator + TAB
706                + "sessions = " + this.sessions + TAB + "nextTest = " + this.nextTest + TAB + "continueAfterFailure = "
707                + this.continueAfterFailure + TAB + " )";
708
709        return result;
710    }
711
712}