All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kaazing.robot.driver.behavior.PlayBackScript Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014 "Kaazing Corporation," (www.kaazing.com)
 *
 * This file is part of Robot.
 *
 * Robot is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see .
 */

package org.kaazing.robot.driver.behavior;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.kaazing.robot.driver.behavior.handler.LogLastEventHandler;
import org.kaazing.robot.driver.behavior.visitor.GatherStreamsLocationVisitor.StreamResultLocationInfo;
import org.kaazing.robot.lang.LocationInfo;

// TODO: Re-write this monstrosity! :)

// @formatter:off
/**
 * Given an original script and a list of StreamResultLocationInfo's
 * PlayBackScript will construct the 'observed' script.
 *
 * PlayBackScript pbs = new PlayBackScript(
 * "connect tcp:...\nconnected\nclose\nclosed\n", streamResultLocInfoList );
 * String observedScript = pbs.createPlayBackScript;
 *
 * A StreamResultLocation info provides the locations of a stream in the
 * original script as well as the last location successfully executed, in the
 * form of start, end, and observed end LocationInfos. A StreamResultLocation
 * info with an observed point of null is special. It represents an accept
 * stream.
 *
 * The StreamResultLocationInfo list is expected to be in order from the first
 * stream to the last stream in the script. If not an assertion error will
 * occur.
 *
 * When observed.equals(end) the stream executed to completion and the stream is
 * copied verbatim into the observed script (start to end).
 *
 * When observed < end the stream only partially executed. The script is copied
 * verbatim from start to observed inclusive. Lines from observed + 1 to end are
 * omitted. However, the first line non-comment line will be replaced with the observed
 * deviated behavior: So for example given the script:
 *

 *
 * Assume that the last thing we did successfully was connected and a CLOSED
 * event was received before the write. There will be only 1
 * StreamResultLocationInfo for this script (since there is only one stream), it
 * would contain:
 *
 * start = 1:0 end = 6:0 observed = 2:0
 *
 * The resulting script will look like this
 *
 * connect tcp://localhost:8080\n
 * connected\n
 * CLOSED\n
 *
 *
 * In between streams and at the beginning and end of the script, white space
 * and comments are copied verbatim.
 *
 * When a stream fails entirely (say bind for an accept fails). We should not
 * have a StreamResultLocationInfo for the stream. The stream will be 'skipped'
 * in the same manner as described above when a stream was partially executed
 * (white space and comment lines are copied only).
 *
 * Today there is only support for Unix line ending in the script.
 */
/*
 * We apply a state pattern to do this work. We have four states BlankState,
 * CopyScriptState, SkipScriptState, and FinishState. Each state has a
 * transition method. The transition method will read the inputScript starting
 * at the last read location until it determines a transition is needed. It will
 * return a result string (observed) during that state transition. The
 * PlayBackScript simply acts as a state context and the createPlayBackScript
 * transitions states one at a time until the finish state is reached.
 *
 * BlankState -> This is the state we will be 'in between' streams and before
 * the first one. It simply copies the script verbatim until it sees a non-white
 * space character that isn't inside of a comment.
 *
 * CopyScriptState -> This is the state we will be in when we have found an
 * executed stream. We copy bytes verbatim in this state.
 *
 * SkipScriptState -> This is the state we will be in when a stream or some
 * portion of a stream was not observed. In this state ignore the original script characters
 *
 * FinishState -> In this state we have seen all observed streams. In some cases
 * there will be text at the stream that we have not read. Comments, whitespace,
 * and perhaps not executed streams. In this state we copy white space and
 * comment lines, ignoring other characters up to the end of the starting
 * script.
 */
// @formatter:on
public class PlayBackScript {

    public static final char COMMENT = '#';

    private final String startScript;
    private State my_state;
    private boolean inComment;
    private int atLine;
    private int lastColumnAt;
    private final Iterator currentLocationIterator;
    private final Map failedLocations;

    public PlayBackScript(String startScript, Iterable results) {
        this(startScript, results, new HashMap(0, 100));
    }

