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

dev.galasa.zos3270.spi.Screen Maven / Gradle / Ivy

The newest version!
/*
 * Copyright contributors to the Galasa project
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package dev.galasa.zos3270.spi;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import javax.validation.constraints.NotNull;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dev.galasa.zos3270.AttentionIdentification;
import dev.galasa.zos3270.ErrorTextFoundException;
import dev.galasa.zos3270.FieldNotFoundException;
import dev.galasa.zos3270.IDatastreamListener;
import dev.galasa.zos3270.IDatastreamListener.DatastreamDirection;
import dev.galasa.zos3270.IScreenUpdateListener;
import dev.galasa.zos3270.IScreenUpdateListener.Direction;
import dev.galasa.zos3270.common.screens.TerminalSize;
import dev.galasa.zos3270.KeyboardLockedException;
import dev.galasa.zos3270.TerminalInterruptedException;
import dev.galasa.zos3270.TextNotFoundException;
import dev.galasa.zos3270.TimeoutException;
import dev.galasa.zos3270.Zos3270Exception;
import dev.galasa.zos3270.internal.comms.Inbound3270Message;
import dev.galasa.zos3270.internal.comms.Network;
import dev.galasa.zos3270.internal.datastream.AbstractCommandCode;
import dev.galasa.zos3270.internal.datastream.AbstractOrder;
import dev.galasa.zos3270.internal.datastream.AbstractQueryReply;
import dev.galasa.zos3270.internal.datastream.AttributeBackgroundColour;
import dev.galasa.zos3270.internal.datastream.AttributeExtendedHighlighting;
import dev.galasa.zos3270.internal.datastream.AttributeForegroundColour;
import dev.galasa.zos3270.internal.datastream.BufferAddress;
import dev.galasa.zos3270.internal.datastream.CommandEraseWrite;
import dev.galasa.zos3270.internal.datastream.CommandEraseWriteAlternate;
import dev.galasa.zos3270.internal.datastream.CommandReadBuffer;
import dev.galasa.zos3270.internal.datastream.CommandReadModified;
import dev.galasa.zos3270.internal.datastream.CommandReadModifiedAll;
import dev.galasa.zos3270.internal.datastream.CommandWriteStructured;
import dev.galasa.zos3270.internal.datastream.OrderCarrageReturn;
import dev.galasa.zos3270.internal.datastream.OrderEndOfMedium;
import dev.galasa.zos3270.internal.datastream.OrderEraseUnprotectedToAddress;
import dev.galasa.zos3270.internal.datastream.OrderFormFeed;
import dev.galasa.zos3270.internal.datastream.OrderGraphicsEscape;
import dev.galasa.zos3270.internal.datastream.OrderInsertCursor;
import dev.galasa.zos3270.internal.datastream.OrderModifyField;
import dev.galasa.zos3270.internal.datastream.OrderNewLine;
import dev.galasa.zos3270.internal.datastream.OrderRepeatToAddress;
import dev.galasa.zos3270.internal.datastream.OrderSetAttribute;
import dev.galasa.zos3270.internal.datastream.OrderSetBufferAddress;
import dev.galasa.zos3270.internal.datastream.OrderStartField;
import dev.galasa.zos3270.internal.datastream.OrderStartFieldExtended;
import dev.galasa.zos3270.internal.datastream.OrderText;
import dev.galasa.zos3270.internal.datastream.QueryReplyCharactersets;
import dev.galasa.zos3270.internal.datastream.QueryReplyColor;
import dev.galasa.zos3270.internal.datastream.QueryReplyHighlite;
import dev.galasa.zos3270.internal.datastream.QueryReplyImplicitPartition;
import dev.galasa.zos3270.internal.datastream.QueryReplyNull;
import dev.galasa.zos3270.internal.datastream.QueryReplySummary;
import dev.galasa.zos3270.internal.datastream.QueryReplyUsableArea;
import dev.galasa.zos3270.internal.datastream.StructuredField;
import dev.galasa.zos3270.internal.datastream.StructuredField3270DS;
import dev.galasa.zos3270.internal.datastream.StructuredFieldReadPartition;
import dev.galasa.zos3270.internal.datastream.WriteControlCharacter;
import dev.galasa.zos3270.internal.terminal.ScreenUpdateTextListener;

/**
 * Screen representation of the 3270 terminal
 * 
 *  
 *
 */
public class Screen {

    private static final String                     CANT_FIND_TEXT  = "Unable to find a field containing '";
    
    private final Charset                           codePage;

    private final Log                               logger          = LogFactory.getLog(getClass());

    private final Network                           network;

    private boolean                                 usingAlternate;
    private IBufferHolder[]                         buffer;
    private int                                     screenSize;
    private int                                     columns;
    private int                                     rows;

    private final boolean                           hasAlternate;
    private final int                               primaryColumns;
    private final int                               primaryRows;
    private final int                               alternateColumns;
    private final int                               alternateRows;

    private int                                     workingCursor   = 0;
    private int                                     screenCursor    = 0;

    private Semaphore                               keyboardLock    = new Semaphore(1, true);
    private boolean                                 keyboardLockSet = false;

    private final LinkedList datastreamListeners =  new LinkedList<>();

    private final LinkedList updateListeners = new LinkedList<>();

    private AttentionIdentification                 lastAid = AttentionIdentification.NONE;
    
    private boolean                                 detectedSetAttribute = false;

    /**
     * @deprecated use the {@link #Screen(TerminalSize primarySize, TerminalSize alternateSize, Network network, Charset codePage)}
     * constructor instead.  
     */
    @Deprecated(since = "0.28.0", forRemoval = true)
    public Screen() throws TerminalInterruptedException {
        this(80, 24, null);
    }

    /**
     * @deprecated use the {@link #Screen(TerminalSize primarySize, TerminalSize alternateSize, Network network, Charset codePage)}
     * constructor instead.  
     */
    @Deprecated(since = "0.28.0", forRemoval = true)
    public Screen(int columns, int rows, Network network) throws TerminalInterruptedException {
        this(columns, rows, 0, 0, network);
    }

    /**
     * @deprecated use the {@link #Screen(TerminalSize primarySize, TerminalSize alternateSize, Network network, Charset codePage)}
     * constructor instead.  
     */
    @Deprecated(since = "0.28.0", forRemoval = true)
    public Screen(int columns, int rows, int alternateColumns, int alternateRows, Network network) throws TerminalInterruptedException {
        this.codePage = Charset.forName("Cp037");
        this.network = network;
        this.primaryColumns = columns;
        this.primaryRows = rows;
        this.usingAlternate = false;
        if (alternateRows < 1 || alternateColumns < 1) {
            this.hasAlternate = false;
            this.alternateColumns = 0;
            this.alternateRows    = 0;
        } else {
            this.hasAlternate = true;
            this.alternateColumns = alternateColumns;
            this.alternateRows    = alternateRows;
        }

        erase();
        lockKeyboard();
    }


    public Screen(TerminalSize primarySize, TerminalSize alternateSize, Network network, Charset codePage) throws TerminalInterruptedException {
        this.codePage = codePage;
        this.network = network;
        this.primaryColumns = primarySize.getColumns();
        this.primaryRows = primarySize.getRows();
        this.usingAlternate = false;

        int alternateRows = alternateSize.getRows();
        int alternateColumns = alternateSize.getColumns();
        if (alternateRows < 1 || alternateColumns < 1) {
            this.hasAlternate = false;
            this.alternateColumns = 0;
            this.alternateRows    = 0;
        } else {
            this.hasAlternate = true;
            this.alternateColumns = alternateColumns;
            this.alternateRows    = alternateRows;
        }

        erase();
        lockKeyboard();
    }

    public synchronized void lockKeyboard() throws TerminalInterruptedException {
        if (!keyboardLockSet) {
            logger.trace("Locking keyboard");
            keyboardLockSet = true;
            try {
                keyboardLock.acquire();
            } catch (InterruptedException e) {
                throw new TerminalInterruptedException("Lock keyboard was interrupted", e);
            }
        }
    }

    public void networkClosed() throws TerminalInterruptedException {
        lockKeyboard();
    }

    private synchronized void unlockKeyboard() {
        if (keyboardLockSet) {
            logger.trace("Unlocking keyboard");
            keyboardLockSet = false;
            keyboardLock.release();
        }
    }

    public synchronized void processInboundMessage(Inbound3270Message inbound) throws DatastreamException {
        AbstractCommandCode commandCode = inbound.getCommandCode();
        if (commandCode instanceof CommandWriteStructured) {
            processStructuredFields(inbound.getStructuredFields());
        } else if (commandCode instanceof CommandReadBuffer) {
            processReadBuffer();
        } else if (commandCode instanceof CommandReadModified) {
            processReadModified(false);
        } else if (commandCode instanceof CommandReadModifiedAll) {
            processReadModified(true);
        } else {
            WriteControlCharacter writeControlCharacter = inbound.getWriteControlCharacter();
            List orders = inbound.getOrders();

            if (commandCode instanceof CommandEraseWrite) {
                erase();
            } else if (commandCode instanceof CommandEraseWriteAlternate) {
                eraseAlternate();
            }

            if (writeControlCharacter.isResetMDT()) {
                resetMdt();
            }

            this.workingCursor = this.screenCursor;

            processOrders(orders, writeControlCharacter);

        }

    }

    private void resetMdt() {
        for(int i = 0; i < this.screenSize; i++) {
            IBufferHolder bh = this.buffer[i];
            if (bh instanceof BufferStartOfField) {
                BufferStartOfField sof = (BufferStartOfField) bh;
                sof.clearFieldModified();
            }
        }
    }

    private synchronized void processReadBuffer() throws DatastreamException {
        try {
            ByteArrayOutputStream outboundBuffer = new ByteArrayOutputStream();

            outboundBuffer.write(this.lastAid.getKeyValue());

            BufferAddress cursor = new BufferAddress(this.screenCursor);
            outboundBuffer.write(cursor.getCharRepresentation());

            for(IBufferHolder bh : this.buffer) {
                if (bh == null) {
                    outboundBuffer.write(0);
                } else if (bh instanceof BufferGraphicsEscape) {
                    BufferGraphicsEscape bc = (BufferGraphicsEscape) bh;
                    outboundBuffer.write(OrderGraphicsEscape.ID);
                    outboundBuffer.write(bc.getFieldEbcdic(this.codePage));
                } else if (bh instanceof BufferChar) {
                    BufferChar bc = (BufferChar) bh;
                    outboundBuffer.write(bc.getFieldEbcdic(this.codePage));
                } else if (bh instanceof BufferStartOfField) {
                    BufferStartOfField sf = (BufferStartOfField) bh;
                    OrderStartField osf = new OrderStartField(sf.isProtected(), sf.isNumeric(), sf.isDisplay(), sf.isIntenseDisplay(), sf.isSelectorPen(), sf.isFieldModifed());
                    outboundBuffer.write(osf.getBytes());
                } else {
                    throw new DatastreamException("Unrecognised Buffer Holder - " + bh.getClass().getName());
                }
            }
            writeTrace(outboundBuffer);
            this.network.sendDatastream(outboundBuffer.toByteArray());
        } catch(Exception e) {
            throw new DatastreamException("Error whilst processing READ BUFFER", e);
        }

    }

    private void writeTrace(ByteArrayOutputStream outboundBuffer) {
        if (logger.isTraceEnabled() || !this.datastreamListeners.isEmpty()) {
            String hex = new String(Hex.encodeHex(outboundBuffer.toByteArray()));
            if (logger.isTraceEnabled()) {
                logger.trace("outbound=" + hex);
            }

            for(IDatastreamListener listener : this.datastreamListeners) {
                listener.datastreamUpdate(DatastreamDirection.OUTBOUND, hex);
            }
        }
    }

    private synchronized void processReadModified(boolean all) throws DatastreamException {
        try {
            ByteArrayOutputStream outboundBuffer = new ByteArrayOutputStream();

            outboundBuffer.write(this.lastAid.getKeyValue());

            if (!all && (this.lastAid == AttentionIdentification.CLEAR 
                    || this.lastAid == AttentionIdentification.PA1
                    || this.lastAid == AttentionIdentification.PA2
                    || this.lastAid == AttentionIdentification.PA3)) {
                //  dont send anything other than the aid key
            } else {
                BufferAddress cursor = new BufferAddress(this.screenCursor);
                outboundBuffer.write(cursor.getCharRepresentation());

                readModifiedBuffer(outboundBuffer);
            }
            writeTrace(outboundBuffer);
            this.network.sendDatastream(outboundBuffer.toByteArray());
        } catch(Exception e) {
            throw new DatastreamException("Error whilst processing READ BUFFER", e);
        }

    }

    private void readModifiedBuffer(ByteArrayOutputStream outboundBuffer) throws IOException {
        boolean fieldModified = false;

        // *** Locate the first StartOfField in the buffer, if absent, then unformatted,
        // send everything back.

        int start = 0;
        int end = 0;
        for (; start < buffer.length; start++) {
            if (buffer[start] instanceof BufferStartOfField) {
                break;
            }
        }

        if (start >= buffer.length) { // indicates unfromatted, send it all
            start = 0;
            end = buffer.length - 1;

            // OrderSetBufferAddress sba = new OrderSetBufferAddress(new BufferAddress(0));
            // outboundBuffer.write(sba.getCharRepresentation());
            fieldModified = true;
        } else { // formatted
            end = start - 1;
            if (end < 0) {
                end = buffer.length - 1;
            }
        }

        int pos = start;
        while (true) {
            IBufferHolder bh = buffer[pos];
            if (bh instanceof BufferStartOfField) {
                BufferStartOfField bsf = (BufferStartOfField) bh;
                fieldModified = bsf.isFieldModifed();

                if (fieldModified) { // Send whether unprotected or not
                    OrderSetBufferAddress sba = new OrderSetBufferAddress(new BufferAddress(pos + 1));
                    outboundBuffer.write(sba.getCharRepresentation());
                }
            } else if (bh instanceof BufferGraphicsEscape) {
                BufferGraphicsEscape bc = (BufferGraphicsEscape) bh;
                if (fieldModified) {
                    outboundBuffer.write(OrderGraphicsEscape.ID);
                    byte value = bc.getFieldEbcdic(this.codePage);
                    outboundBuffer.write(value);
                }
            } else if (bh instanceof BufferChar) {
                BufferChar bc = (BufferChar) bh;
                if (fieldModified) {
                    byte value = bc.getFieldEbcdic(this.codePage);
                    if (value != 0) {
                        outboundBuffer.write(value);
                    }
                }
            }

            if (pos == end) {
                break;
            }

            pos++;
            if (pos >= buffer.length) {
                pos = 0;
            }
        }
    }

    private synchronized void processStructuredFields(List structuredFields)
            throws DatastreamException {
        for (StructuredField structuredField : structuredFields) {
            if (structuredField instanceof StructuredFieldReadPartition) {
                processReadPartition((StructuredFieldReadPartition) structuredField);
            } else if (structuredField instanceof StructuredField3270DS) {
                processInboundMessage(((StructuredField3270DS) structuredField).getInboundMessage());
            } else {
                throw new DatastreamException("Unsupported Structured Field - " + structuredField.getClass().getName());
            }
        }
    }

    private synchronized void processReadPartition(StructuredFieldReadPartition readPartition)
            throws DatastreamException {
        switch (readPartition.getType()) {
        case QUERY:
            processReadPartitionQuery();
            return;
        case QUERY_LIST:
            processReadPartitionQueryList(readPartition);
            return;
        default:
            throw new DatastreamException(
                    "Unsupported Read Partition Type - " + readPartition.getType().toString());
        }

    }

    private synchronized void processReadPartitionQuery() throws DatastreamException {
        List replies = getAllSupportedReplies();
        QueryReplySummary summary = new QueryReplySummary(replies);

        sendQueryReplies(summary, replies);
    }

    private synchronized void processReadPartitionQueryList(StructuredFieldReadPartition readPartition) throws DatastreamException {
        switch (readPartition.getRequestType()) {
        case StructuredFieldReadPartition.REQTYP_LIST:
            List supportedReplies = getAllSupportedReplies();
            ArrayList replies = prepareQueryListResponse(supportedReplies, readPartition.getQcodes());

            sendQueryReplies(new QueryReplySummary(supportedReplies), replies);
            return;
        case StructuredFieldReadPartition.REQTYP_ALL:
        case StructuredFieldReadPartition.REQTYP_EQUIVALENT:
            processReadPartitionQuery();
            return;
        default:
            throw new DatastreamException(
                    "Unsupported Read Partition Request Type code = " + readPartition.getRequestType());
        }
    }

    private ArrayList prepareQueryListResponse(List supportedReplies, Set requestedQcodes) {
        ArrayList replies = new ArrayList<>();
        for (AbstractQueryReply reply : supportedReplies) {
            if (requestedQcodes.contains(reply.getID())) {
                replies.add(reply);
            }
        }
        if (replies.isEmpty()) {
            replies.add(new QueryReplyNull());
        }
        return replies;
    }

    private List getAllSupportedReplies() {
        ArrayList replies = new ArrayList<>();

        replies.add(new QueryReplyUsableArea(this));
        replies.add(new QueryReplyImplicitPartition(this));
        replies.add(new QueryReplyCharactersets());
        replies.add(new QueryReplyColor());
        replies.add(new QueryReplyHighlite());
        return replies;
    }

    private void sendQueryReplies(QueryReplySummary summary, List replies) throws DatastreamException {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            baos.write(AttentionIdentification.STRUCTURED_FIELD.getKeyValue());
            baos.write(summary.toByte());

            for (AbstractQueryReply reply : replies) {
                baos.write(reply.toByte());
            }

            if (logger.isTraceEnabled() || !this.datastreamListeners.isEmpty()) {
                String hex = new String(Hex.encodeHex(baos.toByteArray()));
                if (logger.isTraceEnabled()) {
                    logger.trace("outbound sf=" + hex);
                }

                for(IDatastreamListener listener : this.datastreamListeners) {
                    listener.datastreamUpdate(DatastreamDirection.OUTBOUND, hex);
                }
            }

            network.sendDatastream(baos.toByteArray());
        } catch (Exception e) {
            throw new DatastreamException("Unable able to write Query Reply", e);
        }
    }

    public synchronized void processOrders(List orders, WriteControlCharacter writeControlCharacter) throws DatastreamException {
        logger.trace("Processing orders");
        for (AbstractOrder order : orders) {
            if (order instanceof OrderSetBufferAddress) {
                processSBA((OrderSetBufferAddress) order);
            } else if (order instanceof OrderRepeatToAddress) {
                processRA((OrderRepeatToAddress) order);
            } else if (order instanceof OrderText) {
                processText((OrderText) order);
            } else if (order instanceof OrderStartField) {
                processSF((OrderStartField) order);
            } else if (order instanceof OrderStartFieldExtended) {
                processSFE((OrderStartFieldExtended) order);
            } else if (order instanceof OrderSetAttribute) {
                processSA((OrderSetAttribute) order);
            } else if (order instanceof OrderModifyField) {
                processMF((OrderModifyField) order);
            } else if (order instanceof OrderInsertCursor) {
                this.screenCursor = this.workingCursor;
            } else if (order instanceof OrderEraseUnprotectedToAddress) {
                processEUA((OrderEraseUnprotectedToAddress)order);
            } else if (order instanceof OrderNewLine) {
                processNewLine();
            } else if (order instanceof OrderFormFeed) {
                processFormFeed();
            } else if (order instanceof OrderCarrageReturn) {
                processCarrageReturn();
            } else if (order instanceof OrderEndOfMedium) {
                processEndOfMedium();
            } else if (order instanceof OrderGraphicsEscape) {
                processGraphicsEscape((OrderGraphicsEscape) order);
            } else {
                throw new DatastreamException("Unsupported Order - " + order.getClass().getName());
            }
        }


        if (writeControlCharacter.isKeyboardReset()) {
            this.lastAid = AttentionIdentification.NONE;
            unlockKeyboard();
            this.workingCursor = 0;
        }

        synchronized (updateListeners) {
            for (IScreenUpdateListener listener : updateListeners) {
                listener.screenUpdated(Direction.RECEIVED, null);
            }
        }
    }

    public synchronized void erase() {

        if (this.usingAlternate || this.buffer == null) {
            this.columns = primaryColumns;
            this.rows    = primaryRows;
            allocateBuffer();

            this.usingAlternate = false;
        }

        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = null;
        }

        this.screenCursor  = 0;
        this.workingCursor = 0;
    }

    public synchronized void eraseAlternate() {
        if (!hasAlternate) {
            erase();
            return;
        }


        if (!this.usingAlternate || this.buffer == null) {
            this.columns = alternateColumns;
            this.rows    = alternateRows;
            allocateBuffer();

            this.usingAlternate = true;
        }

        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = null;
        }

        this.screenCursor  = 0;
        this.workingCursor = 0;
    }

    private void allocateBuffer() {
        this.screenSize = this.columns * this.rows;
        this.buffer = new IBufferHolder[this.screenSize];        
    }

    /**
     * Process a Set Buffer Address order
     * 
     * @param order - the order to process
     */
    private synchronized void processSBA(OrderSetBufferAddress order) {
        this.workingCursor = order.getBufferAddress();
        if (this.workingCursor >= this.screenSize) {
            this.workingCursor = this.workingCursor - this.screenSize;
        }
    }

    /**
     * Process the Report to Address order
     * 
     * @param order - the order to process
     * @throws DatastreamException
     */
    private synchronized void processRA(OrderRepeatToAddress order) throws DatastreamException {
        int endOfRepeat = order.getBufferAddress();

        if (endOfRepeat > this.screenSize || endOfRepeat < 0) {
            throw new DatastreamException(
                    "Impossible RA end address " + endOfRepeat + ", screen size is " + screenSize);
        }

        boolean firstPosition = true;
        while (firstPosition || this.workingCursor != endOfRepeat) {
            this.buffer[this.workingCursor] = new BufferChar(order.getChar());
            if (endOfRepeat == this.screenSize && this.workingCursor == (this.screenSize - 1)) {
                endOfRepeat = 0;
                break;
            }
            firstPosition = false;
            incrementWorkingCursor();
        }

        this.workingCursor = endOfRepeat;
    }

    private void incrementWorkingCursor() {
        this.workingCursor++;
        if (this.workingCursor >= this.screenSize) {
            this.workingCursor = 0;
        }
    }

    private void processSF(OrderStartField order) {
        this.buffer[this.workingCursor] = new BufferStartOfField(this.workingCursor, order.isFieldProtected(),
                order.isFieldNumeric(), order.isFieldDisplay(), order.isFieldIntenseDisplay(),
                order.isFieldSelectorPen(), order.isFieldModifed());
        incrementWorkingCursor();
    }

    private void processSFE(OrderStartFieldExtended order) {
        OrderStartField sf = order.getOrderStartField();
        BufferStartOfField bsf = null;

        if (sf != null) {
            bsf = new BufferStartOfField(this.workingCursor, sf.isFieldProtected(), sf.isFieldNumeric(),
                    sf.isFieldDisplay(), sf.isFieldIntenseDisplay(), sf.isFieldSelectorPen(), sf.isFieldModifed(),
                    order.getHighlight(), order.getForegroundColour(), order.getBackgroundColor());
        }

        if (bsf == null) {
            bsf = new BufferStartOfField(this.workingCursor, false, false, true, false, false, false);
        }

        this.buffer[this.workingCursor] = bsf;
        incrementWorkingCursor();
    }

    private void processMF(OrderModifyField order) {
        OrderStartField sf = order.getOrderStartField();
        BufferStartOfField bsf = null;

        if (sf != null) {
            bsf = new BufferStartOfField(this.workingCursor, sf.isFieldProtected(), sf.isFieldNumeric(),
                    sf.isFieldDisplay(), sf.isFieldIntenseDisplay(), sf.isFieldSelectorPen(), sf.isFieldModifed(),
                    order.getHighlight(), order.getForegroundColour(), order.getBackgroundColor());
        }

        if (bsf == null) {
            bsf = new BufferStartOfField(this.workingCursor, false, false, true, false, false, false);
        }

        this.buffer[this.workingCursor] = bsf;
        incrementWorkingCursor();
    }

    private void processEUA(OrderEraseUnprotectedToAddress order) {
        boolean charProtected = true;
        IBufferHolder bh = this.buffer[this.workingCursor];
        // are we on a SF, if so take the protected setting
        if (bh instanceof BufferStartOfField) {
            charProtected = ((BufferStartOfField)bh).isProtected();
        } else {
            // we have to go looking backwards for it
            int searchCursor = this.workingCursor - 1;
            if (searchCursor < 0) {
                searchCursor = this.screenSize - 1;
            }
            boolean found = false;
            while(searchCursor != this.workingCursor) {
                bh = this.buffer[searchCursor];

                if (bh instanceof BufferStartOfField) {
                    charProtected = ((BufferStartOfField)bh).isProtected();
                    found = true;
                    break;
                }

                searchCursor--;
                if (searchCursor < 0) {
                    searchCursor = this.screenSize - 1;
                }
            }

            if (!found) {
                // assume no fields, so unprotected;
                charProtected = false;
            }
        }


        int toAddress = order.getBufferAddress();
        if (toAddress >= this.screenSize) {
            toAddress = this.screenSize - 1;
        }
        if (toAddress < 0) {
            toAddress = 0;
        }

        int eraseCursor = this.workingCursor;
        while(true) {
            bh = this.buffer[eraseCursor];
            if (bh instanceof BufferStartOfField) {
                charProtected = ((BufferStartOfField)bh).isProtected();
            } else {
                if (!charProtected) {
                    this.buffer[eraseCursor] = null;
                }
            }

            eraseCursor++;
            if (eraseCursor >= this.screenSize) {
                eraseCursor = 0;
            }

            if (eraseCursor == toAddress) {
                break;
            }
        }

    }



    private void processSA(OrderSetAttribute order) {
        if (!detectedSetAttribute) {
            detectedSetAttribute = true;
            
            logger.warn("SetAttribute order has been received, please send a trace to the Galasa team");
        }
    }

    private void processNewLine() {
        this.buffer[this.workingCursor] = new BufferNewLine();
        incrementWorkingCursor();
    }

    private void processFormFeed() {
        this.buffer[this.workingCursor] = new BufferFormFeed();
        incrementWorkingCursor();
    }

    private void processCarrageReturn() {
        this.buffer[this.workingCursor] = new BufferCarrageReturn();
        incrementWorkingCursor();
    }

    private void processEndOfMedium() {
        this.buffer[this.workingCursor] = new BufferEndOfMedium();
        incrementWorkingCursor();
    }

    private void processGraphicsEscape(OrderGraphicsEscape order) {
        this.buffer[this.workingCursor] = new BufferGraphicsEscape(order.getByte());
        incrementWorkingCursor();
    }

    private void processText(OrderText order) {
        String text = order.getText();
        for (int i = 0; i < text.length(); i++) {
            this.buffer[this.workingCursor] = new BufferChar(text.charAt(i));
            incrementWorkingCursor();
        }

    }

    public String printScreen() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < this.buffer.length; i++) {
            if (this.buffer[i] == null) {
                sb.append(" ");
            } else {
                sb.append(this.buffer[i].getStringWithoutNulls());
            }
        }

        StringBuilder screenSB = new StringBuilder();
        String screenString = sb.toString();
        for (int i = 0; i < this.screenSize; i += this.columns) {
            screenSB.append(screenString.substring(i, i + this.columns));
            screenSB.append('\n');
        }
        return screenSB.toString();
    }

    public String printScreenTextWithCursor() {
        int cursorRow = screenCursor / columns;
        int cursorCol = screenCursor % columns;

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < this.buffer.length; i++) {
            if (this.buffer[i] == null) {
                sb.append(" ");
            } else {
                sb.append(this.buffer[i].getStringWithoutNulls());
            }
        }

        StringBuilder screenSB = new StringBuilder();
        String screenString = sb.toString();
        int row = 0;
        for (int i = 0; i < this.screenSize; i += this.columns) {
            screenSB.append("=|");
            screenSB.append(screenString.substring(i, i + this.columns));
            screenSB.append("|");
            screenSB.append('\n');
            if (row == cursorRow) {
                screenSB.append("^|");
                for (int j = 0; j < cursorCol; j++) {
                    screenSB.append(" ");
                }
                screenSB.append("^");
                screenSB.append('\n');
            }

            row++;
        }


        screenSB.append("!| ");
        screenSB.append(reportOperator());
        screenSB.append("\n");


        return screenSB.toString();
    }

    public String printExtendedScreen(boolean printCursor, boolean printColour, boolean printHighlight, boolean printIntensity, boolean printProtected, boolean printNumeric, boolean printModified) throws Zos3270Exception {
        int cursorRow = screenCursor / columns;
        int cursorCol = screenCursor % columns;

        StringBuilder screenBuffer   = new StringBuilder();
        StringBuilder intensityLine  = new StringBuilder();
        StringBuilder protectedLine  = new StringBuilder();
        StringBuilder numericLine    = new StringBuilder();
        StringBuilder modifiedLine   = new StringBuilder();
        StringBuilder foregroundLine = new StringBuilder(); 
        StringBuilder backgroundLine = new StringBuilder();
        StringBuilder highlightLine  = new StringBuilder();

        int row = 0;
        int col = 0;

        // *** Check to see if the screen is wrapped or unformatted
        BufferStartOfField currentBufferStartOfField = new BufferStartOfField(0, false, false, true, false, false, false);
        if (!(this.buffer[0] instanceof BufferStartOfField)) {
            for (int i = this.buffer.length - 1; i >= 0; i--) {
                IBufferHolder bh = this.buffer[i];
                if (bh instanceof BufferStartOfField) {
                    currentBufferStartOfField = (BufferStartOfField) bh;
                    break;
                }
            }
        }  // no need for else as it will be picked up in the loop

        for (int i = 0; i < this.buffer.length; i++) {
            // print row header
            if (col == 0) {
                screenBuffer.append("=");
                screenBuffer.append(String.format("%03d", row+1));
                screenBuffer.append("|");
            }

            // Print actual text
            IBufferHolder bufferHolder = this.buffer[i];
            if (bufferHolder == null) {
                screenBuffer.append(" ");
            } else {
                screenBuffer.append(bufferHolder.getStringWithoutNulls());

                if (bufferHolder instanceof BufferStartOfField) {
                    currentBufferStartOfField = (BufferStartOfField)bufferHolder;
                }
            }


            if (bufferHolder == null || bufferHolder == currentBufferStartOfField) {
                foregroundLine.append(" ");
                backgroundLine.append(" ");
                highlightLine.append(" ");
                intensityLine.append(" ");
                protectedLine.append(" ");
                numericLine.append(" ");
                modifiedLine.append(" ");
            } else {
                AttributeForegroundColour foregroundColour = currentBufferStartOfField.getAttributeForegroundColour();
                if (foregroundColour == null) {
                    foregroundLine.append(" ");
                } else {
                    foregroundLine.append(foregroundColour.getColour().getLetter());
                }
                
                AttributeBackgroundColour backgroundColour = currentBufferStartOfField.getAttributeBackgroundColour();
                if (backgroundColour == null) {
                    backgroundLine.append(" ");
                } else {
                    backgroundLine.append(backgroundColour.getColour().getLetter());
                }
                




                // Calculate Highlight
                AttributeExtendedHighlighting extendedHighlighting = currentBufferStartOfField.getAttributeExtendedHighlighting();
                if (extendedHighlighting == null) {
                    highlightLine.append(" ");
                } else {
                    switch(extendedHighlighting.getHighlight()) {
                    case BLINK:
                        highlightLine.append("b");
                        break;
                    case NORMAL:
                        highlightLine.append("n");
                        break;
                    case REVERSE:
                        highlightLine.append("r");
                        break;
                    case UNDERSCORE:
                        highlightLine.append("u");
                        break;
                    case DEFAULT:
                        highlightLine.append("d");
                        break;
                    default:
                        highlightLine.append("?");
                        break;
                    }
                }

                // Calculate intensity
                if (currentBufferStartOfField.isIntenseDisplay()) {
                    intensityLine.append("i");
                } else {
                    intensityLine.append(" ");
                }

                // Calculate Protected
                if (currentBufferStartOfField.isProtected()) {
                    protectedLine.append("p");
                } else {
                    protectedLine.append("u");
                }

                // Calculate Numeric
                if (currentBufferStartOfField.isNumeric()) {
                    numericLine.append("n");
                } else {
                    numericLine.append(" ");
                }

                // Calculate Modified
                if (currentBufferStartOfField.isFieldModifed()) {
                    modifiedLine.append("m");
                } else {
                    modifiedLine.append(" ");
                }
            }

            // NOT doing selectable as very unlikely to be used


            col++;
            if (col >= columns) {
                screenBuffer.append("|\n");

                // Check this is the cursor row
                if (printCursor) {
                    if (row == cursorRow) {
                        screenBuffer.append("^   |");
                        for (int j = 0; j < cursorCol; j++) {
                            screenBuffer.append(" ");
                        }
                        screenBuffer.append("^");
                        screenBuffer.append('\n');
                    }
                }
                // if requested, print colour
                if (printColour) {
                    screenBuffer.append("f   |");
                    screenBuffer.append(foregroundLine.toString());
                    screenBuffer.append('\n');
                    screenBuffer.append("b   |");
                    screenBuffer.append(backgroundLine.toString());
                    screenBuffer.append('\n');
                }
                // if requested, print intensity
                if (printIntensity) {
                    screenBuffer.append("i   |");
                    screenBuffer.append(intensityLine.toString());
                    screenBuffer.append('\n');
                }
                // if requested, print highlight
                if (printHighlight) {
                    screenBuffer.append("h   |");
                    screenBuffer.append(highlightLine.toString());
                    screenBuffer.append('\n');
                }
                // if requested, print protected
                if (printProtected) {
                    screenBuffer.append("p   |");
                    screenBuffer.append(protectedLine.toString());
                    screenBuffer.append('\n');
                }
                // if requested, print numeric
                if (printNumeric) {
                    screenBuffer.append("n   |");
                    screenBuffer.append(numericLine.toString());
                    screenBuffer.append('\n');
                }
                // if requested, print modifles
                if (printModified) {
                    screenBuffer.append("m   |");
                    screenBuffer.append(modifiedLine.toString());
                    screenBuffer.append('\n');
                }


                // Reset for next row
                col = 0;
                row++;

                // Reset the report lines
                intensityLine  = new StringBuilder();
                protectedLine  = new StringBuilder();
                numericLine    = new StringBuilder();
                modifiedLine   = new StringBuilder();
                foregroundLine = new StringBuilder();
                backgroundLine = new StringBuilder();
                highlightLine  = new StringBuilder();
            }
        }

        screenBuffer.append("!   | ");
        screenBuffer.append(reportOperator());
        screenBuffer.append("\n");

        return screenBuffer.toString();
    }


    private String reportOperator() {
        int cursorRow = (screenCursor / columns) + 1;
        int cursorCol = screenCursor % columns;

        StringBuilder operator = new StringBuilder();

        if (network.isConnected()) {
            operator.append("Connected-");
        } else {
            operator.append("Disconnected-");
        }
        if (network.isTls() || network.isSwitchedSSL()) {
            operator.append("SSL ");
        } else {
            operator.append("Plain");
        }
        operator.append(" Size=" + this.rows + "x" + this.columns);
        operator.append(" Cursor=" + screenCursor + "," + cursorRow + "x" + cursorCol);

        if (this.keyboardLockSet) {
            operator.append(" Keyboard Locked");
        } else {
            operator.append(" Keyboard Unlocked");
        }

        return operator.toString();
    }

    public String retrieveFlatScreen() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < this.buffer.length; i++) {
            if (this.buffer[i] == null) {
                sb.append(" ");
            } else {
                sb.append(this.buffer[i].getStringWithoutNulls());
            }
        }
        return sb.toString();
    }



    public synchronized @NotNull Field[] calculateFields() {
        ArrayList fields = new ArrayList<>();

        Field currentField = null;

        // *** Check to see if the screen is wrapped or unformatted
        if (!(this.buffer[0] instanceof BufferStartOfField)) {
            BufferStartOfField wrapSoField = null;
            for (int i = this.buffer.length - 1; i >= 0; i--) {
                IBufferHolder bh = this.buffer[i];
                if (bh instanceof BufferStartOfField) {
                    wrapSoField = (BufferStartOfField) bh;
                    break;
                }
            }

            if (wrapSoField == null) {
                currentField = new Field();
            } else {
                currentField = new Field(-1, wrapSoField);
            }
        }

        for (int i = 0; i < this.buffer.length; i++) {
            IBufferHolder bh = this.buffer[i];
            if (bh == null) {
                currentField.appendChar((char) 0x00); // NOSONAR, can't be null
            } else if (bh instanceof BufferStartOfField) {
                if (currentField != null) {
                    fields.add(currentField);
                }
                currentField = new Field(i, (BufferStartOfField) bh);
            } else if (bh instanceof BufferChar) {
                currentField.appendChar(((BufferChar) bh).getChar());// NOSONAR, can't be null
            } else {
                throw new UnsupportedOperationException("Unrecognised buffer type " + bh.getClass().getName());
            }
        }
        if (currentField != null) {
            fields.add(currentField);
        }

        // *** If the SBA were not in order, possibility that the safeguard first field
        // was left there
        if (fields.size() >= 2) {
            if (fields.get(0).getStart() == 0 && fields.get(1).getStart() == 0) {
                fields.remove(0);
            }
        }

        return fields.toArray(new Field[fields.size()]);
    }

    public void searchFieldContaining(String text) throws TextNotFoundException {
        for (Field field : calculateFields()) {
            if (field.containsText(text)) {
                return;
            }
        }

        throw new TextNotFoundException(CANT_FIND_TEXT + text + "'");
    }

    public int searchFieldContaining(@NotNull String[] okText, String[] errorText) throws TextNotFoundException, ErrorTextFoundException {
        if (errorText != null) {
            for(int i = 0; i < errorText.length; i++) {
                for (Field field : calculateFields()) {
                    if (field.containsText(errorText[i])) {
                        throw new ErrorTextFoundException("Found error text '" + errorText[i] + "' on screen", i);
                    }
                }
            }
        }

        for(int i = 0; i < okText.length; i++) {
            for (Field field : calculateFields()) {
                if (field.containsText(okText[i])) {
                    return i;
                }
            }
        }

        throw new TextNotFoundException("Unable to locate text on sreen");
    }

    public boolean isTextInField(String text) {
        for (Field field : calculateFields()) {
            if (field.containsText(text)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Wait on the keyboard being free
     * 
     * @param maxWait - time in milliseconds
     * @throws KeyboardLockedException
     * @throws InterruptedException
     */
    public void waitForKeyboard(int maxWait) throws TimeoutException, TerminalInterruptedException {
        try {
            if (!keyboardLock.tryAcquire(maxWait, TimeUnit.MILLISECONDS)) {
                throw new TimeoutException("Wait for keyboard took longer than " + maxWait + "ms");
            }
        } catch(InterruptedException e) {
            throw new TerminalInterruptedException("Wait for keyboard was interrupted", e);
        }
        keyboardLock.release();
    }

    public int waitForTextInField(String text, long maxWait) throws TerminalInterruptedException, TextNotFoundException, Zos3270Exception {
        return waitForTextInField(new String[] {text}, null, maxWait);
    }

    public int waitForTextInField(String[] ok, String[] error, long timeoutInMilliseconds) throws TerminalInterruptedException, TextNotFoundException, ErrorTextFoundException, Zos3270Exception {
        int foundIndex = -1;
        try {
            foundIndex = ScreenUpdateTextListener.waitForText(this, ok, error, timeoutInMilliseconds);
            if (foundIndex < 0) {
                if (ok != null && ok.length == 1 && error == null) {
                    throw new TextNotFoundException(CANT_FIND_TEXT + ok[0] + "'");
                }
                throw new TextNotFoundException("Unable to find a field containing any of the request text");
            }
        } catch(InterruptedException e) {
            throw new TerminalInterruptedException("Wait for text was interrupted", e);
        }
        return foundIndex;
    }



    public synchronized void positionCursorToFieldContaining(@NotNull String text)
            throws KeyboardLockedException, TextNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        for (Field field : calculateFields()) {
            if (field.containsText(text)) {
                this.screenCursor = field.getStart();
                return;
            }
        }

        throw new TextNotFoundException(CANT_FIND_TEXT + text + "'");
    }

    public synchronized void eraseEof() throws KeyboardLockedException, FieldNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        if (buffer[screenCursor] != null && !(buffer[screenCursor] instanceof BufferChar)) {
            throw new FieldNotFoundException("Unable to type where the cursor is pointing to - " + this.screenCursor);
        }

        BufferStartOfField sf = null;
        int sfPos = screenCursor - 1;
        if (sfPos < 0) {
            sfPos = buffer.length - 1;
        }
        while(sfPos != screenCursor) {
            if (buffer[sfPos] instanceof BufferStartOfField) {
                sf = (BufferStartOfField) buffer[sfPos];
                break;
            }

            sfPos--;
            if (sfPos < 0) {
                sfPos = buffer.length - 1;
            }
        }

        // *** if no field found, assume unprotected
        if (sf != null && sf.isProtected()) {
            throw new FieldNotFoundException("Unable to type where the cursor is pointing to - " + screenCursor);
        }

        //*** Set this and following characters to null
        int pos = this.screenCursor;
        while(true) {
            if (!(buffer[pos] instanceof BufferChar)) {
                break;
            }

            buffer[pos] = new BufferChar((char) 0);
            pos++;
            if (pos >= this.screenSize) {
                pos = 0;
            }

            if (pos == this.screenCursor) {
                break;
            }
        }

        if (sf != null) {
            sf.setFieldModified();
        }
    }


    public synchronized void eraseInput() throws KeyboardLockedException, FieldNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to erase input as keyboard is locked");
        }

        boolean unprotected = false;
        BufferStartOfField startOfFieldUnprotected = null;

        // *** Check to see if the screen is wrapped or unformatted
        if (!(this.buffer[0] instanceof BufferStartOfField)) {
            BufferStartOfField wrapSoField = null;
            for (int i = this.buffer.length - 1; i >= 0; i--) {
                IBufferHolder bh = this.buffer[i];
                if (bh instanceof BufferStartOfField) {
                    wrapSoField = (BufferStartOfField) bh;
                    break;
                }
            }

            if (wrapSoField == null) {
                unprotected = true;  // unformatted, screen, so all unprotected
            } else {
                unprotected = !wrapSoField.isProtected();
                startOfFieldUnprotected = wrapSoField;
            }
        }



        for(int i = 0; i < this.screenSize; i++) {
            IBufferHolder bh = this.buffer[i];

            if (bh instanceof BufferStartOfField) {
                BufferStartOfField sof = (BufferStartOfField) bh;
                unprotected = !sof.isProtected();
                if (unprotected) {
                    startOfFieldUnprotected = sof;
                } else {
                    startOfFieldUnprotected = null;
                }
            } else {
                if (unprotected) {
                    this.buffer[i] = null;
                    if (startOfFieldUnprotected != null) {
                        startOfFieldUnprotected.setFieldModified();
                    }
                }
            }
        }

    }


    public synchronized void tab() throws KeyboardLockedException, FieldNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        int startPosition = this.screenCursor;
        boolean foundUnprotectedField = false;

        IBufferHolder sfCheck = this.buffer[this.screenCursor];    
        if (sfCheck instanceof BufferStartOfField) {
            foundUnprotectedField = !((BufferStartOfField)sfCheck).isProtected();
        }
        while(true) {
            // advance the cursor
            this.screenCursor++;
            if (this.screenCursor >= this.screenSize) {
                this.screenCursor = 0;
            }

            // Get the entry at this position
            IBufferHolder previousBuffer = this.buffer[this.screenCursor];
            if (previousBuffer == null || previousBuffer instanceof BufferChar) {
                // if this is a character and we are in an unprotected field, use it
                if (foundUnprotectedField) {
                    return;
                }
            } else  if (previousBuffer instanceof BufferStartOfField) {
                // we have a start of field
                BufferStartOfField sof = (BufferStartOfField) previousBuffer;
                // record if it is unprotected or not
                foundUnprotectedField = !sof.isProtected();
            } else {
                throw new FieldNotFoundException("Unrecognised buffer type at pos " + this.screenCursor);
            }

            if (this.screenCursor == startPosition) {
                // we have completely wrapped, so no unprotected chars position to zero
                this.screenCursor = 0;
                return;
            }
        }

    }

    public synchronized void backTab() throws KeyboardLockedException, FieldNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        int startPosition = this.screenCursor;
        int lastCharField = -1;
        boolean foundUnprotectedField = false;
        while(true) {
            // Get the previous position in buffer, wrapped if necessary
            int previousPositionInBuffer = this.screenCursor - 1;
            if (previousPositionInBuffer < 0) {
                previousPositionInBuffer = this.screenSize - 1;
            }

            // Get the entry in the previous position
            IBufferHolder previousBuffer = this.buffer[previousPositionInBuffer];
            if (previousBuffer == null || previousBuffer instanceof BufferChar) {
                // if it is null or a character, mark position as the last valid position whether unprotected or not
                lastCharField = previousPositionInBuffer;
            } else  if (previousBuffer instanceof BufferStartOfField) {
                // we have a start of field
                BufferStartOfField sof = (BufferStartOfField) previousBuffer;
                //if it is protected, invalidate the last valid char position
                if (sof.isProtected()) {
                    lastCharField = -1;
                } else {
                    // as unprotected field,  indicate that there is atleast one on the screen
                    foundUnprotectedField = true;
                    // if we have found a valid char position then use it
                    if (lastCharField != -1) {
                        this.screenCursor = lastCharField;
                        return;
                    }
                }
            } else {
                throw new FieldNotFoundException("Unrecognised buffer type at pos " + previousPositionInBuffer);
            }

            this.screenCursor = previousPositionInBuffer;
            if (this.screenCursor == startPosition) {
                // we have completely wrapped, either was original at the only unprotected field
                // or there was no unprotected fields, so move to origin.
                if (!foundUnprotectedField) {
                    this.screenCursor = 0;
                }
                return;
            }
        }

    }

    public synchronized void cursorUp() throws KeyboardLockedException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        this.screenCursor = this.screenCursor - this.columns;
        if (this.screenCursor < 0) {
            this.screenCursor = this.screenSize +this.screenCursor;
        }
    }

    public synchronized void cursorDown() throws KeyboardLockedException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        this.screenCursor = this.screenCursor + this.columns;
        if (this.screenCursor >= this.screenSize) {
            this.screenCursor = this.screenCursor - this.screenSize;
        }
    }

    public synchronized void cursorLeft() throws KeyboardLockedException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        this.screenCursor--;
        if (this.screenCursor < 0) {
            this.screenCursor = this.screenSize - this.screenCursor;
        }
    }

    public synchronized void cursorRight() throws KeyboardLockedException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        this.screenCursor++;
        if (this.screenCursor >= this.screenSize) {
            this.screenCursor = this.screenCursor - this.screenSize;
        }

    }

    public synchronized void home() throws KeyboardLockedException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        Field[] fields = calculateFields();

        if (fields == null || fields.length == 0) {
            this.screenCursor = 0;
            return;
        }

        //*** find first unprotected field
        for(Field field : fields) {
            if (!field.isProtected() && field.length() > 1) {
                if (field.isDummyField()) {
                    this.screenCursor = 0;
                } else {
                    this.screenCursor = field.getStart() + 1;
                }
                return;
            }
        }

        this.screenCursor = 0;
        return;
    }


    public synchronized void newLine() throws KeyboardLockedException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to move cursor as keyboard is locked");
        }

        Field[] fields = calculateFields();


        int newCursor = ((this.screenCursor / this.columns) + 1) * this.columns;
        if (newCursor >= this.screenSize) {
            newCursor = 0;
        }

        if (fields == null || fields.length == 0) {
            this.screenCursor = newCursor;
            return;
        }

        // locate the field that contains the new position
        int fieldPos = 0;
        Field startField = null;
        for(; fieldPos < fields.length; fieldPos++) {
            if (fields[fieldPos].containsPosition(newCursor)) {
                startField = fields[fieldPos];
                break;
            }
        }

        if (!startField.isProtected() && startField.length() > 1) {
            if (newCursor == startField.getStart() && !startField.isDummyField()) {
                newCursor++;
            }
            this.screenCursor = newCursor;
            return;
        }

        // This field is protected, so locate the next unprotected field
        while(true) {
            fieldPos++;
            if (fieldPos >= fields.length) {
                fieldPos = 0;
            }

            Field nextField = fields[fieldPos];

            if (!nextField.isProtected() && nextField.length() > 1) {
                if (nextField.isDummyField()) {
                    this.screenCursor = nextField.getStart();
                } else {
                    this.screenCursor = nextField.getStart() + 1;
                }
                return;
            }

            if (nextField == startField) {
                break;
            }
        }

        this.screenCursor = newCursor;
        return;
    }

    public void backSpace() throws KeyboardLockedException, FieldNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to type as keyboard is locked");
        }

        int position = this.screenCursor;

        if (buffer[position] != null && !(buffer[position] instanceof BufferChar)) {
            throw new FieldNotFoundException("Unable to type where the cursor is pointing to - " + this.screenCursor);
        }

        BufferStartOfField sf = null;
        int sfPos = position - 1;
        if (sfPos < 0) {
            sfPos = buffer.length - 1;
        }
        while(sfPos != position) {
            if (buffer[sfPos] instanceof BufferStartOfField) {
                sf = (BufferStartOfField) buffer[sfPos];
                break;
            }

            sfPos--;
            if (sfPos < 0) {
                sfPos = buffer.length - 1;
            }
        }

        // *** if no field found, assume unprotected
        if (sf != null && sf.isProtected()) {
            throw new FieldNotFoundException("Unable to type where the cursor is pointing to - " + position);
        }

        if (position == 0) {
            return;  // NOOP, as dont go beyond the start or wrap or through an error
        }

        if (sfPos == (position - 1)) {
            throw new FieldNotFoundException("Unable to backspace where the cursor is pointing to - " + position + ", start of field");
        }

        while(true) {
            this.buffer[position - 1] = this.buffer[position];
            this.buffer[position] = null;

            position++;
            if (position >= this.screenSize) {
                break;
            }

            if (buffer[position] != null && !(buffer[position] instanceof BufferChar)) {
                break;
            }
        }

        this.screenCursor--;

    }


    public int getNoOfColumns() {
        return this.columns;
    }

    public int getNoOfRows() {
        return this.rows;
    }

    public synchronized void registerScreenUpdateListener(IScreenUpdateListener listener) {
        synchronized (updateListeners) {
            updateListeners.add(listener);
        }
    }

    public synchronized void unregisterScreenUpdateListener(IScreenUpdateListener listener) {
        synchronized (updateListeners) {
            updateListeners.remove(listener);
        }
    }

    public int getCursor() {
        return this.screenCursor;
    }

    public Field locateFieldAt(int cursorPos) {
        Field[] fields = calculateFields();

        int fieldPosition = 0;
        for (; fieldPosition < fields.length; fieldPosition++) {
            Field field = fields[fieldPosition];
            if (field.containsPosition(cursorPos)) {
                return field;
            }
        }
        return null;
    }

    public String getValueFromFieldContaining(String text) throws TextNotFoundException {
        Boolean foundHeader = false;
        for (Field field : calculateFields()) {
            if (!foundHeader) {
                if (field.containsText(text)) {
                    foundHeader = true;
                }
            } else {
                String foundText = field.getFieldWithoutNulls();
                if (foundText.length() > 0) {
                    return foundText;
                }
            }
        }

        throw new TextNotFoundException(CANT_FIND_TEXT + text + "'");
    }

    public synchronized void type(String text) throws KeyboardLockedException, FieldNotFoundException {
        this.screenCursor = type(text, this.screenCursor);
    }

    public synchronized int type(String text, int column, int row) throws KeyboardLockedException, FieldNotFoundException {
        int position = column + (row * columns);

        return type(text, position);
    }

    public synchronized int type(String text, int position) throws KeyboardLockedException, FieldNotFoundException {
        if (keyboardLockSet) {
            throw new KeyboardLockedException("Unable to type as keyboard is locked");
        }

        if (buffer[position] != null && !(buffer[position] instanceof BufferChar)) {
            throw new FieldNotFoundException("Unable to type where the cursor is pointing to - " + position);
        }

        BufferStartOfField sf = null;
        int sfPos = position - 1;
        if (sfPos < 0) {
            sfPos = buffer.length - 1;
        }
        while(sfPos != position) {
            if (buffer[sfPos] instanceof BufferStartOfField) {
                sf = (BufferStartOfField) buffer[sfPos];
                break;
            }

            sfPos--;
            if (sfPos < 0) {
                sfPos = buffer.length - 1;
            }
        }

        // *** if no field found, assume unprotected
        if (sf != null && sf.isProtected()) {
            throw new FieldNotFoundException("Unable to type where the cursor is pointing to - " + position);
        }

        if (text.length() == 0) {
            return position;
        }

        for (int i = 0; i < text.length(); i++) {
            IBufferHolder bh = buffer[position];
            if (bh != null && !(bh instanceof BufferChar)) {
                throw new FieldNotFoundException(
                        "Unable to type where the cursor is pointing to - " + position);
            }

            buffer[position] = new BufferChar(text.charAt(i));

            if (sf != null) {
                sf.setFieldModified();
            }

            // We have successfully typed a character, so make sure the cursor is positioned
            // at the next unprotected char, even if it is the position we were just at

            boolean unprotected = true;
            while(true) {
                position++;
                if (position >= screenSize) {
                    position = 0;
                }

                this.screenCursor = position;
                bh = buffer[position];

                if (unprotected && (bh == null || (bh instanceof BufferChar))) {
                    break;
                }

                if (bh != null && (bh instanceof BufferStartOfField)) {
                    BufferStartOfField sof = (BufferStartOfField) bh;
                    unprotected = !sof.isProtected();
                    if (unprotected) {
                        sf = sof;
                    }
                }
            }
        }

        return position;
    }

    public synchronized byte[] aid(AttentionIdentification aid) throws DatastreamException, TerminalInterruptedException {
        lockKeyboard();

        try {
            ByteArrayOutputStream outboundBuffer = new ByteArrayOutputStream();

            outboundBuffer.write(aid.getKeyValue());


            if (aid == AttentionIdentification.CLEAR) {
                erase();
            } else if (aid == AttentionIdentification.PA1) {
            } else if (aid == AttentionIdentification.PA2) {
            } else if (aid == AttentionIdentification.PA3) {
            } else {
                BufferAddress cursor = new BufferAddress(this.screenCursor);
                outboundBuffer.write(cursor.getCharRepresentation());
                readModifiedBuffer(outboundBuffer);
            }
            writeTrace(outboundBuffer);

            for (IScreenUpdateListener listener : updateListeners) {
                listener.screenUpdated(Direction.SENDING, aid);
            }

            this.lastAid = aid;

            return outboundBuffer.toByteArray();
        } catch (IOException e) {
            throw new DatastreamException("Unable to generate outbound datastream", e);
        }
    }

    public int getScreenSize() {
        return this.screenSize;
    }

    public String printFields() {
        Field[] fields = calculateFields();

        StringBuilder sb = new StringBuilder();
        for (Field field : fields) {
            sb.append(field.toString());
            sb.append("\n");
        }

        return sb.toString();
    }

    public void setBuffer(IBufferHolder[] newBuffer) {
        for (int i = 0; i < this.buffer.length && i < newBuffer.length; i++) {
            this.buffer[i] = newBuffer[i];
        }
    }

    public void setBuffer(int col, int row, String text) {
        int pos = (row * columns) + col;
        for (int i = 0; i < text.length(); i++) {
            buffer[pos] = new BufferChar(text.charAt(i));
            pos++;
        }
    }

    public void nullify(int col, int row, int len) {
        int pos = (row * columns) + col;
        for (int i = 0; i < len; i++) {
            buffer[pos] = null;
            pos++;
        }
    }

    public Field getFieldAt(int col, int row) {
        int pos = (row * columns) + col;

        Field[] fields = calculateFields();
        Field currentField = fields[0];
        for (int i = 1; i < fields.length; i++) {
            if (fields[i].getStart() > pos) {
                return currentField;
            }
            currentField = fields[i];
        }

        return currentField;
    }

    public void setCursorPosition(int newPosition) {
        this.screenCursor = newPosition;
    }

    public void setCursorPosition(int column, int row) {
        this.screenCursor = (row * this.columns) + column;
    }

    public synchronized void registerDatastreamListener(IDatastreamListener listener) {
        if (listener == null) {
            return;
        }

        if (!this.datastreamListeners.contains(listener)) {
            this.datastreamListeners.add(listener);
        }
    }

    public synchronized void unregisterDatastreamListener(IDatastreamListener listener) {
        this.datastreamListeners.remove(listener);
    }

    public List getDatastreamListeners() {
        return this.datastreamListeners;
    }

    public int getPrimaryColumns() {
        return this.primaryColumns;
    }

    public int getPrimaryRows() {
        return this.primaryRows;
    }

    public int getAlternateColumns() {
        return this.alternateColumns;
    }

    public int getAlternateRows() {
        return this.alternateRows;
    }

    public void testingSetLastAid(AttentionIdentification aid) {
        this.lastAid = aid;
    }

    public synchronized boolean isClearScreen() {
        for(IBufferHolder bh : buffer) {
            if (bh != null) {
                return false;
            }
        }

        return true;
    }

    public Colour getColourAtPosition(int pos) {
        
        Field[] fields = calculateFields();
        Field currentField = fields[0];
        for (int i = 1; i < fields.length; i++) {
            if (fields[i].getStart() > pos) {
                break;
            }
            currentField = fields[i];
        }
        
        return currentField.getForegroundColour();
    }

    public Highlight getHighlightAtPosition(int pos) {
        Field[] fields = calculateFields();
        Field currentField = fields[0];
        for (int i = 1; i < fields.length; i++) {
            if (fields[i].getStart() > pos) {
                break;
            }
            currentField = fields[i];
        }
        
        return currentField.getHighlight();
    }

    public Charset getCodePage() {
        return codePage;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy