├── src ├── test │ ├── resources │ │ ├── keystore.jks │ │ ├── login-extended-field-without-field-attribute.yml │ │ ├── login-without-fields.yml │ │ ├── test_capabilities.yml │ │ ├── login-welcome-screen.txt │ │ ├── success-apl-screen.txt │ │ ├── user-menu-screen.txt │ │ ├── sscplu-login-middle-screen │ │ ├── login-3278-M2-E-final-screen.txt │ │ ├── sscplu-login-success-screen.txt │ │ ├── attribute_not_present_expected_screen.txt │ │ ├── login-special-character-charset-CP1047.txt │ │ ├── login-special-character-charset-CP1147.txt │ │ ├── field_without_start_attribute_expected_screen.txt │ │ ├── field_without_start_attribute.yml │ │ ├── login-3270-model-5.yml │ │ ├── login-special-characters.yml │ │ ├── user-menu-for-screen-type-5.txt │ │ ├── attribute_not_present.yml │ │ └── sscplu-login.yml │ └── java │ │ └── com │ │ └── bytezone │ │ └── dm3270 │ │ ├── UnlockWaiter.java │ │ ├── ConditionWaiter.java │ │ ├── ScreenTextWaiter.java │ │ └── ScreenPositionTest.java └── main │ └── java │ └── com │ └── bytezone │ └── dm3270 │ ├── orders │ ├── BufferAddressSource.java │ ├── InsertCursorOrder.java │ ├── NewlineOrder.java │ ├── EraseUnprotectedToAddressOrder.java │ ├── SetBufferAddressOrder.java │ ├── ModifyFieldOrder.java │ ├── StartFieldOrder.java │ ├── ProgramTabOrder.java │ ├── SetAttributeOrder.java │ ├── GraphicsEscapeOrder.java │ ├── TextOrder.java │ ├── FormatControlOrder.java │ ├── RepeatToAddressOrder.java │ ├── BufferAddress.java │ ├── StartFieldExtendedOrder.java │ └── Order.java │ ├── display │ ├── FieldChangeListener.java │ ├── ScreenChangeListener.java │ ├── CursorMoveListener.java │ ├── DisplayScreen.java │ ├── ScreenDimensions.java │ ├── Pen.java │ ├── ScreenContext.java │ └── ScreenPosition.java │ ├── application │ ├── KeyboardStatusListener.java │ ├── KeyboardStatusChangedEvent.java │ ├── Site.java │ └── ConsolePane.java │ ├── buffers │ ├── ReplyBuffer.java │ ├── AbstractTN3270Command.java │ ├── AbstractTelnetCommand.java │ ├── AbstractReplyBuffer.java │ ├── AbstractBuffer.java │ ├── Buffer.java │ └── MultiBuffer.java │ ├── replyfield │ ├── Segment.java │ ├── Transparency.java │ ├── DefaultReply.java │ ├── AuxiliaryDevices.java │ ├── RPQNames.java │ ├── ReplyModes.java │ ├── OEMAuxiliaryDevice.java │ ├── ImplicitPartition.java │ ├── AlphanumericPartitions.java │ ├── Summary.java │ ├── DistributedDataManagement.java │ ├── Highlight.java │ ├── Color.java │ └── CharacterSets.java │ ├── streams │ ├── BufferListener.java │ ├── TelnetSocket.java │ └── TerminalServer.java │ ├── telnet │ ├── TelnetCommandProcessor.java │ ├── TelnetSubcommand.java │ ├── TerminalTypeSubcommand.java │ └── TelnetProcessor.java │ ├── attributes │ ├── ResetAttribute.java │ ├── ForegroundColor.java │ ├── BackgroundColor.java │ ├── Charset.java │ ├── ExtendedHighlight.java │ ├── ColorAttribute.java │ ├── StartFieldAttribute.java │ └── Attribute.java │ ├── extended │ ├── UnbindCommand.java │ ├── ResponseCommand.java │ ├── LogicalUnit.java │ ├── AbstractExtendedCommand.java │ ├── SscpLuDataCommand.java │ └── TN3270ExtendedCommand.java │ ├── ConnectionListener.java │ ├── commands │ ├── EraseAllUnprotectedCommand.java │ ├── WriteControlCharacter.java │ ├── ReadCommand.java │ ├── ReadPartitionQuery.java │ ├── WriteCommand.java │ └── WriteStructuredFieldCommand.java │ ├── structuredfields │ ├── QueryReplySF.java │ ├── DefaultStructuredField.java │ ├── EraseResetSF.java │ ├── StructuredField.java │ ├── Outbound3270DS.java │ ├── SetReplyModeSF.java │ └── ReadPartitionSF.java │ ├── ConnectionListenerBroadcast.java │ ├── session │ └── SessionRecord.java │ ├── Charset.java │ └── assistant │ └── Dataset.java ├── .github ├── maven-central-deploy.sh ├── fix-docs-version.sh ├── semver-check.sh ├── settings.xml └── workflows │ └── release.yml ├── .travis.yml ├── README.md └── .gitignore /src/test/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blazemeter/dm3270/master/src/test/resources/keystore.jks -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/BufferAddressSource.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | public interface BufferAddressSource { 4 | 5 | BufferAddress getBufferAddress(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/FieldChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | public interface FieldChangeListener { 4 | 5 | void fieldChanged(Field oldField, Field newField); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/ScreenChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | public interface ScreenChangeListener { 4 | 5 | void screenChanged(ScreenWatcher screenWatcher); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/CursorMoveListener.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | public interface CursorMoveListener { 4 | 5 | void cursorMoved(int oldLocation, int newLocation, Field field); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/application/KeyboardStatusListener.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.application; 2 | 3 | public interface KeyboardStatusListener { 4 | 5 | void keyboardStatusChanged(KeyboardStatusChangedEvent evt); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/ReplyBuffer.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | import java.util.Optional; 4 | 5 | public interface ReplyBuffer extends Buffer { 6 | 7 | Optional getReply(); 8 | 9 | void setReply(Buffer reply); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/Segment.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | public class Segment extends QueryReplyField { 4 | 5 | public Segment(byte[] buffer) { 6 | super(buffer); 7 | 8 | assert data[1] == SEGMENT_REPLY; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/Transparency.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | public class Transparency extends QueryReplyField { 4 | 5 | public Transparency(byte[] buffer) { 6 | super(buffer); 7 | 8 | assert data[1] == TRANSPARENCY_REPLY; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/streams/BufferListener.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.streams; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public interface BufferListener { 6 | 7 | void listen(TelnetSocket.Source targetRole, byte[] message, LocalDateTime dateTime, 8 | boolean genuine); 9 | 10 | void close(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/AbstractTN3270Command.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | public abstract class AbstractTN3270Command extends AbstractReplyBuffer { 4 | 5 | protected AbstractTN3270Command() { 6 | } 7 | 8 | public AbstractTN3270Command(byte[] buffer, int offset, int length) { 9 | super(buffer, offset, length); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/telnet/TelnetCommandProcessor.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.telnet; 2 | 3 | public interface TelnetCommandProcessor { 4 | 5 | void processData(byte[] buffer, int length); 6 | 7 | void processRecord(byte[] buffer, int length); 8 | 9 | void processTelnetCommand(byte[] buffer, int length); 10 | 11 | void processTelnetSubcommand(byte[] buffer, int length); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/DisplayScreen.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | import com.bytezone.dm3270.display.Screen.ScreenOption; 4 | 5 | public interface DisplayScreen { 6 | 7 | Pen getPen(); 8 | 9 | ScreenDimensions getScreenDimensions(); 10 | 11 | ScreenPosition getScreenPosition(int position); 12 | 13 | int validate(int position); 14 | 15 | void clearScreen(ScreenOption newScreen); 16 | 17 | void insertCursor(int position); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/ResetAttribute.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | 5 | public class ResetAttribute extends Attribute { 6 | 7 | public ResetAttribute(byte value) { 8 | super(AttributeType.RESET, XA_RESET, value); 9 | } 10 | 11 | @Override 12 | public ScreenContext process(ScreenContext defaultContext, ScreenContext currentContext) { 13 | return defaultContext; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/ForegroundColor.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | 5 | public class ForegroundColor extends ColorAttribute { 6 | 7 | public ForegroundColor(byte value) { 8 | super(AttributeType.FOREGROUND_COLOR, Attribute.XA_FGCOLOR, value); 9 | } 10 | 11 | @Override 12 | public ScreenContext process(ScreenContext defaultContext, ScreenContext currentContext) { 13 | return currentContext.withForeground(color); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/BackgroundColor.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | 5 | public class BackgroundColor extends ColorAttribute { 6 | 7 | public BackgroundColor(byte value) { 8 | super(AttributeType.BACKGROUND_COLOR, Attribute.XA_BGCOLOR, value); 9 | } 10 | 11 | @Override 12 | public ScreenContext process(ScreenContext defaultContext, ScreenContext currentContext) { 13 | return currentContext.withBackgroundColor(color); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/extended/UnbindCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.extended; 2 | 3 | public class UnbindCommand extends AbstractExtendedCommand { 4 | 5 | public UnbindCommand(CommandHeader commandHeader, byte[] buffer, int offset, 6 | int length) { 7 | super(commandHeader, buffer, offset, length); 8 | } 9 | 10 | @Override 11 | public String getName() { 12 | return "Unbind"; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return String.format("UNB: %02X", data[0]); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /.github/maven-central-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of deploying tagged versions to maven central and updating the pom.xml 4 | # version with next development version. 5 | # 6 | # Required environment variables: GPG_SECRET_KEYS, GPG_OWNERTRUST, GPG_EXECUTABLE 7 | 8 | set -eo pipefail 9 | 10 | echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --batch --import 11 | echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --batch --import-ownertrust 12 | mvn --batch-mode deploy -Prelease -DskipTests --settings .github/settings.xml 13 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/Charset.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | 5 | public class Charset extends Attribute { 6 | 7 | private final boolean aplCharset; 8 | 9 | public Charset(byte charset) { 10 | super(AttributeType.CHARSET, Attribute.XA_CHARSET, charset); 11 | aplCharset = (charset == (byte) 0xf1); 12 | } 13 | 14 | @Override 15 | public ScreenContext process(ScreenContext defaultContext, ScreenContext currentContext) { 16 | return currentContext.withGraphic(aplCharset); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.github/fix-docs-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of setting proper version in docs. 4 | # 5 | set -eo pipefail 6 | 7 | VERSION=$1 8 | 9 | update_file_versions() { 10 | local VERSION="$1" 11 | local FILE="$2" 12 | sed -i "s/.*<\/version>/${VERSION}-lib<\/version>/g" ${FILE} 13 | } 14 | 15 | update_file_versions ${VERSION} README.md 16 | 17 | git add README.md 18 | git config --local user.email "$(git log --format='%ae' HEAD^!)" 19 | git config --local user.name "$(git log --format='%an' HEAD^!)" 20 | git commit -m "[skip ci] Updated README version" 21 | git push origin HEAD:master 22 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/AbstractTelnetCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | import com.bytezone.dm3270.streams.TelnetState; 4 | 5 | public abstract class AbstractTelnetCommand extends AbstractReplyBuffer { 6 | 7 | protected TelnetState telnetState; 8 | 9 | public AbstractTelnetCommand(byte[] buffer, int offset, int length, 10 | TelnetState telnetState) { 11 | super(buffer, offset, length); 12 | this.telnetState = telnetState; 13 | } 14 | 15 | @Override 16 | public byte[] getTelnetData() { 17 | return data; // do not expand anything, do not append EOR bytes 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/ScreenDimensions.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | import com.bytezone.dm3270.orders.BufferAddress; 4 | 5 | public class ScreenDimensions { 6 | 7 | public final int rows; 8 | public final int columns; 9 | public final int size; 10 | 11 | public ScreenDimensions(int rows, int columns) { 12 | this.rows = rows; 13 | this.columns = columns; 14 | 15 | size = rows * columns; 16 | 17 | BufferAddress.setScreenWidth(columns); // for debugging output 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return String.format("[Rows:%d, Columns:%d, Size:%d]", rows, columns, size); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/ConnectionListener.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | /** 4 | * Interface to be invoked when there is some change on terminal connection. 5 | */ 6 | public interface ConnectionListener { 7 | 8 | /** 9 | * Method invoked when connection is established to terminal server. 10 | */ 11 | void onConnection(); 12 | 13 | /** 14 | * Method invoked when an {@link Exception} is thrown. 15 | * 16 | * @param ex Exception thrown while connected to the terminal server. 17 | */ 18 | void onException(Exception ex); 19 | 20 | /** 21 | * Method invoked when connection is closed by terminal server. 22 | */ 23 | void onConnectionClosed(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/AbstractReplyBuffer.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | import java.util.Optional; 4 | 5 | public abstract class AbstractReplyBuffer extends AbstractBuffer implements ReplyBuffer { 6 | 7 | private Buffer reply; 8 | 9 | protected AbstractReplyBuffer() { 10 | } 11 | 12 | public AbstractReplyBuffer(byte[] buffer, int offset, int length) { 13 | super(buffer, offset, length); 14 | } 15 | 16 | @Override 17 | public void setReply(Buffer reply) { 18 | this.reply = reply; 19 | } 20 | 21 | @Override 22 | public Optional getReply() { 23 | return reply == null ? Optional.empty() : Optional.of(reply); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/InsertCursorOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import com.bytezone.dm3270.display.Pen; 5 | 6 | public class InsertCursorOrder extends Order { 7 | 8 | public InsertCursorOrder(byte[] buffer, int offset) { 9 | assert buffer[offset] == Order.INSERT_CURSOR; 10 | 11 | this.buffer = new byte[1]; 12 | this.buffer[0] = buffer[offset]; 13 | } 14 | 15 | @Override 16 | public void process(DisplayScreen screen) { 17 | Pen pen = screen.getPen(); 18 | screen.insertCursor(pen.getPosition()); 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "IC"; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.github/semver-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script checks that the version number of the release is an expected one, and avoid erroneous releases which don't follow semver 4 | set -eo pipefail 5 | 6 | git fetch --tags --quiet 7 | VERSION="$1" 8 | PREV_VERSION=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`) 9 | PREV_VERSION=${PREV_VERSION#v} 10 | PREV_MAJOR="${PREV_VERSION%%.*}" 11 | PREV_VERSION="${PREV_VERSION#*.}" 12 | PREV_MINOR="${PREV_VERSION%%.*}" 13 | PREV_PATCH="${PREV_VERSION#*.}" 14 | if [[ "$PREV_VERSION" == "$PREV_PATCH" ]]; then 15 | PREV_PATCH="0" 16 | fi 17 | 18 | [[ "$VERSION" == "$PREV_MAJOR.$PREV_MINOR.$((PREV_PATCH + 1))-lib" || "$VERSION" == "$PREV_MAJOR.$((PREV_MINOR + 1))-lib" || "$VERSION" == "$((PREV_MAJOR + 1)).0-lib" ]] 19 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/DefaultReply.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class DefaultReply extends QueryReplyField { 8 | 9 | private static final Logger LOG = LoggerFactory.getLogger(DefaultReply.class); 10 | private final Charset charset; 11 | 12 | public DefaultReply(byte[] buffer, Charset charset) { 13 | super(buffer); 14 | this.charset = charset; 15 | LOG.warn("Unknown reply field: {}\n{}", String.format("%02X", buffer[0]), 16 | charset.toHex(buffer)); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return super.toString() + String.format("%n%n%s", charset.toHex(data)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/application/KeyboardStatusChangedEvent.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.application; 2 | 3 | public final class KeyboardStatusChangedEvent { 4 | 5 | public final boolean keyboardLocked; 6 | private final boolean insertMode; 7 | private final String keyName; 8 | 9 | public KeyboardStatusChangedEvent(boolean insertMode, boolean keyboardLocked, 10 | String keyName) { 11 | this.insertMode = insertMode; 12 | this.keyboardLocked = keyboardLocked; 13 | this.keyName = keyName; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return String.format("Keyboard locked ... %s%n", keyboardLocked) 19 | + String.format("Insert mode on .... %s%n", insertMode) 20 | + String.format("Key pressed ....... %s%n", keyName); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/commands/EraseAllUnprotectedCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.commands; 2 | 3 | import com.bytezone.dm3270.display.Screen; 4 | 5 | public class EraseAllUnprotectedCommand extends Command { 6 | 7 | // This command has no WCC or data. 8 | public EraseAllUnprotectedCommand(byte[] buffer, int offset, int length) { 9 | super(buffer, offset, length); 10 | assert buffer[offset] == Command.ERASE_ALL_UNPROTECTED_0F 11 | || buffer[offset] == Command.ERASE_ALL_UNPROTECTED_6F; 12 | } 13 | 14 | @Override 15 | public void process(Screen screen) { 16 | screen.eraseAllUnprotected(); 17 | } 18 | 19 | @Override 20 | public String getName() { 21 | return "Erase All Unprotected"; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return "EAU :"; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/QueryReplySF.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.replyfield.QueryReplyField; 5 | 6 | public class QueryReplySF extends StructuredField { 7 | 8 | private final QueryReplyField queryReplyField; 9 | 10 | public QueryReplySF(byte[] buffer, int offset, int length, Charset charset) { 11 | super(buffer, offset, length, charset); 12 | assert data[0] == StructuredField.QUERY_REPLY; 13 | queryReplyField = QueryReplyField.getReplyField(data, charset); 14 | } 15 | 16 | public QueryReplyField getQueryReplyField() { 17 | return queryReplyField; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return String.format("Struct Field : %02X QueryReply%n", type) 23 | + queryReplyField; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/AuxiliaryDevices.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class AuxiliaryDevices extends QueryReplyField { 6 | 7 | private final short flags; 8 | 9 | public AuxiliaryDevices(byte[] buffer) { 10 | super(buffer); 11 | ByteBuffer dataBuffer = ByteBuffer.wrap(buffer); 12 | //skip queryReply id 13 | dataBuffer.get(); 14 | assert dataBuffer.get() == AUXILIARY_DEVICE_REPLY; 15 | flags = dataBuffer.getShort(); 16 | } 17 | 18 | public AuxiliaryDevices() { 19 | super(AUXILIARY_DEVICE_REPLY); 20 | flags = 0; 21 | ByteBuffer buffer = createReplyBuffer(2); 22 | buffer.putShort(flags); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return super.toString() + String.format("%n flags : %02X", flags); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/extended/ResponseCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.extended; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class ResponseCommand extends AbstractExtendedCommand { 8 | 9 | private static final Logger LOG = LoggerFactory.getLogger(ResponseCommand.class); 10 | 11 | public ResponseCommand(CommandHeader commandHeader, byte[] buffer, int offset, 12 | int length, Charset charset) { 13 | super(commandHeader, buffer, offset, length); 14 | 15 | if (length != 1) { 16 | LOG.debug(charset.toHex(buffer, offset, length)); 17 | } 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return "Response"; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return String.format("Rsp: %02X", data[0]); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.github/settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | ossrh 9 | ${env.SONATYPE_USERNAME} 10 | ${env.SONATYPE_PASSWORD} 11 | 12 | 13 | 14 | 15 | 16 | ossrh 17 | 18 | true 19 | 20 | 21 | ${env.GPG_EXECUTABLE} 22 | ${env.GPG_PASSPHRASE} 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/ExtendedHighlight.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | 5 | public class ExtendedHighlight extends Attribute { 6 | 7 | private static String[] highlights = {"xx", "Blink", "Reverse video", "bb", "Underscore"}; 8 | 9 | public ExtendedHighlight(byte value) { 10 | super(AttributeType.HIGHLIGHT, Attribute.XA_HIGHLIGHTING, value); 11 | } 12 | 13 | @Override 14 | public ScreenContext process(ScreenContext defaultContext, ScreenContext currentContext) { 15 | return currentContext.withHighlight(attributeValue); 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | String valueText = attributeValue == 0 ? "Reset" : highlights[attributeValue & 0x0F]; 21 | return String.format("%-12s : %02X %s", name(), attributeValue, valueText); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/DefaultStructuredField.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.Screen; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public class DefaultStructuredField extends StructuredField { 9 | 10 | private static final Logger LOG = LoggerFactory.getLogger(DefaultStructuredField.class); 11 | 12 | public DefaultStructuredField(byte[] buffer, int offset, int length, Charset charset) { 13 | super(buffer, offset, length, charset); 14 | LOG.debug("Default Structured Field !!"); 15 | } 16 | 17 | @Override 18 | public void process(Screen screen) { 19 | LOG.debug("Processing a DefaultStructuredField: {}", String.format("%02x", type)); 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return String.format("Unknown SF : %02X%n", data[0]) 25 | + charset.toHex(data); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/NewlineOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import com.bytezone.dm3270.display.Pen; 5 | 6 | public class NewlineOrder extends Order { 7 | 8 | public NewlineOrder(byte[] buffer, int offset) { 9 | this.buffer = new byte[1]; 10 | this.buffer[0] = buffer[offset]; 11 | } 12 | 13 | @Override 14 | public void process(DisplayScreen screen) { 15 | Pen pen = screen.getPen(); 16 | pen.moveToNextLine(); 17 | for (int i = 0; i < duplicates; i++) { 18 | pen.moveToNextLine(); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean matchesPreviousOrder(Order order) { 24 | return order instanceof NewlineOrder; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | String duplicateText = duplicates == 0 ? "" : "x " + (duplicates + 1); 30 | return String.format("FCO : %-12s : %02X %s", "FCO_NEWLINE", buffer[0], duplicateText); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/EraseResetSF.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.Screen; 5 | 6 | public class EraseResetSF extends StructuredField { 7 | 8 | private byte flags; 9 | private Size size; 10 | 11 | private enum Size { 12 | DEFAULT, ALTERNATE 13 | } 14 | 15 | public EraseResetSF(byte[] buffer, int offset, int length, Charset charset) { 16 | super(buffer, offset, length, charset); 17 | 18 | assert data[0] == StructuredField.ERASE_RESET; 19 | flags = data[1]; 20 | 21 | if ((flags & 0xC0) == 0) { 22 | size = Size.DEFAULT; 23 | } else { 24 | size = Size.ALTERNATE; 25 | } 26 | } 27 | 28 | @Override 29 | public void process(Screen screen) { 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "Struct Field : 03 Erase/Reset\n" + String 35 | .format(" flags : %02X (%s)", flags, size); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/RPQNames.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.buffers.Buffer; 5 | 6 | public class RPQNames extends QueryReplyField { 7 | 8 | private String deviceType; 9 | private long model; 10 | private String rpqName; 11 | 12 | public RPQNames(byte[] buffer, Charset charset) { 13 | super(buffer); 14 | assert data[1] == RPQ_NAMES_REPLY; 15 | 16 | deviceType = charset.getString(data, 2, 4); 17 | model = Buffer.unsignedLong(data, 6); 18 | int len = (data[10] & 0xFF) - 1; 19 | if (len > 0) { 20 | rpqName = charset.getString(data, 11, len); 21 | } else { 22 | rpqName = ""; 23 | } 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return super.toString() + String.format("%n device : %s", deviceType) 29 | + String.format("%n model : %d", model) 30 | + String.format("%n RPQ name : %s", rpqName); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/application/Site.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.application; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class Site { 7 | 8 | private static final Logger LOG = LoggerFactory.getLogger(Site.class); 9 | 10 | public final boolean extended; 11 | private final String url; 12 | private int port; 13 | 14 | public Site(String url, int port, boolean extended) { 15 | this.url = url; 16 | this.port = port; 17 | this.extended = extended; 18 | } 19 | 20 | public String getURL() { 21 | return url; 22 | } 23 | 24 | public int getPort() { 25 | if (port <= 0) { 26 | LOG.warn("Invalid port value: {}. Fallback to default value {}", port, 23); 27 | port = 23; 28 | } 29 | return port; 30 | } 31 | 32 | public boolean getExtended() { 33 | return extended; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return String.format("Site [url=%s, port=%d]", getURL(), getPort()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/EraseUnprotectedToAddressOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class EraseUnprotectedToAddressOrder extends Order { 8 | 9 | private static final Logger LOG = LoggerFactory.getLogger(EraseUnprotectedToAddressOrder.class); 10 | 11 | private final BufferAddress stopAddress; 12 | 13 | public EraseUnprotectedToAddressOrder(byte[] buffer, int offset) { 14 | assert buffer[offset] == Order.ERASE_UNPROTECTED; 15 | stopAddress = new BufferAddress(buffer[offset + 1], buffer[offset + 2]); 16 | 17 | this.buffer = new byte[3]; 18 | System.arraycopy(buffer, offset, this.buffer, 0, this.buffer.length); 19 | } 20 | 21 | @Override 22 | public void process(DisplayScreen screen) { 23 | LOG.warn("EraseUnprotectedToAddress not finished"); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "EUA : " + stopAddress; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/SetBufferAddressOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import com.bytezone.dm3270.display.Pen; 5 | 6 | public class SetBufferAddressOrder extends Order implements BufferAddressSource { 7 | 8 | private final BufferAddress bufferAddress; 9 | 10 | public SetBufferAddressOrder(byte[] buffer, int offset) { 11 | assert buffer[offset] == Order.SET_BUFFER_ADDRESS; 12 | 13 | bufferAddress = new BufferAddress(buffer[offset + 1], buffer[offset + 2]); 14 | 15 | this.buffer = new byte[3]; 16 | System.arraycopy(buffer, offset, this.buffer, 0, 3); 17 | } 18 | 19 | @Override 20 | public BufferAddress getBufferAddress() { 21 | return bufferAddress; 22 | } 23 | 24 | @Override 25 | public void process(DisplayScreen screen) { 26 | Pen pen = screen.getPen(); 27 | pen.moveTo(bufferAddress.getLocation()); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return String.format("SBA : %s", bufferAddress); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/ModifyFieldOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.attributes.Attribute; 4 | import com.bytezone.dm3270.display.DisplayScreen; 5 | import java.util.Optional; 6 | 7 | public class ModifyFieldOrder extends Order { 8 | 9 | public ModifyFieldOrder(byte[] buffer, int offset) { 10 | assert buffer[offset] == Order.MODIFY_FIELD; 11 | 12 | int totalAttributePairs = buffer[offset + 1] & 0xFF; 13 | 14 | this.buffer = new byte[totalAttributePairs * 2 + 2]; 15 | this.buffer[0] = buffer[offset]; 16 | this.buffer[1] = buffer[offset + 1]; 17 | 18 | int ptr = offset + 2; 19 | int bptr = 2; 20 | for (int i = 0; i < totalAttributePairs; i++) { 21 | Optional attribute = 22 | Attribute.getAttribute(buffer[ptr], buffer[ptr + 1]); 23 | assert attribute.isPresent(); 24 | this.buffer[bptr++] = buffer[ptr++]; 25 | this.buffer[bptr++] = buffer[ptr++]; 26 | } 27 | } 28 | 29 | @Override 30 | public void process(DisplayScreen screen) { 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/Pen.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.attributes.Attribute; 5 | import com.bytezone.dm3270.attributes.StartFieldAttribute; 6 | 7 | public interface Pen extends Iterable { 8 | 9 | static Pen getInstance(ScreenPosition[] screenPositions, ScreenDimensions screenDimensions, 10 | Charset charset) { 11 | return new PenType1(screenPositions, screenDimensions, charset); 12 | } 13 | 14 | void clearScreen(); 15 | 16 | void startField(StartFieldAttribute startFieldAttribute); 17 | 18 | void addAttribute(Attribute attribute); 19 | 20 | int getPosition(); 21 | 22 | void writeGraphics(byte b); 23 | 24 | void write(byte b); 25 | 26 | void moveRight(); 27 | 28 | void moveToNextLine(); 29 | 30 | void eraseEOF(); 31 | 32 | void tab(); 33 | 34 | void moveTo(int position); 35 | 36 | int validate(int position); 37 | 38 | void setScreenDimensions(ScreenDimensions screenDimensions); 39 | 40 | Iterable fromCurrentPosition(); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/StartFieldOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.attributes.StartFieldAttribute; 4 | import com.bytezone.dm3270.display.DisplayScreen; 5 | import com.bytezone.dm3270.display.Pen; 6 | 7 | public class StartFieldOrder extends Order { 8 | 9 | private final StartFieldAttribute startFieldAttribute; 10 | private int location = -1; 11 | 12 | public StartFieldOrder(byte[] buffer, int offset) { 13 | assert buffer[offset] == Order.START_FIELD; 14 | 15 | startFieldAttribute = new StartFieldAttribute(buffer[offset + 1]); 16 | 17 | this.buffer = new byte[2]; 18 | this.buffer[0] = buffer[offset]; 19 | this.buffer[1] = buffer[offset + 1]; 20 | } 21 | 22 | @Override 23 | public void process(DisplayScreen screen) { 24 | Pen pen = screen.getPen(); 25 | location = pen.getPosition(); 26 | pen.startField(startFieldAttribute); 27 | pen.moveRight(); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return String.format("SF : %s (%04d)", startFieldAttribute, location); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/ReplyModes.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | public class ReplyModes extends QueryReplyField { 4 | 5 | private static String[] modeTypes = {"Field mode", "Extended field mode", 6 | "Character mode"}; 7 | private int[] modes; 8 | 9 | public ReplyModes() { 10 | super(REPLY_MODES_REPLY); 11 | int ptr = createReply(3); 12 | 13 | reply[ptr++] = 0x00; 14 | reply[ptr++] = 0x01; 15 | reply[ptr++] = 0x02; 16 | 17 | checkDataLength(ptr); 18 | } 19 | 20 | public ReplyModes(byte[] buffer) { 21 | super(buffer); 22 | 23 | assert data[1] == REPLY_MODES_REPLY; 24 | 25 | modes = new int[data.length - 2]; 26 | for (int i = 0; i < modes.length; i++) { 27 | modes[i] = data[i + 2] & 0xFF; 28 | } 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | StringBuilder text = new StringBuilder(super.toString()); 34 | 35 | for (int mode : modes) { 36 | text.append(String.format("%n mode : %02X - %s", mode, modeTypes[mode])); 37 | } 38 | return text.toString(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | jdk: oraclejdk8 4 | cache: 5 | directories: 6 | - "$HOME/.m2" 7 | before_deploy: 8 | - mvn --batch-mode versions:set -DnewVersion="$(T=${TRAVIS_TAG%%-lib}; echo ${T:1})" 9 | - mvn clean package -DskipTests 10 | deploy: 11 | provider: releases 12 | api_key: 13 | secure: CGBw5K5DgRDlUbw9jcfFj11YC25uf5gkyPyaFfPZ+Csbexk6XyZAA2aykUoyKaScZwamFBl0+inwL+ZInXiTJfUo/YODbob4R4PXd9VeDr64jTCCjJAumLmsSWTW/hUiIZ+0g1h3TETFJkUunA0IpMZw/NzW91k4iaQ4NNUz0WrboJmBLUdHTo6TWImhhmV7EBJH3Wc+96d8CJUHTbmHURxIYUHQFH5JhtsmG7ppci7idCkCuqAgktw0V8vu5nAYK8DcBWFADRekUppbC0OsbvZhvbCXJuFbfbWKGe3aaXp2Zek+e3SLugoqmwGIpVM06klmscAGWZXtYY0ISpl26fiQFuGrm+ZIl4/+Tr6h5in1noyd1gi71QTibyq4m9qeJvCEIzBCjERjLM21Y1q9mcybMutptOGamWFscGchLIW+bcokScsE+ESWKUyKPebcDsUSsP8cY+FZwb1tDmOX7IyOg4d6/MxmS3Kwu4T108bsAzlIV/Aiexv6iW88MzjISD/oxBwm0bnjgpy008CYIQGK6gHhWo1/ofKD/gBdqZB/gFk7o0wBTkokvQcprsUoJMTwbCySTG/yBlyGfv9b6zsi3waFaLvlCAzb4i9BcLxkKjYx8r0l7NaP/i3Rda761Y8JsBkDQyDqvxVeR+BLFYLZjs6IK0vQNFKpTjIQ0Ic= 14 | file_glob: true 15 | file: target/dm3270-lib-*.jar 16 | skip_cleanup: true 17 | on: 18 | repo: Blazemeter/dm3270 19 | tags: true 20 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/ProgramTabOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import com.bytezone.dm3270.display.Pen; 5 | 6 | public class ProgramTabOrder extends Order { 7 | 8 | private Order previousOrder; 9 | 10 | public ProgramTabOrder(byte[] buffer, int offset) { 11 | assert buffer[offset] == Order.PROGRAM_TAB; 12 | 13 | this.buffer = new byte[1]; 14 | this.buffer[0] = buffer[offset]; 15 | } 16 | 17 | @Override 18 | public void process(DisplayScreen screen) { 19 | Pen pen = screen.getPen(); 20 | 21 | // if the previous data was text then erase the remainder of the field 22 | if (previousOrder instanceof TextOrder) { 23 | pen.eraseEOF(); 24 | } 25 | 26 | pen.tab(); 27 | } 28 | 29 | @Override 30 | public boolean matchesPreviousOrder(Order previousOrder) { 31 | this.previousOrder = previousOrder; 32 | return false; // we don't care if it matched, but we want to know what it was 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "PT :"; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/resources/login-extended-field-without-field-attribute.yml: -------------------------------------------------------------------------------- 1 | # Do TN3270E 2 | - !server {data: FFFD28, delayMillis: 342} 3 | # Won't TN3270E 4 | - !client {data: FFFC28} 5 | # Do Terminal Type 6 | - !server {data: FFFD18, delayMillis: 196} 7 | # Will Terminal Type 8 | - !client {data: FFFB18} 9 | # Send your Terminal Type 10 | - !server {data: FFFA1801FFF0, delayMillis: 196} 11 | # terminal-type: IBM-3278-2 12 | - !client {data: FFFA180049424D2D333237382D32FFF0} 13 | # Do End of Record 14 | - !server {data: FFFD19, delayMillis: 197} 15 | # Will End of Record 16 | - !server {data: FFFB19} 17 | # Will End of Record 18 | - !client {data: FFFB19} 19 | # Do Binary Transmission + Will Binary Transmission 20 | - !server {data: FFFD00FFFB00, delayMillis: 198} 21 | # Do End of Record 22 | - !client {data: FFFD19} 23 | # Will Binary Transmission + Do Binary Transmission 24 | - !client {data: FFFB00FFFD00} 25 | # restore keyboard + user input screen + cursor=2,1 26 | - !server {data: 05C1115D7F1D401140401DC8C1C1C1C1C1C1C1C1C140C5D5E3C5D940E4E2C5D9C9C44060290142F71D4011C15013FFEF, 27 | delayMillis: 877} 28 | - !server {data: 01C2FFEF} 29 | - !client {data: FFFF} 30 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/SetAttributeOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.attributes.Attribute; 4 | import com.bytezone.dm3270.display.DisplayScreen; 5 | import com.bytezone.dm3270.display.Pen; 6 | import java.util.Optional; 7 | 8 | public class SetAttributeOrder extends Order { 9 | 10 | private final Attribute attribute; 11 | 12 | public SetAttributeOrder(byte[] buffer, int offset) { 13 | assert buffer[offset] == Order.SET_ATTRIBUTE; 14 | 15 | Optional opt = 16 | Attribute.getAttribute(buffer[offset + 1], buffer[offset + 2]); 17 | assert opt.isPresent(); 18 | attribute = opt.get(); 19 | 20 | this.buffer = new byte[3]; 21 | System.arraycopy(buffer, offset, this.buffer, 0, this.buffer.length); 22 | } 23 | 24 | public Attribute getAttribute() { 25 | return attribute; 26 | } 27 | 28 | @Override 29 | public void process(DisplayScreen screen) { 30 | Pen pen = screen.getPen(); 31 | pen.addAttribute(attribute); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return String.format("SA : %s", attribute); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/OEMAuxiliaryDevice.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.structuredfields.StructuredField; 5 | 6 | public class OEMAuxiliaryDevice extends QueryReplyField { 7 | 8 | private final byte flags; 9 | private final byte refID; 10 | private final String deviceType; 11 | private final String userName; 12 | 13 | public OEMAuxiliaryDevice(byte[] buffer, Charset charset) { 14 | super(buffer); 15 | 16 | assert data[0] == StructuredField.QUERY_REPLY; 17 | assert data[1] == QueryReplyField.OEM_AUXILIARY_DEVICE_REPLY; 18 | 19 | flags = data[2]; 20 | refID = data[3]; 21 | deviceType = charset.getString(data, 4, 8).trim(); 22 | userName = charset.getString(data, 12, 8).trim(); 23 | 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return super.toString() + String.format("%n flags1 : %02X", flags) 29 | + String.format("%n ref ID : %02X", refID) 30 | + String.format("%n type : %s", deviceType) 31 | + String.format("%n name : %s", userName); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/telnet/TelnetSubcommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.telnet; 2 | 3 | import com.bytezone.dm3270.buffers.AbstractTelnetCommand; 4 | import com.bytezone.dm3270.streams.TelnetState; 5 | 6 | public abstract class TelnetSubcommand extends AbstractTelnetCommand { 7 | 8 | // subcommands 9 | public static final byte BINARY = 0x00; 10 | public static final byte TERMINAL_TYPE = 0x18; 11 | public static final byte EOR = 0x19; 12 | public static final byte TN3270E = 0x28; 13 | public static final byte START_TLS = 0x2E; 14 | 15 | protected SubcommandType type; 16 | protected String value; 17 | 18 | public enum SubcommandType { 19 | SEND, IS, DEVICE_TYPE, FUNCTIONS 20 | } 21 | 22 | public TelnetSubcommand(byte[] buffer, int offset, int length, TelnetState telnetState) { 23 | super(buffer, offset, length, telnetState); 24 | } 25 | 26 | public String getValue() { 27 | return value; 28 | } 29 | 30 | public String getName() { 31 | return toString(); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return String.format("Subcommand: %s %s", type, (value == null ? "" : value)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/ConnectionListenerBroadcast.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | import java.util.Set; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.function.Consumer; 6 | 7 | public class ConnectionListenerBroadcast implements ConnectionListener { 8 | 9 | private final Set connectionListeners = ConcurrentHashMap.newKeySet(); 10 | 11 | public void add(ConnectionListener connectionListener) { 12 | connectionListeners.add(connectionListener); 13 | } 14 | 15 | public void remove(ConnectionListener connectionListener) { 16 | connectionListeners.remove(connectionListener); 17 | } 18 | 19 | @Override 20 | public void onConnection() { 21 | notify(ConnectionListener::onConnection); 22 | } 23 | 24 | @Override 25 | public void onException(Exception ex) { 26 | notify(connectionListener -> connectionListener.onException(ex)); 27 | } 28 | 29 | @Override 30 | public void onConnectionClosed() { 31 | notify(ConnectionListener::onConnectionClosed); 32 | } 33 | 34 | private void notify(Consumer event) { 35 | connectionListeners.forEach(event); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/StructuredField.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.buffers.AbstractTN3270Command; 5 | import com.bytezone.dm3270.display.Screen; 6 | 7 | public abstract class StructuredField extends AbstractTN3270Command { 8 | 9 | public static final byte RESET_PARTITION = 0x00; 10 | public static final byte READ_PARTITION = 0x01; 11 | public static final byte ERASE_RESET = 0x03; 12 | public static final byte SET_REPLY_MODE = 0x09; 13 | public static final byte ACTIVATE_PARTITION = 0x0E; 14 | public static final byte OUTBOUND_3270DS = 0x40; 15 | 16 | public static final byte QUERY_REPLY = (byte) 0x81; 17 | protected final Charset charset; 18 | 19 | protected byte type; 20 | 21 | public StructuredField(byte[] buffer, int offset, int length, Charset charset) { 22 | super(buffer, offset, length); 23 | this.charset = charset; 24 | type = buffer[offset]; 25 | } 26 | 27 | @Override 28 | public void process(Screen screen) { 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return String.format("StrF: %s", charset.toHex(data).substring(8)); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/GraphicsEscapeOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import com.bytezone.dm3270.display.Pen; 5 | 6 | public class GraphicsEscapeOrder extends Order { 7 | 8 | private final byte code; 9 | 10 | public GraphicsEscapeOrder(byte[] buffer, int offset) { 11 | assert buffer[offset] == Order.GRAPHICS_ESCAPE; 12 | code = buffer[offset + 1]; 13 | 14 | this.buffer = new byte[2]; 15 | this.buffer[0] = buffer[offset]; 16 | this.buffer[1] = buffer[offset + 1]; 17 | } 18 | 19 | @Override 20 | public void process(DisplayScreen screen) { 21 | Pen pen = screen.getPen(); 22 | int max = duplicates; 23 | // always do at least one 24 | while (max-- >= 0) { 25 | pen.writeGraphics(code); 26 | } 27 | } 28 | 29 | @Override 30 | public boolean matchesPreviousOrder(Order order) { 31 | return order instanceof GraphicsEscapeOrder 32 | && this.code == ((GraphicsEscapeOrder) order).code; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | String duplicateText = duplicates == 0 ? "" : "x " + (duplicates + 1); 38 | return String.format("GE : %02X %s", code, duplicateText); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/login-without-fields.yml: -------------------------------------------------------------------------------- 1 | # Do TN3270E 2 | - !server {data: FFFD28, delayMillis: 342} 3 | # Will TN3270E 4 | - !client {data: FFFB28} 5 | # Send DEVICE-TYPE 6 | - !server {data: FFFA280802FFF0, delayMillis: 196} 7 | # DEVICE-TYPE REQUEST IBM-3278-2 8 | - !client {data: FFFA28020749424D2D333237382D32FFF0} 9 | # Connect TEST0003 10 | - !server {data: FFFA28020449424D2D333237382D322D45015445535430303033FFF0, delayMillis: 197} 11 | # Functions Request Unknown + associate DEVICE-TYPE is reason 12 | - !client {data: fffa280307000204fff0} 13 | # Functions associate device-type is reason 14 | - !server {data: FFFA280304000204FFF0} 15 | # Welcome screen 16 | - !server {data: 03000000373101030391903080008487F88000028000000000185000007E000008E3F1F2F3F7E2D6D300FFEF0000000038F5C71DF011D7F0C595A3859940C1979793898381A389969540958194854D8381A28540A28595A289A389A5855D407E7E7E7E7E7E6E1D401340404040404040404040404000FFEF, delayMillis: 570} 17 | # 20,48,testusr 18 | - !client {data: 00000000007DD8E611D85FA385A2A3A4A299FFEF } 19 | # restore keyboard 20 | - !server {data: 00000000ACF5C3FFEF, delayMillis: 215} 21 | # 1,1,testusr 22 | - !client {data: 00000000017D40C7A385A2A3A4A299FFEF} 23 | - !server {data: 00000000B1F5C21DF08885939396FFEF, delayMillis: 235} 24 | - !client {data: FFFF} 25 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/TextOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.DisplayScreen; 5 | import com.bytezone.dm3270.display.Pen; 6 | 7 | public class TextOrder extends Order { 8 | 9 | private final Charset charset; 10 | 11 | public TextOrder(byte[] buffer, int ptr, int max, Charset charset) { 12 | this.charset = charset; 13 | int dataLength = getDataLength(buffer, ptr, max); 14 | this.buffer = new byte[dataLength]; 15 | System.arraycopy(buffer, ptr, this.buffer, 0, dataLength); 16 | } 17 | 18 | private int getDataLength(byte[] buffer, int offset, int max) { 19 | int ptr = offset + 1; 20 | int length = 1; 21 | while (ptr < max) { 22 | byte value = buffer[ptr++]; 23 | for (byte orderValue : orderValues) { 24 | if (value == orderValue) { 25 | return length; 26 | } 27 | } 28 | length++; 29 | } 30 | 31 | return length; 32 | } 33 | 34 | @Override 35 | public boolean isText() { 36 | return true; 37 | } 38 | 39 | @Override 40 | public void process(DisplayScreen screen) { 41 | Pen pen = screen.getPen(); 42 | for (byte b : buffer) { 43 | pen.write(b); 44 | } 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return buffer.length == 0 ? "" : "Text: [" + charset.getString(buffer) + "]"; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/AbstractBuffer.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | public abstract class AbstractBuffer implements Buffer { 4 | 5 | protected byte[] data; 6 | 7 | public AbstractBuffer() { 8 | data = new byte[0]; 9 | } 10 | 11 | public AbstractBuffer(byte[] buffer, int offset, int length) { 12 | data = new byte[length]; 13 | System.arraycopy(buffer, offset, data, 0, length); 14 | } 15 | 16 | @Override 17 | public byte[] getData() { 18 | return data; 19 | } 20 | 21 | @Override 22 | public int size() { 23 | return data.length; 24 | } 25 | 26 | @Override 27 | public byte[] getTelnetData() { 28 | int length = data.length + countFF(data) + 2; // allow for expanded 0xFF and IAC/EOR 29 | byte[] buffer = new byte[length]; 30 | copyAndExpand(data, buffer); 31 | buffer[--length] = (byte) 0xEF; // EOR 32 | buffer[--length] = (byte) 0xFF; // IAC 33 | return buffer; 34 | } 35 | 36 | protected int countFF(byte[] buffer) { 37 | int count = 0; 38 | for (byte b : buffer) { 39 | if (b == (byte) 0xFF) { 40 | count++; 41 | } 42 | } 43 | return count; 44 | } 45 | 46 | protected void copyAndExpand(byte[] source, byte[] dest) { 47 | int ptr = 0; 48 | for (byte b : source) { 49 | dest[ptr++] = b; 50 | if (b == (byte) 0xFF) { 51 | dest[ptr++] = b; 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/extended/LogicalUnit.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.extended; 2 | 3 | /* 4 | * Logical units (LUs) are the ports through which users access the network. Type 1 and 5 | * type 2 peripheral node architecture support dependent LUs only. Type 2.1 peripheral 6 | * node architecture supports independent and dependent LUs. 7 | */ 8 | public class LogicalUnit { 9 | 10 | private final int commit; 11 | 12 | private final int chainingUse; 13 | private final int modeSelection; 14 | private final int responseProtocol; 15 | private final int scbCompression; 16 | private final int sendEndBracket; 17 | 18 | public LogicalUnit(byte value) { 19 | chainingUse = (value & 0x80) >> 7; 20 | modeSelection = (value & 0x40) >> 6; 21 | responseProtocol = (value & 0x30) >> 4; 22 | commit = (value & 0x08) >> 3; 23 | scbCompression = (value & 0x02) >> 1; 24 | sendEndBracket = (value & 0x01); 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return String.format("Chaining use ......... %02X%n", chainingUse) 30 | + String.format("Mode selection ....... %02X%n", modeSelection) 31 | + String.format("Response protocol .... %02X%n", responseProtocol) 32 | + String.format("Commit ............... %02X%n", commit) 33 | + String.format("SCB compression ...... %02X%n", scbCompression) 34 | + String.format("Send End Bracket ..... %02X%n", sendEndBracket); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/Outbound3270DS.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.commands.Command; 5 | import com.bytezone.dm3270.display.Screen; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class Outbound3270DS extends StructuredField { 10 | 11 | private static final Logger LOG = LoggerFactory.getLogger(Outbound3270DS.class); 12 | 13 | private final byte partitionID; 14 | private final Command command; 15 | 16 | // wrapper for original write commands - W. EW, EWA, EAU 17 | public Outbound3270DS(byte[] buffer, int offset, int length, Charset charset) { 18 | super(buffer, offset, length, charset); // copies buffer -> data 19 | 20 | assert data[0] == StructuredField.OUTBOUND_3270DS; 21 | partitionID = data[1]; 22 | assert (partitionID & (byte) 0x80) == 0; // must be 0x00 - 0x7F 23 | 24 | // can only be W/EW/EWA/EAU (i.e. one of the write commands) 25 | command = Command.getCommand(buffer, offset + 2, length - 2, charset); 26 | } 27 | 28 | @Override 29 | public void process(Screen screen) { 30 | command.process(screen); 31 | if (command.getReply().isPresent()) { 32 | LOG.debug("Non-null reply: {}, {}", command, 33 | command.getReply().get()); // reply should always be null 34 | } 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return String.format("Struct Field : %02X Outbound3270DS\n", type) 40 | + String.format(" partition : %02X%n", partitionID) 41 | + command; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/commands/WriteControlCharacter.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.commands; 2 | 3 | import com.bytezone.dm3270.display.Screen; 4 | 5 | public class WriteControlCharacter { 6 | 7 | private final byte value; 8 | private final boolean resetPartition; 9 | private final boolean startPrinter; 10 | private final boolean soundAlarm; 11 | private final boolean restoreKeyboard; 12 | private final boolean resetModified; 13 | 14 | public WriteControlCharacter(byte value) { 15 | this.value = value; 16 | resetPartition = (value & 0x40) > 0; 17 | startPrinter = (value & 0x08) > 0; 18 | soundAlarm = (value & 0x04) > 0; 19 | restoreKeyboard = (value & 0x02) > 0; 20 | resetModified = (value & 0x01) > 0; 21 | } 22 | 23 | byte getValue() { 24 | return value; 25 | } 26 | 27 | boolean isResetModified() { 28 | return resetModified; 29 | } 30 | 31 | void process(Screen screen) { 32 | screen.resetInsertMode(); 33 | 34 | if (soundAlarm) { 35 | screen.soundAlarm(); 36 | } 37 | if (resetModified) { 38 | screen.resetModified(); 39 | } 40 | if (restoreKeyboard) { 41 | screen.restoreKeyboard(); 42 | } 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return String.format("reset MDT=%s", resetModified ? "yes" : "no ") 48 | + String.format(", keyboard=%s", restoreKeyboard ? "yes" : "no ") 49 | + String.format(", alarm=%s", soundAlarm ? "yes" : "no ") 50 | + String.format(", printer=%s", startPrinter ? "yes" : "no ") 51 | + String.format(", partition=%s", resetPartition ? "yes" : "no"); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/extended/AbstractExtendedCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.extended; 2 | 3 | import com.bytezone.dm3270.buffers.AbstractReplyBuffer; 4 | import com.bytezone.dm3270.display.Screen; 5 | 6 | public abstract class AbstractExtendedCommand extends AbstractReplyBuffer { 7 | 8 | protected final CommandHeader commandHeader; 9 | 10 | public AbstractExtendedCommand(CommandHeader commandHeader) { 11 | this.commandHeader = commandHeader; 12 | } 13 | 14 | public AbstractExtendedCommand(CommandHeader commandHeader, byte[] buffer, int offset, 15 | int length) { 16 | super(buffer, offset, length); 17 | this.commandHeader = commandHeader; 18 | } 19 | 20 | @Override 21 | public byte[] getData() { 22 | byte[] buffer = new byte[data.length + 5]; 23 | System.arraycopy(commandHeader.getData(), 0, buffer, 0, 5); 24 | System.arraycopy(data, 0, buffer, 5, data.length); 25 | return buffer; 26 | } 27 | 28 | @Override 29 | public int size() { 30 | return data.length + 5; 31 | } 32 | 33 | @Override 34 | public byte[] getTelnetData() { 35 | byte[] data = getData(); // prepend the command header 36 | int length = data.length + countFF(data) + 2; // add in expanded 0xFF and IAC/EOR 37 | byte[] buffer = new byte[length]; 38 | copyAndExpand(data, buffer); 39 | buffer[--length] = (byte) 0xEF; // EOR 40 | buffer[--length] = (byte) 0xFF; // IAC 41 | return buffer; 42 | } 43 | 44 | public abstract String getName(); 45 | 46 | @Override 47 | public void process(Screen screen) { 48 | commandHeader.process(screen); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/ImplicitPartition.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.buffers.Buffer; 4 | 5 | public class ImplicitPartition extends QueryReplyField { 6 | 7 | private int width; 8 | private int height; 9 | private int alternateWidth; 10 | private int alternateHeight; 11 | 12 | public ImplicitPartition(int rows, int columns) { 13 | super(IMP_PART_QUERY_REPLY); 14 | 15 | int ptr = createReply(13); 16 | 17 | reply[ptr++] = 0x00; 18 | reply[ptr++] = 0x00; 19 | 20 | reply[ptr++] = 0x0B; 21 | reply[ptr++] = 0x01; 22 | reply[ptr++] = 0x00; 23 | 24 | ptr = Buffer.packUnsignedShort(0x50, reply, ptr); // width 25 | ptr = Buffer.packUnsignedShort(0x18, reply, ptr); // height 26 | 27 | ptr = Buffer.packUnsignedShort(columns, reply, ptr); // alt width 28 | ptr = Buffer.packUnsignedShort(rows, reply, ptr); // alt height 29 | 30 | checkDataLength(ptr); 31 | } 32 | 33 | public ImplicitPartition(byte[] buffer) { 34 | super(buffer); 35 | 36 | assert data[1] == IMP_PART_QUERY_REPLY; 37 | 38 | width = Buffer.unsignedShort(data, 7); 39 | height = Buffer.unsignedShort(data, 9); 40 | alternateWidth = Buffer.unsignedShort(data, 11); 41 | alternateHeight = Buffer.unsignedShort(data, 13); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return super.toString() + String.format("%n width : %d", width) 47 | + String.format("%n height : %d", height) 48 | + String.format("%n alt width : %d", alternateWidth) 49 | + String.format("%n alt height : %d", alternateHeight); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/FormatControlOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.display.DisplayScreen; 4 | import com.bytezone.dm3270.display.Pen; 5 | 6 | public class FormatControlOrder extends Order { 7 | 8 | private static byte[] orderValues = 9 | {FCO_NULL, FCO_SUBSTITUTE, FCO_DUPLICATE, FCO_FIELD_MARK, FCO_FORM_FEED, 10 | FCO_CARRIAGE_RETURN, FCO_NEWLINE, FCO_END_OF_MEDIUM, FCO_EIGHT_ONES}; 11 | private static String[] orderNames = 12 | {"Null", "Substitute", "Duplicate", "Field Mark", "Form Feed", "Return", "Newline", 13 | "EOM", "8 ones"}; 14 | 15 | public FormatControlOrder(byte[] buffer, int offset) { 16 | this.buffer = new byte[1]; 17 | this.buffer[0] = buffer[offset]; 18 | } 19 | 20 | @Override 21 | public void process(DisplayScreen screen) { 22 | Pen pen = screen.getPen(); 23 | int max = duplicates; 24 | // always do at least one 25 | while (max-- >= 0) { 26 | pen.write((byte) 0x40); 27 | } 28 | } 29 | 30 | @Override 31 | public boolean matchesPreviousOrder(Order order) { 32 | return order instanceof FormatControlOrder 33 | && this.buffer[0] == ((FormatControlOrder) order).buffer[0]; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | byte value = buffer[0]; 39 | String text = "????"; 40 | for (int i = 0; i < orderValues.length; i++) { 41 | if (value == orderValues[i]) { 42 | text = orderNames[i]; 43 | break; 44 | } 45 | } 46 | String duplicateText = duplicates == 0 ? "" : "x " + (duplicates + 1); 47 | return String.format("FCO : %-12s : %02X %s", text, buffer[0], duplicateText); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/session/SessionRecord.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.session; 2 | 3 | import com.bytezone.dm3270.buffers.ReplyBuffer; 4 | import com.bytezone.dm3270.commands.Command; 5 | import com.bytezone.dm3270.extended.TN3270ExtendedCommand; 6 | import com.bytezone.dm3270.streams.TelnetSocket; 7 | import java.time.LocalDateTime; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | public class SessionRecord { 11 | 12 | private static final DateTimeFormatter FORMATTER = 13 | DateTimeFormatter.ofPattern("dd MMM uuuu HH:mm:ss.S"); 14 | private final ReplyBuffer message; 15 | 16 | private final TelnetSocket.Source source; 17 | private final LocalDateTime dateTime; 18 | 19 | public enum SessionRecordType { 20 | TELNET, TN3270, TN3270E 21 | } 22 | 23 | public SessionRecord(ReplyBuffer message, TelnetSocket.Source source, LocalDateTime dateTime) { 24 | this.message = message; 25 | this.source = source; 26 | this.dateTime = dateTime; 27 | } 28 | 29 | public boolean isCommand() { 30 | return message instanceof Command || message instanceof TN3270ExtendedCommand; 31 | } 32 | 33 | public Command getCommand() { 34 | if (message instanceof Command) { 35 | return (Command) message; 36 | } 37 | if (message instanceof TN3270ExtendedCommand) { 38 | return ((TN3270ExtendedCommand) message).getCommand(); 39 | } 40 | return null; 41 | } 42 | 43 | public byte[] getBuffer() { 44 | return message.getData(); 45 | } 46 | 47 | public int size() { 48 | return message.size(); 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return String.format("%s : %s", source, FORMATTER.format(dateTime)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: publish release 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | concurrency: blazemeter_test 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Setup Java 1.8 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 1.8 15 | - name: Get version 16 | id: version 17 | run: echo "release_version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 18 | - name: Check version 19 | run: .github/semver-check.sh ${{ env.release_version }} 20 | - name: set maven project version 21 | run: mvn --batch-mode --no-transfer-progress versions:set -DnewVersion=${{ env.release_version }} --settings .github/settings.xml 22 | - name: package release 23 | run: mvn --batch-mode --no-transfer-progress clean package --settings .github/settings.xml 24 | - name: Upload built jar into release 25 | uses: svenstaro/upload-release-action@v2 26 | with: 27 | repo_token: ${{ secrets.GITHUB_TOKEN }} 28 | file: target/*.jar 29 | file_glob: true 30 | tag: ${{ github.ref }} 31 | - name: publish to Nexus 32 | run: .github/maven-central-deploy.sh 33 | env: 34 | GPG_SECRET_KEYS: ${{ secrets.GPG_SECRET_KEYS }} 35 | GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }} 36 | GPG_EXECUTABLE: gpg 37 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 38 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 39 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 40 | - name: update docs version 41 | run: .github/fix-docs-version.sh ${{ env.release_version }} 42 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/SetReplyModeSF.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.attributes.Attribute; 5 | import com.bytezone.dm3270.display.Screen; 6 | 7 | public class SetReplyModeSF extends StructuredField { 8 | 9 | public static final byte RM_FIELD = 0x00; 10 | public static final byte RM_CHARACTER = 0x02; 11 | 12 | private static final String[] MODES = {"Field", "Extended field", "Character"}; 13 | private final byte partition; 14 | private final byte replyMode; 15 | private final byte[] types; 16 | 17 | public SetReplyModeSF(byte[] buffer, int offset, int length, Charset charset) { 18 | super(buffer, offset, length, charset); 19 | 20 | assert data[0] == StructuredField.SET_REPLY_MODE; 21 | 22 | int ptr = offset + 1; 23 | partition = buffer[ptr++]; 24 | replyMode = buffer[ptr++]; 25 | 26 | int totalTypes = length - 3; 27 | types = new byte[totalTypes]; 28 | System.arraycopy(buffer, ptr, types, 0, types.length); 29 | } 30 | 31 | @Override 32 | public void process(Screen screen) { 33 | screen.setReplyMode(replyMode, types); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | StringBuilder text = new StringBuilder("Struct Field : 09 Set Reply Mode\n"); 39 | text.append(String.format(" partition : %02X%n", partition)); 40 | text.append(String.format(" mode : %02X %s mode", replyMode, 41 | MODES[replyMode])); 42 | for (byte type : types) { 43 | String typeName = Attribute.getTypeName(type); 44 | text.append(String.format("%n type : %02X %s", type, typeName)); 45 | } 46 | 47 | return text.toString(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/resources/test_capabilities.yml: -------------------------------------------------------------------------------- 1 | - !server {data: FFFD28} 2 | - !client {data: FFFB28} 3 | - !server {data: FFFA280802FFF0} 4 | - !client {data: FFFA28020749424D2D333237382D32FFF0} 5 | - !server {data: FFFA28020449424D2D333237382D32014130315443353830FFF0} 6 | - !client {data: FFFA280307000204FFF0} 7 | - !server {data: FFFA280304000204FFF0} 8 | - !server {data: 030000000031010303B1903080008787F88700028000000000185000007E000008C1F0F1C9E3D7E7E70005007E683B1008C1F0F1E3C3F5F8F0FFEF} 9 | - !server {data: 0000020001F5C0FFEF} 10 | - !client {data: 020000000100FFEF} 11 | - !server {data: 0000000000F1C2FFEF} 12 | - !server {data: 0000010002F3000501FFFF02FFEF} 13 | - !client {data: 000000000088000B818080818687A68885001781810100005000180100D30320009E0258070C078000268186001000F4F1F1F2F2F3F3F4F4F5F5F6F6F7F7F8F8F9F9FAFAFBFBFCFCFDFDFEFEFFFFFFFF000F81870500F0F1F1F2F2F4F4F8F8001181A600000B0100005000180050001800078188000102001B81858200070C000000000700000002B904170100F103C30136FFEF} 14 | - !server {data: 0000010003F540FFEF} 15 | - !server {data: 0000010004F3000D01FFFF034080818687A68885FFEF} 16 | - !client {data: 000000000188000B818080818687A68885001781810100005000180100D30320009E0258070C078000268186001000F4F1F1F2F2F3F3F4F4F5F5F6F6F7F7F8F8F9F9FAFAFBFBFCFCFDFDFEFEFFFFFFFF000F81870500F0F1F1F2F2F4F4F8F8001181A600000B0100005000180050001800078188000102001B81858200070C000000000700000002B904170100F103C30136FFEF} 17 | - !server {data: 0000010010f5c31140c1131140401d40e3e2e2f7f0f0f0c940d8c1c7c5d540d381a2a360e4a2858440f1f840d1a49540f2f140f1f77af0f340e2a8a2a385947ec3c1f3f140c68183899389a3a87ee5d4c1d511c1501d40e3e2e2f7f0f0f1c940c396a495a37ef0f1f4f9f340d49684857ee681999540d3968392a38994857ed596958540d58194857ed5e4d5d5e240d4c1d9c3c5d3d3c111c2601d4011c3f01d4011c5401d4011c6501d4011c7601d4011c8f01d40114a401d40114b501d40114c601d40ffef} 18 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/AlphanumericPartitions.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.buffers.Buffer; 4 | 5 | public class AlphanumericPartitions extends QueryReplyField { 6 | 7 | private int maxPartitions; 8 | private int totalAvailableStorage; 9 | private byte flags; 10 | private boolean vertWin; 11 | private boolean horWin; 12 | private boolean allPointsAddressability; 13 | private boolean partitionProtection; 14 | private boolean localCopy; 15 | private boolean modifyPartition; 16 | 17 | public AlphanumericPartitions(byte[] buffer) { 18 | super(buffer); 19 | assert data[1] == ALPHANUMERIC_PARTITIONS_REPLY; 20 | 21 | maxPartitions = data[2] & 0xFF; 22 | totalAvailableStorage = Buffer.unsignedShort(data, 3); 23 | 24 | flags = data[5]; 25 | vertWin = (flags & 0x80) != 0; 26 | horWin = (flags & 0x40) != 0; 27 | allPointsAddressability = (flags & 0x10) != 0; 28 | partitionProtection = (flags & 0x08) != 0; 29 | localCopy = (flags & 0x04) != 0; 30 | modifyPartition = (flags & 0x02) != 0; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return super.toString() + String.format("%n max : %d", maxPartitions) 36 | + String.format("%n storage : %d", totalAvailableStorage) 37 | + String.format("%n flags : %02X", flags) 38 | + String.format("%n vert win : %s", vertWin) 39 | + String.format("%n hor win : %s", horWin) 40 | + String.format("%n APA : %s", allPointsAddressability) 41 | + String.format("%n protect : %s", partitionProtection) 42 | + String.format("%n lcopy : %s", localCopy) 43 | + String.format("%n modpart : %s", modifyPartition); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/bytezone/dm3270/UnlockWaiter.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | import com.bytezone.dm3270.application.KeyboardStatusChangedEvent; 4 | import com.bytezone.dm3270.application.KeyboardStatusListener; 5 | import java.util.concurrent.ScheduledExecutorService; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class UnlockWaiter extends ConditionWaiter implements KeyboardStatusListener { 10 | 11 | private static final Logger LOG = LoggerFactory.getLogger(UnlockWaiter.class); 12 | 13 | private boolean isInputInhibited; 14 | 15 | public UnlockWaiter(TerminalClient client, ScheduledExecutorService stableTimeoutExecutor) { 16 | super(client, stableTimeoutExecutor); 17 | client.addKeyboardStatusListener(this); 18 | isInputInhibited = client.isKeyboardLocked(); 19 | if (!isInputInhibited) { 20 | LOG.debug("Start stable period since input is not inhibited"); 21 | startStablePeriod(); 22 | } 23 | } 24 | 25 | @Override 26 | public void keyboardStatusChanged(KeyboardStatusChangedEvent keyboardStatusChangedEvent) { 27 | LOG.debug("keyboardStatusChanged {}", keyboardStatusChangedEvent.toString()); 28 | 29 | boolean wasInputInhibited = isInputInhibited; 30 | isInputInhibited = keyboardStatusChangedEvent.keyboardLocked; 31 | if (isInputInhibited != wasInputInhibited) { 32 | if (isInputInhibited) { 33 | LOG.debug("Cancel stable period since input has been inhibited"); 34 | endStablePeriod(); 35 | } else { 36 | LOG.debug("Start stable period since input is no longer inhibited"); 37 | startStablePeriod(); 38 | } 39 | } 40 | } 41 | 42 | @Override 43 | protected void stop() { 44 | super.stop(); 45 | client.removeKeyboardStatusListener(this); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/Summary.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class Summary extends QueryReplyField { 8 | 9 | public Summary(byte[] buffer) { 10 | super(buffer); 11 | assert data[1] == SUMMARY_QUERY_REPLY; 12 | } 13 | 14 | public Summary(List replyFields) { 15 | super(SUMMARY_QUERY_REPLY); 16 | replies = replyFields; 17 | ByteBuffer buffer = createReplyBuffer(replyFields.size() + 1); 18 | buffer.put(SUMMARY_QUERY_REPLY); 19 | replyFields.forEach(r -> buffer.put(r.replyType.type)); 20 | } 21 | 22 | protected boolean isListed(byte type) { 23 | for (int i = 2; i < data.length; i++) { 24 | if (data[i] == type) { 25 | return true; 26 | } 27 | } 28 | return false; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | StringBuilder text = new StringBuilder(super.toString()); 34 | 35 | for (int i = 2; i < data.length; i++) { 36 | text.append(String.format("%n %-30s %s", ReplyType.fromId(data[i]), 37 | isProvided(data[i]) ? "" : "** missing **")); 38 | } 39 | 40 | // check for QueryReplyFields sent but not listed in the summary 41 | List missingFields = new ArrayList<>(4); 42 | for (QueryReplyField reply : replies) { 43 | if (!isListed(reply.replyType.type)) { 44 | missingFields.add(reply); 45 | } 46 | } 47 | 48 | if (missingFields.size() > 0) { 49 | text.append("\n\nNot listed in Summary:"); 50 | for (QueryReplyField qrf : missingFields) { 51 | text.append(String.format("%n %s", qrf.replyType)); 52 | } 53 | } 54 | 55 | return text.toString(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/bytezone/dm3270/ConditionWaiter.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | import java.util.concurrent.CountDownLatch; 4 | import java.util.concurrent.ScheduledExecutorService; 5 | import java.util.concurrent.ScheduledFuture; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.concurrent.TimeoutException; 8 | 9 | public abstract class ConditionWaiter { 10 | 11 | private static final int STABLE_PERIOD_MILLIS = 1000; 12 | 13 | private final CountDownLatch lock = new CountDownLatch(1); 14 | protected final TerminalClient client; 15 | private final ScheduledExecutorService stableTimeoutExecutor; 16 | private ScheduledFuture stableTimeoutTask; 17 | private boolean ended; 18 | 19 | public ConditionWaiter(TerminalClient client, ScheduledExecutorService stableTimeoutExecutor) { 20 | this.client = client; 21 | this.stableTimeoutExecutor = stableTimeoutExecutor; 22 | } 23 | 24 | protected synchronized void startStablePeriod() { 25 | if (ended) { 26 | return; 27 | } 28 | endStablePeriod(); 29 | stableTimeoutTask = stableTimeoutExecutor 30 | .schedule(lock::countDown, STABLE_PERIOD_MILLIS, TimeUnit.MILLISECONDS); 31 | } 32 | 33 | protected synchronized void endStablePeriod() { 34 | if (stableTimeoutTask != null) { 35 | stableTimeoutTask.cancel(false); 36 | } 37 | } 38 | 39 | public void await(long timeoutMillis) throws InterruptedException, TimeoutException { 40 | try { 41 | if (!lock.await(timeoutMillis, TimeUnit.MILLISECONDS)) { 42 | throw new TimeoutException(); 43 | } 44 | } finally { 45 | stop(); 46 | } 47 | } 48 | 49 | private synchronized void cancelWait() { 50 | ended = true; 51 | lock.countDown(); 52 | endStablePeriod(); 53 | } 54 | 55 | protected void stop() { 56 | cancelWait(); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/Buffer.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | import com.bytezone.dm3270.display.Screen; 4 | 5 | public interface Buffer { 6 | 7 | int HEX_LINE_SIZE = 16; 8 | 9 | static int unsignedShort(byte[] buffer, int offset) { 10 | return (buffer[offset] & 0xFF) * 0x100 + (buffer[offset + 1] & 0xFF); 11 | } 12 | 13 | static int packUnsignedShort(int value, byte[] buffer, int offset) { 14 | buffer[offset++] = (byte) ((value >> 8) & 0xFF); 15 | buffer[offset++] = (byte) (value & 0xFF); 16 | return offset; 17 | } 18 | 19 | static int unsignedLong(byte[] buffer, int offset) { 20 | return (buffer[offset] & 0xFF) * 0x1000000 + (buffer[offset + 1] & 0xFF) * 0x10000 21 | + (buffer[offset + 2] & 0xFF) * 0x100 + (buffer[offset + 3] & 0xFF); 22 | } 23 | 24 | static String toHex(byte[] b, int offset, int length) { 25 | StringBuilder text = new StringBuilder(); 26 | for (int ptr = offset, max = offset + length; ptr < max; ptr += HEX_LINE_SIZE) { 27 | StringBuilder hexLine = new StringBuilder(); 28 | StringBuilder textLine = new StringBuilder(); 29 | for (int linePtr = 0; linePtr < HEX_LINE_SIZE && ptr + linePtr < max; linePtr++) { 30 | int val = b[ptr + linePtr] & 0xFF; 31 | hexLine.append(String.format("%02X ", val)); 32 | if (val < 0x20 || val >= 0xF0) { 33 | textLine.append('.'); 34 | } else { 35 | textLine.append(new String(b, ptr + linePtr, 1)); 36 | } 37 | } 38 | text.append(String.format("%04X %-48s %s%n", ptr, hexLine.toString(), textLine.toString())); 39 | } 40 | return text.length() > 0 ? text.substring(0, text.length() - 1) : text.toString(); 41 | } 42 | 43 | byte[] getData(); 44 | 45 | byte[] getTelnetData(); 46 | 47 | int size(); 48 | 49 | void process(Screen screen); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/DistributedDataManagement.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class DistributedDataManagement extends QueryReplyField { 6 | 7 | private final short flags; 8 | private final short maximumInboundBytes; 9 | private final short maximumOutboundBytes; 10 | private final byte supportedSubsetsCount; 11 | private final byte ddmSubsetId; 12 | 13 | public DistributedDataManagement(byte[] buffer) { 14 | super(buffer); 15 | ByteBuffer dataBuffer = ByteBuffer.wrap(buffer); 16 | //skip queryReply id 17 | dataBuffer.get(); 18 | assert dataBuffer.get() == DISTRIBUTED_DATA_MANAGEMENT_REPLY; 19 | flags = dataBuffer.getShort(); 20 | maximumInboundBytes = dataBuffer.getShort(); 21 | maximumOutboundBytes = dataBuffer.getShort(); 22 | supportedSubsetsCount = dataBuffer.get(); 23 | ddmSubsetId = dataBuffer.get(); 24 | } 25 | 26 | public DistributedDataManagement() { 27 | super(DISTRIBUTED_DATA_MANAGEMENT_REPLY); 28 | flags = 0; 29 | maximumInboundBytes = 8192; 30 | maximumOutboundBytes = 8192; 31 | supportedSubsetsCount = 1; 32 | ddmSubsetId = 1; 33 | ByteBuffer buffer = createReplyBuffer(8); 34 | buffer.putShort(flags); 35 | buffer.putShort(maximumInboundBytes); 36 | buffer.putShort(maximumOutboundBytes); 37 | buffer.put(supportedSubsetsCount); 38 | buffer.put(ddmSubsetId); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return super.toString() + String.format("%n flags : %04X", flags) 44 | + String.format("%n limit in : %d", maximumInboundBytes) 45 | + String.format("%n limit out : %d", maximumOutboundBytes) 46 | + String.format("%n subsets : %d", supportedSubsetsCount) 47 | + String.format("%n DDMSS : %d", ddmSubsetId); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/RepeatToAddressOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.DisplayScreen; 5 | import com.bytezone.dm3270.display.Pen; 6 | import com.bytezone.dm3270.display.Screen; 7 | 8 | public class RepeatToAddressOrder extends Order { 9 | 10 | private final BufferAddress stopAddress; 11 | private char repeatCharacter; 12 | private byte rptChar; 13 | 14 | public RepeatToAddressOrder(byte[] buffer, int offset, Charset charset) { 15 | assert buffer[offset] == Order.REPEAT_TO_ADDRESS; 16 | 17 | stopAddress = new BufferAddress(buffer[offset + 1], buffer[offset + 2]); 18 | 19 | if (buffer[offset + 3] == Order.GRAPHICS_ESCAPE) { 20 | repeatCharacter = charset.getChar(buffer[offset + 4]); 21 | // offset + 5 can be used, but I haven't seen one yet 22 | rptChar = buffer[offset + 4]; 23 | 24 | this.buffer = new byte[6]; 25 | } else { 26 | repeatCharacter = charset.getChar(buffer[offset + 3]); 27 | rptChar = buffer[offset + 3]; 28 | 29 | this.buffer = new byte[4]; 30 | } 31 | 32 | System.arraycopy(buffer, offset, this.buffer, 0, this.buffer.length); 33 | 34 | if (rptChar == 0) { 35 | repeatCharacter = ' '; 36 | } 37 | } 38 | 39 | @Override 40 | public void process(DisplayScreen screen) { 41 | int stopLocation = stopAddress.getLocation(); 42 | 43 | Pen pen = screen.getPen(); 44 | if (pen.getPosition() == stopLocation) { 45 | screen.clearScreen(((Screen) screen).getCurrentScreenOption()); 46 | } else { 47 | while (pen.getPosition() != stopLocation) { 48 | pen.write(rptChar); 49 | } 50 | } 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return String.format("RTA : %-12s : %02X [%1.1s]", stopAddress, rptChar, 56 | repeatCharacter); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dm3270-lib 2 | 3 | This is a trimmed down version of the [dm3270 emulator](https://github.com/dmolony/dm3270) to be used as TN3270 client library. 4 | 5 | In particular it removes all references to JavaFX (which is not required to use code as lib and is not included by default in some [OpenJDK](http://openjdk.java.net/) distributions), and keeps only logic for simple terminal interaction. 6 | Additionally it includes some basic refactor (not too deep refactor to keep some traceability to original code) to simplify code. 7 | 8 | ## Usage 9 | 10 | To use the library is required [JRE8+](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html). 11 | 12 | To use the emulator as maven dependency include in `pom.xml`: 13 | 14 | ```xml 15 | 16 | com.github.blazemeter 17 | dm3270 18 | 0.15-lib-lib 19 | 20 | ``` 21 | 22 | >Check latest version in [releases](https://github.com/blazemeter/xtn5250/releases). 23 | 24 | And then use provided API. An example of such usage can be found in [TerminalClientTest](src/test/java/com/bytezone/dm3270/TerminalClientTest.java). 25 | 26 | ## Build 27 | 28 | To build the project is required [JDK8+](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html), [maven](https://maven.apache.org/) 3.3+. 29 | 30 | Then just run `mvn clean install` and the library will be built and installed in the local maven repository. 31 | 32 | ## Release 33 | 34 | To release the project, define the version to be released by checking included changes since last release and following [semantic versioning](https://semver.org/). 35 | Then, create a [release](https://github.com/blazemeter/dm3270/releases) (including `v` as prefix and `-lib` as suffix of the version, e.g. `v0.1-lib`), this will trigger a Travis build which will publish the jars to the created github release. 36 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/BufferAddress.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | public class BufferAddress { 4 | 5 | public static final byte[] ADDRESS = new byte[64]; 6 | private static int columns = 80; // default value 7 | 8 | private int location; 9 | private final byte b1; 10 | private final byte b2; 11 | 12 | static { 13 | int value = 0x40; 14 | int ptr = 0; 15 | 16 | for (int i = 0; i < 4; i++) { 17 | ADDRESS[ptr++] = (byte) value++; 18 | for (int j = 0; j < 9; j++) { 19 | ADDRESS[ptr++] = (byte) (value++ | 0x80); 20 | } 21 | for (int j = 0; j < 6; j++) { 22 | ADDRESS[ptr++] = (byte) value++; 23 | } 24 | } 25 | 26 | ADDRESS[33] &= 0x7F; // = 0x61; // was 0xE1 27 | ADDRESS[48] |= (byte) 0x80; // = (byte) 0xF0; // was 0x70 28 | } 29 | 30 | public BufferAddress(byte b1, byte b2) { 31 | this.b1 = b1; 32 | this.b2 = b2; 33 | int flag = b1 & 0xC0; // top two bits 34 | 35 | // using 14-bit method 36 | if (flag == 0) { 37 | location = (b1 & 0x3F) << 8; 38 | location |= (b2 & 0xFF); 39 | } else { 40 | location = (b1 & 0x3F) << 6; 41 | location |= (b2 & 0x3F); 42 | } 43 | } 44 | 45 | public BufferAddress(int location) { 46 | this.location = location; 47 | b1 = ADDRESS[location >> 6]; 48 | b2 = ADDRESS[location & 0x3F]; 49 | } 50 | 51 | public static void setScreenWidth(int width) { 52 | columns = width; 53 | } 54 | 55 | public int getLocation() { 56 | return location; 57 | } 58 | 59 | public int packAddress(byte[] buffer, int offset) { 60 | buffer[offset++] = ADDRESS[location >> 6]; 61 | buffer[offset++] = ADDRESS[location & 0x3F]; 62 | 63 | return offset; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return String.format("%04d %03d/%03d : %02X %02X", location, location / columns, 69 | location % columns, b1, b2); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/ScreenContext.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | import com.bytezone.dm3270.attributes.ColorAttribute; 4 | import java.awt.Color; 5 | 6 | public class ScreenContext { 7 | 8 | public static final ScreenContext DEFAULT_CONTEXT = new ScreenContext(ColorAttribute.COLORS[0], 9 | ColorAttribute.COLORS[8], (byte) 0, false, false); 10 | 11 | public final Color foregroundColor; 12 | public final Color backgroundColor; 13 | public final byte highlight; 14 | public final boolean highIntensity; 15 | public final boolean isGraphic; 16 | 17 | public ScreenContext(Color foregroundColor, Color backgroundColor, byte highlight, 18 | boolean highIntensity, boolean isGraphic) { 19 | this.foregroundColor = foregroundColor; 20 | this.backgroundColor = backgroundColor; 21 | this.highlight = highlight; 22 | this.highIntensity = highIntensity; 23 | this.isGraphic = isGraphic; 24 | } 25 | 26 | public ScreenContext withBackgroundColor(Color color) { 27 | return new ScreenContext(foregroundColor, color, highlight, highIntensity, isGraphic); 28 | } 29 | 30 | public ScreenContext withHighlight(byte highlight) { 31 | return new ScreenContext(foregroundColor, backgroundColor, highlight, highIntensity, 32 | isGraphic); 33 | } 34 | 35 | public ScreenContext withForeground(Color color) { 36 | return new ScreenContext(color, backgroundColor, highlight, highIntensity, isGraphic); 37 | } 38 | 39 | public ScreenContext withGraphic(boolean isGraphic) { 40 | return new ScreenContext(foregroundColor, backgroundColor, highlight, highIntensity, 41 | isGraphic); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return String.format("[Fg:%-10s Bg:%-10s In:%s Hl:%02X]", 47 | ColorAttribute.getName(foregroundColor), 48 | ColorAttribute.getName(backgroundColor), 49 | (highIntensity ? 'x' : ' '), highlight); 50 | } 51 | 52 | public boolean isGraphic() { 53 | return isGraphic; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/resources/login-welcome-screen.txt: -------------------------------------------------------------------------------- 1 | AAAAAAAAA ENTER USERID - 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/resources/success-apl-screen.txt: -------------------------------------------------------------------------------- 1 | FAQMENU0.0 ** QA FAQS -TESTING SHORT MENU- 05.1.00 ** ID=QA_TEST_LONG_ID 2 | ===> 3 | ┌───────────────────────────────────────────────────────────────────────┐ 4 | │ *** QA FAQS ASO Main Menu *** │ 5 | ├────┬─────────────────┬────────────────────────────────────────────────┤ 6 | │ 1 │ HELP │ Display help information │ 7 | │ 2 │ QA FAQS ASO │ QA FAQS ASO Online menu driver │ 8 | │ 3 │ TERMINATE │ Terminate FAQS session │ 9 | │ 4 │ AO │ Automated System Operation Menu Panels │ 10 | │ 5 │ QA FAQS PCS │ QA FAQS Production Control System │ 11 | │ 6 │ QA FLEE │ QA FLEE Online Library Maintenance │ 12 | │ 7 │ QA EXPLORE │ QA EXPLORE for z/VSE Online │ 13 | │ 8 │ QA EXPLORE │ QA EXPLORE for CICS Online │ 14 | │ 9 │ QA VSAMAID │ QA VSAMAID for TESTING APP │ 15 | │ A │ QA MASTERQAT │ QA MASTERQAT for TESTING APP │ 16 | │ B │ QA HYPER-BUF │ QA HYPER-BUF for TESTING APP │ 17 | │ C │ QA CPR │ CICS PRINT FACILITY Menu Panels │ 18 | │ E │ QA EXTEND DASD │ QA EXTEND DASD for z/VSE Online │ 19 | │ O │ QA EPIC │ QA-EPIC For z/VSE │ 20 | │ N │ LEGAL INFO │ Legal Notice and Copyright statement │ 21 | └────┴─────────────────┴────────────────────────────────────────────────┘ 22 | Copyright (c) 2011 QA. All rights reserved. 23 | 24 | PF01=Help PF03=Return PF12=Exit 25 | -------------------------------------------------------------------------------- /src/test/resources/user-menu-screen.txt: -------------------------------------------------------------------------------- 1 | ------------------------------- TSO/E LOGON ----------------------------------- 2 | 3 | 4 | Enter LOGON parameters below: RACF LOGON parameters: 5 | 6 | Userid ===> TESTUSR 7 | 8 | Password ===> New Password ===> 9 | 10 | Procedure ===> PROC000 Group Ident ===> 11 | 12 | Acct Nmbr ===> 1000000 13 | 14 | Size ===> 4096 15 | 16 | Perform ===> 17 | 18 | Command ===> 19 | 20 | Enter an 'S' before each option desired below: 21 | -Nomail -Nonotice -Reconnect -OIDcard 22 | 23 | PF1/PF13 ==> Help PF3/PF15 ==> Logoff PA1 ==> Attention PA2 ==> Reshow 24 | You may request specific help information by entering a '?' in any entry field 25 | -------------------------------------------------------------------------------- /src/test/resources/sscplu-login-middle-screen: -------------------------------------------------------------------------------- 1 | 2 | SYSTEM: TESTAPP WELCOME TO DM3270 TESTING APPLICATION 3 | TST SERVER 1.0.0 . 4 | TERMINAL: 9999 5 | NODE: XXXXXXXX 6 | 7 | DAY: TUESDAY 8 | 9 | SYSTEM DATE: JANUARY 08, 2019 10 | SYSTEM TIME: 01:36 PM 11 | 12 | LOGONID: ===> 13 | PASSWORD: ===> 14 | 15 | NEW PASSWORD: ===> 16 | (ENTER TWICE) ===> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/resources/login-3278-M2-E-final-screen.txt: -------------------------------------------------------------------------------- 1 | MSGID: TEN0025 2 | 3 | 4 | This system is for the use only of persons authorized by 5 | Testing Resurce ISLANDIA 6 | Individuals using this system without authority, or in excess of their 7 | authority, do so at their own risk and are subject to having all of 8 | their activities on this system monitored or recorded. In the course of 9 | monitoring individuals improperly using this system, or in the course of 10 | system maintenance, the activities of authorized users may also be 11 | monitored. Anyone using this system consents to such monitoring. 12 | 13 | ***************************************************************************** 14 | 15 | ESTING PURPOSES Last-Used 29 Dec 20 09:23 System=0001 Facility=VMAN 16 | TSS7001I Count=00026 Mode=Fail Locktime=None Name=GENERIC-BALJO03 17 | 18 | 19 | ***************************************************************************** 20 | 21 | 22 | TO PROCEED, HIT ENTER 23 | 24 | === 25 | -------------------------------------------------------------------------------- /src/test/resources/sscplu-login-success-screen.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TEST1111 TEST001 LAST SYSTEM ACCESS 11.13-01/08/19 FROM XXXXXXXX 5 | TEST1111 CICS XXX: 1111 SIGNON OK: USER=TESTUSR NAME=TEST USER 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/resources/attribute_not_present_expected_screen.txt: -------------------------------------------------------------------------------- 1 | MDIV DIVISION MAINTENANCE 06-18-21 19:17:24 2 | 3 | 4 | Starting Div: 000 MORE: + 5 | ------- Division Manager ------- 6 | ACT Div Division Name Emp # Employee Name 7 | 000 DIV111 000000 8 | 000 000000 9 | 000 000000 10 | 000 000000 11 | 000 000000 12 | 000 000000 13 | 000 000000 14 | 000 000000 15 | 000 000000 16 | 000 000000 17 | 000 000000 18 | 000 000000 19 | 000 000000 20 | 21 | ACTION: A=Add M=Modify D=Delete L=List Departments 22 | 23 | DIV003 Requested divisions displayed. 24 | F01=HELP F03=MENU F07=PREV F08=NEXT 25 | -------------------------------------------------------------------------------- /src/test/resources/login-special-character-charset-CP1047.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________________________ 3 | ¨GROUPAMA ¨ ¨ S y s t } m e s 4 | ¨ / ç ¨ I n f o r m a t i o n 5 | ¨ / ç ¨ G R O U P A M A 6 | ¨ / ç ¨ 7 | ¨ / ç ¨ SIG 8 | ¨ ¨_______¨____________ ¨ 9 | ¨ ¨ / ç ç ¨ "machine RESEAU N2" 10 | ¨ _________¨_ / ç ç ¨ 11 | ¨ / / ç /_____ç___________ç ¨ 12 | ¨ /_________/___ç ¨ ¨ ¨ ¨ 13 | _____¨_¨_________¨___¨_¨_____¨___________¨___¨________________________________ 14 | ¨ / / / / ç ç ç ç ¨ 15 | ¨ / / / / ç ç ç ç ¨ 01/08/19 - 10:19:49 16 | ¨/ / / / ç ç ç ç¨ Terminal: TNN2L861 17 | ¨_______________________________________¨ 18 | 19 | Code utilisateur ==> 20 | Mot de passe ==> 21 | Nouveau mot de passe ==> 22 | 23 | 24 | PF 1=Aide 25 | -------------------------------------------------------------------------------- /src/test/resources/login-special-character-charset-CP1147.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________________________ 3 | |GROUPAMA | | S y s t è m e s 4 | | / \ | I n f o r m a t i o n 5 | | / \ | G R O U P A M A 6 | | / \ | 7 | | / \ | SIG 8 | | |_______|____________ | 9 | | | / \ \ | "machine RESEAU N2" 10 | | _________|_ / \ \ | 11 | | / / \ /_____\___________\ | 12 | | /_________/___\ | | | | 13 | _____|_|_________|___|_|_____|___________|___|________________________________ 14 | | / / / / \ \ \ \ | 15 | | / / / / \ \ \ \ | 01/08/19 - 10:19:49 16 | |/ / / / \ \ \ \| Terminal: TNN2L861 17 | |_______________________________________| 18 | 19 | Code utilisateur ==> 20 | Mot de passe ==> 21 | Nouveau mot de passe ==> 22 | 23 | 24 | PF 1=Aide 25 | -------------------------------------------------------------------------------- /src/test/resources/field_without_start_attribute_expected_screen.txt: -------------------------------------------------------------------------------- 1 | TSS7000I QAGEN Last-Used 18 Jun 21 17:03 System=CA31 Facility=VMAN 2 | TSS7001I Count=01493 Mode=Warn Locktime=None Name=NUNNS MARCELLA 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/telnet/TerminalTypeSubcommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.telnet; 2 | 3 | import com.bytezone.dm3270.display.Screen; 4 | import com.bytezone.dm3270.streams.TelnetState; 5 | import java.nio.charset.StandardCharsets; 6 | import java.security.InvalidParameterException; 7 | 8 | public class TerminalTypeSubcommand extends TelnetSubcommand { 9 | 10 | private static final byte OPTION_IS = 0; 11 | private static final byte OPTION_SEND = 1; 12 | 13 | public TerminalTypeSubcommand(byte[] buffer, int offset, int length, 14 | TelnetState telnetState) { 15 | super(buffer, offset, length, telnetState); 16 | 17 | if (buffer[3] == OPTION_IS) { 18 | type = SubcommandType.IS; 19 | value = new String(buffer, 4, length - 6); 20 | } else if (buffer[3] == OPTION_SEND) { 21 | type = SubcommandType.SEND; 22 | value = ""; 23 | } else { 24 | throw new InvalidParameterException( 25 | String.format("Unknown subcommand type: %02X%n", buffer[3])); 26 | } 27 | } 28 | 29 | @Override 30 | public void process(Screen screen) { 31 | if (type == SubcommandType.SEND) { 32 | byte[] header = {TelnetCommand.IAC, TelnetCommand.SB, TERMINAL_TYPE, OPTION_IS}; 33 | byte[] terminal = getTerminalString().getBytes(StandardCharsets.US_ASCII); 34 | byte[] reply = new byte[header.length + terminal.length + 2]; 35 | 36 | System.arraycopy(header, 0, reply, 0, header.length); 37 | System.arraycopy(terminal, 0, reply, header.length, terminal.length); 38 | 39 | reply[reply.length - 2] = TelnetCommand.IAC; 40 | reply[reply.length - 1] = TelnetCommand.SE; 41 | 42 | telnetState.setTerminal(getTerminalString()); 43 | 44 | setReply(new TerminalTypeSubcommand(reply, 0, reply.length, telnetState)); 45 | } 46 | } 47 | 48 | private String getTerminalString() { 49 | return telnetState.doDeviceType() + (telnetState.do3270Extended() ? "-E" : ""); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | switch (type) { 55 | case SEND: 56 | return type + " TerminalType"; 57 | case IS: 58 | return type + " TerminalType " + getTerminalString(); 59 | default: 60 | return "SUB: " + "Unknown"; 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/commands/ReadCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.commands; 2 | 3 | import com.bytezone.dm3270.display.Screen; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class ReadCommand extends Command { 8 | 9 | private static final Logger LOG = LoggerFactory.getLogger(ReadCommand.class); 10 | 11 | private final String name; 12 | private final CommandType type; 13 | 14 | private enum CommandType { 15 | READ_BUFFER, READ_MODIFIED, READ_MODIFIED_ALL 16 | } 17 | 18 | public ReadCommand(byte[] buffer, int offset, int length) { 19 | super(buffer, offset, length); 20 | 21 | assert buffer[offset] == Command.READ_BUFFER_02 22 | || buffer[offset] == Command.READ_BUFFER_F2 23 | || buffer[offset] == Command.READ_MODIFIED_06 24 | || buffer[offset] == Command.READ_MODIFIED_F6 25 | || buffer[offset] == Command.READ_MODIFIED_ALL_0E 26 | || buffer[offset] == Command.READ_MODIFIED_ALL_6E; 27 | 28 | switch (data[0]) { 29 | case READ_BUFFER_F2: 30 | case READ_BUFFER_02: 31 | name = "Read Buffer"; 32 | type = CommandType.READ_BUFFER; 33 | break; 34 | 35 | case READ_MODIFIED_F6: 36 | case READ_MODIFIED_06: 37 | name = "Read Modified"; 38 | type = CommandType.READ_MODIFIED; 39 | break; 40 | 41 | case READ_MODIFIED_ALL_6E: 42 | case READ_MODIFIED_ALL_0E: 43 | name = "Read Modified All"; 44 | type = CommandType.READ_MODIFIED_ALL; 45 | break; 46 | 47 | default: 48 | name = "Not found"; 49 | type = null; 50 | } 51 | } 52 | 53 | @Override 54 | public String getName() { 55 | return name; 56 | } 57 | 58 | @Override 59 | public void process(Screen screen) { 60 | // Create an AID command 61 | if (type == CommandType.READ_BUFFER) { 62 | setReply(screen.readBuffer()); 63 | } else if (type == CommandType.READ_MODIFIED) { 64 | setReply(screen.readModifiedFields(READ_MODIFIED_F6)); 65 | } else if (type == CommandType.READ_MODIFIED_ALL) { 66 | setReply(screen.readModifiedFields(READ_MODIFIED_ALL_6E)); 67 | } else { 68 | LOG.warn("Unknown READ command: {}", String.format("%02X", data[0])); 69 | } 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return name; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/test/resources/field_without_start_attribute.yml: -------------------------------------------------------------------------------- 1 | - !server {data: FFFD28} 2 | - !client {data: FFFB28} 3 | - !server {data: FFFA280802FFF0} 4 | - !client {data: FFFA28020749424D2D333237382D32FFF0} 5 | - !server {data: FFFA28020449424D2D333237382D32014130315443373837FFF0} 6 | - !client {data: FFFA280307000204FFF0} 7 | - !server {data: FFFA28030400020405FFF0} 8 | #Screen with field without attribute 9 | - !server {data: 000302000FF5C2114BE91311405B290242F1C0F8E2898795969540A39640404040404011C140290242F4C0F0C1D7D7D3C9C4290242F5C0F0404040404040404011C8F0290242F4C0F0E3A8978540A896A49940A4A28599898440819584409781A2A2A69699846B40A388859540979985A2A240C5D5E3C5D97A114BD9290242F4C0F0E4A285998984404B404B404B404B290241F442F5114BF11DF0114BF4290242F4C0F0C79996A4978984404B404B404B290241F442F5114C4B1DF0114CE9290242F4C0F0D781A2A2A6969984404B404B404B290341F442F5C04C114DC11DF0114DF9290242F4C0F0D3819587A4818785404B404B404B290241F442F5114E4C1DF01150D5290242F4C0F0D585A640D781A2A2A6969984404B404B404B290341F442F5C04C1150F11DF0115A501D7C115B5B1DF0115B60290242F2C04040404040404040404040D7938581A28540A3A8978540A896A49940A4A2859989844B404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040290242F1C0F0C6F37EC5A789A3115D7E1DF011C2601DF840404040404040404040404040404040404040F54BF240404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040401DF8404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040401DF8404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040401DF84040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404011D2F0290242F2C0F0FFEF} 10 | - !client {data: 020000000F00FFEF} 11 | - !client {data: 00000000007d4d40114be9e3c5e2e3e4e2d9114cf9e3c5e2e3d7e2e6ffef} 12 | - !server {data: 0000010010f5c31140c1131140401d40e3e2e2f7f0f0f0c940d8c1c7c5d540d381a2a360e4a2858440f1f840d1a49540f2f140f1f77af0f340e2a8a2a385947ec3c1f3f140c68183899389a3a87ee5d4c1d511c1501d40e3e2e2f7f0f0f1c940c396a495a37ef0f1f4f9f340d49684857ee681999540d3968392a38994857ed596958540d58194857ed5e4d5d5e240d4c1d9c3c5d3d3c111c2601d4011c3f01d4011c5401d4011c6501d4011c7601d4011c8f01d40114a401d40114b501d40114c601d40ffef} 13 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/buffers/MultiBuffer.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.buffers; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.Screen; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class MultiBuffer implements Buffer { 11 | 12 | private static final Logger LOG = LoggerFactory.getLogger(MultiBuffer.class); 13 | private final Charset charset; 14 | 15 | private List buffers = new ArrayList<>(); 16 | 17 | public MultiBuffer(Charset charset) { 18 | this.charset = charset; 19 | } 20 | 21 | public void addBuffer(Buffer buffer) { 22 | buffers.add(buffer); 23 | } 24 | 25 | @Override 26 | public byte[] getData() { 27 | byte[] data = new byte[size()]; 28 | int ptr = 0; 29 | for (Buffer buffer : buffers) { 30 | LOG.debug(charset.toHex(buffer.getData())); 31 | System.arraycopy(buffer.getData(), 0, data, ptr, buffer.size()); 32 | ptr += buffer.size(); 33 | } 34 | LOG.debug(charset.toHex(data)); 35 | return data; 36 | } 37 | 38 | @Override 39 | public byte[] getTelnetData() { 40 | List telnets = new ArrayList<>(); 41 | 42 | int size = 0; 43 | for (Buffer buffer : buffers) { 44 | byte[] telnet = buffer.getTelnetData(); 45 | telnets.add(telnet); 46 | size += telnet.length; 47 | } 48 | 49 | byte[] returnBuffer = new byte[size]; 50 | int ptr = 0; 51 | for (byte[] buffer : telnets) { 52 | System.arraycopy(buffer, 0, returnBuffer, ptr, buffer.length); 53 | ptr += buffer.length; 54 | } 55 | 56 | return returnBuffer; 57 | } 58 | 59 | @Override 60 | public int size() { 61 | int size = 0; 62 | for (Buffer buffer : buffers) { 63 | size += buffer.size(); 64 | } 65 | return size; 66 | } 67 | 68 | @Override 69 | public void process(Screen screen) { 70 | for (Buffer buffer : buffers) { 71 | buffer.process(screen); 72 | } 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | StringBuilder text = new StringBuilder(); 78 | 79 | for (Buffer buffer : buffers) { 80 | text.append(buffer.toString()); 81 | text.append("\n\n"); 82 | } 83 | 84 | if (text.length() > 0) { 85 | text.deleteCharAt(text.length() - 1); 86 | text.deleteCharAt(text.length() - 1); 87 | } 88 | 89 | return text.toString(); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/com/bytezone/dm3270/ScreenTextWaiter.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | import com.bytezone.dm3270.application.KeyboardStatusChangedEvent; 4 | import com.bytezone.dm3270.application.KeyboardStatusListener; 5 | import com.bytezone.dm3270.display.CursorMoveListener; 6 | import com.bytezone.dm3270.display.Field; 7 | import com.bytezone.dm3270.display.ScreenChangeListener; 8 | import com.bytezone.dm3270.display.ScreenWatcher; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | public class ScreenTextWaiter extends ConditionWaiter implements KeyboardStatusListener, 14 | CursorMoveListener, ScreenChangeListener { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(ScreenTextWaiter.class); 17 | 18 | private final String text; 19 | private boolean matched; 20 | 21 | public ScreenTextWaiter(String text, TerminalClient client, ScheduledExecutorService stableTimeoutExecutor) { 22 | super(client, stableTimeoutExecutor); 23 | this.text = text; 24 | client.addCursorMoveListener(this); 25 | client.addKeyboardStatusListener(this); 26 | client.addScreenChangeListener(this); 27 | checkIfScreenMatchesCondition(); 28 | if (matched) { 29 | startStablePeriod(); 30 | } 31 | } 32 | 33 | @Override 34 | public void keyboardStatusChanged(KeyboardStatusChangedEvent keyboardStatusChangedEvent) { 35 | handleReceivedEvent("keyboardStatusChanged"); 36 | } 37 | 38 | @Override 39 | public void cursorMoved(int i, int i1, Field field) { 40 | handleReceivedEvent("cursorMoved"); 41 | } 42 | 43 | @Override 44 | public void screenChanged(ScreenWatcher screenWatcher) { 45 | checkIfScreenMatchesCondition(); 46 | handleReceivedEvent("screenChanged"); 47 | } 48 | 49 | private void handleReceivedEvent(String event) { 50 | if (matched) { 51 | LOG.debug("Restart screen text stable period since received event {}", event); 52 | startStablePeriod(); 53 | } 54 | } 55 | 56 | private void checkIfScreenMatchesCondition() { 57 | if (client.getScreenText().contains(text)) { 58 | LOG.debug("Found matching text in screen, now waiting for silent period."); 59 | matched = true; 60 | } 61 | } 62 | 63 | protected void stop() { 64 | super.stop(); 65 | client.removeCursorMoveListener(this); 66 | client.removeKeyboardStatusListener(this); 67 | client.removeScreenChangeListener(this); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/Charset.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | import com.bytezone.dm3270.buffers.Buffer; 4 | import java.nio.charset.UnsupportedCharsetException; 5 | 6 | public enum Charset { 7 | CP1025, 8 | CP1026, 9 | CP1047, 10 | CP1140, 11 | CP1141, 12 | CP1142, 13 | CP1143, 14 | CP1144, 15 | CP1145, 16 | CP1146, 17 | CP1147, 18 | CP1148, 19 | CP1149, 20 | CP1153, 21 | CP1154, 22 | CP1166, 23 | CP1377, 24 | CP850, 25 | CP870, 26 | CP930, 27 | CP931, 28 | CP935, 29 | CP937, 30 | CP939; 31 | 32 | private char[] charsMapping; 33 | private java.nio.charset.Charset charset; 34 | 35 | public synchronized void load() throws UnsupportedCharsetException { 36 | if (charset != null) { 37 | return; 38 | } 39 | charset = java.nio.charset.Charset.forName(name()); 40 | byte[] baseBytes = new byte[256]; 41 | for (int i = 0; i < 256; i++) { 42 | baseBytes[i] = (byte) i; 43 | } 44 | charsMapping = new String(baseBytes, charset).toCharArray(); 45 | } 46 | 47 | public char getChar(byte value) { 48 | return charsMapping[value & 0xFF]; 49 | } 50 | 51 | public String getString(byte[] buffer) { 52 | return new String(buffer, charset); 53 | } 54 | 55 | public String getString(byte[] buffer, int offset, int length) { 56 | return new String(buffer, 57 | offset + length > buffer.length ? buffer.length - offset - 1 : offset, 58 | length, charset); 59 | } 60 | 61 | public String toHex(byte[] b) { 62 | return toHex(b, 0, b.length); 63 | } 64 | 65 | public String toHex(byte[] b, int offset, int length) { 66 | StringBuilder text = new StringBuilder(); 67 | for (int ptr = offset, max = offset + length; ptr < max; ptr += Buffer.HEX_LINE_SIZE) { 68 | StringBuilder hexLine = new StringBuilder(); 69 | StringBuilder textLine = new StringBuilder(); 70 | for (int linePtr = 0; linePtr < Buffer.HEX_LINE_SIZE && ptr + linePtr < max; linePtr++) { 71 | int val = b[ptr + linePtr] & 0xFF; 72 | hexLine.append(String.format("%02X ", val)); 73 | if (val < 0x40 || val == 0xFF) { 74 | textLine.append('.'); 75 | } else { 76 | textLine.append(new String(b, ptr + linePtr, 1, charset)); 77 | } 78 | } 79 | text.append(String.format("%04X %-48s %s%n", ptr, hexLine.toString(), textLine.toString())); 80 | } 81 | return text.length() > 0 ? text.substring(0, text.length() - 1) : text.toString(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/structuredfields/ReadPartitionSF.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.structuredfields; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.buffers.Buffer; 5 | import com.bytezone.dm3270.commands.Command; 6 | import com.bytezone.dm3270.commands.ReadPartitionQuery; 7 | import com.bytezone.dm3270.display.Screen; 8 | import java.util.Optional; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class ReadPartitionSF extends StructuredField { 13 | 14 | private static final Logger LOG = LoggerFactory.getLogger(ReadPartitionSF.class); 15 | 16 | private final byte partitionID; 17 | private final Command command; 18 | 19 | public ReadPartitionSF(byte[] buffer, int offset, int length, Charset charset) { 20 | super(buffer, offset, length, charset); 21 | 22 | assert data[0] == StructuredField.READ_PARTITION; 23 | partitionID = data[1]; 24 | 25 | if (partitionID == (byte) 0xFF) { 26 | switch (data[2]) { 27 | case (byte) 0x02: 28 | case (byte) 0x03: 29 | command = new ReadPartitionQuery(buffer, offset, length, charset); 30 | break; 31 | 32 | default: 33 | command = null; 34 | } 35 | } else { 36 | // wrapper for original read commands - RB, RM, RMA 37 | assert (partitionID & (byte) 0x80) == 0; // must be 0x00 - 0x7F 38 | 39 | // can only be RB/RM/RMA (i.e. one of the read commands) 40 | command = Command.getCommand(buffer, offset + 2, length - 2, charset); 41 | LOG.debug("RB/RM/RMA: {}", command); 42 | } 43 | } 44 | 45 | @Override 46 | public void process(Screen screen) { 47 | // replay mode 48 | if (getReply().isPresent()) { 49 | return; 50 | } 51 | 52 | if (partitionID == (byte) 0xFF) { 53 | command.process(screen); 54 | Optional opt = command.getReply(); 55 | if (opt.isPresent()) { 56 | setReply(opt.get()); 57 | } else { 58 | setReply(null); 59 | } 60 | } else { 61 | command.process(screen); 62 | Optional opt = command.getReply(); 63 | if (opt.isPresent()) { 64 | setReply(opt.get()); 65 | } else { 66 | setReply(null); 67 | } 68 | LOG.debug("testing read command reply"); 69 | } 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return "Struct Field : 01 Read Partition\n" + String 75 | .format(" partition : %02X%n", partitionID) 76 | + command; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/extended/SscpLuDataCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.extended; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.commands.Command; 5 | import com.bytezone.dm3270.display.Screen; 6 | import com.bytezone.dm3270.display.Screen.ScreenOption; 7 | import com.bytezone.dm3270.orders.InsertCursorOrder; 8 | import com.bytezone.dm3270.orders.Order; 9 | import com.bytezone.dm3270.orders.TextOrder; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class SscpLuDataCommand extends Command { 14 | 15 | private final List orders = new ArrayList<>(); 16 | 17 | public SscpLuDataCommand(byte[] buffer, int offset, int length, Charset charset) { 18 | super(buffer, offset, length); 19 | 20 | int ptr = offset; 21 | Order previousOrder = null; 22 | 23 | int max = offset + length; 24 | while (ptr < max) { 25 | Order order = Order.getOrder(buffer, ptr, max, charset); 26 | 27 | if (order.matchesPreviousOrder(previousOrder)) { 28 | previousOrder.incrementDuplicates(); // and discard this Order 29 | } else { 30 | orders.add(order); 31 | previousOrder = order; 32 | } 33 | 34 | ptr += order.size(); 35 | } 36 | byte[] insertCursorBuffer = {Order.INSERT_CURSOR}; 37 | orders.add(new InsertCursorOrder(insertCursorBuffer, 0)); 38 | } 39 | 40 | @Override 41 | public void process(Screen screen) { 42 | screen.setCurrentScreen(ScreenOption.DEFAULT); 43 | screen.lockKeyboard("Erase Write"); 44 | screen.clearScreen(ScreenOption.DEFAULT); 45 | screen.setSscpLuData(); 46 | 47 | if (orders.size() > 0) { 48 | for (Order order : orders) { 49 | order.process(screen); 50 | } 51 | 52 | screen.buildFields(); 53 | 54 | screen.resetInsertMode(); 55 | screen.restoreKeyboard(); 56 | 57 | screen.draw(); 58 | } 59 | 60 | } 61 | 62 | @Override 63 | public String getName() { 64 | return "SSCP_LU_DATA"; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | StringBuilder text = new StringBuilder(); 70 | text.append(getName()); 71 | 72 | // if the list begins with a TextOrder then tab out the missing columns 73 | if (orders.size() > 0 && orders.get(0) instanceof TextOrder) { 74 | text.append(String.format("%40s", "")); 75 | } 76 | 77 | for (Order order : orders) { 78 | String fmt = (order.isText()) ? "%s" : "%n%-40s"; 79 | text.append(String.format(fmt, order)); 80 | } 81 | 82 | return text.toString(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/commands/ReadPartitionQuery.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.commands; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.Screen; 5 | import com.bytezone.dm3270.replyfield.QueryReplyField.ReplyType; 6 | import com.bytezone.dm3270.structuredfields.StructuredField; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class ReadPartitionQuery extends Command { 13 | 14 | private static final Logger LOG = LoggerFactory.getLogger(ReadPartitionQuery.class); 15 | private final Charset charset; 16 | 17 | private String typeName; 18 | 19 | public ReadPartitionQuery(byte[] buffer, int offset, int length, Charset charset) { 20 | super(buffer, offset, length); 21 | this.charset = charset; 22 | 23 | assert data[0] == StructuredField.READ_PARTITION; 24 | assert data[1] == (byte) 0xFF; 25 | } 26 | 27 | @Override 28 | public void process(Screen screen) { 29 | if (getReply().isPresent()) { 30 | return; 31 | } 32 | 33 | switch (data[2]) { 34 | case (byte) 0x02: 35 | setReply(new ReadStructuredFieldCommand(screen.getTelnetState(), charset)); 36 | typeName = "Read Partition (Query)"; 37 | break; 38 | 39 | case (byte) 0x03: 40 | switch (data[3] & 0xFF) { 41 | case 0x00: 42 | LOG.warn("QCODE list not yet supported"); 43 | break; 44 | 45 | case 0x40: 46 | typeName = "Read Partition (QueryList): Equivalent + QCODE list"; 47 | List queryList = new ArrayList<>(); 48 | for (int i = 4; i < data.length; i++) { 49 | queryList.add(ReplyType.fromId(data[i])); 50 | } 51 | setReply(new ReadStructuredFieldCommand(queryList, screen.getTelnetState(), charset)); 52 | break; 53 | 54 | case 0x80: 55 | typeName = "Read Partition (QueryList): all"; 56 | setReply(new ReadStructuredFieldCommand(screen.getTelnetState(), charset)); 57 | break; 58 | 59 | default: 60 | LOG.warn("Unknown query type: {}", String.format("%02X", data[3])); 61 | } 62 | break; 63 | 64 | default: 65 | LOG.warn("Unknown ReadStructuredField type: {}", String.format("%02X", data[2])); 66 | } 67 | } 68 | 69 | @Override 70 | public String getName() { 71 | return typeName; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return String.format("%s", typeName); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/Highlight.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | public class Highlight extends QueryReplyField { 4 | 5 | private static final byte HIGHLIGHT_DEFAULT = 0x00; 6 | private static final byte HIGHLIGHT_NORMAL = (byte) 0xF0; 7 | private static final byte HIGHLIGHT_BLINK = (byte) 0xF1; 8 | private static final byte HIGHLIGHT_REVERSE = (byte) 0xF2; 9 | private static final byte HIGHLIGHT_UNDERSCORE = (byte) 0xF4; 10 | private static final byte HIGHLIGHT_INTENSIFY = (byte) 0xF8; 11 | 12 | private static final String[] VALUES = {"Normal", "Blink", "Reverse video", "", 13 | "Underscore", "", "", "", "Intensity"}; 14 | 15 | private int pairs; 16 | private byte[] attributeValue; 17 | private byte[] action; 18 | 19 | public Highlight() { 20 | super(HIGHLIGHT_QUERY_REPLY); 21 | 22 | int ptr = createReply(11); // 5 pairs x 2 plus 1 23 | 24 | reply[ptr++] = 0x05; //Number of attribute-value/action pairs 25 | 26 | // Byte 1: Data stream attribute 27 | // Byte 2: Data stream action 28 | 29 | reply[ptr++] = HIGHLIGHT_DEFAULT; 30 | reply[ptr++] = HIGHLIGHT_NORMAL; 31 | 32 | reply[ptr++] = HIGHLIGHT_BLINK; 33 | reply[ptr++] = HIGHLIGHT_BLINK; 34 | 35 | reply[ptr++] = HIGHLIGHT_REVERSE; 36 | reply[ptr++] = HIGHLIGHT_REVERSE; 37 | 38 | reply[ptr++] = HIGHLIGHT_UNDERSCORE; 39 | reply[ptr++] = HIGHLIGHT_UNDERSCORE; 40 | 41 | reply[ptr++] = HIGHLIGHT_INTENSIFY; 42 | reply[ptr++] = HIGHLIGHT_INTENSIFY; 43 | 44 | checkDataLength(ptr); 45 | } 46 | 47 | public Highlight(byte[] buffer) { 48 | super(buffer); 49 | 50 | assert data[1] == HIGHLIGHT_QUERY_REPLY; 51 | 52 | pairs = data[2] & 0xFF; 53 | attributeValue = new byte[pairs]; 54 | action = new byte[pairs]; 55 | 56 | for (int i = 0; i < pairs; i++) { 57 | attributeValue[i] = data[i * 2 + 3]; 58 | action[i] = data[i * 2 + 4]; 59 | } 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | StringBuilder text = new StringBuilder(super.toString()); 65 | 66 | text.append(String.format("%n pairs : %d", pairs)); 67 | for (int i = 0; i < pairs; i++) { 68 | int av = attributeValue[i]; 69 | String attrText = av == 0 ? "Default" : VALUES[av & 0x0F]; 70 | String actionText = VALUES[action[i] & 0x0F]; 71 | String out = 72 | attrText.equals(actionText) ? attrText : attrText + " -> " + actionText; 73 | text.append(String.format("%n val/actn : %02X/%02X - %s", attributeValue[i], 74 | action[i], out)); 75 | } 76 | 77 | return text.toString(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/StartFieldExtendedOrder.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.attributes.Attribute; 4 | import com.bytezone.dm3270.attributes.StartFieldAttribute; 5 | import com.bytezone.dm3270.display.DisplayScreen; 6 | import com.bytezone.dm3270.display.Pen; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public class StartFieldExtendedOrder extends Order { 12 | 13 | private StartFieldAttribute startFieldAttribute; 14 | private final List attributes = new ArrayList<>(); 15 | private int location = -1; 16 | 17 | public StartFieldExtendedOrder(byte[] buffer, int offset) { 18 | assert buffer[offset] == Order.START_FIELD_EXTENDED; 19 | 20 | int totalAttributePairs = buffer[offset + 1] & 0xFF; 21 | this.buffer = new byte[totalAttributePairs * 2 + 2]; 22 | this.buffer[0] = buffer[offset]; 23 | this.buffer[1] = buffer[offset + 1]; 24 | 25 | int bptr = 2; 26 | int ptr = offset + 2; 27 | 28 | while (totalAttributePairs-- > 0) { 29 | Optional opt = Attribute.getAttribute(buffer[ptr], buffer[ptr + 1]); 30 | this.buffer[bptr++] = buffer[ptr++]; 31 | this.buffer[bptr++] = buffer[ptr++]; 32 | 33 | if (opt.isPresent()) { 34 | Attribute attribute = opt.get(); 35 | // There has to be a StartFieldAttribute, but it could be anywhere in the list 36 | if (attribute.getAttributeType() == Attribute.AttributeType.START_FIELD) { 37 | startFieldAttribute = (StartFieldAttribute) attribute; 38 | } else { 39 | attributes.add(attribute); 40 | } 41 | } 42 | } 43 | if (startFieldAttribute != null) { 44 | startFieldAttribute.setExtended(); 45 | } 46 | } 47 | 48 | @Override 49 | public void process(DisplayScreen screen) { 50 | Pen pen = screen.getPen(); 51 | location = pen.getPosition(); 52 | if (startFieldAttribute != null) { 53 | pen.startField(startFieldAttribute); 54 | } else { 55 | pen.startField(new StartFieldAttribute((byte) 0)); 56 | } 57 | 58 | for (Attribute attribute : attributes) { 59 | pen.addAttribute(attribute); 60 | } 61 | 62 | pen.moveRight(); 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | StringBuilder text = new StringBuilder(); 68 | String locationText = location >= 0 ? String.format("(%04d)", location) : ""; 69 | if (startFieldAttribute != null) { 70 | text.append(String.format("SFE : %s %s", startFieldAttribute, locationText)); 71 | } 72 | 73 | for (Attribute attr : attributes) { 74 | text.append(String.format("\n %-34s", attr)); 75 | } 76 | 77 | return text.toString(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/assistant/Dataset.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.assistant; 2 | 3 | public class Dataset { 4 | 5 | private String datasetName; 6 | 7 | private String volume; 8 | private String device; 9 | private String dsorg; 10 | private String recfm; 11 | private String catalog; 12 | private String created; 13 | private String expires; 14 | private String referredDate; 15 | 16 | private int tracks; 17 | private int extents; 18 | private int percentUsed; 19 | private int lrecl; 20 | private int blksize; 21 | 22 | public Dataset(String name) { 23 | datasetName = name; 24 | } 25 | 26 | public void setVolume(String volume) { 27 | this.volume = volume; 28 | } 29 | 30 | public void setDevice(String device) { 31 | this.device = device; 32 | } 33 | 34 | public void setDsorg(String dsorg) { 35 | this.dsorg = dsorg; 36 | } 37 | 38 | public void setRecfm(String recfm) { 39 | this.recfm = recfm; 40 | } 41 | 42 | public void setCatalog(String catalog) { 43 | this.catalog = catalog; 44 | } 45 | 46 | public void setCreated(String created) { 47 | this.created = created; 48 | } 49 | 50 | public void setExpires(String expires) { 51 | this.expires = expires; 52 | } 53 | 54 | public void setReferredDate(String referredDate) { 55 | this.referredDate = referredDate; 56 | } 57 | 58 | public void setTracks(int tracks) { 59 | this.tracks = tracks; 60 | } 61 | 62 | public void setExtents(int extents) { 63 | this.extents = extents; 64 | } 65 | 66 | public void setPercentUsed(int percentUsed) { 67 | this.percentUsed = percentUsed; 68 | } 69 | 70 | public void setLrecl(int lrecl) { 71 | this.lrecl = lrecl; 72 | } 73 | 74 | public void setBlksize(int blksize) { 75 | this.blksize = blksize; 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return String.format("Name ............ %s%n", datasetName) 81 | + String.format("Volume .......... %s%n", volume) 82 | + String.format("Device .......... %s%n", device) 83 | + String.format("DSORG ........... %s%n", dsorg) 84 | + String.format("RECFM ........... %s%n", recfm) 85 | + String.format("Catalog ......... %s%n", catalog) 86 | + String.format("Created ......... %s%n", created) 87 | + String.format("Expires ......... %s%n", expires) 88 | + String.format("Referred ........ %s%n", referredDate) 89 | + String.format("Tracks .......... %s%n", tracks) 90 | + String.format("Extents ......... %s%n", extents) 91 | + String.format("Percent used .... %s%n", percentUsed) 92 | + String.format("LRECL ........... %s%n", lrecl) 93 | + String.format("BLKSIZE ......... %s ", blksize); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/Color.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.attributes.ColorAttribute; 4 | import java.nio.ByteBuffer; 5 | 6 | public class Color extends QueryReplyField { 7 | 8 | private static final byte[] COLORS_ARRAY = { 9 | ColorAttribute.COLOR_NEUTRAL1, ColorAttribute.COLOR_GREEN, 10 | ColorAttribute.COLOR_BLUE, ColorAttribute.COLOR_BLUE, 11 | ColorAttribute.COLOR_RED, ColorAttribute.COLOR_RED, 12 | ColorAttribute.COLOR_PINK, ColorAttribute.COLOR_PINK, 13 | ColorAttribute.COLOR_GREEN, ColorAttribute.COLOR_GREEN, 14 | ColorAttribute.COLOR_TURQUOISE, ColorAttribute.COLOR_TURQUOISE, 15 | ColorAttribute.COLOR_YELLOW, ColorAttribute.COLOR_YELLOW, 16 | ColorAttribute.COLOR_NEUTRAL2, ColorAttribute.COLOR_NEUTRAL2, 17 | ColorAttribute.COLOR_BLACK, ColorAttribute.COLOR_BLACK, 18 | ColorAttribute.COLOR_DEEP_BLUE, ColorAttribute.COLOR_DEEP_BLUE, 19 | ColorAttribute.COLOR_ORANGE, ColorAttribute.COLOR_ORANGE, 20 | ColorAttribute.COLOR_PURPLE, ColorAttribute.COLOR_PURPLE, 21 | ColorAttribute.COLOR_PALE_GREEN, ColorAttribute.COLOR_PALE_GREEN, 22 | ColorAttribute.COLOR_PALE_TURQUOISE, ColorAttribute.COLOR_PALE_TURQUOISE, 23 | ColorAttribute.COLOR_GREY, ColorAttribute.COLOR_GREY, 24 | ColorAttribute.COLOR_WHITE, ColorAttribute.COLOR_WHITE 25 | }; 26 | 27 | private final byte flags; 28 | private final byte[] colors; 29 | 30 | public Color() { 31 | super(COLOR_QUERY_REPLY); 32 | flags = 0; 33 | colors = COLORS_ARRAY; 34 | ByteBuffer buffer = createReplyBuffer( 35 | colors.length + 1 + 1); // adding flags and colors length bytes 36 | buffer.put(flags); 37 | buffer.put((byte) (colors.length / 2)); 38 | buffer.put(colors, 0, colors.length); 39 | } 40 | 41 | public Color(byte[] buffer) { 42 | super(buffer); 43 | ByteBuffer dataBuffer = ByteBuffer.wrap(buffer); 44 | //skip queryReply id 45 | dataBuffer.get(); 46 | assert dataBuffer.get() == COLOR_QUERY_REPLY; 47 | flags = dataBuffer.get(); 48 | byte colorsCount = dataBuffer.get(); 49 | colors = new byte[colorsCount * 2]; 50 | dataBuffer.get(colors, 0, colors.length); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | StringBuilder text = new StringBuilder(super.toString()); 56 | text.append(String.format("%n flags : %02X", flags)); 57 | text.append(String.format("%n pairs : %d", colors.length / 2)); 58 | for (int i = 0; i < colors.length; i += 2) { 59 | text.append(String.format("%n val/actn : %02X/%02X - %s", colors[i], 60 | colors[i + 1], ColorAttribute.colorName(colors[i]))); 61 | if (colors[i] != colors[i + 1]) { 62 | text.append("/").append(ColorAttribute.colorName(colors[i + 1])); 63 | } 64 | } 65 | return text.toString(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/ColorAttribute.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import java.awt.Color; 4 | 5 | public abstract class ColorAttribute extends Attribute { 6 | 7 | public static final byte COLOR_NEUTRAL1 = 0x00; 8 | public static final byte COLOR_BLUE = (byte) 0xF1; 9 | public static final byte COLOR_RED = (byte) 0xF2; 10 | public static final byte COLOR_PINK = (byte) 0xF3; 11 | public static final byte COLOR_GREEN = (byte) 0xF4; 12 | public static final byte COLOR_TURQUOISE = (byte) 0xF5; 13 | public static final byte COLOR_YELLOW = (byte) 0xF6; 14 | public static final byte COLOR_NEUTRAL2 = (byte) 0xF7; 15 | public static final byte COLOR_BLACK = (byte) 0xF8; 16 | public static final byte COLOR_DEEP_BLUE = (byte) 0xF9; 17 | public static final byte COLOR_ORANGE = (byte) 0xFA; 18 | public static final byte COLOR_PURPLE = (byte) 0xFB; 19 | public static final byte COLOR_PALE_GREEN = (byte) 0xFC; 20 | public static final byte COLOR_PALE_TURQUOISE = (byte) 0xFD; 21 | public static final byte COLOR_GREY = (byte) 0xFE; 22 | public static final byte COLOR_WHITE = (byte) 0xFF; 23 | 24 | public static final Color[] COLORS = // 25 | {new Color(0.9607843f, 0.9607843f, 0.9607843f), // WHITESMOKE 26 | new Color(0.11764706f, 0.5647059f, 1.0f), // DODGERBLUE 27 | Color.RED, 28 | Color.PINK, 29 | new Color(0.0f, 1.0f, 0.0f), // LIME 30 | new Color(0.28235295f, 0.81960785f, 0.8f), // TURQUOISE 31 | Color.YELLOW, 32 | new Color(0.9607843f, 0.9607843f, 0.9607843f), // WHITESMOKE 33 | Color.BLACK, 34 | new Color(0.0f, 0.0f, 0.54509807f), // DARKBLUE 35 | Color.ORANGE, 36 | new Color(0.5019608f, 0.0f, 0.5019608f), // PURPLE 37 | new Color(0.59607846f, 0.9843137f, 0.59607846f), // PALEGREEN 38 | new Color(0.6862745f, 0.93333334f, 0.93333334f), // PALETURQUOISE 39 | Color.GRAY, 40 | new Color(0.9607843f, 0.9607843f, 0.9607843f), // WHITESMOKE 41 | }; 42 | 43 | private static final String[] COLOR_NAMES = 44 | {"Neutral1", "Blue", "Red", "Pink", "Green", "Turquoise", "Yellow", "Neutral2", 45 | "Black", "Deep blue", "Orange", "Purple", "Pale green", "Pale turquoise", "Grey", 46 | "White"}; 47 | 48 | protected final Color color; 49 | 50 | public ColorAttribute(AttributeType type, byte byteType, byte value) { 51 | super(type, byteType, value); 52 | color = COLORS[value & 0x0F]; 53 | } 54 | 55 | public static String getName(Color searchColor) { 56 | int count = 0; 57 | for (Color color : COLORS) { 58 | if (color == searchColor) { 59 | return COLOR_NAMES[count]; 60 | } 61 | ++count; 62 | } 63 | return searchColor.toString(); 64 | } 65 | 66 | public static String colorName(byte value) { 67 | return COLOR_NAMES[value & 0x0F]; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return String.format("%-12s : %02X %-12s", name(), attributeValue, colorName(attributeValue)); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/resources/login-3270-model-5.yml: -------------------------------------------------------------------------------- 1 | - !server {data: FFFD28, delayMillis: 248} 2 | - !client {data: FFFB28} 3 | - !server {data: FFFA280802FFF0, delayMillis: 249} 4 | - !client {data: FFFA28020749424D2D333237382D35FFF0} 5 | - !server {data: FFFA28020449424D2D333237382D352D45014132305443303130FFF0, delayMillis: 249} 6 | - !client {data: fffa280307000204fff0} 7 | # restore keyboard + user input screen + cursor=2,1 8 | - !server {data: 0000020002F1C21140401DC81D401140401DC8C1C1C1C1C1C1C1C1C140C5D5E3C5D940E4E2C5D9C9C440601D4011C15013FFEF, 9 | delayMillis: 877} 10 | # user: testusr + enter 11 | - !client {data: 7D406111405AA385A2A3A4A299FFEF} 12 | # reset + cursor=1,1 13 | - !server {data: 05C1115D7F1D4011404013FFEF, delayMillis: 289} 14 | # restore keyboard + password input screen + cursor=8,20 15 | - !server {data: 0000020002F1C21140403C4040401140401DE86060606060606060606060606060606060606060606060606060606060606040E3E2D661C540D3D6C7D6D54060606060606060606060606060606060606060606060606060606060606060606060606011C1501DE8404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404011C2601DE84040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040115B601DE8D7C6F161D7C6F1F3407E7E6E40C885939740404040D7C6F361D7C6F1F5407E7E6E40D3968796868640404040D7C1F1407E7E6E40C1A3A38595A389969540404040D7C1F2407E7E6E40D985A28896A6115CF01DE8E896A4409481A840998598A485A2A340A29785838986898340888593974089958696999481A38996954082A8408595A385998995874081407D6F7D408995408195A8408595A399A840868985938411C3F31DE8C595A3859940D3D6C7D6D540978199819485A38599A24082859396A67A11C4E31DE8D9C1C3C640D3D6C7D6D540978199819485A38599A27A11C6D21D6040E4A285998984404040407E7E7E6E11C6E21DE8E3C5E2E3E4E2D9401DF011C8F21D6040D781A2A2A696998440407E7E7E6E11C9C21D4C00000000000000001DF0114DF21D6040C18383A340D5948299407E7E7E6E114EC21DC8F1F0F0F0F0F0F00000000000000000000000000000000000000000000000000000000000000000001DF0114BD21D6040D79996838584A49985407E7E7E6E114BE21DC8D7D9D6C3F0F0F0401DF01150D21D6040E289A9854040404040407E7E7E6E1150E21DC8F4F0F9F60000001DF011D2F21D6040D78599869699944040407E7E7E6E11D3C21DC80000001DF0114CC21D6040C79996A49740C9848595A340407E7E7E6E114CD51DC800000000000000001DF011C9E21D6040D585A640D781A2A2A6969984407E7E7E6E11C9F51D4C00000000000000001DF011D7F31DE8C595A38599408195407DE27D408285869699854085818388409697A3899695408485A2899985844082859396A67A1D6011D9C71DE80011D9C91DC8401DF060D596948189931D6011D9D71DE80011D9D91DC8401DF060D5969596A38983851D6011D9E81DE80011D96A1DC8001DF060D985839695958583A31D6011D97A1DE80011D97C1DC8401DF060D6C9C483819984401D6011D5D21D6040C39694948195844040407E7E7E6E11D5E21DC840404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040401DF011C7C21D7C40E28583938182859340404040407E7E7E6E11C7D51D7C40404040404040401DF011C9C313FFEF, 16 | delayMillis: 197} 17 | # password: testpsw + enter 18 | - !client {data: 7DC94A11C9C3A385A2A397A2A6FFEF} 19 | #this login was created just to emulate the connection with a different TerminalType, to be precise IBM 3278-M5-E 20 | -------------------------------------------------------------------------------- /src/test/resources/login-special-characters.yml: -------------------------------------------------------------------------------- 1 | - !server {data: FFFD18, delayMillis: 13} 2 | - !client {data: FFFB18} 3 | - !server {data: FFFA1801FFF0, delayMillis: 12} 4 | - !client {data: FFFA180049424D2D333237382D332D45FFF0} 5 | - !server {data: FFFD19, delayMillis: 12} 6 | - !server {data: FFFB19} 7 | - !client {data: FFFB19} 8 | - !server {data: FFFD00FFFB00, delayMillis: 12} 9 | - !client {data: FFFD19} 10 | - !client {data: FFFB00FFFD00} 11 | - !server {data: 1100064000F1C2000501FFFF02FFEF, delayMillis: 12} 12 | - !client {data: 88000b818080818687a68885001781810100005000180100d30320009e0258070c078000268186001000f4f1f1f2f2f3f3f4f4f5f5f6f6f7f7f8f8f9f9fafafbfbfcfcfdfdfefeffffffff000f81870500f0f1f1f2f2f4f4f8f8001181a600000b0100005000180050001800078188000102001b81858200070c000000000700000002b904170100f103c30136ffef} 13 | - !server {data: 05C22842002902C07D42F4C5D4E2D7F0F02902C0F042F13CC150402902C0F842F43CC1D7403CC17E6D3CC260402902C0F842F44040404040BBC7D9D6E4D7C1D4C13CC2F740BB3CC34E40BB3CC3D5402902C0F842F540404040E22902C0F842F4A840A240A340D04094408540A23CC3F0402902C0F842F44040404040BB3CC4C6406140483CC45E40BB3CC4E5402902C0F842F540C92902C0F842F4954086409640994094408140A3408940964095404040402902C0F842F44040404040BB3CC5D54061404040483CC56E40BB3CC5F5402902C0F842F540404040C740D940D640E440D740C140D440C13CC650402902C0F842F44040404040BB3CC6E440614040404040483CC67E40BB3CC760402902C0F842F44040404040BB3CC7F340613CC77B40483CC84E40BB3CC8D5402902C0F842F73CC86040E2C9C73CC8F0402902C0F842F44040404040BB3CC9C340BB3CC94B6DBB3CC9D86D3CC95E40BB3CC9E5402902C0F042F13C4A40402902C0F842F44040404040BB3C4AD340BB3C4A5A406140483C4AE840484040404040BB3C4AF5402902C0F842F240407F9481838889958540D9C5E2C5C1E440D5F27F40404040402902C0F842F44040404040BB4040403C4BE36DBB6D4040404061404040483C4BF9404840404040BB3C4C60402902C0F842F44040404040BB4040613C4CF3406140484040616D6D6D6D6D483C4D4A6D48404040BB3C4DF0402902C0F842F44040404040BB40613C4EC26D616D6D6D4840BB4040404040BB3C4E5A40BB404040BB3C4F40402902C0F842F46D6D6D6D6DBB6DBB3C4FD26DBB6D6D6DBB6DBB6D6D6D6D6DBB3C4F6A6DBB6D6D6DBB3C504F6D2902C0F042F12902C0F842F44040404040BB4040404061404040614040406140406140484040404840404040484040404040483C507E40BB3CD15F402902C0F042F12902C0F842F44040404040BB404061404040614040404061404061404040484040404048404040404048404040404048404040BB3CD2D5402902C0F842F5F0F161F0F861F1F9402902C0F842F560402902C0F042F1F1F07AF1F97AF4F9404040402902C0F042F12902C0F842F44040404040BB61404040614040404061404040614040404040484040404040483CD3D74048404040404048BB3CD3E5402902C0F842F4E3859994899581937A2902C0F842F5E3D5D5F2D3F8F6F13CD37F402902C0F042F12902C0F842F44040404040BB3CD46E6DBB3CD550402902C0F842F13CD660402902C0F042F13CD66B402902C0F842F140C396848540A4A3899389A281A385A4992902C0F842F77E7E6E2902C04042F23CD74B402902C0F842F43CD7F0402902C0F042F13CD77B402902C0F842F24040404040D496A3408485409781A2A2852902C0F842F77E7E6E2902C04C42F73CD85B402902C0F842F43CD940402902C0F042F13CD9C7402902C0F842F240D596A4A58581A4409496A3408485409781A2A2852902C0F842F77E7E6E2902C04C42F23CD96B402902C0F842F13C5A50402902C0F842F1115B602902C0F842F1115CF02902C0F842F1D7C640F17EC18984853C5DC5402902C0F042F12902C04C42F13C5D4D402902C0F042F12902C04042F13C5DD7402902C0F042F12902C04C42F13C5D5F402902C0F042F12902C04C42F13C5DE7402902C0F042F13C40404011D7C213FFEF, 14 | delayMillis: 13} 15 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/streams/TelnetSocket.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.streams; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.net.Socket; 6 | import java.time.LocalDateTime; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class TelnetSocket implements Runnable { 11 | 12 | private static final Logger LOG = LoggerFactory.getLogger(TelnetSocket.class); 13 | 14 | private static final boolean GENUINE = true; 15 | 16 | private final String name; 17 | private final Source source; 18 | 19 | private Socket socket; 20 | private InputStream inputStream; 21 | 22 | private final byte[] buffer = new byte[4096]; 23 | 24 | private final BufferListener telnetListener; 25 | private volatile boolean running; 26 | 27 | public enum Source { 28 | CLIENT, SERVER 29 | } 30 | 31 | // Only used by a SpyServer, which creates two SocketListeners. Each SocketListener 32 | // copies its inputStream to its partner's outputStream after sending a copy to 33 | // the listener. 34 | 35 | public TelnetSocket(Source source, Socket socket, BufferListener listener) 36 | throws IOException { 37 | if (source == null) { 38 | throw new IllegalArgumentException("Source cannot be null"); 39 | } 40 | if (socket == null) { 41 | throw new IllegalArgumentException("Socket cannot be null"); 42 | } 43 | if (listener == null) { 44 | throw new IllegalArgumentException("Listener cannot be null"); 45 | } 46 | 47 | this.name = source == Source.CLIENT ? "Client" : "Server"; 48 | this.source = source; 49 | this.socket = socket; 50 | this.telnetListener = listener; 51 | 52 | this.inputStream = socket.getInputStream(); 53 | } 54 | 55 | @Override 56 | public void run() { 57 | running = true; 58 | while (running) { 59 | if (Thread.interrupted()) { 60 | LOG.debug("TelnetSocket interrupted"); 61 | break; 62 | } 63 | try { 64 | int bytesRead = inputStream.read(buffer); 65 | if (bytesRead == -1) { 66 | LOG.debug("{} has no data on input stream", name); 67 | close(); 68 | return; 69 | } 70 | 71 | // take a copy of the input buffer and send it to the TelnetListener 72 | byte[] message = new byte[bytesRead]; 73 | System.arraycopy(buffer, 0, message, 0, message.length); 74 | telnetListener.listen(source, message, LocalDateTime.now(), GENUINE); 75 | } catch (IOException e) { 76 | if (running) { 77 | LOG.error("{} closing due to IOException", name, e); 78 | } else { 79 | LOG.debug("{} quitting", name, e); 80 | } 81 | close(); 82 | return; 83 | } 84 | } 85 | 86 | LOG.debug("{} closing - bye everyone", name); 87 | close(); 88 | } 89 | 90 | private void close() { 91 | running = false; 92 | 93 | try { 94 | if (socket != null) { 95 | socket.close(); 96 | } 97 | } catch (IOException e) { 98 | e.printStackTrace(); 99 | } 100 | 101 | socket = null; 102 | inputStream = null; 103 | } 104 | 105 | @Override 106 | public String toString() { 107 | return String.format("TelnetSocket: Source=%s, name=%s", source, name); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/resources/user-menu-for-screen-type-5.txt: -------------------------------------------------------------------------------- 1 | ------------------------------- TSO/E LOGON ----------------------------------- 2 | Enter LOGON paramete 3 | rs below: RACF LOGON parameters: 4 | Userid ===> TESTUSR 5 | Password ===> New Password ===> 6 | Procedure ===> PROC000 Group Ident ===> 7 | Acct Nmbr ===> 1000000 8 | Size == 9 | => 4096 10 | Perform ===> 11 | Command ===> 12 | Enter an 'S' before each option desired below: 13 | -Nomail -Nonotice -Reconnect -OIDcard 14 | PF1/PF13 ==> Help PF3/PF15 ==> Logoff PA1 ==> Attention PA2 ==> Reshow You may 15 | request specific help information by entering a '?' in any entry field 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/extended/TN3270ExtendedCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.extended; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.buffers.Buffer; 5 | import com.bytezone.dm3270.buffers.MultiBuffer; 6 | import com.bytezone.dm3270.commands.Command; 7 | import com.bytezone.dm3270.display.Screen; 8 | import com.bytezone.dm3270.streams.TelnetState; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | public class TN3270ExtendedCommand extends AbstractExtendedCommand { 14 | 15 | private final Command command; 16 | private final TelnetState telnetState; 17 | private final Charset charset; 18 | 19 | public TN3270ExtendedCommand(CommandHeader commandHeader, Command command, 20 | TelnetState telnetState, Charset charset) { 21 | super(commandHeader); 22 | this.command = command; 23 | this.telnetState = telnetState; 24 | this.charset = charset; 25 | } 26 | 27 | public Command getCommand() { 28 | return command; 29 | } 30 | 31 | @Override 32 | public byte[] getData() { 33 | byte[] combinedData = new byte[command.size() + 5]; 34 | System.arraycopy(commandHeader.getData(), 0, combinedData, 0, 35 | commandHeader.size()); 36 | System.arraycopy(command.getData(), 0, combinedData, commandHeader.size(), 37 | command.size()); 38 | return combinedData; 39 | } 40 | 41 | @Override 42 | public int size() { 43 | return command.size() + commandHeader.size(); 44 | } 45 | 46 | @Override 47 | public void process(Screen screen) { 48 | commandHeader.process(screen); 49 | command.process(screen); 50 | } 51 | 52 | @Override 53 | public Optional getReply() { 54 | List buffers = new ArrayList<>(); 55 | 56 | Optional headerReply = commandHeader.getReply(); 57 | headerReply.ifPresent(buffers::add); 58 | 59 | // need to add a header for the command reply before the reply 60 | Optional reply = command.getReply(); 61 | if (reply.isPresent()) { 62 | byte[] headerBuffer = new byte[5]; 63 | Buffer.packUnsignedShort(telnetState.nextCommandHeaderSeq(), headerBuffer, 3); 64 | CommandHeader header = new CommandHeader(headerBuffer, charset); 65 | TN3270ExtendedCommand command = new TN3270ExtendedCommand(header, (Command) reply.get(), 66 | telnetState, charset); 67 | buffers.add(command); 68 | } 69 | 70 | if (buffers.size() == 0) { 71 | return Optional.empty(); 72 | } 73 | 74 | if (buffers.size() == 1) { 75 | return Optional.of(buffers.get(0)); 76 | } 77 | 78 | MultiBuffer multiBuffer = new MultiBuffer(charset); 79 | for (Buffer buffer : buffers) { 80 | multiBuffer.addBuffer(buffer); 81 | } 82 | return Optional.of(multiBuffer); 83 | } 84 | 85 | @Override 86 | public byte[] getTelnetData() { 87 | byte[] headerTelnetBuffer = commandHeader.getTelnetData(); 88 | byte[] commandTelnetBuffer = command.getTelnetData(); 89 | 90 | int length = headerTelnetBuffer.length + commandTelnetBuffer.length; 91 | byte[] returnBuffer = new byte[length]; 92 | System.arraycopy(headerTelnetBuffer, 0, returnBuffer, 0, headerTelnetBuffer.length); 93 | System.arraycopy(commandTelnetBuffer, 0, returnBuffer, headerTelnetBuffer.length, 94 | commandTelnetBuffer.length); 95 | 96 | return returnBuffer; 97 | } 98 | 99 | @Override 100 | public String getName() { 101 | return command.getName(); 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | return command.toString(); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/streams/TerminalServer.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.streams; 2 | 3 | import com.bytezone.dm3270.ConnectionListener; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.net.InetSocketAddress; 8 | import java.net.Socket; 9 | import java.time.LocalDateTime; 10 | import javax.net.SocketFactory; 11 | 12 | public class TerminalServer implements Runnable { 13 | 14 | private final String serverURL; 15 | private final int serverPort; 16 | private final SocketFactory socketFactory; 17 | private int connectionTimeoutMillis; 18 | private Socket serverSocket; 19 | private OutputStream serverOut; 20 | 21 | private final byte[] buffer = new byte[4096]; 22 | private volatile boolean running; 23 | 24 | private final BufferListener telnetListener; 25 | private ConnectionListener connectionListener; 26 | 27 | public TerminalServer(String serverURL, int serverPort, SocketFactory socketFactory, 28 | BufferListener listener) { 29 | this.serverPort = serverPort; 30 | this.serverURL = serverURL; 31 | this.socketFactory = socketFactory; 32 | this.telnetListener = listener; 33 | } 34 | 35 | public void setConnectionTimeoutMillis(int connectionTimeoutMillis) { 36 | this.connectionTimeoutMillis = connectionTimeoutMillis; 37 | } 38 | 39 | public void setConnectionListener(ConnectionListener connectionListener) { 40 | this.connectionListener = connectionListener; 41 | } 42 | 43 | @Override 44 | public void run() { 45 | try { 46 | try { 47 | serverSocket = socketFactory.createSocket(); 48 | serverSocket.connect(new InetSocketAddress(serverURL, serverPort), connectionTimeoutMillis); 49 | connectionListener.onConnection(); 50 | } catch (IOException ex) { 51 | handleException(ex); 52 | return; 53 | } 54 | 55 | InputStream serverIn = serverSocket.getInputStream(); 56 | serverOut = serverSocket.getOutputStream(); 57 | 58 | running = true; 59 | while (running) { 60 | int bytesRead = serverIn.read(buffer); 61 | if (bytesRead < 0) { 62 | close(); 63 | if (connectionListener != null) { 64 | connectionListener.onConnectionClosed(); 65 | } 66 | break; 67 | } 68 | 69 | byte[] message = new byte[bytesRead]; 70 | System.arraycopy(buffer, 0, message, 0, bytesRead); 71 | telnetListener.listen(TelnetSocket.Source.SERVER, message, LocalDateTime.now(), true); 72 | } 73 | } catch (IOException e) { 74 | if (running) { 75 | close(); 76 | handleException(e); 77 | } 78 | } 79 | } 80 | 81 | private void handleException(IOException ex) { 82 | if (connectionListener != null) { 83 | connectionListener.onException(ex); 84 | } else { 85 | ex.printStackTrace(); 86 | } 87 | } 88 | 89 | public synchronized void write(byte[] buffer) { 90 | // the no-op may come here if socket is closed from remote end and client has not been closed 91 | if (!running && buffer == TelnetState.NO_OP) { 92 | return; 93 | } 94 | 95 | try { 96 | serverOut.write(buffer); 97 | serverOut.flush(); 98 | } catch (IOException e) { 99 | handleException(e); 100 | } 101 | } 102 | 103 | public void close() { 104 | try { 105 | running = false; 106 | 107 | if (serverSocket != null) { 108 | serverSocket.close(); 109 | } 110 | 111 | if (telnetListener != null) { 112 | telnetListener.close(); 113 | } 114 | 115 | } catch (IOException e) { 116 | handleException(e); 117 | } 118 | } 119 | 120 | @Override 121 | public String toString() { 122 | return String.format("TerminalSocket listening to %s : %d", serverURL, serverPort); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/bytezone/dm3270/ScreenPositionTest.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.when; 5 | 6 | import com.bytezone.dm3270.attributes.Attribute; 7 | import com.bytezone.dm3270.attributes.StartFieldAttribute; 8 | import com.bytezone.dm3270.display.ScreenContext; 9 | import com.bytezone.dm3270.display.ScreenPosition; 10 | import java.util.Arrays; 11 | import java.util.Collection; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.junit.runners.Parameterized; 17 | import org.junit.runners.Parameterized.Parameter; 18 | import org.junit.runners.Parameterized.Parameters; 19 | import org.mockito.Mock; 20 | import org.mockito.junit.MockitoJUnit; 21 | import org.mockito.junit.MockitoJUnitRunner; 22 | import org.mockito.junit.MockitoRule; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class ScreenPositionTest { 26 | 27 | private ScreenPosition screenPosition; 28 | private static final byte VALUE = 0x01; 29 | 30 | @Before 31 | public void setup() { 32 | screenPosition = new ScreenPosition(0, ScreenContext.DEFAULT_CONTEXT, Charset.CP1147); 33 | } 34 | 35 | @Test 36 | public void shouldResetCharSizeWhenReset() { 37 | screenPosition.setChar(VALUE); 38 | screenPosition.reset(); 39 | assertThat(screenPosition.getByte()).isEqualTo((byte) 0); 40 | } 41 | 42 | @Test(expected = IllegalArgumentException.class) 43 | public void shouldThrowExceptionWhenNullContext() { 44 | screenPosition.setScreenContext(null); 45 | } 46 | 47 | @Test 48 | public void shouldClearAttributesWhenNullStartField() { 49 | StartFieldAttribute startFieldAttribute = new StartFieldAttribute(VALUE); 50 | screenPosition.setStartField(startFieldAttribute); 51 | screenPosition 52 | .addAttribute(Attribute.getAttribute(Attribute.XA_START_FIELD, VALUE).orElse(null)); 53 | screenPosition.addAttribute(Attribute.getAttribute(Attribute.XA_BGCOLOR, VALUE).orElse(null)); 54 | screenPosition.setStartField(null); 55 | assertThat(screenPosition.getAttributes()).isEmpty(); 56 | } 57 | 58 | @RunWith(Parameterized.class) 59 | public static class CharConversionTest { 60 | 61 | @Rule 62 | public MockitoRule rule = MockitoJUnit.rule(); 63 | 64 | @Mock 65 | public ScreenContext screenContextMock; 66 | 67 | @Parameter() 68 | public byte value; 69 | @Parameter(1) 70 | public char expectedChar; 71 | @Parameter(2) 72 | public boolean isAplCharset; 73 | 74 | private ScreenPosition screenPosition; 75 | 76 | @Before 77 | public void setup() { 78 | Charset.CP1047.load(); 79 | screenPosition = new ScreenPosition(0, screenContextMock, Charset.CP1047); 80 | when(screenContextMock.isGraphic()).thenReturn(isAplCharset); 81 | } 82 | 83 | @Parameters 84 | public static Collection data() { 85 | return Arrays.asList(new Object[][]{ 86 | {(byte) 0x85, '│', true}, 87 | {(byte) 0xA2, '─', true}, 88 | {(byte) 0xC4, '└', true}, 89 | {(byte) 0xC5, '┌', true}, 90 | {(byte) 0xC6, '├', true}, 91 | {(byte) 0xC7, '┴', true}, 92 | {(byte) 0xD3, '┼', true}, 93 | {(byte) 0xD4, '┘', true}, 94 | {(byte) 0xD5, '┐', true}, 95 | {(byte) 0xD6, '┤', true}, 96 | {(byte) 0xD7, '┬', true}, 97 | {(byte) 0x85, 'e', false}, 98 | {(byte) 0XA2, 's', false}, 99 | {(byte) 0xC5, 'E', false}, 100 | {(byte) 0xC7, 'G', false} 101 | }); 102 | } 103 | 104 | @Test 105 | public void shouldConvertToCharWhenGetChar() { 106 | screenPosition.setChar(value); 107 | assertThat(screenPosition.getChar()).isEqualTo(expectedChar); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/orders/Order.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.orders; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.DisplayScreen; 5 | 6 | public abstract class Order { 7 | 8 | // Buffer Control Orders 9 | public static final byte PROGRAM_TAB = 0x05; 10 | public static final byte GRAPHICS_ESCAPE = 0x08; 11 | public static final byte SET_BUFFER_ADDRESS = 0x11; 12 | public static final byte ERASE_UNPROTECTED = 0x12; 13 | public static final byte INSERT_CURSOR = 0x13; 14 | public static final byte START_FIELD = 0x1D; 15 | public static final byte SET_ATTRIBUTE = 0x28; 16 | public static final byte START_FIELD_EXTENDED = 0x29; 17 | public static final byte MODIFY_FIELD = 0x2C; 18 | public static final byte REPEAT_TO_ADDRESS = 0x3C; 19 | 20 | // Format Control Orders 21 | public static final byte FCO_NULL = 0x00; 22 | public static final byte FCO_SUBSTITUTE = 0x3F; 23 | public static final byte FCO_DUPLICATE = 0x1C; 24 | public static final byte FCO_FIELD_MARK = 0x1E; 25 | public static final byte FCO_FORM_FEED = 0x0C; 26 | public static final byte FCO_CARRIAGE_RETURN = 0x0D; 27 | public static final byte FCO_NEWLINE = 0x15; 28 | public static final byte FCO_END_OF_MEDIUM = 0x19; 29 | public static final byte FCO_EIGHT_ONES = (byte) 0xFF; 30 | 31 | public static byte[] orderValues = 32 | {START_FIELD, START_FIELD_EXTENDED, SET_BUFFER_ADDRESS, INSERT_CURSOR, 33 | GRAPHICS_ESCAPE, REPEAT_TO_ADDRESS, ERASE_UNPROTECTED, PROGRAM_TAB, SET_ATTRIBUTE, 34 | MODIFY_FIELD, FCO_NULL, FCO_SUBSTITUTE, FCO_DUPLICATE, FCO_FIELD_MARK, 35 | FCO_FORM_FEED, FCO_CARRIAGE_RETURN, FCO_NEWLINE, FCO_END_OF_MEDIUM, 36 | FCO_EIGHT_ONES}; 37 | 38 | protected byte[] buffer; 39 | protected int duplicates; 40 | 41 | public static Order getOrder(byte[] buffer, int ptr, int max, Charset charset) { 42 | switch (buffer[ptr]) { 43 | case START_FIELD: 44 | return new StartFieldOrder(buffer, ptr); 45 | 46 | case START_FIELD_EXTENDED: 47 | return new StartFieldExtendedOrder(buffer, ptr); 48 | 49 | case SET_BUFFER_ADDRESS: 50 | return new SetBufferAddressOrder(buffer, ptr); 51 | 52 | case SET_ATTRIBUTE: 53 | return new SetAttributeOrder(buffer, ptr); 54 | 55 | case MODIFY_FIELD: 56 | return new ModifyFieldOrder(buffer, ptr); 57 | 58 | case INSERT_CURSOR: 59 | return new InsertCursorOrder(buffer, ptr); 60 | 61 | case PROGRAM_TAB: 62 | return new ProgramTabOrder(buffer, ptr); 63 | 64 | case REPEAT_TO_ADDRESS: 65 | return new RepeatToAddressOrder(buffer, ptr, charset); 66 | 67 | case ERASE_UNPROTECTED: 68 | return new EraseUnprotectedToAddressOrder(buffer, ptr); 69 | 70 | case GRAPHICS_ESCAPE: 71 | return new GraphicsEscapeOrder(buffer, ptr); 72 | 73 | case FCO_NULL: 74 | case FCO_SUBSTITUTE: 75 | case FCO_DUPLICATE: 76 | case FCO_FIELD_MARK: 77 | case FCO_FORM_FEED: 78 | case FCO_CARRIAGE_RETURN: 79 | case FCO_END_OF_MEDIUM: 80 | case FCO_EIGHT_ONES: 81 | return new FormatControlOrder(buffer, ptr); 82 | case FCO_NEWLINE: 83 | return new NewlineOrder(buffer, ptr); 84 | default: 85 | return new TextOrder(buffer, ptr, max, charset); 86 | } 87 | } 88 | 89 | public void incrementDuplicates() { 90 | duplicates++; 91 | } 92 | 93 | public byte getType() { 94 | return buffer[0]; 95 | } 96 | 97 | public boolean isText() { 98 | return false; 99 | } 100 | 101 | // this is so that a GraphicsEscapeOrder can override it - it is used to report 102 | // that there are x duplicate orders. 103 | public boolean matchesPreviousOrder(Order order) { 104 | return false; 105 | } 106 | 107 | public int size() { 108 | return buffer.length; 109 | } 110 | 111 | public byte[] getBuffer() { 112 | return buffer; 113 | } 114 | 115 | public abstract void process(DisplayScreen screen); 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/test/resources/attribute_not_present.yml: -------------------------------------------------------------------------------- 1 | - !server {data: FFFD28} 2 | - !client {data: FFFB28} 3 | - !server {data: FFFA280802FFF0} 4 | - !client {data: FFFA28020749424D2D333237382D32FFF0} 5 | - !server {data: FFFA28020449424D2D333237382D32014130315443353639FFF0} 6 | - !client {data: FFFA280307000204FFF0} 7 | - !server {data: FFFA28030400020405FFF0} 8 | - !server {data: 0001020044F5C21140402902C0F142F4D4C5D5E4404040401DF01140D51DF0C3C140C7859540E2819497938540D496848593406040D695938995851DF011407E2902C0F042F4F0F660F1F860F2F111C1C72902C0F042F4F1F97AF1F77AF2F11DF011C16F1DF0D4C1C9D540D4C5D5E41DF011C54C1DF0E285938583A34096958540968640A38885408696939396A6899587409697A3899695A24081958440979985A2A2408595A385994B1DF011C9C62902C04042F7F01DF011C9C91DF0F14B1DF011C94D1DF0C489A589A289969540D4818995A38595819583851DF0114BE91DF0F24B1DF0114B6D1DF0C485978199A3948595A340D4818995A38595819583851DF0114EC91DF0F34B1DF0114E4D1DF0C594979396A8858540D4818995A38595819583851DF011D8C52902C0F042F4C4C9E5C9E2C9D6D51DF011D8502902C0F042F4C4C5D7C1D9E3D4C5D5E31DF011D85D2902C0F042F4C5D4D7D3D6E8C5C51DF011D9D82902C05042F6F0F0F01DF011D9E32902C05042F6F0F0F0F01DF011D96E2902C05042F63CD9F5F01DF0115B601DF03C5C6F401DF0115CF02902C0F042F5C6F0F17EC8C5D3D73C5D7F401DF01140C91D7DD4C5D5E4E2C3D9D51140D21D7D401DF011C9C713FFEF} 9 | - !client {data: 020000004400FFEF} 10 | - !client {data: 00000000007DD9D91140C1D4C5D5E44040404011404AD4C5D5E4E2C3D9D51140D34011C9C7F1FFEF} 11 | - !server {data: 0001020045F5C21140402902C0F142F4D4C4C9E5404040401DF011405C1DF0C4C9E5C9E2C9D6D540D4C1C9D5E3C5D5C1D5C3C51DF011407E2902C0F042F4F0F660F1F860F2F111C1C72902C0F042F4F1F97AF1F77AF2F41DF011C3F12902C0F042F5E2A38199A389958740C489A57A1DF011C4402902C05042F6F0F0F01DF011C4F22902C0F042F7D4D6D9C57A4040404E1DF011C5E92902C0F042F53CC5F16040C489A589A289969540D4819581878599403CC64A601DF011C6D11DF0C1C3E311C6D51DF0C489A511C6D91DF0C489A589A289969540D58194851DF011C6F91DF0C59497407B1DF011C7401DF0C594979396A8858540D58194851DF011C7612902C04042F740401DF011C7E52902C05042F7F0F0F011C7E92903C04042F7FE01C4C9E5F1F1F13CC8C8401DF011C8C92902C05042F73CC850F011C8502903C04042F7FE013CC86F401DF011C8F12902C04042F740401DF011C8F52902C05042F7F0F0F011C8F92903C04042F7FE013CC9D8401DF011C9D92902C05042F73CC960F011C9602903C04042F7FE013CC97F401DF0114AC12902C04042F740401DF0114AC52902C05042F7F0F0F0114AC92903C04042F7FE013C4AE8401DF0114AE92902C05042F73C4AF0F0114AF02903C04042F7FE013C4B4F401DF0114BD12902C04042F740401DF0114BD52902C05042F7F0F0F0114BD92903C04042F7FE013C4BF8401DF0114BF92902C05042F73C4C40F0114C402903C04042F7FE013C4C5F401DF0114C612902C04042F740401DF0114CE52902C05042F7F0F0F0114CE92903C04042F7FE013C4DC8401DF0114DC92902C05042F73C4D50F0114D502903C04042F7FE013C4D6F401DF0114DF12902C04042F740401DF0114DF52902C05042F7F0F0F0114DF92903C04042F7FE013C4ED8401DF0114ED92902C05042F73C4E60F0114E602903C04042F7FE013C4E7F401DF0114FC12902C04042F740401DF0114FC52902C05042F7F0F0F0114FC92903C04042F7FE013C4FE8401DF0114FE92902C05042F73C4FF0F0114FF02903C04042F7FE013C504F401DF01150D12902C04042F740401DF01150D52902C05042F7F0F0F01150D92903C04042F7FE013C50F8401DF01150F92902C05042F73CD140F011D1402903C04042F7FE013CD15F401DF011D1612902C04042F740401DF011D1E52902C05042F7F0F0F011D1E92903C04042F7FE013CD2C8401DF011D2C92902C05042F73CD250F011D2502903C04042F7FE013CD26F401DF011D2F12902C04042F740401DF011D2F52902C05042F7F0F0F011D2F92903C04042F7FE013CD3D8401DF011D3D92902C05042F73CD360F011D3602903C04042F7FE013CD37F401DF011D4C12902C04042F740401DF011D4C52902C05042F7F0F0F011D4C92903C04042F7FE013CD4E8401DF011D4E92902C05042F73CD4F0F011D4F02903C04042F7FE013CD54F401DF011D5D12902C04042F740401DF011D5D52902C05042F7F0F0F011D5D92903C04042F7FE013CD5F8401DF011D5F92902C05042F73CD640F011D6402903C04042F7FE013CD65F401DF011D6612902C04042F740401DF011D6E52902C05042F7F0F0F011D6E92903C04042F7FE013CD7C8401DF011D7C92902C05042F73CD750F011D7502903C04042F7FE013CD76F401DF011D94B1DF0C1C3E3C9D6D57A4040C17EC184844040D47ED496848986A84040C47EC4859385A3854040D37ED389A2A340C485978199A3948595A3A21DF0115B602902C0F042F7C4C9E5F0F0F340D98598A485A2A38584408489A589A2899695A2408489A2979381A885844B3C5C6F401DF0115CF02902C0F042F5C6F0F17EC8C5D3D740C6F0F37ED4C5D5E440C6F0F77ED7D9C5E540C6F0F87ED5C5E7E33C5D7F401DF01140C91D7DC4C9E5E2C3D9D5401140D21D7DD51DF011C4C113FFEF} 12 | - !client {data: 020000004500FFEF} 13 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/StartFieldAttribute.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | import java.awt.Color; 5 | 6 | public class StartFieldAttribute extends Attribute { 7 | 8 | private static final Color WHITE = ColorAttribute.COLORS[0]; 9 | private static final Color BLUE = ColorAttribute.COLORS[1]; 10 | private static final Color RED = ColorAttribute.COLORS[2]; 11 | private static final Color GREEN = ColorAttribute.COLORS[4]; 12 | private static final Color BLACK = ColorAttribute.COLORS[8]; 13 | 14 | private final boolean isProtected; // bit 2 15 | private final boolean isNumeric; // bit 3 16 | private final boolean isModified; // bit 7 17 | 18 | private boolean isExtended; // created by StartFieldExtendedOrder 19 | private boolean userModified; // used to avoid altering the original bit 7 20 | 21 | // these three fields are stored in two bits (4&5) 22 | private final boolean isHidden; 23 | private final boolean isHighIntensity; 24 | private final boolean selectorPenDetectable; 25 | 26 | public StartFieldAttribute(byte b) { 27 | super(AttributeType.START_FIELD, Attribute.XA_START_FIELD, b); 28 | 29 | isProtected = (b & 0x20) > 0; 30 | isNumeric = (b & 0x10) > 0; 31 | isModified = (b & 0x01) > 0; 32 | 33 | int display = (b & 0x0C) >> 2; 34 | selectorPenDetectable = display == 1 || display == 2; 35 | isHidden = display == 3; 36 | isHighIntensity = display == 2; 37 | } 38 | 39 | public void setExtended() { 40 | isExtended = true; 41 | } 42 | 43 | public boolean isExtended() { 44 | return isExtended; 45 | } 46 | 47 | public boolean isProtected() { 48 | return isProtected; 49 | } 50 | 51 | public boolean isHidden() { 52 | return isHidden; 53 | } 54 | 55 | public boolean isVisible() { 56 | return !isHidden; 57 | } 58 | 59 | public boolean isModified() { 60 | return isModified || userModified; 61 | } 62 | 63 | public void setModified(boolean modified) { 64 | userModified = modified; 65 | } 66 | 67 | /* 68 | * http://www-01.ibm.com/support/knowledgecenter/SSGMGV_3.1.0/com.ibm.cics. 69 | * ts31.doc/dfhp3/dfhp3at.htm%23dfhp3at 70 | * 71 | * Some terminals support base color without, or in addition to, the extended 72 | * colors included in the extended attributes. There is a mode switch on the 73 | * front of such a terminal, allowing the operator to select base or default 74 | * color. Default color shows characters in green unless field attributes specify 75 | * bright intensity, in which case they are white. In base color mode, the 76 | * protection and intensity bits are used in combination to select among four 77 | * colors: normally white, red, blue, and green; the protection bits retain their 78 | * protection functions as well as determining color. (If you use extended color, 79 | * rather than base color, for 3270 terminals, note that you cannot specify 80 | * "white" as a color. You need to specify "neutral", which is displayed as white 81 | * on a terminal.) 82 | */ 83 | @Override 84 | public ScreenContext process(ScreenContext unused1, ScreenContext unused2) { 85 | assert unused1 == null && unused2 == null; 86 | 87 | Color color = isHighIntensity ? isProtected ? WHITE : RED : isProtected ? BLUE : GREEN; 88 | 89 | return new ScreenContext(color, BLACK, (byte) 0, isHighIntensity, false); 90 | } 91 | 92 | private String getColorName() { 93 | return isHighIntensity ? isProtected ? "WH" : "RE" : isProtected ? "BL" : "GR"; 94 | } 95 | 96 | public String getAcronym() { 97 | return (isProtected ? "P" : "p") 98 | + (isNumeric ? "a" : "A") 99 | + (isHidden ? "v" : "V") 100 | + (isHighIntensity ? "I" : "i") 101 | + (selectorPenDetectable ? "D" : "d") 102 | + (isModified() ? "M" : "m"); 103 | } 104 | 105 | @Override 106 | public String toString() { 107 | return String.format("Attribute %s : %02X %s", getColorName(), attributeValue, 108 | getAcronym()); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/attributes/Attribute.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.attributes; 2 | 3 | import com.bytezone.dm3270.display.ScreenContext; 4 | import java.util.Optional; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public abstract class Attribute { 9 | 10 | public static final byte XA_RESET = 0x00; 11 | public static final byte XA_HIGHLIGHTING = 0x41; 12 | public static final byte XA_FGCOLOR = 0x42; 13 | public static final byte XA_CHARSET = 0x43; 14 | public static final byte XA_BGCOLOR = 0x45; 15 | public static final byte XA_TRANSPARENCY = 0x46; 16 | public static final byte XA_START_FIELD = (byte) 0xC0; 17 | 18 | private static final Logger LOG = LoggerFactory.getLogger(Attribute.class); 19 | 20 | private static final byte XA_VALIDATION = (byte) 0xC1; 21 | private static final byte XA_OUTLINING = (byte) 0xC2; 22 | 23 | protected final byte attributeValue; 24 | 25 | private final AttributeType attributeType; 26 | private final byte attributeCode; 27 | 28 | public enum AttributeType { 29 | START_FIELD, HIGHLIGHT, FOREGROUND_COLOR, BACKGROUND_COLOR, RESET, CHARSET 30 | } 31 | 32 | public Attribute(AttributeType attributeType, byte attributeCode, byte attributeValue) { 33 | this.attributeType = attributeType; 34 | this.attributeCode = attributeCode; 35 | this.attributeValue = attributeValue; 36 | } 37 | 38 | public boolean matches(byte... types) { 39 | for (byte type : types) { 40 | if (attributeCode == type) { 41 | return true; 42 | } 43 | } 44 | return false; 45 | } 46 | 47 | public int pack(byte[] buffer, int offset) { 48 | buffer[offset++] = attributeCode; 49 | buffer[offset++] = attributeValue; 50 | return offset; 51 | } 52 | 53 | public abstract ScreenContext process(ScreenContext defaultContext, ScreenContext currentContext); 54 | 55 | public byte getAttributeValue() { 56 | return attributeValue; 57 | } 58 | 59 | public AttributeType getAttributeType() { 60 | return attributeType; 61 | } 62 | 63 | public static Optional getAttribute(byte attributeCode, byte attributeValue) { 64 | switch (attributeCode) { 65 | case 0: 66 | return Optional.of(new ResetAttribute(attributeValue)); 67 | case XA_START_FIELD: 68 | return Optional.of(new StartFieldAttribute(attributeValue)); 69 | case XA_HIGHLIGHTING: 70 | return Optional.of(new ExtendedHighlight(attributeValue)); 71 | case XA_BGCOLOR: 72 | return Optional.of(new BackgroundColor(attributeValue)); 73 | case XA_FGCOLOR: 74 | return Optional.of(new ForegroundColor(attributeValue)); 75 | case XA_CHARSET: 76 | return Optional.of(new Charset(attributeValue)); 77 | case XA_VALIDATION: 78 | LOG.warn("Validation not written"); 79 | return Optional.empty(); 80 | case XA_OUTLINING: 81 | LOG.warn("Outlining not written"); 82 | return Optional.empty(); 83 | case XA_TRANSPARENCY: 84 | LOG.warn("Transparency not written"); 85 | return Optional.empty(); 86 | default: 87 | LOG.warn("Unknown attribute: {}", String.format("%02X", attributeCode)); 88 | return Optional.empty(); 89 | } 90 | } 91 | 92 | protected String name() { 93 | return getTypeName(attributeCode); 94 | } 95 | 96 | public static String getTypeName(byte type) { 97 | switch (type) { 98 | case XA_RESET: 99 | return "Reset"; 100 | case XA_HIGHLIGHTING: 101 | return "Highlight"; 102 | case XA_FGCOLOR: 103 | return "Foreground"; 104 | case XA_BGCOLOR: 105 | return "Background"; 106 | case XA_TRANSPARENCY: 107 | return "Transparency"; 108 | case XA_START_FIELD: 109 | return "Start Field"; 110 | case XA_VALIDATION: 111 | return "Validation"; 112 | case XA_OUTLINING: 113 | return "Outlining"; 114 | case XA_CHARSET: 115 | return "Charset"; 116 | default: 117 | return "Unknown"; 118 | } 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | return String.format("%-12s : %02X", name(), attributeValue); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/telnet/TelnetProcessor.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.telnet; 2 | 3 | public class TelnetProcessor { 4 | 5 | // single-byte commands 6 | public static final byte EOR = (byte) 0xEF; // End of record 7 | 8 | // command prefix 9 | private static final byte IAC = (byte) 0xFF; 10 | 11 | // single-byte commands 12 | private static final byte SE = (byte) 0xF0; // End of subcommmand 13 | private static final byte NOP = (byte) 0xF1; // No Operation 14 | private static final byte IP = (byte) 0xF4; // Interrupt process 15 | 16 | // double-byte commands 17 | private static final byte SB = (byte) 0xFA; // Begin subcommand 18 | private static final byte WILL = (byte) 0xFB; 19 | private static final byte WONT = (byte) 0xFC; 20 | private static final byte DO = (byte) 0xFD; 21 | private static final byte DONT = (byte) 0xFE; 22 | 23 | // state variables 24 | private final byte[] data = new byte[16500]; // see also SessionReader 25 | private int dataPtr; 26 | private boolean pending; // last byte was IAC, must check next byte 27 | private boolean weirdData; // when stream starts with two IACs 28 | private byte command; // one of DO, DONT, WILL, WONT 29 | 30 | // command processor 31 | private final TelnetCommandProcessor commandProcessor; 32 | 33 | public TelnetProcessor(TelnetCommandProcessor commandProcessor) { 34 | this.commandProcessor = commandProcessor; 35 | } 36 | 37 | public void listen(byte... buffer) { 38 | for (byte thisByte : buffer) { 39 | data[dataPtr++] = thisByte; // store every byte we receive 40 | 41 | if (thisByte == IAC) { 42 | // previous byte might have been an IAC 43 | if (pending) { 44 | pending = false; // treat it as a data 0xFF 45 | --dataPtr; // remove the second one 46 | // if there is just that data 0xFF in the 47 | if (dataPtr == 1) { 48 | weirdData = true; // buffer, then flag it 49 | } 50 | } else { 51 | pending = true; // this byte might be an IAC 52 | } 53 | continue; 54 | } 55 | 56 | // previous byte really was an IAC 57 | if (pending) { 58 | pending = false; 59 | 60 | // first check for a valid 3270 data record 61 | if (thisByte == EOR) { 62 | commandProcessor.processRecord(data, dataPtr); 63 | reset(); 64 | continue; 65 | } 66 | 67 | // next remove any non-telnet data 68 | // some non-telnet data is in the buffer 69 | if (data[0] != IAC || weirdData) { 70 | dataPtr -= 2; // hide IAC and this byte 71 | commandProcessor.processData(data, dataPtr); 72 | reset(); 73 | 74 | data[dataPtr++] = IAC; // drop through and process the new byte 75 | data[dataPtr++] = thisByte; 76 | } 77 | 78 | // leave IAC SB in buffer 79 | if (thisByte == SB) { 80 | continue; 81 | } 82 | 83 | if (thisByte == SE) { 84 | commandProcessor.processTelnetSubcommand(data, dataPtr); 85 | reset(); 86 | continue; 87 | } 88 | 89 | // known three-byte commands 90 | if (thisByte == DO || thisByte == DONT || thisByte == WILL | thisByte == WONT) { 91 | command = thisByte; // save it and wait for the third byte 92 | continue; 93 | } 94 | 95 | // known two-byte commands 96 | if (thisByte == NOP || thisByte == IP) { 97 | commandProcessor.processTelnetCommand(data, dataPtr); 98 | reset(); 99 | continue; 100 | } 101 | 102 | System.err.printf("Unknown command: %02X%n", thisByte); // handle error somehow 103 | // the third byte has arrived (in thisByte) 104 | } else if (command != 0) { 105 | commandProcessor.processTelnetCommand(data, dataPtr); 106 | reset(); 107 | } 108 | } 109 | } 110 | 111 | private void reset() { 112 | dataPtr = 0; 113 | command = 0; 114 | weirdData = false; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/test/resources/sscplu-login.yml: -------------------------------------------------------------------------------- 1 | # Do TN3270E 2 | - !server {data: FFFD28} 3 | # Will TN3270E 4 | - !client {data: FFFB28} 5 | # Send DEVICE-TYPE 6 | - !server {data: FFFA280802FFF0} 7 | # DEVICE-TYPE REQUEST IBM-3278-2 8 | - !client {data: FFFA28020749424D2D333237382D32FFF0} 9 | # Connect XXXXXXXX 10 | - !server {data: FFFA28020449424D2D333237382D322D4501E7E7E7E7E7E7E7E7FFF0} 11 | # Functions Request Unknown + associate DEVICE-TYPE is reason 12 | - !client {data: FFFA280307000204FFF0} 13 | # Functions associate device-type is reason 14 | - !server {data: FFFA28030400020405FFF0} 15 | # SSCP_LU_DATA: welcome screen 16 | - !server {data: 0700000000154040404040407C7C7C7C7C7C7C7C4040407C7C7C7C40404040407C7C7C7C4040407C7C7C7C7C7C40404040407C7C7C7C7C7C4040407C7C7C7C7C7C7C7C7C7C4040407C7C7C7C7C7C154040404040407C7C7C40407C7C7C7C7C407C7C7C7C7C4040407C7C7C7C7C407C7C7C7C40407C7C7C7C407C7C7C7C40407C7C7C7C404040404040407C7C7C7C407C7C7C7C40407C7C7C7C154040404040407C7C7C404040407C7C7C407C7C7C7C7C7C407C7C7C7C7C7C404040404040407C7C7C7C40404040404040407C7C7C4040404040407C7C7C7C40407C7C7C404040407C7C7C154040404040407C7C7C404040407C7C7C407C7C7C7C7C7C7C7C7C7C7C7C7C40404040407C7C7C7C7C40404040404040407C7C7C7C40404040407C7C7C7C4040407C7C7C404040407C7C7C154040404040407C7C7C404040407C7C7C407C7C7C407C7C7C7C7C407C7C7C4040404040407C7C7C7C404040407C7C7C7C7C7C7C4040407C7C7C7C7C7C7C7C40407C7C7C404040407C7C7C154040404040407C7C7C404040407C7C7C407C7C7C40407C7C7C40407C7C7C407C7C7C404040407C7C7C407C7C7C7C7C40404040404040407C7C7C7C40404040407C7C7C404040407C7C7C154040404040407C7C7C4040407C7C7C7C407C7C7C4040407C4040407C7C7C407C7C7C7C40407C7C7C7C407C7C7C7C40404040404040407C7C7C7C4040404040407C7C7C7C40407C7C7C7C154040404040407C7C7C7C7C7C7C7C7C40407C7C7C404040404040407C7C7C40407C7C7C7C7C7C7C7C40407C7C7C7C7C7C7C7C7C40407C7C7C7C40404040404040407C7C7C7C7C7C7C7C1515C5D5E3C5D940C1D7D7D3C9C3C1E3C9D6D540D5C1D4C57A40FFEF} 17 | # SSCP_LU_DATA: testapp 18 | - !client {data: 0700000000A385A2A3819797FFEF} 19 | # Bind Image 20 | - !server {data: 030000000031010303B1903080008787F88700028000000000185000007E000007A385A2A3814A4A0005E7E7E7E7E708E7E7E7E7E7E7E7E7FFEF} 21 | # Unknown 22 | - !server {data: 0900020001FFEF} 23 | # Response 24 | - !client {data: 020000000100FFEF} 25 | # App screen 26 | - !server {data: 0000010002F5C31140401D6DE3C5E2E37EE3C5E2E3C9D5C7114EC41311C1D61D60E2E8E2E3C5D47A1DE8E3C5E2E3C1D7D74040E6C5D3C3D6D4C540E3D6404040C4D4F3F2F7F040E3C5E2E3C9D5C740C1D7D7D3C9C3C1E3C9D6D54040404040404040404040404040404011C2F71DE8E3E2E340E2C5D9E5C5D940F14BF04BF0404B4040404040404040404040404040404040404040404040404040404040404040404040404011C3F41D60E3C5D9D4C9D5C1D37A1DE8F9F9F9F911C5C81D60D5D6C4C57A1DE8E7E7E7E7E7E7E7E711C7E91D60C4C1E87A1DE8E3E4C5E2C4C1E84040114AC11D60E2E8E2E3C5D440C4C1E3C57A1DE8D1C1D5E4C1D9E840F0F86B40F2F0F1F90000114BD11D60E2E8E2E3C5D440E3C9D4C57A1DE8F0F17AF3F640D7D4114DF51DE8D3D6C7D6D5C9C47A407E7E7E6E1DC140404040404040401D60114FC41DE8D7C1E2E2E6D6D9C47A407E7E7E6E1D4D40404040404040401D6011D1601DE8D5C5E640D7C1E2E2E6D6D9C47A407E7E7E6E1D4D40404040404040401D6011D2F01DE84DC5D5E3C5D940E3E6C9C3C55D407E7E7E6E1D4D40404040404040401D6011D76F1DE8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011D87F1DE80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115A4F1DE80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115B5F1D60404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040FFEF} 27 | - !server {data: 0003010003FFEF} 28 | # testusr + testpsw 29 | - !client {data: 00000000017D4F5B1140C1E3C5E2E37EE3C5E2E3C9D5C7114EC4A385A2A3A4A299114FD4A385A2A397A2A611D1F4404040404040404011D3C44040404040404040FFEF} 30 | # login success screen 31 | - !server {data: 0000010004F5C31140401DC81311C36F1DF8E3C5E2E3F1F1F1F140E3C5E2E3F0F0F140D3C1E2E340E2E8E2E3C5D440C1C3C3C5E2E240F1F14BF1F360F0F161F0F861F1F940C6D9D6D440E7E7E7E7E7E7E7E70000000000000000000000000000000011C540E3C5E2E3F1F1F1F140C3C9C3E240E7E7E77A40F1F1F1F14040404040E2C9C7D5D6D540D6D27A40E4E2C5D97EE3C5E2E3E4E2D94040D5C1D4C57EE3C5E2E340E4E2C5D940000000000000000000000011C6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFEF} 32 | - !server {data: 0003010005FFEF} 33 | - !client {data: 00000000037C40C41140C1E7E7E7FFEF} 34 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/commands/WriteCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.commands; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.display.Cursor; 5 | import com.bytezone.dm3270.display.Screen; 6 | import com.bytezone.dm3270.display.Screen.ScreenOption; 7 | import com.bytezone.dm3270.orders.Order; 8 | import com.bytezone.dm3270.orders.TextOrder; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class WriteCommand extends Command { 13 | 14 | private final boolean eraseWrite; 15 | private final boolean alternate; 16 | private final WriteControlCharacter writeControlCharacter; 17 | private final List orders = new ArrayList<>(); 18 | 19 | public WriteCommand(byte[] buffer, int offset, int length, Charset charset) { 20 | super(buffer, offset, length); 21 | 22 | assert buffer[offset] == Command.WRITE_01 || buffer[offset] == Command.WRITE_F1 23 | || buffer[offset] == Command.ERASE_WRITE_05 24 | || buffer[offset] == Command.ERASE_WRITE_F5 25 | || buffer[offset] == Command.ERASE_WRITE_ALTERNATE_0D 26 | || buffer[offset] == Command.ERASE_WRITE_ALTERNATE_7E; 27 | 28 | eraseWrite = buffer[offset] != Command.WRITE_F1 && buffer[offset] != Command.WRITE_01; 29 | alternate = buffer[offset] == Command.ERASE_WRITE_ALTERNATE_0D 30 | || buffer[offset] == Command.ERASE_WRITE_ALTERNATE_7E; 31 | writeControlCharacter = 32 | length > 1 ? new WriteControlCharacter(buffer[offset + 1]) : null; 33 | 34 | int ptr = offset + 2; 35 | Order previousOrder = null; 36 | 37 | int max = offset + length; 38 | while (ptr < max) { 39 | Order order = Order.getOrder(buffer, ptr, max, charset); 40 | 41 | if (order.matchesPreviousOrder(previousOrder)) { 42 | previousOrder.incrementDuplicates(); // and discard this Order 43 | } else { 44 | orders.add(order); 45 | previousOrder = order; 46 | } 47 | 48 | ptr += order.size(); 49 | } 50 | } 51 | 52 | @Override 53 | public void process(Screen screen) { 54 | Cursor cursor = screen.getScreenCursor(); 55 | int cursorLocation = cursor.getLocation(); 56 | boolean screenDrawRequired = false; 57 | 58 | if (eraseWrite) { 59 | ScreenOption requestedScreenOption = alternate ? Screen.ScreenOption.ALTERNATE 60 | : Screen.ScreenOption.DEFAULT; 61 | screen.clearScreen(requestedScreenOption); // resets pen 62 | screen.setCurrentScreen( 63 | alternate ? Screen.ScreenOption.ALTERNATE : Screen.ScreenOption.DEFAULT); 64 | screen.lockKeyboard("Erase Write"); 65 | } else { 66 | screen.lockKeyboard("Write"); 67 | } 68 | 69 | if (orders.size() > 0) { 70 | for (Order order : orders) { 71 | order.process(screen); // modifies pen 72 | } 73 | 74 | cursor.moveTo(cursorLocation); 75 | screen.buildFields(); 76 | screenDrawRequired = true; 77 | } 78 | 79 | if (writeControlCharacter != null) { 80 | writeControlCharacter.process(screen); // may unlock the keyboard 81 | if (screen.getFieldManager().size() > 0 && !screen.isKeyboardLocked()) { 82 | screen.checkRecording(); // make a copy of the screen 83 | } 84 | } 85 | 86 | // should check for suppressDisplay 87 | if (!screen.isKeyboardLocked() && screen.getFieldManager().size() > 0) { 88 | if (orders.size() > 0 || !writeControlCharacter.isResetModified()) { 89 | setReply(null); 90 | } 91 | } 92 | 93 | if (screenDrawRequired) { 94 | screen.draw(); 95 | } 96 | 97 | } 98 | 99 | @Override 100 | public String getName() { 101 | return eraseWrite ? alternate ? "Erase Write Alternate" : "Erase Write" : "Write"; 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | StringBuilder text = new StringBuilder(); 107 | text.append(getName()); 108 | text.append("\nWCC : ").append(writeControlCharacter); 109 | 110 | // if the list begins with a TextOrder then tab out the missing columns 111 | if (orders.size() > 0 && orders.get(0) instanceof TextOrder) { 112 | text.append(String.format("%40s", "")); 113 | } 114 | 115 | for (Order order : orders) { 116 | String fmt = (order.isText()) ? "%s" : "%n%-40s"; 117 | text.append(String.format(fmt, order)); 118 | } 119 | 120 | return text.toString(); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/application/ConsolePane.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.application; 2 | 3 | import com.bytezone.dm3270.ConnectionListener; 4 | import com.bytezone.dm3270.buffers.Buffer; 5 | import com.bytezone.dm3270.commands.Command; 6 | import com.bytezone.dm3270.display.CursorMoveListener; 7 | import com.bytezone.dm3270.display.Field; 8 | import com.bytezone.dm3270.display.FieldChangeListener; 9 | import com.bytezone.dm3270.display.Screen; 10 | import com.bytezone.dm3270.extended.CommandHeader; 11 | import com.bytezone.dm3270.extended.TN3270ExtendedCommand; 12 | import com.bytezone.dm3270.streams.TelnetListener; 13 | import com.bytezone.dm3270.streams.TelnetState; 14 | import com.bytezone.dm3270.streams.TerminalServer; 15 | import javax.net.SocketFactory; 16 | 17 | public class ConsolePane implements FieldChangeListener, CursorMoveListener, 18 | KeyboardStatusListener { 19 | 20 | private final Screen screen; 21 | 22 | private final TelnetState telnetState; 23 | private final Site server; 24 | private final SocketFactory socketFactory; 25 | 26 | private TerminalServer terminalServer; 27 | private Thread terminalServerThread; 28 | private int connectionTimeoutMillis; 29 | private ConnectionListener connectionListener; 30 | 31 | public ConsolePane(Screen screen, Site server, SocketFactory socketFactory) { 32 | this.screen = screen; 33 | this.telnetState = screen.getTelnetState(); 34 | this.server = server; 35 | this.socketFactory = socketFactory; 36 | 37 | screen.setConsolePane(this); 38 | screen.getScreenCursor().addFieldChangeListener(this); 39 | screen.getScreenCursor().addCursorMoveListener(this); 40 | } 41 | 42 | public void setConnectionTimeoutMillis(int connectionTimeoutMillis) { 43 | this.connectionTimeoutMillis = connectionTimeoutMillis; 44 | } 45 | 46 | public void setConnectionListener( 47 | ConnectionListener connectionListener) { 48 | this.connectionListener = connectionListener; 49 | } 50 | 51 | public void sendAID(byte aid, String name) { 52 | if (screen.isInsertMode()) { 53 | screen.toggleInsertMode(); 54 | } 55 | 56 | screen.lockKeyboard(name); 57 | screen.setAID(aid); 58 | 59 | Command command = screen.readModifiedFields(); 60 | sendAID(command); 61 | } 62 | 63 | private void sendAID(Command command) { 64 | assert telnetState != null; 65 | 66 | if (telnetState.does3270Extended()) { 67 | byte[] buffer = new byte[5]; 68 | if (screen.isSscpLuData()) { 69 | buffer[0] = 0x07; 70 | } 71 | Buffer.packUnsignedShort(telnetState.nextCommandHeaderSeq(), buffer, 3); 72 | CommandHeader header = new CommandHeader(buffer, screen.getCharset()); 73 | TN3270ExtendedCommand extendedCommand = new TN3270ExtendedCommand(header, command, 74 | telnetState, screen.getCharset()); 75 | telnetState.write(extendedCommand.getTelnetData()); 76 | } else { 77 | telnetState.write(command.getTelnetData()); 78 | } 79 | } 80 | 81 | public void connect() { 82 | if (server == null) { 83 | throw new IllegalArgumentException("Server must not be null"); 84 | } 85 | 86 | // set preferences for this session 87 | telnetState.setDo3270Extended(server.getExtended()); 88 | telnetState.setDoTerminalType(true); 89 | 90 | TelnetListener telnetListener = new TelnetListener(screen, telnetState); 91 | terminalServer = 92 | new TerminalServer(server.getURL(), server.getPort(), socketFactory, telnetListener); 93 | terminalServer.setConnectionTimeoutMillis(connectionTimeoutMillis); 94 | terminalServer.setConnectionListener(connectionListener); 95 | telnetState.setTerminalServer(terminalServer); 96 | 97 | terminalServerThread = new Thread(terminalServer); 98 | terminalServerThread.start(); 99 | } 100 | 101 | public void disconnect() throws InterruptedException { 102 | telnetState.close(); 103 | 104 | if (terminalServer != null) { 105 | terminalServer.close(); 106 | } 107 | 108 | if (terminalServerThread != null) { 109 | terminalServerThread.interrupt(); 110 | terminalServerThread.join(); 111 | } 112 | } 113 | 114 | @Override 115 | public void fieldChanged(Field oldField, Field newField) { 116 | } 117 | 118 | @Override 119 | public void cursorMoved(int oldLocation, int newLocation, Field currentField) { 120 | fieldChanged(currentField, currentField); // update the acronym 121 | } 122 | 123 | @Override 124 | public void keyboardStatusChanged(KeyboardStatusChangedEvent evt) { 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/display/ScreenPosition.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.display; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.attributes.Attribute; 5 | import com.bytezone.dm3270.attributes.StartFieldAttribute; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public final class ScreenPosition { 10 | 11 | private final int position; 12 | 13 | private StartFieldAttribute startFieldAttribute; 14 | private final List attributes = new ArrayList<>(); 15 | 16 | private byte value; 17 | private ScreenContext screenContext; 18 | private final Charset charset; 19 | 20 | public ScreenPosition(int position, ScreenContext screenContext, 21 | Charset charset) { 22 | this.position = position; 23 | this.screenContext = screenContext; 24 | this.charset = charset; 25 | reset(); 26 | } 27 | 28 | public void reset() { 29 | value = 0; 30 | screenContext = screenContext.withGraphic(false); 31 | startFieldAttribute = null; 32 | attributes.clear(); 33 | } 34 | 35 | public void setChar(byte value) { 36 | this.value = value; 37 | screenContext = screenContext.withGraphic(false); 38 | } 39 | 40 | public void setAplGraphicChar(byte value) { 41 | this.value = value; 42 | screenContext = screenContext.withGraphic(true); 43 | } 44 | 45 | public StartFieldAttribute getStartFieldAttribute() { 46 | return startFieldAttribute; 47 | } 48 | 49 | public void setStartField(StartFieldAttribute startFieldAttribute) { 50 | if (startFieldAttribute == null) { 51 | if (this.startFieldAttribute != null) { 52 | attributes.clear(); 53 | } 54 | } 55 | this.startFieldAttribute = startFieldAttribute; 56 | } 57 | 58 | public void addAttribute(Attribute attribute) { 59 | attributes.add(attribute); 60 | } 61 | 62 | public List getAttributes() { 63 | return attributes; 64 | } 65 | 66 | public int getPosition() { 67 | return position; 68 | } 69 | 70 | // All the colour and highlight options 71 | public void setScreenContext(ScreenContext screenContext) { 72 | if (screenContext == null) { 73 | throw new IllegalArgumentException("ScreenContext cannot be null"); 74 | } 75 | this.screenContext = screenContext; 76 | } 77 | 78 | public ScreenContext getScreenContext() { 79 | return screenContext; 80 | } 81 | 82 | public boolean isStartField() { 83 | return startFieldAttribute != null; 84 | } 85 | 86 | public boolean isGraphic() { 87 | return screenContext.isGraphic(); 88 | } 89 | 90 | public char getChar() { 91 | if (value == 0) { 92 | return '\u0000'; 93 | } 94 | if ((value & 0xC0) == 0) { 95 | return ' '; 96 | } 97 | 98 | if (screenContext.isGraphic()) { 99 | return convertGraphicChar(value); 100 | } 101 | 102 | return charset.getChar(value); 103 | } 104 | 105 | private static char convertGraphicChar(byte val) { 106 | switch (val) { 107 | case (byte) 0x85: 108 | return '│'; 109 | case (byte) 0xA2: 110 | return '─'; 111 | case (byte) 0xC4: 112 | return '└'; 113 | case (byte) 0xC5: 114 | return '┌'; 115 | case (byte) 0xC6: 116 | return '├'; 117 | case (byte) 0xC7: 118 | return '┴'; 119 | case (byte) 0xD3: 120 | return '┼'; 121 | case (byte) 0xD4: 122 | return '┘'; 123 | case (byte) 0xD5: 124 | return '┐'; 125 | case (byte) 0xD6: 126 | return '┤'; 127 | case (byte) 0xD7: 128 | return '┬'; 129 | default: 130 | return ' '; 131 | } 132 | } 133 | 134 | public String getCharString() { 135 | if (isStartField()) { 136 | return " "; 137 | } 138 | 139 | if (screenContext.isGraphic()) { 140 | return String.valueOf(convertGraphicChar(value)); 141 | } 142 | 143 | char ret = charset.getChar(value); 144 | return ret < ' ' ? " " : String.valueOf(ret); 145 | } 146 | 147 | public byte getByte() { 148 | return value; 149 | } 150 | 151 | public boolean isNull() { 152 | return value == 0; 153 | } 154 | 155 | @Override 156 | public String toString() { 157 | StringBuilder text = new StringBuilder(); 158 | if (isStartField()) { 159 | text.append("..").append(startFieldAttribute); 160 | } else { 161 | for (Attribute attribute : attributes) { 162 | text.append("--").append(attribute); 163 | } 164 | } 165 | 166 | text.append(", byte: ").append(getCharString()); 167 | 168 | return text.toString(); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/commands/WriteStructuredFieldCommand.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.commands; 2 | 3 | import com.bytezone.dm3270.Charset; 4 | import com.bytezone.dm3270.buffers.Buffer; 5 | import com.bytezone.dm3270.buffers.MultiBuffer; 6 | import com.bytezone.dm3270.display.Screen; 7 | import com.bytezone.dm3270.structuredfields.DefaultStructuredField; 8 | import com.bytezone.dm3270.structuredfields.EraseResetSF; 9 | import com.bytezone.dm3270.structuredfields.Outbound3270DS; 10 | import com.bytezone.dm3270.structuredfields.ReadPartitionSF; 11 | import com.bytezone.dm3270.structuredfields.SetReplyModeSF; 12 | import com.bytezone.dm3270.structuredfields.StructuredField; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | public class WriteStructuredFieldCommand extends Command { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(WriteStructuredFieldCommand.class); 22 | 23 | private static final String LINE = 24 | "\n-------------------------------------------------------------------------"; 25 | 26 | private final List structuredFields = 27 | new ArrayList<>(); 28 | private final List replies = new ArrayList<>(); 29 | private final Charset charset; 30 | 31 | public WriteStructuredFieldCommand(byte[] buffer, int offset, int length, Charset charset) { 32 | super(buffer, offset, length); 33 | this.charset = charset; 34 | 35 | assert buffer[offset] == Command.WRITE_STRUCTURED_FIELD_11 36 | || buffer[offset] == Command.WRITE_STRUCTURED_FIELD_F3; 37 | 38 | int ptr = offset + 1; 39 | int max = offset + length; 40 | 41 | while (ptr < max) { 42 | int size = Buffer.unsignedShort(buffer, ptr) - 2; 43 | ptr += 2; 44 | 45 | switch (buffer[ptr]) { 46 | // wrapper for original write commands - W. EW, EWA, EAU 47 | case StructuredField.OUTBOUND_3270DS: 48 | structuredFields.add(new Outbound3270DS(buffer, ptr, size, charset)); 49 | break; 50 | 51 | // wrapper for original read commands - RB, RM, RMA 52 | case StructuredField.READ_PARTITION: 53 | structuredFields.add(new ReadPartitionSF(buffer, ptr, size, charset)); 54 | break; 55 | 56 | case StructuredField.RESET_PARTITION: 57 | LOG.warn("SF_RESET_PARTITION (00) not written yet"); 58 | structuredFields.add(new DefaultStructuredField(buffer, ptr, size, charset)); 59 | break; 60 | 61 | case StructuredField.SET_REPLY_MODE: 62 | structuredFields.add(new SetReplyModeSF(buffer, ptr, size, charset)); 63 | break; 64 | 65 | case StructuredField.ACTIVATE_PARTITION: 66 | LOG.warn("SF_ACTIVATE_PARTITION (0E) not written yet"); 67 | structuredFields.add(new DefaultStructuredField(buffer, ptr, size, charset)); 68 | break; 69 | 70 | case StructuredField.ERASE_RESET: 71 | structuredFields.add(new EraseResetSF(buffer, ptr, size, charset)); 72 | break; 73 | 74 | default: 75 | structuredFields.add(new DefaultStructuredField(buffer, ptr, size, charset)); 76 | break; 77 | } 78 | 79 | ptr += size; 80 | } 81 | } 82 | 83 | @Override 84 | public void process(Screen screen) { 85 | replies.clear(); 86 | 87 | for (StructuredField structuredField : structuredFields) { 88 | structuredField.process(screen); 89 | Optional reply = structuredField.getReply(); 90 | reply.ifPresent(replies::add); 91 | } 92 | } 93 | 94 | @Override 95 | public Optional getReply() { 96 | if (replies.size() == 0) { 97 | return Optional.empty(); 98 | } 99 | 100 | if (replies.size() == 1) { 101 | return Optional.of(replies.get(0)); 102 | } 103 | 104 | MultiBuffer multiBuffer = new MultiBuffer(charset); 105 | for (Buffer reply : replies) { 106 | multiBuffer.addBuffer(reply); 107 | } 108 | 109 | return Optional.of(multiBuffer); 110 | } 111 | 112 | @Override 113 | public String getName() { 114 | return "Write SF"; 115 | } 116 | 117 | @Override 118 | public String toString() { 119 | StringBuilder text = 120 | new StringBuilder(String.format("WSF (%d):", structuredFields.size())); 121 | 122 | for (StructuredField sf : structuredFields) { 123 | text.append(LINE); 124 | text.append("\n"); 125 | text.append(sf); 126 | } 127 | 128 | if (structuredFields.size() > 0) { 129 | text.append(LINE); 130 | } 131 | 132 | return text.toString(); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/bytezone/dm3270/replyfield/CharacterSets.java: -------------------------------------------------------------------------------- 1 | package com.bytezone.dm3270.replyfield; 2 | 3 | import com.bytezone.dm3270.buffers.Buffer; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class CharacterSets extends QueryReplyField { 8 | 9 | private static final int GRAPHIC_ESCAPE_SUPPORT_FLAG = 0x80; 10 | private static final int CGCSGID_PRESENT_FLAG = 0x02; 11 | private static final int DEFAULT_CHARACTER_SLOT_WIDTH = 7; 12 | private static final int DEFAULT_CHARACTER_SLOT_HEIGHT = 12; 13 | private static final int[] FORM_TYPES = {0x00, 0x00, 0x00, 0x00}; 14 | private static final int DESCRIPTOR_LENGTH = 7; 15 | private static final byte[][] CHARSETS = new byte[][]{ 16 | {0x00, 0x00, 0x00, 0x02, (byte) 0xB9, 0x04, 0x17}, 17 | {0x01, 0x00, (byte) 0xF1, 0x03, (byte) 0xC3, 0x01, 0x36} 18 | }; 19 | 20 | private byte flags1; 21 | private byte flags2; 22 | private int defaultSlotWidth; 23 | private int defaultSlotHeight; 24 | private int loadTypes; 25 | private int descriptorLength; 26 | private final List descriptors = new ArrayList<>(); 27 | 28 | public CharacterSets() { 29 | super(CHARACTER_SETS_REPLY); 30 | 31 | int ptr = createReply(4 + FORM_TYPES.length + 1 + CHARSETS.length * DESCRIPTOR_LENGTH); 32 | reply[ptr++] = (byte) (GRAPHIC_ESCAPE_SUPPORT_FLAG | CGCSGID_PRESENT_FLAG & 0xff); 33 | reply[ptr++] = 0x00; 34 | reply[ptr++] = DEFAULT_CHARACTER_SLOT_WIDTH; 35 | reply[ptr++] = DEFAULT_CHARACTER_SLOT_HEIGHT; 36 | for (int formType : FORM_TYPES) { 37 | reply[ptr++] = (byte) formType; 38 | } 39 | reply[ptr++] = DESCRIPTOR_LENGTH; 40 | for (byte[] charset : CHARSETS) { 41 | System.arraycopy(charset, 0, reply, ptr, DESCRIPTOR_LENGTH); 42 | ptr += DESCRIPTOR_LENGTH; 43 | } 44 | 45 | checkDataLength(ptr); 46 | } 47 | 48 | public CharacterSets(byte[] buffer) { 49 | super(buffer); 50 | 51 | assert data[1] == CHARACTER_SETS_REPLY; 52 | 53 | flags1 = data[2]; 54 | flags2 = data[3]; 55 | defaultSlotWidth = data[4] & 0xFF; 56 | defaultSlotHeight = data[5] & 0xFF; 57 | 58 | for (int i = 0; i < 4; i++) { 59 | loadTypes = ((loadTypes << 8) | (data[i + 6] & 0xFF)); 60 | } 61 | 62 | descriptorLength = data[10] & 0xFF; 63 | 64 | for (int ptr = 11; ptr < data.length; ptr += descriptorLength) { 65 | descriptors.add(new Descriptor(data, ptr, descriptorLength)); 66 | } 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | StringBuilder text = new StringBuilder(super.toString()); 72 | 73 | text.append(String.format("%n flags1 : %02X", flags1)); 74 | text.append(String.format("%n flags2 : %02X", flags2)); 75 | text.append(String.format("%n SDW : %d", defaultSlotWidth)); 76 | text.append(String.format("%n SDH : %d", defaultSlotHeight)); 77 | text.append(String.format("%n load types : %08X", loadTypes)); 78 | text.append(String.format("%n desc len : %d", descriptorLength)); 79 | for (Descriptor descriptor : descriptors) { 80 | text.append(String.format("%n Descriptor : %n%s", descriptor)); 81 | } 82 | 83 | return text.toString(); 84 | } 85 | 86 | public static class Descriptor { 87 | 88 | private final int set; 89 | private final byte flags; 90 | private final int localCharsetID; 91 | private final int slotWidth; 92 | private final int slotHeight; 93 | private final int startSubsection; 94 | private final int endSubsection; 95 | private int cgcsID; 96 | 97 | private Descriptor(byte[] buffer, int offset, int length) { 98 | set = buffer[offset] & 0xFF; 99 | flags = buffer[offset + 1]; 100 | localCharsetID = buffer[offset + 2] & 0xFF; 101 | slotWidth = buffer[offset + 3] & 0xFF; 102 | slotHeight = buffer[offset + 4] & 0xFF; 103 | startSubsection = buffer[offset + 5] & 0xFF; 104 | endSubsection = buffer[offset + 6] & 0xFF; 105 | if (length > 7) { 106 | cgcsID = Buffer.unsignedLong(buffer, 7); 107 | } 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return String.format(" Set : %d", set) 113 | + String.format("%n flags : %02X", flags) 114 | + String.format("%n charset : %02X", localCharsetID) 115 | + String.format("%n slot w : %02X", slotWidth) 116 | + String.format("%n slot h : %02X", slotHeight) 117 | + String.format("%n start : %02X", startSubsection) 118 | + String.format("%n end : %02X", endSubsection) 119 | + String.format("%n graphics : %d", cgcsID); 120 | } 121 | 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,maven,eclipse,intellij+all,intellij+iml,macos 3 | 4 | ### Eclipse ### 5 | 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # Java annotation processor (APT) 31 | .factorypath 32 | 33 | # PDT-specific (PHP Development Tools) 34 | .buildpath 35 | 36 | # sbteclipse plugin 37 | .target 38 | 39 | # Tern plugin 40 | .tern-project 41 | 42 | # TeXlipse plugin 43 | .texlipse 44 | 45 | # STS (Spring Tool Suite) 46 | .springBeans 47 | 48 | # Code Recommenders 49 | .recommenders/ 50 | 51 | # Scala IDE specific (Scala & Java development for Eclipse) 52 | .cache-main 53 | .scala_dependencies 54 | .worksheet 55 | 56 | ### Eclipse Patch ### 57 | # Eclipse Core 58 | .project 59 | 60 | # JDT-specific (Eclipse Java Development Tools) 61 | .classpath 62 | 63 | ### Intellij+all ### 64 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 65 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 66 | 67 | # User-specific stuff: 68 | .idea/**/workspace.xml 69 | .idea/**/tasks.xml 70 | .idea/dictionaries 71 | 72 | # Sensitive or high-churn files: 73 | .idea/**/dataSources/ 74 | .idea/**/dataSources.ids 75 | .idea/**/dataSources.xml 76 | .idea/**/dataSources.local.xml 77 | .idea/**/sqlDataSources.xml 78 | .idea/**/dynamic.xml 79 | .idea/**/uiDesigner.xml 80 | 81 | # Gradle: 82 | .idea/**/gradle.xml 83 | .idea/**/libraries 84 | 85 | # CMake 86 | cmake-build-debug/ 87 | 88 | # Mongo Explorer plugin: 89 | .idea/**/mongoSettings.xml 90 | 91 | ## File-based project format: 92 | *.iws 93 | 94 | ## Plugin-specific files: 95 | 96 | # IntelliJ 97 | /out/ 98 | 99 | # mpeltonen/sbt-idea plugin 100 | .idea_modules/ 101 | 102 | # JIRA plugin 103 | atlassian-ide-plugin.xml 104 | 105 | # Cursive Clojure plugin 106 | .idea/replstate.xml 107 | 108 | # Ruby plugin and RubyMine 109 | /.rakeTasks 110 | 111 | # Crashlytics plugin (for Android Studio and IntelliJ) 112 | com_crashlytics_export_strings.xml 113 | crashlytics.properties 114 | crashlytics-build.properties 115 | fabric.properties 116 | 117 | ### Intellij+all Patch ### 118 | # Ignores the whole idea folder 119 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 120 | 121 | .idea/ 122 | 123 | ### Intellij+iml ### 124 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 125 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 126 | 127 | # User-specific stuff: 128 | 129 | # Sensitive or high-churn files: 130 | 131 | # Gradle: 132 | 133 | # CMake 134 | 135 | # Mongo Explorer plugin: 136 | 137 | ## File-based project format: 138 | 139 | ## Plugin-specific files: 140 | 141 | # IntelliJ 142 | 143 | # mpeltonen/sbt-idea plugin 144 | 145 | # JIRA plugin 146 | 147 | # Cursive Clojure plugin 148 | 149 | # Ruby plugin and RubyMine 150 | 151 | # Crashlytics plugin (for Android Studio and IntelliJ) 152 | 153 | ### Intellij+iml Patch ### 154 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 155 | 156 | *.iml 157 | modules.xml 158 | .idea/misc.xml 159 | *.ipr 160 | 161 | ### Java ### 162 | # Compiled class file 163 | *.class 164 | 165 | # Log file 166 | *.log 167 | 168 | # BlueJ files 169 | *.ctxt 170 | 171 | # Mobile Tools for Java (J2ME) 172 | .mtj.tmp/ 173 | 174 | # Package Files # 175 | *.jar 176 | *.war 177 | *.ear 178 | *.zip 179 | *.tar.gz 180 | *.rar 181 | 182 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 183 | hs_err_pid* 184 | 185 | ### macOS ### 186 | *.DS_Store 187 | .AppleDouble 188 | .LSOverride 189 | 190 | # Icon must end with two \r 191 | Icon 192 | 193 | # Thumbnails 194 | ._* 195 | 196 | # Files that might appear in the root of a volume 197 | .DocumentRevisions-V100 198 | .fseventsd 199 | .Spotlight-V100 200 | .TemporaryItems 201 | .Trashes 202 | .VolumeIcon.icns 203 | .com.apple.timemachine.donotpresent 204 | 205 | # Directories potentially created on remote AFP share 206 | .AppleDB 207 | .AppleDesktop 208 | Network Trash Folder 209 | Temporary Items 210 | .apdisk 211 | 212 | ### Maven ### 213 | target/ 214 | pom.xml.tag 215 | pom.xml.releaseBackup 216 | pom.xml.versionsBackup 217 | pom.xml.next 218 | release.properties 219 | dependency-reduced-pom.xml 220 | buildNumber.properties 221 | .mvn/timing.properties 222 | 223 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 224 | !/.mvn/wrapper/maven-wrapper.jar 225 | 226 | 227 | # End of https://www.gitignore.io/api/java,maven,eclipse,intellij+all,intellij+iml,macos 228 | --------------------------------------------------------------------------------