    public PlayBackScript(String startScript, Iterable results,
            Map failedLocations) {
        this.startScript = startScript;
        this.failedLocations = failedLocations;
        atLine = 0;
        currentLocationIterator = results.iterator();
        lastColumnAt = -1;
        setInComment(false);
        if (!currentLocationIterator.hasNext()) {
            /* If we have no locations then we are already done */
            setState(new FinishState(this.startScript, true));
        } else {
            StreamResultLocationInfo currentLocation = currentLocationIterator.next();
            setState(new BlankState(this.startScript, currentLocation, currentLocation.start));
        }
    }

    public String createPlayBackScript() {

        StringBuilder result = new StringBuilder();

        /* Transition until we find ourselves in the finish state */
        while (!(my_state instanceof FinishState)) {
            result.append(my_state.transition());
        }

        // Need to finish with the FinishState
        return result.append(my_state.transition()).toString();
    }

    private void setState(final State s) {
        my_state = s;
    }

    private boolean isInComment() {
        return inComment;
    }

    private void setInComment(boolean val) {
        inComment = val;
    }

    private interface State {
        String transition();
    }

    private abstract class AbstractState implements State {

        protected String originalScriptFragment;
        protected final StringBuilder observedStream = new StringBuilder();
        protected LocationInfo lookingForLocation;
        protected StreamResultLocationInfo currentStream;
        protected LocationInfo deviatedStreamStartLoc;
        protected boolean switchStates;
        protected boolean lineJustWhiteSpace;

        /*
         * Currently we always create this with a null originalScript. This is
         * because today we don't transition states immediately. We actually
         * read the remainder of the line before setting the new state.
         */
        public AbstractState(String originalScript, StreamResultLocationInfo loc, LocationInfo changeStateInfo) {
            this.originalScriptFragment = originalScript;
            this.lookingForLocation = changeStateInfo;
            this.currentStream = loc;
        }

        public void setOriginalScript(String s) {
            originalScriptFragment = s;
        }

        /**
         * Implement transitionIfNeeded, it should create a new State object
         * should a transition be needed.
         *
         * @return the new state if a transition is needed otherwise null.
         */
        protected abstract AbstractState transitionIfNeeded();

        protected boolean wantAppend(char c) {
            return true;
        }

        protected boolean appendIfWanted(char c) {
            if (wantAppend(c)) {
                observedStream.append(c);
                return true;
            }
            return false;
        }

        @Override
        public String transition() {

            assert originalScriptFragment != null : "NullPointer. originalScript not set";

            final int strLen = originalScriptFragment.length();

            AbstractState newState = null;
            lineJustWhiteSpace = true;

            // Loop through the originalScript starting at the beginning
            for (int currentIndex = 0; currentIndex < strLen; currentIndex++) {

                lastColumnAt++;
                final char c = originalScriptFragment.charAt(currentIndex);

                // Only support unix line endings for now.
                if (c == '\n') {

                    atLine++;
                    lastColumnAt = -1;

                    /*
                     * If this state transitioned due to a deviation, we must emit the deviation on the first non comment
                     * line.
                     */
                    if (deviatedStreamStartLoc != null && !isInComment() && !lineJustWhiteSpace) {
                        /*
                         * Add any deviation if needed. The purpose here is to replace the first command/event line with the
                         * deviation
                         */
                        Throwable failure = failedLocations.get(deviatedStreamStartLoc);
                        if (failure != null) {
                            // TODO: Eventually instead of LogLastEventHandler ... the thing we want to display will be in
                            // the failure we just retrieved
                            String lastEvent = LogLastEventHandler.getLastEvent(deviatedStreamStartLoc);
                            if (lastEvent != null) {
                                observedStream.append(lastEvent + "\n");
                            }
                        }
                        deviatedStreamStartLoc = null;
                    }

                    appendIfWanted(c);
                    // No multi-line comments.
                    setInComment(false);

                    // We don't set the newState until we get to the end of the
                    // line because we want to copy the last line seen to the
                    // result buffer
                    if (newState != null) {
                        currentIndex++;

                        // If we have reached the end of the originalScript. We
                        // probably have the finish state and there is no more
                        // script left.
                        newState.setOriginalScript(currentIndex < strLen ? originalScriptFragment.substring(currentIndex)
                                : null);
                        setState(newState);
                        return observedStream.toString();
                    }
                    lineJustWhiteSpace = true;
                } else if (!isInComment()) {
                    if (c == COMMENT) {
                        lineJustWhiteSpace = false;
                        setInComment(true);
                    } else if (!Character.isWhitespace(c)) {
                        lineJustWhiteSpace = false;
                        if (newState == null) {
                            newState = transitionIfNeeded();
                        }
                    }
                    appendIfWanted(c);
                } else { // it isn't a new line and we are in a comment ...
                    appendIfWanted(c);
                }
            }
            // If we go through the entire originalScript we have to be at the
            // finish state. Otherwise things are screwy.
            assert newState == null || newState instanceof FinishState : "End of input script before last end location";
            setState(new FinishState());
            return observedStream.toString();
        }
    }

    private final class BlankState extends AbstractState {

        private boolean appendAll;
        private StringBuilder skipedCharsOnLine = new StringBuilder();

        public BlankState(String originalScript, StreamResultLocationInfo loc, LocationInfo changeStateInfo) {
            super(originalScript, loc, changeStateInfo);
        }

        /*
         * Normally in the BlankState we are copying comments and whitespace only. Except when we are switching states
         */
         @Override
        protected boolean wantAppend(char c) {
            return appendAll || (!switchStates && (isInComment() || Character.isWhitespace(c)));
         }

        @Override
        protected boolean appendIfWanted(char c) {
            boolean appended = super.appendIfWanted(c);
            if (!appended) {
                skipedCharsOnLine.append(c);
            }
            if (c == '\n') {
                skipedCharsOnLine = new StringBuilder();
            }
            return appended;
        }

        @Override
        protected AbstractState transitionIfNeeded() {

            AbstractState newState = null;
            final LocationInfo currentLoc = new LocationInfo(atLine + 1, lastColumnAt);

            /* Ok. By definition, We are ready for a state transition. */
            switchStates = true;
            if (lookingForLocation.equals(currentLoc)) {
                appendAll = true;

                // We need to start copying text now. But we have to go grab any leading white space!
                if (lastColumnAt == 0) {
                    observedStream.append(skipedCharsOnLine);
                }
                skipedCharsOnLine = new StringBuilder();

                /*
                 * So if observed is not null we copy the stream up to the
                 * observed point. If observed equals current then there is just
                 * a single line in this stream. We will need to go look for the
                 * next stream or finish
                 */
                if (currentStream.observed != null && !currentStream.observed.equals(currentLoc)) {
                    newState = new CopyScriptState(null, currentStream, currentStream.observed);
                } else {
                    LocationInfo deviate = null;
                    if (currentStream.observed != null) {
                        deviate = currentStream.start;
                    }
                    /*
                     * If observed is null or observed=start then we transition
                     * back to the BlankState but with a new start location. In
                     * both cases we are either at a start of a stream (accept
                     * or connect). The observed location for an accept stream
                     * is really the set of observed locations of all its
                     * streams (accepted). So we treat this case for what it is
                     * between streams. So we go to the BlankState until the
                     * start of the next stream. Note that the next stream may
                     * not be for this accept. In this case no streams for the
                     * accept executed. Same for the connect case we will be
                     * moving to the next stream.
                     */

                    if (currentLocationIterator.hasNext()) {
                        currentStream = currentLocationIterator.next();
                        newState = new BlankState(null, currentStream, currentStream.start);
                    } else {
                        /*
                         * Then we are done. The last stream was an accept or connect and it didn't execute
                         */
                        newState = new FinishState(true);
                    }
                    if (deviate != null) {
                        newState.deviatedStreamStartLoc = deviate;
                    }

                }
            } else {
                /*
                 * So in this case. We have encountered a stream which we need
                 * to skip. Oh and the current location better not be past what
                 * we are looking for
                 */
                assert currentLoc.compareTo(lookingForLocation) < 0 : String.format(
                        "Current location %s is greater than changeStateInfo %s,", currentLoc, lookingForLocation);
                appendAll = false;
                newState = new SkipScriptState(null, currentStream, lookingForLocation);
            }
            return newState;
        }
    }

    private final class CopyScriptState extends AbstractState {

        public CopyScriptState(String originalScript, StreamResultLocationInfo loc, LocationInfo changeStateInfo) {
            super(originalScript, loc, changeStateInfo);
        }

        @Override
        protected AbstractState transitionIfNeeded() {

            AbstractState newState = null;
            final LocationInfo currentLoc = new LocationInfo(atLine + 1, lastColumnAt);

            if (lookingForLocation.equals(currentLoc)) {
                /* Ok the successful stream case */
                if (currentStream.observed.equals(currentStream.end)) {
                    switchStates = true;
                    /* Either we have more streams or not. */
                    if (currentLocationIterator.hasNext()) {
                        currentStream = currentLocationIterator.next();
                        newState = new BlankState(null, currentStream, currentStream.start);
                    } else {
                        newState = new FinishState();
                    }
                } else {
                    switchStates = true;
                    /* The unsuccessful stream. Skip the remainder of the stream */
                    newState = new SkipScriptState(null, currentStream, currentStream.end);
                    newState.deviatedStreamStartLoc = currentStream.start;
                }
            } else {
                /* Ok. So no transition is needed. */
                assert currentLoc.compareTo(lookingForLocation) < 0 : String.format(
                        "Current location %s is greater than changeStateInfo %s,", currentLoc, lookingForLocation);
            }
            return newState;
        }
    }

    private final class SkipScriptState extends AbstractState {

        private boolean appendAll;
        private StringBuilder skipedCharsOnLine = new StringBuilder();

        public SkipScriptState(String original, StreamResultLocationInfo loc, LocationInfo changeStateInfo) {
            super(original, loc, changeStateInfo);
        }

        /*
         * Normally in the SkipScriptState we are skipping everything ... except: At the start we emit comments and leading
         * white space until we have emitted our deviation script and we want to append everything when we are transitioning
         * states.
         */
        @Override
        protected boolean wantAppend(char c) {
            return appendAll
                    || (deviatedStreamStartLoc != null && (isInComment() || (lineJustWhiteSpace && Character.isWhitespace(c))));
        }

        @Override
        protected boolean appendIfWanted(char c) {
            boolean appended = super.appendIfWanted(c);
            if (!appended) {
                skipedCharsOnLine.append(c);
            }
            if (c == '\n') {
                skipedCharsOnLine = new StringBuilder();
            }
            return appended;
        }

        @Override
        protected AbstractState transitionIfNeeded() {

            final LocationInfo currentLoc = new LocationInfo(atLine + 1, lastColumnAt);
            AbstractState newState = null;

            /*
             * If we have found the location we are skipping to. We need to know
             * if we were skipping to the start or end of a stream so we know
             * which state to transition to.
             */
            if (lookingForLocation.equals(currentLoc)) {
                switchStates = true;
                // We need to start copying text now. But we have to go grab any leading white space!
                if (lastColumnAt == 0) {
                    observedStream.append(skipedCharsOnLine);
                }
                skipedCharsOnLine = new StringBuilder();
                /*
                 * In this case we were skipping an entire stream until the
                 * start of the next one
                 */
                if (lookingForLocation.equals(currentStream.start)) {
                    appendAll = true;

                    /*
                     * We are transitioning to copying. Or we are transitioning to the BlankState because we encountered an
                     * accept stream. See comment in BlankState class.
                     */
                    if (currentStream.observed != null && !currentStream.observed.equals(currentStream.start)) {
                        newState = new CopyScriptState(null, currentStream, currentStream.observed);
                    } else {
                        if (currentLocationIterator.hasNext()) {
                            currentStream = currentLocationIterator.next();
                            newState = new BlankState(null, currentStream, currentStream.start);
                        } else {
                            /*
                             * Then we are done. The last stream was an accept
                             * and none of its streams executed
                             */
                            newState = new FinishState(true);
                        }
                    }
                    /* Ok we were skipping to the end of a stream */
                } else if (lookingForLocation.equals(currentStream.end)) {
                    switchStates = true;
                    appendAll = false;
                    /* Either we are done or we are in between streams */
                    if (currentLocationIterator.hasNext()) {
                        currentStream = currentLocationIterator.next();
                        newState = new BlankState(null, currentStream, currentStream.start);
                    } else {
                        newState = new FinishState();
                    }

                } else {
                    /* We should never be skipping to the observed location */
                    assert false : "Invalid state. Can not skip up to observed location.";
                }
            } else {
                /* Otherwise we are till in skip mode */
                assert currentLoc.compareTo(lookingForLocation) < 0 : String.format(
                        "Current location |%s| greater than changeStateInfo |%s|", currentLoc, lookingForLocation);
            }
            return newState;
        }
    }

    private final class FinishState extends AbstractState {

        private boolean skipAll;

        public FinishState() {
            super(null, null, null);
        }

        public FinishState(String script) {
            super(script, null, null);
        }

        public FinishState(String script, boolean skipAll) {
            this(script);
            this.skipAll = skipAll;
        }

        public FinishState(boolean skipAll) {
            super(null, null, null);
            this.skipAll = skipAll;
        }

        @Override
        protected boolean wantAppend(char c) {
            return !skipAll && (isInComment() || Character.isWhitespace(c));
            // return false || isInComment();
        }

        // I suppose I should have FinishState have a different subclass
        @Override
        protected AbstractState transitionIfNeeded() {
            throw new RuntimeException("Invalid method call. transitionIfNeeded should not be called");
        }

        @Override
        public String transition() {

            /* Then we hit the end of the script. So there is nothing more to do */
            if (originalScriptFragment == null || originalScriptFragment.equals("")) {
                return "";
            }

            /*
             * Otherwise we are a the end and we want to copy just the
             * whitespace and comment lines. Note we could be skipping the last
             * stream(s)
             */
            final int strLen = originalScriptFragment.length();
            int currentIndex = 0;
            boolean seenNonCommentLine = false;

            while (currentIndex < strLen) {
                char c = originalScriptFragment.charAt(currentIndex);
                lastColumnAt++;
                if (c == '\n') {
                    // Only support unix new lines for now
                    atLine++;
                    lastColumnAt = -1;

                    if (!seenNonCommentLine && !isInComment()) {
                        seenNonCommentLine = true;
                        /*
                         * Add any deviation if needed. The purpose here is to
                         * replace the first command/event line with the
                         * deviation
                         */
                        if (deviatedStreamStartLoc != null) {
                            Throwable failure = failedLocations.get(deviatedStreamStartLoc);
                            if (failure != null) {
                                // TODO: Eventually instead of LogLastEventHandler ... the thing we want to display will be
                                // in the failure we just retrieved
                                String lastEvent = LogLastEventHandler.getLastEvent(deviatedStreamStartLoc);
                                if (lastEvent != null) {
                                    observedStream.append(lastEvent + "\n");
                                }
                            }
                            deviatedStreamStartLoc = null;
                        }
                    }

                    setInComment(false);
                } else if (c == COMMENT) {
                    setInComment(true);
                } else {
                    /*
                     * Actually this could be false because we may be skipping
                     * the last stream. In this case we transitioned to finish
                     * rather than SkipStreamState
                     */
                    // assert isInComment() || Character.isWhitespace(c) :
                    // String.format(
                    // "Found script text at end with no matching Location info at %d:%d",
                    // atLine, lastColumnAt );
                }
                currentIndex++;
                appendIfWanted(c);
            }
            return observedStream.toString();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy