Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
14bf7ba4b1
|
|||
|
bdfe6eec2a
|
|||
|
c28c097e25
|
|||
|
46a022d86b
|
@@ -712,6 +712,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
return terminalPanel;
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
void connect(ConnectionConfig config) {
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
@@ -998,8 +1002,72 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure application logging. When debug is false, disables all logging
|
||||
* to console and file, preventing creation of j3270.log.
|
||||
*/
|
||||
public static void configureLogging(boolean debug) {
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
try {
|
||||
h.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
Level logLevel = Level.ALL;
|
||||
globalRoot.setLevel(Level.ALL);
|
||||
|
||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||
@Override
|
||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(fileHandler);
|
||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
globalRoot.setLevel(Level.OFF);
|
||||
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
|
||||
Logger l = Logger.getLogger(pkg);
|
||||
l.setLevel(Level.OFF);
|
||||
for (java.util.logging.Handler h : l.getHandlers()) {
|
||||
l.removeHandler(h);
|
||||
try {
|
||||
h.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
boolean cliTls = false;
|
||||
@@ -1041,42 +1109,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}
|
||||
|
||||
Level logLevel = debug ? Level.FINE : Level.INFO;
|
||||
configureLogging(debug);
|
||||
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
|
||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||
@Override
|
||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(fileHandler);
|
||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (configFile != null) {
|
||||
try {
|
||||
@@ -1167,6 +1201,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||
config.setTlsVerifyCert(verify);
|
||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
|
||||
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
|
||||
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
|
||||
@@ -141,6 +141,14 @@ public class Settings {
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getAutoReconnect() {
|
||||
return getAutoConnectAutoReconnect();
|
||||
}
|
||||
|
||||
public static void setAutoReconnect(boolean autoReconnect) {
|
||||
setAutoConnectAutoReconnect(autoReconnect);
|
||||
}
|
||||
|
||||
public static int getAutoConnectReconnectMaxRetries() {
|
||||
return prefs.getInt("autoConnectReconnectMaxRetries", 5);
|
||||
}
|
||||
@@ -150,6 +158,32 @@ public class Settings {
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getInputMask() {
|
||||
return prefs.getBoolean("inputMask", true);
|
||||
}
|
||||
|
||||
public static void setInputMask(boolean mask) {
|
||||
prefs.putBoolean("inputMask", mask);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getInputMaskEnabled() {
|
||||
return getInputMask();
|
||||
}
|
||||
|
||||
public static void setInputMaskEnabled(boolean enabled) {
|
||||
setInputMask(enabled);
|
||||
}
|
||||
|
||||
public static String getInputMaskChar() {
|
||||
return prefs.get("inputMaskChar", "*");
|
||||
}
|
||||
|
||||
public static void setInputMaskChar(String ch) {
|
||||
prefs.put("inputMaskChar", (ch != null && !ch.trim().isEmpty()) ? ch.trim().substring(0, 1) : "*");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||
@@ -574,6 +608,26 @@ public class Settings {
|
||||
setDynamicCols(Integer.parseInt(value));
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
case "autoReconnect":
|
||||
case "auto_reconnect":
|
||||
case "autoConnectAutoReconnect":
|
||||
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "reconnectMaxRetries":
|
||||
case "autoConnectReconnectMaxRetries":
|
||||
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
|
||||
break;
|
||||
case "inputMask":
|
||||
case "input_mask":
|
||||
case "inputMaskEnabled":
|
||||
case "maskInput":
|
||||
setInputMask(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "inputMaskChar":
|
||||
case "input_mask_char":
|
||||
case "maskChar":
|
||||
setInputMaskChar(value);
|
||||
break;
|
||||
case "autoSysUnlock":
|
||||
case "auto_sys_unlock":
|
||||
setAutoSysUnlock(Boolean.parseBoolean(value));
|
||||
@@ -729,6 +783,10 @@ public class Settings {
|
||||
w.println("dynamicRows = " + getDynamicRows());
|
||||
w.println("dynamicCols = " + getDynamicCols());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
|
||||
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
|
||||
w.println("inputMask = " + getInputMask());
|
||||
w.println("inputMaskChar = " + getInputMaskChar());
|
||||
w.println("autoSysUnlock = " + getAutoSysUnlock());
|
||||
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
|
||||
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
|
||||
|
||||
@@ -31,6 +31,9 @@ public class SettingsDialog extends JDialog {
|
||||
private JPanel autoConnectPanel;
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JCheckBox autoReconnectCheck;
|
||||
private JCheckBox inputMaskCheck;
|
||||
private JTextField inputMaskCharField;
|
||||
private JCheckBox blockSelectCheck;
|
||||
private JSpinner dynamicRowsSpinner;
|
||||
private JSpinner dynamicColsSpinner;
|
||||
@@ -268,14 +271,45 @@ public class SettingsDialog extends JDialog {
|
||||
});
|
||||
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
// Block select mode checkbox
|
||||
// Auto-Reconnect checkbox
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.gridwidth = 2;
|
||||
autoReconnectCheck = new JCheckBox("Auto-Reconnect on Disconnect", Settings.getAutoConnectAutoReconnect());
|
||||
ThemeManager.styleCheckBox(autoReconnectCheck);
|
||||
panel.add(autoReconnectCheck, gbc);
|
||||
|
||||
// Input Mask feature control
|
||||
gbc.gridy = 3;
|
||||
gbc.gridwidth = 2;
|
||||
JPanel inputMaskPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
|
||||
inputMaskPanel.setOpaque(false);
|
||||
inputMaskCheck = new JCheckBox("Enable Input Mask (Password Masking)", Settings.getInputMask());
|
||||
ThemeManager.styleCheckBox(inputMaskCheck);
|
||||
inputMaskPanel.add(inputMaskCheck);
|
||||
|
||||
JLabel maskCharLabel = new JLabel("Mask Character:");
|
||||
inputMaskCharField = new JTextField(Settings.getInputMaskChar(), 2);
|
||||
ThemeManager.styleTextField(inputMaskCharField);
|
||||
inputMaskCharField.setEnabled(inputMaskCheck.isSelected());
|
||||
maskCharLabel.setEnabled(inputMaskCheck.isSelected());
|
||||
inputMaskCheck.addActionListener(e -> {
|
||||
boolean sel = inputMaskCheck.isSelected();
|
||||
inputMaskCharField.setEnabled(sel);
|
||||
maskCharLabel.setEnabled(sel);
|
||||
});
|
||||
inputMaskPanel.add(maskCharLabel);
|
||||
inputMaskPanel.add(inputMaskCharField);
|
||||
panel.add(inputMaskPanel, gbc);
|
||||
|
||||
// Block select mode checkbox
|
||||
gbc.gridy = 4;
|
||||
gbc.gridwidth = 2;
|
||||
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
|
||||
panel.add(blockSelectCheck, gbc);
|
||||
|
||||
// Default Dynamic Screen Size
|
||||
gbc.gridy = 3;
|
||||
gbc.gridy = 5;
|
||||
gbc.gridwidth = 1;
|
||||
gbc.gridx = 0;
|
||||
panel.add(new JLabel("Default Dynamic Screen:"), gbc);
|
||||
@@ -296,16 +330,16 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Clipboard & Tabular Paste options
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridy = 6;
|
||||
gbc.gridwidth = 2;
|
||||
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
|
||||
panel.add(enablePasteFromExcelCheck, gbc);
|
||||
|
||||
gbc.gridy = 5;
|
||||
gbc.gridy = 7;
|
||||
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
|
||||
panel.add(pasteStopAtProtectedCheck, gbc);
|
||||
|
||||
gbc.gridy = 6;
|
||||
gbc.gridy = 8;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
@@ -752,6 +786,24 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Reconnect
|
||||
if (autoReconnectCheck != null) {
|
||||
boolean ar = autoReconnectCheck.isSelected();
|
||||
Settings.setAutoConnectAutoReconnect(ar);
|
||||
if (parentApp != null && parentApp.getClient() != null && parentApp.getClient().getConfig() != null) {
|
||||
parentApp.getClient().getConfig().setAutoReconnect(ar);
|
||||
}
|
||||
}
|
||||
|
||||
// Input Mask
|
||||
if (inputMaskCheck != null) {
|
||||
Settings.setInputMask(inputMaskCheck.isSelected());
|
||||
}
|
||||
if (inputMaskCharField != null) {
|
||||
String charText = inputMaskCharField.getText().trim();
|
||||
Settings.setInputMaskChar(charText.isEmpty() ? "*" : charText.substring(0, 1));
|
||||
}
|
||||
|
||||
// Block select mode
|
||||
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
|
||||
private boolean crosshairRulerEnabled = false;
|
||||
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
|
||||
private boolean inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
|
||||
private String inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
|
||||
private boolean textBlinkVisible = true;
|
||||
private Image wallpaperImage = null;
|
||||
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
|
||||
@@ -1239,12 +1241,32 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
applyModeSettings();
|
||||
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
|
||||
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
|
||||
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
|
||||
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
|
||||
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public boolean isInputMaskEnabled() {
|
||||
return inputMaskEnabled;
|
||||
}
|
||||
|
||||
public void setInputMaskEnabled(boolean enabled) {
|
||||
this.inputMaskEnabled = enabled;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public String getInputMaskChar() {
|
||||
return inputMaskChar;
|
||||
}
|
||||
|
||||
public void setInputMaskChar(String maskChar) {
|
||||
this.inputMaskChar = maskChar;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setFontSize(int size) {
|
||||
currentFontSize = size;
|
||||
haus.nightmare.j3270.config.Settings.setFontSize(size);
|
||||
@@ -1527,12 +1549,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
// Password fields
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
g2.drawString("*", x, y + fontAscent);
|
||||
if (inputMaskEnabled) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*";
|
||||
g2.drawString(mask, x, y + fontAscent);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCellSelected(row, col)) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class LoggingConfigurationTest {
|
||||
|
||||
private PrintStream originalOut;
|
||||
private PrintStream originalErr;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalOut = System.out;
|
||||
originalErr = System.err;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
System.setOut(originalOut);
|
||||
System.setErr(originalErr);
|
||||
// Ensure all handlers are closed and logging reset
|
||||
J3270App.configureLogging(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingDisabledWhenDebugFalse() {
|
||||
File logFile = new File("j3270.log");
|
||||
if (logFile.exists()) {
|
||||
logFile.delete();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
|
||||
System.setOut(new PrintStream(outContent));
|
||||
System.setErr(new PrintStream(errContent));
|
||||
|
||||
J3270App.configureLogging(false);
|
||||
|
||||
// Root logger should have no handlers attached
|
||||
Logger rootLogger = Logger.getLogger("");
|
||||
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
|
||||
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
|
||||
|
||||
// Application loggers should be OFF
|
||||
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
|
||||
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
|
||||
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||
|
||||
// Emit log records at all levels
|
||||
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
|
||||
appLogger.severe("Test SEVERE message");
|
||||
appLogger.warning("Test WARNING message");
|
||||
appLogger.info("Test INFO message");
|
||||
appLogger.fine("Test FINE message");
|
||||
|
||||
// Verify nothing was written to stdout or stderr
|
||||
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
|
||||
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
|
||||
|
||||
// Verify j3270.log was NOT created
|
||||
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingEnabledWhenDebugTrue() {
|
||||
J3270App.configureLogging(true);
|
||||
|
||||
Logger rootLogger = Logger.getLogger("");
|
||||
Handler[] handlers = rootLogger.getHandlers();
|
||||
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
|
||||
|
||||
assertEquals(Level.ALL, rootLogger.getLevel());
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||
|
||||
File logFile = new File("j3270.log");
|
||||
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
|
||||
|
||||
// Clean up
|
||||
J3270App.configureLogging(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class BehaviorSettingsTest {
|
||||
|
||||
private boolean origAutoReconnect;
|
||||
private int origReconnectMaxRetries;
|
||||
private boolean origInputMask;
|
||||
private String origInputMaskChar;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
|
||||
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
|
||||
origInputMask = Settings.getInputMask();
|
||||
origInputMaskChar = Settings.getInputMaskChar();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
|
||||
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
|
||||
Settings.setInputMask(origInputMask);
|
||||
Settings.setInputMaskChar(origInputMaskChar);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Auto-reconnect settings get/set and alias consistency")
|
||||
public void testAutoReconnectSettingsPersistence() {
|
||||
Settings.setAutoConnectAutoReconnect(true);
|
||||
assertTrue(Settings.getAutoConnectAutoReconnect());
|
||||
assertTrue(Settings.getAutoReconnect());
|
||||
|
||||
Settings.setAutoReconnect(false);
|
||||
assertFalse(Settings.getAutoConnectAutoReconnect());
|
||||
assertFalse(Settings.getAutoReconnect());
|
||||
|
||||
Settings.setAutoConnectReconnectMaxRetries(8);
|
||||
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Input mask settings get/set and defaults")
|
||||
public void testInputMaskSettingsPersistence() {
|
||||
Settings.setInputMask(false);
|
||||
assertFalse(Settings.getInputMask());
|
||||
assertFalse(Settings.getInputMaskEnabled());
|
||||
|
||||
Settings.setInputMaskEnabled(true);
|
||||
assertTrue(Settings.getInputMask());
|
||||
assertTrue(Settings.getInputMaskEnabled());
|
||||
|
||||
Settings.setInputMaskChar("#");
|
||||
assertEquals("#", Settings.getInputMaskChar());
|
||||
|
||||
Settings.setInputMaskChar("*");
|
||||
assertEquals("*", Settings.getInputMaskChar());
|
||||
|
||||
// Empty string should fall back to '*'
|
||||
Settings.setInputMaskChar("");
|
||||
assertEquals("*", Settings.getInputMaskChar());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("INI export and load preserves autoReconnect and inputMask")
|
||||
public void testIniExportAndLoad() throws Exception {
|
||||
Settings.setAutoConnectAutoReconnect(true);
|
||||
Settings.setAutoConnectReconnectMaxRetries(12);
|
||||
Settings.setInputMask(false);
|
||||
Settings.setInputMaskChar("@");
|
||||
|
||||
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
|
||||
tempFile.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(tempFile.getAbsolutePath());
|
||||
|
||||
// Reset to different values
|
||||
Settings.setAutoConnectAutoReconnect(false);
|
||||
Settings.setAutoConnectReconnectMaxRetries(3);
|
||||
Settings.setInputMask(true);
|
||||
Settings.setInputMaskChar("*");
|
||||
|
||||
// Load back from INI
|
||||
Settings.loadFromIniFile(tempFile.getAbsolutePath());
|
||||
|
||||
assertTrue(Settings.getAutoConnectAutoReconnect());
|
||||
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
|
||||
assertFalse(Settings.getInputMask());
|
||||
assertEquals("@", Settings.getInputMaskChar());
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TerminalPanel reloads input mask settings correctly")
|
||||
public void testTerminalPanelInputMaskReload() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
Settings.setInputMask(false);
|
||||
Settings.setInputMaskChar("$");
|
||||
panel.reloadSettings();
|
||||
|
||||
assertFalse(panel.isInputMaskEnabled());
|
||||
assertEquals("$", panel.getInputMaskChar());
|
||||
|
||||
panel.setInputMaskEnabled(true);
|
||||
assertTrue(panel.isInputMaskEnabled());
|
||||
|
||||
panel.setInputMaskChar("#");
|
||||
assertEquals("#", panel.getInputMaskChar());
|
||||
} catch (HeadlessException ignored) {
|
||||
// Safe fallback for headless runner
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
|
||||
public void testSettingsDialogBehaviorControls() throws Exception {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
// In automated/headless environments, JFrame cannot be initialized
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
// Access private fields in SettingsDialog to verify component bindings
|
||||
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
|
||||
autoRecField.setAccessible(true);
|
||||
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
|
||||
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
|
||||
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
|
||||
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
|
||||
|
||||
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
|
||||
inputMaskCheckField.setAccessible(true);
|
||||
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
|
||||
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
|
||||
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
|
||||
|
||||
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
|
||||
inputMaskCharField.setAccessible(true);
|
||||
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
|
||||
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
|
||||
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
|
||||
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
|
||||
|
||||
// Test interaction: unchecking inputMask disables character field
|
||||
inputMaskCheck.setSelected(false);
|
||||
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
|
||||
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
|
||||
}
|
||||
assertFalse(maskCharField.isEnabled());
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,6 +435,14 @@ public class ConnectionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
|
||||
String parsedLu = null;
|
||||
int atIdx = s.indexOf('@');
|
||||
if (atIdx > 0 && atIdx < s.length() - 1) {
|
||||
parsedLu = s.substring(0, atIdx).trim();
|
||||
s = s.substring(atIdx + 1).trim();
|
||||
}
|
||||
|
||||
String host = s;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
@@ -458,6 +466,9 @@ public class ConnectionConfig {
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
if (parsedLu != null && !parsedLu.isEmpty()) {
|
||||
config.setLuName(parsedLu);
|
||||
}
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
config.setKeepAliveEnabled(keepAlive);
|
||||
|
||||
@@ -4,10 +4,10 @@ import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14).
|
||||
* EBCDIC ↔ Unicode character translator conforming to Host On-Demand (HoD v14).
|
||||
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
|
||||
* Supports custom per-instance character translation override tables and complete
|
||||
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings.
|
||||
* 3270 APL / Graphic Escape (GA23-0059) character mappings.
|
||||
* Default: Code Page 037 (US/Canada EBCDIC).
|
||||
*/
|
||||
public class EbcdicTranslator {
|
||||
@@ -210,9 +210,11 @@ public class EbcdicTranslator {
|
||||
*/
|
||||
public char ebcdicToUnicode(int ebc) {
|
||||
int b = ebc & 0xFF;
|
||||
Character custom = customEbcdicToUnicode.get(b);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
if (!customEbcdicToUnicode.isEmpty()) {
|
||||
Character custom = customEbcdicToUnicode.get(b);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
return activeCodePage.ebcdicToUnicode(b);
|
||||
}
|
||||
@@ -236,9 +238,11 @@ public class EbcdicTranslator {
|
||||
* Returns -1 if the character cannot be mapped.
|
||||
*/
|
||||
public int unicodeToEbcdic(char unicode) {
|
||||
Integer custom = customUnicodeToEbcdic.get(unicode);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
if (!customUnicodeToEbcdic.isEmpty()) {
|
||||
Integer custom = customUnicodeToEbcdic.get(unicode);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
return activeCodePage.unicodeToEbcdic(unicode);
|
||||
}
|
||||
@@ -296,8 +300,8 @@ public class EbcdicTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
|
||||
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification.
|
||||
* Map a 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
|
||||
* Conforms to 3270 APL / Text character set and GA23-0059 specification.
|
||||
*/
|
||||
public char mapAPL(int ebcdicCodePoint) {
|
||||
switch (ebcdicCodePoint & 0xFF) {
|
||||
@@ -331,7 +335,7 @@ public class EbcdicTranslator {
|
||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
||||
|
||||
// IBM 3270 APL Operational & Structural Glyphs
|
||||
// 3270 APL Operational & Structural Glyphs
|
||||
case 0x80: return '\u22C4'; // Diamond '⋄'
|
||||
case 0x81: return '\u237A'; // APL Alpha '⍺'
|
||||
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
|
||||
@@ -383,7 +387,7 @@ public class EbcdicTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||
* Translate a 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||
*/
|
||||
public char getAplGraphic(int ec) {
|
||||
return mapAPL(ec);
|
||||
|
||||
+33
-13
@@ -54,6 +54,12 @@ public class DataStreamProcessor {
|
||||
private boolean unlockSysPending = false;
|
||||
private boolean rcvdRead = false;
|
||||
|
||||
// Modal SA (set attribute) character attributes
|
||||
private byte currentFg = 0;
|
||||
private byte currentBg = 0;
|
||||
private byte currentGr = 0;
|
||||
private byte currentCs = 0;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
@@ -240,6 +246,10 @@ public class DataStreamProcessor {
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
@@ -378,11 +388,19 @@ public class DataStreamProcessor {
|
||||
if (wccReset(wcc)) {
|
||||
// Reset all character attributes to defaults
|
||||
log.fine("WCC reset: clearing default attributes");
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
}
|
||||
|
||||
if (eraseFirst) {
|
||||
screen.clear();
|
||||
log.fine("Cleared screen for Erase/Write");
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
}
|
||||
|
||||
// Process orders and data starting at byte 2
|
||||
@@ -390,9 +408,6 @@ public class DataStreamProcessor {
|
||||
int end = offset + length;
|
||||
int baddr = screen.getBufferAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// Current SA (set attribute) values for character-mode
|
||||
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
|
||||
boolean lastWasOrder = false;
|
||||
|
||||
while (pos < end) {
|
||||
@@ -427,10 +442,6 @@ public class DataStreamProcessor {
|
||||
// FA position is a display position that shows as blank
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
screen.setFormatted(true);
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -455,10 +466,6 @@ public class DataStreamProcessor {
|
||||
ea.clear();
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
|
||||
for (int i = 0; i < nPairs; i++) {
|
||||
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
||||
@@ -485,6 +492,12 @@ public class DataStreamProcessor {
|
||||
int attrType = data[pos + 1] & 0xFF;
|
||||
int attrValue = data[pos + 2] & 0xFF;
|
||||
switch (attrType) {
|
||||
case XA_ALL:
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
break;
|
||||
case XA_FOREGROUND:
|
||||
currentFg = (byte) attrValue;
|
||||
break;
|
||||
@@ -984,7 +997,13 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
int targetW = screen.getCols() * qrBuilder.getCharWidth();
|
||||
int targetH = screen.getRows() * qrBuilder.getCharHeight();
|
||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||
graphicsPlane.resize(targetW, targetH);
|
||||
}
|
||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
@@ -1298,9 +1317,10 @@ public class DataStreamProcessor {
|
||||
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
|
||||
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
|
||||
|
||||
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
int targetW = screen.getCols() * 9;
|
||||
int targetH = screen.getRows() * 16;
|
||||
int targetW = screen.getCols() * qrBuilder.getCharWidth();
|
||||
int targetH = screen.getRows() * qrBuilder.getCharHeight();
|
||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||
graphicsPlane.resize(targetW, targetH);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Reusable, resizable byte buffer that provides zero-copy access to its internal
|
||||
* array and supports ByteBuffer slicing to minimize heap allocations during
|
||||
* network stream processing.
|
||||
*/
|
||||
public class FastByteBuffer extends OutputStream {
|
||||
|
||||
private byte[] buf;
|
||||
private int count;
|
||||
|
||||
public FastByteBuffer() {
|
||||
this(32768);
|
||||
}
|
||||
|
||||
public FastByteBuffer(int initialCapacity) {
|
||||
this.buf = new byte[Math.max(32, initialCapacity)];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
private void ensureCapacity(int minCapacity) {
|
||||
if (minCapacity > buf.length) {
|
||||
int newCap = Math.max(buf.length << 1, minCapacity);
|
||||
buf = Arrays.copyOf(buf, newCap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) {
|
||||
ensureCapacity(count + 1);
|
||||
buf[count++] = (byte) b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b, int off, int len) {
|
||||
if (b == null || len <= 0) return;
|
||||
ensureCapacity(count + len);
|
||||
System.arraycopy(b, off, buf, count, len);
|
||||
count += len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct reference to internal buffer array.
|
||||
* Use {@link #size()} to determine active length.
|
||||
*/
|
||||
public synchronized byte[] buffer() {
|
||||
return buf;
|
||||
}
|
||||
|
||||
public synchronized int size() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only ByteBuffer view wrapping active bytes without copying.
|
||||
*/
|
||||
public synchronized ByteBuffer asByteBuffer() {
|
||||
return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only ByteBuffer slice for a sub-range without copying.
|
||||
*/
|
||||
public synchronized ByteBuffer slice(int offset, int length) {
|
||||
if (offset < 0 || length < 0 || offset + length > count) {
|
||||
throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count);
|
||||
}
|
||||
return ByteBuffer.wrap(buf, offset, length).slice().asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a copied byte array if an isolated copy is explicitly required.
|
||||
*/
|
||||
public synchronized byte[] toByteArray() {
|
||||
return Arrays.copyOf(buf, count);
|
||||
}
|
||||
}
|
||||
+154
-138
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
* Matches 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
*/
|
||||
public class QueryReplyBuilder {
|
||||
|
||||
@@ -18,10 +18,102 @@ public class QueryReplyBuilder {
|
||||
private static final int SW_3279_2 = 0x09;
|
||||
private static final int SH_3279_2 = 0x0c;
|
||||
|
||||
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060)
|
||||
// Usable Area physical dimensions matching HOD DS3270.java (Inches, 96 dpi: 0x00010060)
|
||||
private static final int Xr_HOD = 0x00010060;
|
||||
private static final int Yr_HOD = 0x00010060;
|
||||
|
||||
// Pre-computed static query reply payloads to eliminate allocation churn
|
||||
private static final byte[] STATIC_QR_COLOR = new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||
(byte) 0xF2, (byte) 0xF2, // Red
|
||||
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||
(byte) 0xF4, (byte) 0xF4, // Green
|
||||
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_HIGHLIGHTING = new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
(byte) 0xF1, (byte) 0xF1,
|
||||
(byte) 0xF2, (byte) 0xF2,
|
||||
(byte) 0xF4, (byte) 0xF4
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_REPLY_MODES = new byte[] {
|
||||
SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_OUTLINING = new byte[] {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_DBCS_ASIA = new byte[] {
|
||||
0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_AUXDA = new byte[] {
|
||||
0x00, 0x00
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_TRANSPARENCY = new byte[] {
|
||||
0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_SEGMENT = new byte[] {
|
||||
(byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_PROCEDURE = new byte[] {
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_LINETYPE = new byte[] {
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_PORT_BLOCKS;
|
||||
static {
|
||||
ByteArrayOutputStream pOut = new ByteArrayOutputStream(64);
|
||||
byte[][] data = {
|
||||
{ 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
|
||||
{ 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 },
|
||||
{ 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
|
||||
{ 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C }
|
||||
};
|
||||
for (byte[] d : data) {
|
||||
int len = 4 + d.length;
|
||||
pOut.write((len >> 8) & 0xFF);
|
||||
pOut.write(len & 0xFF);
|
||||
pOut.write(SFID_QREPLY);
|
||||
pOut.write(QR_PORT);
|
||||
pOut.write(d, 0, d.length);
|
||||
}
|
||||
STATIC_PORT_BLOCKS = pOut.toByteArray();
|
||||
}
|
||||
|
||||
private static final byte[] STATIC_QR_GRCOLOR;
|
||||
static {
|
||||
ByteArrayOutputStream gOut = new ByteArrayOutputStream(110);
|
||||
gOut.write(0x00); gOut.write(0x04); gOut.write(0x00); gOut.write(0xFF); gOut.write(0xFF);
|
||||
gOut.write(0x00); gOut.write(0x10); gOut.write(0x00); gOut.write(0x10);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
gOut.write(0x00);
|
||||
gOut.write(i);
|
||||
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
gOut.write((argb >> 16) & 0xFF);
|
||||
gOut.write((argb >> 8) & 0xFF);
|
||||
gOut.write(argb & 0xFF);
|
||||
gOut.write(0x00);
|
||||
}
|
||||
STATIC_QR_GRCOLOR = gOut.toByteArray();
|
||||
}
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||
|
||||
@@ -101,27 +193,27 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
|
||||
// Color (0x86)
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
|
||||
|
||||
// Highlighting (0x87)
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
|
||||
|
||||
// Reply Modes (0x88)
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
|
||||
|
||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||
if (isDbcs) {
|
||||
// Outlining (0x8C)
|
||||
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||
appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
|
||||
// DBCS Asia (0x91)
|
||||
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||
appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
|
||||
}
|
||||
|
||||
// Distributed Data Management (0x95)
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
|
||||
// Auxiliary Devices (0x99)
|
||||
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
|
||||
// Implicit Partition (0xA6)
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
@@ -141,27 +233,27 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
|
||||
appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
|
||||
|
||||
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||
appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
|
||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||
if (isDbcs) {
|
||||
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||
appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
|
||||
}
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8
|
||||
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0
|
||||
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1
|
||||
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2
|
||||
appendPort(out); // 0xB3
|
||||
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4
|
||||
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
|
||||
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
|
||||
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
|
||||
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
|
||||
appendPort(out); // 0xB3
|
||||
appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); // 0xB4
|
||||
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")");
|
||||
@@ -197,20 +289,20 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
break;
|
||||
case QR_COLOR:
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
|
||||
break;
|
||||
case QR_HIGHLIGHTING:
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
|
||||
break;
|
||||
case QR_REPLY_MODES:
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
|
||||
break;
|
||||
case QR_OUTLINING: // 0x8C
|
||||
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||
appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
|
||||
break;
|
||||
case QR_DBCS_ASIA: // 0x91
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||
appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -219,35 +311,35 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
break;
|
||||
case QR_AUXDA: // 0x99
|
||||
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
break;
|
||||
case QR_IMP_PART: // 0xA6
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_TRANSPARENCY: // 0xA8
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_SEGMENT: // 0xB0
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows));
|
||||
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_PROCEDURE: // 0xB1
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows));
|
||||
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_LINETYPE: // 0xB2
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_LINETYPE, buildLineType());
|
||||
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -261,7 +353,7 @@ public class QueryReplyBuilder {
|
||||
break;
|
||||
case QR_GRCOLOR: // 0xB4
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GRCOLOR, buildGrColor());
|
||||
appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -321,19 +413,19 @@ public class QueryReplyBuilder {
|
||||
out.write(maxCols & 0xFF); // usable width low
|
||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||
out.write(maxRows & 0xFF); // usable height low
|
||||
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
|
||||
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||
out.write(0x00); // units (0x00 = inches, matching HOD QR_USEAREA_STRING)
|
||||
// Xr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
|
||||
out.write((Xr_HOD >> 24) & 0xFF);
|
||||
out.write((Xr_HOD >> 16) & 0xFF);
|
||||
out.write((Xr_HOD >> 8) & 0xFF);
|
||||
out.write(Xr_HOD & 0xFF);
|
||||
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||
// Yr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
|
||||
out.write((Yr_HOD >> 24) & 0xFF);
|
||||
out.write((Yr_HOD >> 16) & 0xFF);
|
||||
out.write((Yr_HOD >> 8) & 0xFF);
|
||||
out.write(Yr_HOD & 0xFF);
|
||||
int charW = getCharWidth();
|
||||
int charH = getCharHeight();
|
||||
int charW = getCharWidth(maxRows);
|
||||
int charH = getCharHeight(maxRows);
|
||||
out.write(charW); // AW
|
||||
out.write(charH); // AH
|
||||
int buf = maxCols * maxRows;
|
||||
@@ -343,6 +435,11 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public int getCharWidth() {
|
||||
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return getCharWidth(rows);
|
||||
}
|
||||
|
||||
public int getCharWidth(int rows) {
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
return 12;
|
||||
}
|
||||
@@ -350,18 +447,11 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public int getCharHeight() {
|
||||
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
|
||||
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
|
||||
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
|
||||
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
|
||||
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
|
||||
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
|
||||
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
|
||||
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
|
||||
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
|
||||
//
|
||||
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
|
||||
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
|
||||
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return getCharHeight(rows);
|
||||
}
|
||||
|
||||
public int getCharHeight(int rows) {
|
||||
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||
}
|
||||
|
||||
@@ -442,31 +532,15 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildColor() {
|
||||
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||
return new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||
(byte) 0xF2, (byte) 0xF2, // Red
|
||||
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||
(byte) 0xF4, (byte) 0xF4, // Green
|
||||
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||
};
|
||||
return STATIC_QR_COLOR.clone();
|
||||
}
|
||||
|
||||
public byte[] buildHighlighting() {
|
||||
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||
return new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
(byte) 0xF1, (byte) 0xF1,
|
||||
(byte) 0xF2, (byte) 0xF2,
|
||||
(byte) 0xF4, (byte) 0xF4
|
||||
};
|
||||
return STATIC_QR_HIGHLIGHTING.clone();
|
||||
}
|
||||
|
||||
public byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
return STATIC_QR_REPLY_MODES.clone();
|
||||
}
|
||||
|
||||
public byte[] buildDdm() {
|
||||
@@ -517,39 +591,27 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildOutlining() {
|
||||
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000")
|
||||
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
return STATIC_QR_OUTLINING.clone();
|
||||
}
|
||||
|
||||
public byte[] buildDbcsAsia() {
|
||||
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001")
|
||||
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
|
||||
return STATIC_QR_DBCS_ASIA.clone();
|
||||
}
|
||||
|
||||
public byte[] buildAuxDa() {
|
||||
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000")
|
||||
return new byte[]{ 0x00, 0x00 };
|
||||
return STATIC_QR_AUXDA.clone();
|
||||
}
|
||||
|
||||
public byte[] buildTransparency() {
|
||||
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff")
|
||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||
return STATIC_QR_TRANSPARENCY.clone();
|
||||
}
|
||||
|
||||
public byte[] buildSegment() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildSegment(maxCols, maxRows);
|
||||
return STATIC_QR_SEGMENT.clone();
|
||||
}
|
||||
|
||||
public byte[] buildSegment(int maxCols, int maxRows) {
|
||||
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
|
||||
return new byte[]{
|
||||
(byte) 0x80, 0x02,
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00
|
||||
};
|
||||
return STATIC_QR_SEGMENT.clone();
|
||||
}
|
||||
|
||||
public byte[] buildGraphics() {
|
||||
@@ -561,21 +623,11 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildProcedure() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildProcedure(maxCols, maxRows);
|
||||
return STATIC_QR_PROCEDURE.clone();
|
||||
}
|
||||
|
||||
public byte[] buildProcedure(int maxCols, int maxRows) {
|
||||
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
|
||||
return new byte[]{
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
return STATIC_QR_PROCEDURE.clone();
|
||||
}
|
||||
|
||||
public byte[] buildGImage() {
|
||||
@@ -587,12 +639,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildLineType() {
|
||||
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b")
|
||||
return new byte[]{
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
return STATIC_QR_LINETYPE.clone();
|
||||
}
|
||||
|
||||
public byte[] buildAuxDev() {
|
||||
@@ -604,19 +651,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public void appendPort(ByteArrayOutputStream out) {
|
||||
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total)
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||
});
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||
});
|
||||
out.write(STATIC_PORT_BLOCKS, 0, STATIC_PORT_BLOCKS.length);
|
||||
}
|
||||
|
||||
public void appendOemFmt(ByteArrayOutputStream out) {
|
||||
@@ -624,9 +659,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildPort() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
|
||||
appendPort(out);
|
||||
return out.toByteArray();
|
||||
return STATIC_PORT_BLOCKS.clone();
|
||||
}
|
||||
|
||||
public byte[] buildOemFormat() {
|
||||
@@ -634,24 +667,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildGrColor() {
|
||||
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out.write(0x00);
|
||||
out.write(i);
|
||||
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
int r = (argb >> 16) & 0xFF;
|
||||
int g = (argb >> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
out.write(r);
|
||||
out.write(g);
|
||||
out.write(b);
|
||||
out.write(0x00); // 6th byte in HOD color table
|
||||
}
|
||||
return out.toByteArray();
|
||||
return STATIC_QR_GRCOLOR.clone();
|
||||
}
|
||||
|
||||
public byte[] buildGraphicColor() {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* Thread-safe memory pool providing reusable byte buffers for high-throughput
|
||||
* 3270 stream operations, minimizing garbage collector pressure.
|
||||
*/
|
||||
public final class ReusableByteBufferPool {
|
||||
|
||||
public static final int SIZE_SMALL = 512;
|
||||
public static final int SIZE_MEDIUM = 4096;
|
||||
public static final int SIZE_LARGE = 32768;
|
||||
|
||||
private static final int MAX_POOLED_PER_TIER = 32;
|
||||
|
||||
private static final Queue<byte[]> smallPool = new ConcurrentLinkedQueue<>();
|
||||
private static final Queue<byte[]> mediumPool = new ConcurrentLinkedQueue<>();
|
||||
private static final Queue<byte[]> largePool = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private ReusableByteBufferPool() {}
|
||||
|
||||
/**
|
||||
* Acquires a pooled byte array with at least the specified capacity.
|
||||
*/
|
||||
public static byte[] acquire(int minCapacity) {
|
||||
if (minCapacity <= SIZE_SMALL) {
|
||||
byte[] b = smallPool.poll();
|
||||
return (b != null) ? b : new byte[SIZE_SMALL];
|
||||
} else if (minCapacity <= SIZE_MEDIUM) {
|
||||
byte[] b = mediumPool.poll();
|
||||
return (b != null) ? b : new byte[SIZE_MEDIUM];
|
||||
} else if (minCapacity <= SIZE_LARGE) {
|
||||
byte[] b = largePool.poll();
|
||||
return (b != null) ? b : new byte[SIZE_LARGE];
|
||||
}
|
||||
return new byte[minCapacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a ByteBuffer wrapping a pooled array up to minCapacity.
|
||||
*/
|
||||
public static ByteBuffer acquireByteBuffer(int minCapacity) {
|
||||
byte[] b = acquire(minCapacity);
|
||||
return ByteBuffer.wrap(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer to the pool for reuse if it matches a standard tier.
|
||||
*/
|
||||
public static void release(byte[] buffer) {
|
||||
if (buffer == null) return;
|
||||
if (buffer.length == SIZE_SMALL && smallPool.size() < MAX_POOLED_PER_TIER) {
|
||||
smallPool.offer(buffer);
|
||||
} else if (buffer.length == SIZE_MEDIUM && mediumPool.size() < MAX_POOLED_PER_TIER) {
|
||||
mediumPool.offer(buffer);
|
||||
} else if (buffer.length == SIZE_LARGE && largePool.size() < MAX_POOLED_PER_TIER) {
|
||||
largePool.offer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all pools to release held memory.
|
||||
*/
|
||||
public static void clear() {
|
||||
smallPool.clear();
|
||||
mediumPool.clear();
|
||||
largePool.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Attention Identifier (AID) representation for 3270 presentation space.
|
||||
*/
|
||||
public final class AID implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final byte code;
|
||||
private final String name;
|
||||
|
||||
public static final AID clear = new AID((byte) 0x6D, "CLEAR");
|
||||
public static final AID enter = new AID((byte) 0x7D, "ENTER");
|
||||
public static final AID PA1 = new AID((byte) 0x6C, "PA1");
|
||||
public static final AID PA2 = new AID((byte) 0x6E, "PA2");
|
||||
public static final AID PA3 = new AID((byte) 0x6B, "PA3");
|
||||
|
||||
public static final AID PF1 = new AID((byte) 0xF1, "PF1");
|
||||
public static final AID PF2 = new AID((byte) 0xF2, "PF2");
|
||||
public static final AID PF3 = new AID((byte) 0xF3, "PF3");
|
||||
public static final AID PF4 = new AID((byte) 0xF4, "PF4");
|
||||
public static final AID PF5 = new AID((byte) 0xF5, "PF5");
|
||||
public static final AID PF6 = new AID((byte) 0xF6, "PF6");
|
||||
public static final AID PF7 = new AID((byte) 0xF7, "PF7");
|
||||
public static final AID PF8 = new AID((byte) 0xF8, "PF8");
|
||||
public static final AID PF9 = new AID((byte) 0xF9, "PF9");
|
||||
public static final AID PF10 = new AID((byte) 0x7A, "PF10");
|
||||
public static final AID PF11 = new AID((byte) 0x7B, "PF11");
|
||||
public static final AID PF12 = new AID((byte) 0x7C, "PF12");
|
||||
public static final AID PF13 = new AID((byte) 0xC1, "PF13");
|
||||
public static final AID PF14 = new AID((byte) 0xC2, "PF14");
|
||||
public static final AID PF15 = new AID((byte) 0xC3, "PF15");
|
||||
public static final AID PF16 = new AID((byte) 0xC4, "PF16");
|
||||
public static final AID PF17 = new AID((byte) 0xC5, "PF17");
|
||||
public static final AID PF18 = new AID((byte) 0xC6, "PF18");
|
||||
public static final AID PF19 = new AID((byte) 0xC7, "PF19");
|
||||
public static final AID PF20 = new AID((byte) 0xC8, "PF20");
|
||||
public static final AID PF21 = new AID((byte) 0xC9, "PF21");
|
||||
public static final AID PF22 = new AID((byte) 0x4A, "PF22");
|
||||
public static final AID PF23 = new AID((byte) 0x4B, "PF23");
|
||||
public static final AID PF24 = new AID((byte) 0x4C, "PF24");
|
||||
|
||||
public AID(byte code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public byte translate() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AID aid = (AID) o;
|
||||
return code == aid.code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name != null ? name : String.format("AID(0x%02X)", code & 0xFF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
/**
|
||||
* Standard External Presentation Interface (EPI) DataStream processor interface.
|
||||
*/
|
||||
public interface DataStream {
|
||||
|
||||
/**
|
||||
* Analyzes an inbound 3270 data stream buffer and updates the screen model.
|
||||
*
|
||||
* @param buffer Byte array containing the inbound 3270 record
|
||||
* @param length Length of active bytes in the buffer
|
||||
* @throws EPIException If a data stream format or command error is encountered
|
||||
*/
|
||||
void analyze(byte[] buffer, int length) throws EPIException;
|
||||
|
||||
/**
|
||||
* Formats modified screen fields into an outbound 3270 data stream.
|
||||
*
|
||||
* @param buffer Target byte array to receive formatted outbound record
|
||||
* @return Number of bytes written into the buffer
|
||||
* @throws EPIException If encoding or formatting fails
|
||||
*/
|
||||
int format(byte[] buffer) throws EPIException;
|
||||
|
||||
/**
|
||||
* Serializes the entire screen buffer into an outbound 3270 data stream.
|
||||
*
|
||||
* @param buffer Target byte array to receive the full screen dump
|
||||
* @return Number of bytes written into the buffer
|
||||
* @throws EPIException If encoding fails
|
||||
*/
|
||||
int readBuffer(byte[] buffer) throws EPIException;
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* 3270 stream processor implementing the External Presentation Interface (EPI).
|
||||
* Provides high-level stream analysis, buffer formatting, 12/14-bit buffer address
|
||||
* encoding/decoding, and stream-level character translation conforming to GA23-0059.
|
||||
*/
|
||||
public class DataStream3270 implements DataStream, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger log = Logger.getLogger(DataStream3270.class.getName());
|
||||
|
||||
public static final byte[] ENCODE_TABLE = new byte[]{
|
||||
32, 65, 66, 67, 68, 69, 70, 71, 72, 73, 91, 46, 60, 40, 43, 33,
|
||||
38, 74, 75, 76, 77, 78, 79, 80, 81, 82, 93, 36, 42, 41, 59, 94,
|
||||
45, 47, 83, 84, 85, 86, 87, 88, 89, 90, 124, 44, 37, 95, 62, 63,
|
||||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 35, 64, 39, 61, 34
|
||||
};
|
||||
|
||||
public static final int[] DECODE_TABLE = new int[]{
|
||||
0, 15, 63, 59, 27, 44, 16, 61, 13, 29, 28, 14, 43, 32, 11, 33,
|
||||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 30, 12, 62, 46, 47,
|
||||
60, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, 34, 35, 36, 37, 38, 39, 40, 41, 10, -1, 26, 31, 45,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 42, -1, -1, -1
|
||||
};
|
||||
|
||||
public static final char[] EBCDIC_TABLE = new char[]{
|
||||
'@', 'O', '\u007f', '{', '[', 'l', 'P', '}', 'M', ']', '\\', 'N', 'k', '`', 'K', 'a',
|
||||
'\u00f0', '\u00f1', '\u00f2', '\u00f3', '\u00f4', '\u00f5', '\u00f6', '\u00f7', '\u00f8', '\u00f9', 'z', '^', 'L', '~', 'n', 'o',
|
||||
'|', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', '\u00c6', '\u00c7', '\u00c8', '\u00c9', '\u00d1', '\u00d2', '\u00d3', '\u00d4', '\u00d5', '\u00d6',
|
||||
'\u00d7', '\u00d8', '\u00d9', '\u00e2', '\u00e3', '\u00e4', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9', 'J', '\u0000', 'Z', '_', 'm',
|
||||
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000',
|
||||
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', 'j', '\u0000', '\u0000', '\u0000'
|
||||
};
|
||||
|
||||
private static final char[] blankChars = new char[]{'\u0000', '\n', '\f', '\r', '\u000e', '\u000f', '\u0019'};
|
||||
private static final String blanks = new String(blankChars);
|
||||
|
||||
private Screen screen;
|
||||
private boolean formatted = false;
|
||||
|
||||
public DataStream3270(Screen screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public Screen getScreen() {
|
||||
return screen;
|
||||
}
|
||||
|
||||
public void setScreen(Screen screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public boolean isFormatted() {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
public void setFormatted(boolean formatted) {
|
||||
this.formatted = formatted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(byte[] buffer, int length) throws EPIException {
|
||||
if (buffer == null || length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
int fieldCount = 0;
|
||||
int bufCount = 0;
|
||||
Field currentField = null;
|
||||
int curPos = 0;
|
||||
int cursorTarget = 0;
|
||||
int spanTarget = 0;
|
||||
byte[] lineBuffer = new byte[screen.getWidth()];
|
||||
boolean isNewField = true;
|
||||
int wcc = 3;
|
||||
int screenWidth = screen.getWidth();
|
||||
|
||||
if (length > 2) {
|
||||
wcc = toEbcdic(buffer[1]);
|
||||
}
|
||||
|
||||
switch (buffer[index]) {
|
||||
case 49: // Write (0x31)
|
||||
case (byte) 0xF1:
|
||||
curPos = cursorTarget = (screen.getCursorRow() - 1) * screenWidth + (screen.getCursorColumn() - 1);
|
||||
if ((wcc & 1) != 0) {
|
||||
int totalFields = screen.fieldCount();
|
||||
for (int i = 1; i <= totalFields; i++) {
|
||||
Field f = screen.field(i);
|
||||
if (f != null && f.dataTag() == 1) {
|
||||
f.resetDataTag();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 50: // Read Buffer (0x32)
|
||||
case (byte) 0xF2:
|
||||
screen.readMode = true;
|
||||
return;
|
||||
case 53: // Erase / Write (0x35)
|
||||
case (byte) 0xF5:
|
||||
screen.reset();
|
||||
curPos = 0;
|
||||
cursorTarget = 0;
|
||||
this.formatted = false;
|
||||
break;
|
||||
default:
|
||||
throw new EPI3270Exception(96, buffer[index], 4608);
|
||||
}
|
||||
|
||||
try {
|
||||
index = 2;
|
||||
while (index < length) {
|
||||
int op = buffer[index] & 0xFF;
|
||||
switch (op) {
|
||||
case 9: { // PT (Program Tab)
|
||||
log.finer("PT");
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
case 16: { // SFE (Start Field Extended)
|
||||
log.finer("SFE");
|
||||
this.formatted = true;
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
}
|
||||
currentField = screen.getField(curPos);
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
isNewField = true;
|
||||
} else {
|
||||
currentField.setAttribute(true);
|
||||
currentField.setBaseAttribute('\u0000');
|
||||
currentField.setExtAttribute('A', '\u0000');
|
||||
currentField.setExtAttribute('B', '\u0000');
|
||||
currentField.setExtAttribute('E', '\u0000');
|
||||
currentField.setExtAttribute('F', '\u0000');
|
||||
isNewField = false;
|
||||
}
|
||||
int numPairs = buffer[++index] & 0xFF;
|
||||
for (int p = 1; p <= numPairs; p++) {
|
||||
char attrType = (char) buffer[index + 1];
|
||||
char attrVal = toEbcdic(buffer[index + 2]);
|
||||
currentField.setExtAttribute(attrType, attrVal);
|
||||
index += 2;
|
||||
}
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
case 17: { // SBA (Set Buffer Address)
|
||||
log.finer("SBA");
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
currentField = null;
|
||||
}
|
||||
curPos = decodeAddress(buffer[index + 1], buffer[index + 2]);
|
||||
index += 2;
|
||||
break;
|
||||
}
|
||||
case 18: { // EUA (Erase Unprotected to Address)
|
||||
log.finer("EUA");
|
||||
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
|
||||
screen.resetFields(curPos, spanTarget);
|
||||
index += 2;
|
||||
curPos = spanTarget;
|
||||
break;
|
||||
}
|
||||
case 19: { // IC (Insert Cursor)
|
||||
log.finer("IC");
|
||||
cursorTarget = curPos;
|
||||
break;
|
||||
}
|
||||
case 20: { // RA (Repeat to Address)
|
||||
log.finer("RA");
|
||||
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
|
||||
index += 3;
|
||||
byte repeatByte = buffer[index];
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
currentField.setAttribute(false);
|
||||
isNewField = true;
|
||||
}
|
||||
if (spanTarget <= curPos) {
|
||||
int totalSize = screenWidth * screen.getDepth();
|
||||
while (curPos < totalSize) {
|
||||
if (bufCount >= screenWidth) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
|
||||
bufCount = 0;
|
||||
}
|
||||
lineBuffer[bufCount++] = repeatByte;
|
||||
curPos++;
|
||||
}
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
currentField = null;
|
||||
curPos = 0;
|
||||
if (spanTarget > 0) {
|
||||
currentField = new Field(screen, curPos);
|
||||
currentField.setAttribute(false);
|
||||
isNewField = true;
|
||||
}
|
||||
}
|
||||
while (curPos < spanTarget) {
|
||||
if (bufCount >= screenWidth) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
|
||||
bufCount = 0;
|
||||
}
|
||||
lineBuffer[bufCount++] = repeatByte;
|
||||
curPos++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 29: { // SF (Start Field)
|
||||
log.finer("SF");
|
||||
this.formatted = true;
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
}
|
||||
currentField = screen.getField(curPos);
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
isNewField = true;
|
||||
} else {
|
||||
currentField.setAttribute(true);
|
||||
currentField.setExtAttribute('A', '\u0000');
|
||||
currentField.setExtAttribute('B', '\u0000');
|
||||
currentField.setExtAttribute('E', '\u0000');
|
||||
currentField.setExtAttribute('F', '\u0000');
|
||||
isNewField = false;
|
||||
}
|
||||
currentField.setBaseAttribute(toEbcdic(buffer[++index]));
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
case 26:
|
||||
case 30: { // MF (Modify Field)
|
||||
log.finer("MF");
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
currentField = null;
|
||||
}
|
||||
int numPairs = buffer[++index] & 0xFF;
|
||||
currentField = screen.getField(curPos);
|
||||
if (currentField != null) {
|
||||
currentField.setAttribute(true);
|
||||
for (int p = 1; p <= numPairs; p++) {
|
||||
currentField.setExtAttribute((char) buffer[index + 1], toEbcdic(buffer[index + 2]));
|
||||
index += 2;
|
||||
}
|
||||
isNewField = false;
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
for (int p = 1; p <= numPairs; p++) {
|
||||
index += 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 31:
|
||||
case 40: { // SA (Set Attribute)
|
||||
log.finer("SA");
|
||||
index += 2;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (currentField == null) {
|
||||
currentField = screen.getField(curPos - 1);
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
isNewField = true;
|
||||
currentField.setAttribute(false);
|
||||
} else {
|
||||
isNewField = false;
|
||||
}
|
||||
}
|
||||
if (bufCount >= screenWidth) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
|
||||
bufCount = 0;
|
||||
}
|
||||
byte b = buffer[index];
|
||||
lineBuffer[bufCount++] = (blanks.indexOf(b) != -1) ? (byte) 32 : b;
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error during analyze", e);
|
||||
throw new EPI3270Exception(90, e, 4609);
|
||||
}
|
||||
|
||||
int maxCell = screenWidth * screen.getDepth();
|
||||
if (cursorTarget >= 0 && cursorTarget < maxCell) {
|
||||
screen.setCursor(cursorTarget / screenWidth + 1, cursorTarget % screenWidth + 1);
|
||||
} else {
|
||||
log.fine("Cursor address out of range: " + cursorTarget);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int format(byte[] buffer) throws EPIException {
|
||||
if (buffer == null || buffer.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
AID aid = screen.getAID();
|
||||
buffer[pos++] = aid.translate();
|
||||
|
||||
if (aid.equals(AID.clear)) {
|
||||
screen.initList();
|
||||
screen.setCursor(1, 1);
|
||||
this.formatted = false;
|
||||
return pos;
|
||||
}
|
||||
if (aid.equals(AID.PA1) || aid.equals(AID.PA2) || aid.equals(AID.PA3)) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
|
||||
encodeAddress(buffer, pos, cursorAddr);
|
||||
pos += 2;
|
||||
|
||||
int totalFields = screen.fieldCount();
|
||||
try {
|
||||
if (this.formatted) {
|
||||
for (int i = 1; i <= totalFields; i++) {
|
||||
Field field = screen.field(i);
|
||||
if (field != null && field.dataTag() == 1) {
|
||||
buffer[pos++] = 17; // SBA order
|
||||
encodeAddress(buffer, pos, field.getPosition() + 1);
|
||||
pos += 2;
|
||||
byte[] bytes = field.getBytes();
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (totalFields > 0) {
|
||||
Field f1 = screen.field(1);
|
||||
byte[] bytes = (f1 != null) ? f1.getBytes() : null;
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
log.log(Level.WARNING, "Unsupported encoding during format", uee);
|
||||
throw new EPI3270Exception(90, uee, 4609);
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readBuffer(byte[] buffer) throws EPIException {
|
||||
if (buffer == null || buffer.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
buffer[pos++] = screen.getAID().translate();
|
||||
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
|
||||
encodeAddress(buffer, pos, cursorAddr);
|
||||
pos += 2;
|
||||
|
||||
int totalFields = screen.fieldCount();
|
||||
try {
|
||||
for (int i = 1; i <= totalFields; i++) {
|
||||
Field field = screen.field(i);
|
||||
if (field == null) continue;
|
||||
buffer[pos++] = 17; // SBA
|
||||
encodeAddress(buffer, pos, field.getPosition());
|
||||
pos += 2;
|
||||
if (field.hasAttribute()) {
|
||||
buffer[pos++] = 29; // SF
|
||||
buffer[pos++] = toAscii(field.baseAttribute());
|
||||
}
|
||||
byte[] bytes = field.getBytes();
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
log.log(Level.WARNING, "Unsupported encoding during readBuffer", uee);
|
||||
throw new EPI3270Exception(90, uee, 4609);
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes 12-bit / 14-bit presentation space address into 2 bytes.
|
||||
*/
|
||||
public void encodeAddress(byte[] target, int offset, int address) {
|
||||
if (address < 0 || address > 4096) {
|
||||
target[offset] = 32;
|
||||
target[offset + 1] = 32;
|
||||
return;
|
||||
}
|
||||
int hi = address / 64;
|
||||
int lo = address % 64;
|
||||
target[offset] = ENCODE_TABLE[hi];
|
||||
target[offset + 1] = ENCODE_TABLE[lo];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes 2-byte presentation space address into linear buffer position.
|
||||
*/
|
||||
public int decodeAddress(int b1, int b2) {
|
||||
int v1 = b1 & 0xFF;
|
||||
int v2 = b2 & 0xFF;
|
||||
if (v1 < 32 || v1 > 127 || v2 < 32 || v2 > 127) {
|
||||
return -1;
|
||||
}
|
||||
int d1 = DECODE_TABLE[v1 - 32];
|
||||
int d2 = DECODE_TABLE[v2 - 32];
|
||||
if (d1 < 0 || d2 < 0) {
|
||||
return -1;
|
||||
}
|
||||
return d1 * 64 + d2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates single byte to EBCDIC presentation character.
|
||||
*/
|
||||
public char toEbcdic(int b) {
|
||||
int v = b & 0xFF;
|
||||
if (v < 32 || v > 127) {
|
||||
return (char) v;
|
||||
}
|
||||
return EBCDIC_TABLE[v - 32];
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates character back to ASCII byte representation.
|
||||
*/
|
||||
public byte toAscii(char c) {
|
||||
if (c < '@' || c > '\u00f9') {
|
||||
return (byte) c;
|
||||
}
|
||||
for (int i = 0; i < EBCDIC_TABLE.length; i++) {
|
||||
if (EBCDIC_TABLE[i] == c) {
|
||||
return (byte) (i + 32);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
/**
|
||||
* 3270 protocol specific External Presentation Interface (EPI) exception.
|
||||
*/
|
||||
public class EPI3270Exception extends EPIException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int commandOrOrder = 0;
|
||||
|
||||
public EPI3270Exception(int errorCode, int commandOrOrder, int reasonCode) {
|
||||
super(errorCode, reasonCode);
|
||||
this.commandOrOrder = commandOrOrder;
|
||||
}
|
||||
|
||||
public EPI3270Exception(int errorCode, Throwable cause, int reasonCode) {
|
||||
super(errorCode, cause, reasonCode);
|
||||
}
|
||||
|
||||
public EPI3270Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public int getCommandOrOrder() {
|
||||
return commandOrOrder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
/**
|
||||
* Base exception for External Presentation Interface (EPI) stream operations.
|
||||
*/
|
||||
public class EPIException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int errorCode = 0;
|
||||
private int reasonCode = 0;
|
||||
|
||||
public EPIException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EPIException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, Throwable cause) {
|
||||
super(cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, Throwable cause, int reasonCode) {
|
||||
super(cause);
|
||||
this.errorCode = errorCode;
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, int reasonCode) {
|
||||
super("EPI Exception error=" + errorCode + " reason=" + reasonCode);
|
||||
this.errorCode = errorCode;
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public int getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* Bidirectional bridge between EPI Screen model and lib3270j ScreenBuffer.
|
||||
*/
|
||||
public class EpiScreenBufferBridge {
|
||||
|
||||
/**
|
||||
* Copies contents from an EPI Screen into a ScreenBuffer presentation space.
|
||||
*/
|
||||
public static void copyToScreenBuffer(Screen epiScreen, ScreenBuffer target) {
|
||||
if (epiScreen == null || target == null) return;
|
||||
|
||||
synchronized (target.getRenderLock()) {
|
||||
int w = epiScreen.getWidth();
|
||||
int h = epiScreen.getDepth();
|
||||
if (target.getCols() != w || target.getRows() != h) {
|
||||
target.setDimensions(h, w);
|
||||
}
|
||||
target.clear();
|
||||
|
||||
int count = epiScreen.fieldCount();
|
||||
for (int i = 1; i <= count; i++) {
|
||||
Field f = epiScreen.field(i);
|
||||
if (f == null) continue;
|
||||
int pos = f.getPosition();
|
||||
if (pos >= 0 && pos < target.getSize()) {
|
||||
if (f.hasAttribute()) {
|
||||
byte fa = (byte) (f.baseAttribute() & 0xFF);
|
||||
target.setFieldAttribute(pos, fa);
|
||||
}
|
||||
try {
|
||||
byte[] bytes = f.getBytes();
|
||||
if (bytes != null) {
|
||||
int textPos = f.hasAttribute() ? pos + 1 : pos;
|
||||
for (int bIdx = 0; bIdx < bytes.length && (textPos + bIdx) < target.getSize(); bIdx++) {
|
||||
ExtendedAttribute ea = target.getCell(textPos + bIdx);
|
||||
ea.ec = bytes[bIdx];
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
int r = Math.max(1, Math.min(target.getRows(), epiScreen.getCursorRow()));
|
||||
int c = Math.max(1, Math.min(target.getCols(), epiScreen.getCursorColumn()));
|
||||
target.setCursorAddress((r - 1) * target.getCols() + (c - 1));
|
||||
target.translateToUnicode();
|
||||
target.markAllChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts fields from a ScreenBuffer presentation space into an EPI Screen.
|
||||
*/
|
||||
public static void copyFromScreenBuffer(ScreenBuffer source, Screen epiScreen) {
|
||||
if (source == null || epiScreen == null) return;
|
||||
|
||||
synchronized (source.getRenderLock()) {
|
||||
epiScreen.setWidth(source.getCols());
|
||||
epiScreen.setDepth(source.getRows());
|
||||
epiScreen.initList();
|
||||
|
||||
int size = source.getSize();
|
||||
Field currentField = null;
|
||||
byte[] buf = new byte[source.getCols()];
|
||||
int bufLen = 0;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
byte fa = source.getFieldAttributeAt(i);
|
||||
if (fa != 0) {
|
||||
if (currentField != null && bufLen > 0) {
|
||||
currentField.setBytes(0, buf, bufLen);
|
||||
epiScreen.insertField(currentField);
|
||||
bufLen = 0;
|
||||
}
|
||||
currentField = new Field(epiScreen, i);
|
||||
currentField.setAttribute(true);
|
||||
currentField.setBaseAttribute((char) (fa & 0xFF));
|
||||
} else if (currentField != null) {
|
||||
ExtendedAttribute ea = source.getCell(i);
|
||||
if (bufLen >= buf.length) {
|
||||
currentField.setBytes(0, buf, bufLen);
|
||||
bufLen = 0;
|
||||
}
|
||||
buf[bufLen++] = ea.ec;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentField != null && bufLen > 0) {
|
||||
currentField.setBytes(0, buf, bufLen);
|
||||
epiScreen.insertField(currentField);
|
||||
}
|
||||
|
||||
int cursorAddr = source.getCursorAddress();
|
||||
int cols = source.getCols();
|
||||
epiScreen.setCursor((cursorAddr / cols) + 1, (cursorAddr % cols) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Field representation within an EPI 3270 Screen.
|
||||
*/
|
||||
public class Field implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Screen screen;
|
||||
private int position;
|
||||
private int length;
|
||||
private boolean hasAttribute;
|
||||
private char baseAttribute;
|
||||
private final Map<Character, Character> extAttributes = new HashMap<>();
|
||||
private int dataTag; // 1 = modified, 0 = unmodified
|
||||
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
|
||||
|
||||
public Field(Screen screen, int position) {
|
||||
this.screen = screen;
|
||||
this.position = position;
|
||||
this.hasAttribute = false;
|
||||
this.baseAttribute = '\0';
|
||||
this.dataTag = 0;
|
||||
}
|
||||
|
||||
public Screen getScreen() {
|
||||
return screen;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length > 0 ? length : content.size();
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public boolean hasAttribute() {
|
||||
return hasAttribute;
|
||||
}
|
||||
|
||||
public void setAttribute(boolean hasAttribute) {
|
||||
this.hasAttribute = hasAttribute;
|
||||
}
|
||||
|
||||
public char baseAttribute() {
|
||||
return baseAttribute;
|
||||
}
|
||||
|
||||
public void setBaseAttribute(char baseAttribute) {
|
||||
this.hasAttribute = true;
|
||||
this.baseAttribute = baseAttribute;
|
||||
// Bit 0x01 in 3270 attribute indicates Modified Data Tag (MDT)
|
||||
if ((baseAttribute & 0x01) != 0) {
|
||||
this.dataTag = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void setExtAttribute(char type, char value) {
|
||||
this.hasAttribute = true;
|
||||
extAttributes.put(type, value);
|
||||
}
|
||||
|
||||
public char getExtAttribute(char type) {
|
||||
Character val = extAttributes.get(type);
|
||||
return val != null ? val : '\0';
|
||||
}
|
||||
|
||||
public int dataTag() {
|
||||
return dataTag;
|
||||
}
|
||||
|
||||
public void resetDataTag() {
|
||||
this.dataTag = 0;
|
||||
}
|
||||
|
||||
public void setDataTag(int dataTag) {
|
||||
this.dataTag = dataTag;
|
||||
}
|
||||
|
||||
public void setBytes(int offset, byte[] data, int length) {
|
||||
if (data != null && length > 0) {
|
||||
content.write(data, 0, length);
|
||||
this.length = content.size();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getBytes() throws UnsupportedEncodingException {
|
||||
return content.toByteArray();
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
byte[] bytes = content.toByteArray();
|
||||
return new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
content.reset();
|
||||
if (text != null) {
|
||||
byte[] b = text.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
content.write(b, 0, b.length);
|
||||
this.length = b.length;
|
||||
this.dataTag = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
content.reset();
|
||||
this.length = 0;
|
||||
this.dataTag = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Screen representation within an EPI 3270 session.
|
||||
*/
|
||||
public class Screen implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int width = 80;
|
||||
private int depth = 24;
|
||||
private int cursorRow = 1;
|
||||
private int cursorColumn = 1;
|
||||
private AID aid = AID.enter;
|
||||
public boolean readMode = false;
|
||||
|
||||
private final List<Field> fields = new ArrayList<>();
|
||||
|
||||
public Screen() {
|
||||
this(80, 24);
|
||||
}
|
||||
|
||||
public Screen(int width, int depth) {
|
||||
this.width = width;
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public int getCursorRow() {
|
||||
return cursorRow;
|
||||
}
|
||||
|
||||
public int getCursorColumn() {
|
||||
return cursorColumn;
|
||||
}
|
||||
|
||||
public void setCursor(int row, int col) {
|
||||
this.cursorRow = row;
|
||||
this.cursorColumn = col;
|
||||
}
|
||||
|
||||
public AID getAID() {
|
||||
return aid;
|
||||
}
|
||||
|
||||
public void setAID(AID aid) {
|
||||
this.aid = aid != null ? aid : AID.enter;
|
||||
}
|
||||
|
||||
public int fieldCount() {
|
||||
return fields.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves field by 1-based index matching EPI convention.
|
||||
*/
|
||||
public Field field(int oneBasedIndex) {
|
||||
if (oneBasedIndex >= 1 && oneBasedIndex <= fields.size()) {
|
||||
return fields.get(oneBasedIndex - 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the field starting at or covering the given linear buffer position.
|
||||
*/
|
||||
public Field getField(int position) {
|
||||
for (Field f : fields) {
|
||||
if (f.getPosition() == position) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void insertField(Field field) {
|
||||
if (field == null) return;
|
||||
// Keep fields ordered by position
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
if (fields.get(i).getPosition() == field.getPosition()) {
|
||||
fields.set(i, field);
|
||||
return;
|
||||
} else if (fields.get(i).getPosition() > field.getPosition()) {
|
||||
fields.add(i, field);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fields.add(field);
|
||||
}
|
||||
|
||||
public void initList() {
|
||||
fields.clear();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
fields.clear();
|
||||
cursorRow = 1;
|
||||
cursorColumn = 1;
|
||||
readMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets fields within the range [start, end) by erasing unprotected content.
|
||||
*/
|
||||
public void resetFields(int start, int end) {
|
||||
int maxPos = width * depth;
|
||||
for (Field f : fields) {
|
||||
int pos = f.getPosition();
|
||||
boolean inRange;
|
||||
if (start <= end) {
|
||||
inRange = (pos >= start && pos < end);
|
||||
} else {
|
||||
inRange = (pos >= start || pos < end);
|
||||
}
|
||||
if (inRange) {
|
||||
// If unprotected, clear content
|
||||
if (f.hasAttribute() && (f.baseAttribute() & 0x20) == 0) {
|
||||
f.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,18 @@ import java.util.regex.Pattern;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand
|
||||
* (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer).
|
||||
* VM/CMS Spool and Print File Transfer facility matching Host On-Demand specifications.
|
||||
*
|
||||
* Provides:
|
||||
* 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT).
|
||||
* 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+').
|
||||
* 3. IBM 1403/3211 Machine carriage control channel command byte translation.
|
||||
* 3. Machine carriage control channel command byte translation.
|
||||
* 4. High-level print spool stream extraction and transfer helpers.
|
||||
*/
|
||||
public class CMSPrintXfer {
|
||||
|
||||
private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName());
|
||||
private static final Pattern HEADER_PATTERN = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
|
||||
|
||||
private final ECLXfer xfer;
|
||||
private final EbcdicTranslator translator;
|
||||
@@ -126,13 +126,12 @@ public class CMSPrintXfer {
|
||||
if (text == null || text.trim().isEmpty()) return entries;
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue;
|
||||
if (headerPattern.matcher(trimmed).find()) continue;
|
||||
if (HEADER_PATTERN.matcher(trimmed).find()) continue;
|
||||
|
||||
SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice);
|
||||
if (entry != null) {
|
||||
@@ -303,11 +302,11 @@ public class CMSPrintXfer {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IBM 1403/3211 Machine Carriage Control Translation
|
||||
// Machine Carriage Control Translation
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Translates IBM Machine Carriage Control Channel Command bytes into formatted text bytes.
|
||||
* Translates Machine Carriage Control Channel Command bytes into formatted text bytes.
|
||||
*
|
||||
* Command codes:
|
||||
* 0x01: Write without line advance
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Configuration for an IND$FILE file transfer session.
|
||||
* Ported from x3270's ft_conf_t (ft_private.h).
|
||||
*/
|
||||
public class FTConfig {
|
||||
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
|
||||
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
|
||||
/** Host operating system type */
|
||||
public enum HostType {
|
||||
TSO, CMS, CICS
|
||||
@@ -210,26 +221,22 @@ public class FTConfig {
|
||||
String trimmed = opts.trim();
|
||||
|
||||
// Extract and process parenthesized or space-separated tokens
|
||||
java.util.regex.Pattern recfmPattern = java.util.regex.Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed);
|
||||
Matcher recfmMatcher = RECFM_PATTERN.matcher(trimmed);
|
||||
if (recfmMatcher.find()) {
|
||||
setRecfm(recfmMatcher.group(1));
|
||||
}
|
||||
|
||||
java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed);
|
||||
Matcher lreclMatcher = LRECL_PATTERN.matcher(trimmed);
|
||||
if (lreclMatcher.find()) {
|
||||
setLrecl(lreclMatcher.group(1));
|
||||
}
|
||||
|
||||
java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed);
|
||||
Matcher blkMatcher = BLK_PATTERN.matcher(trimmed);
|
||||
if (blkMatcher.find()) {
|
||||
setBlksize(blkMatcher.group(1));
|
||||
}
|
||||
|
||||
java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed);
|
||||
Matcher spaceMatcher = SPACE_PATTERN.matcher(trimmed);
|
||||
if (spaceMatcher.find()) {
|
||||
try {
|
||||
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
|
||||
@@ -239,8 +246,7 @@ public class FTConfig {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed);
|
||||
Matcher avbMatcher = AVB_PATTERN.matcher(trimmed);
|
||||
if (avbMatcher.find()) {
|
||||
try {
|
||||
this.avblock = Integer.parseInt(avbMatcher.group(1));
|
||||
@@ -248,14 +254,12 @@ public class FTConfig {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
|
||||
java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed);
|
||||
Matcher cpMatcher = CP_PATTERN.matcher(trimmed);
|
||||
if (cpMatcher.find()) {
|
||||
this.codePage = cpMatcher.group(1);
|
||||
}
|
||||
|
||||
java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed);
|
||||
Matcher mtuMatcher = MTU_PATTERN.matcher(trimmed);
|
||||
if (mtuMatcher.find()) {
|
||||
try {
|
||||
setDftBufferSize(Integer.parseInt(mtuMatcher.group(1)));
|
||||
|
||||
@@ -395,13 +395,16 @@ public class GocaDecoder {
|
||||
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
|
||||
return 1;
|
||||
}
|
||||
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00)
|
||||
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00)
|
||||
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
|
||||
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
|
||||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG ||
|
||||
(inImage && order == GocaConstants.G_GEIMG_ALT)) {
|
||||
order == GocaConstants.G_GPOP) {
|
||||
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||
}
|
||||
// G_GEIMG (0x93 End Image): self-defining draw order (e.g. 93 02 00 00 or 93 00 or 93)
|
||||
if (order == GocaConstants.G_GEIMG) {
|
||||
return (idx + 1 < end) ? ((data[idx + 1] & 0xFF) + 2) : 1;
|
||||
}
|
||||
if (idx + 1 >= end) {
|
||||
return -1;
|
||||
}
|
||||
@@ -552,31 +555,45 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GEIMG: // 0x93
|
||||
case GocaConstants.G_GEIMG_ALT: { // 0x91
|
||||
if (inImage || order == GocaConstants.G_GEIMG) {
|
||||
case GocaConstants.G_GEIMG: { // 0x93 End Image
|
||||
endImage();
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBIMGC: { // 0x91: Begin Image at Current Position (G_GBIMGC)
|
||||
if (inImage) {
|
||||
endImage();
|
||||
idx += orderLen;
|
||||
} else {
|
||||
// 0x91: Begin Image at Current Position (G_GBIMGC)
|
||||
if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
|
||||
int w = readCoord(inputData, idx + 2);
|
||||
int h = readCoord(inputData, idx + 4);
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 5) {
|
||||
int fmt = inputData[idx + 6] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 6) {
|
||||
compression = inputData[idx + 7] & 0xFF;
|
||||
}
|
||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
||||
}
|
||||
idx += orderLen;
|
||||
}
|
||||
int w = 0, h = 0;
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
|
||||
w = readCoord(inputData, idx + 4);
|
||||
h = readCoord(inputData, idx + 6);
|
||||
if (payloadLen >= 7) {
|
||||
int fmt = inputData[idx + 8] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 8) {
|
||||
compression = inputData[idx + 9] & 0xFF;
|
||||
}
|
||||
} else if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
|
||||
w = readCoord(inputData, idx + 2);
|
||||
h = readCoord(inputData, idx + 4);
|
||||
if (payloadLen >= 5) {
|
||||
int fmt = inputData[idx + 6] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 6) {
|
||||
compression = inputData[idx + 7] & 0xFF;
|
||||
}
|
||||
}
|
||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
||||
@@ -986,21 +1003,39 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
||||
if (inImage) {
|
||||
endImage();
|
||||
}
|
||||
if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
|
||||
int x = readCoord(inputData, idx + 2);
|
||||
int y = readCoord(inputData, idx + 4);
|
||||
int w = readCoord(inputData, idx + 6);
|
||||
int h = readCoord(inputData, idx + 8);
|
||||
int w, h;
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 9) {
|
||||
int fmt = inputData[idx + 10] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 10) {
|
||||
compression = inputData[idx + 11] & 0xFF;
|
||||
w = readCoord(inputData, idx + 8);
|
||||
h = readCoord(inputData, idx + 10);
|
||||
if (payloadLen >= 11) {
|
||||
int fmt = inputData[idx + 12] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 12) {
|
||||
compression = inputData[idx + 13] & 0xFF;
|
||||
}
|
||||
} else {
|
||||
w = readCoord(inputData, idx + 6);
|
||||
h = readCoord(inputData, idx + 8);
|
||||
if (payloadLen >= 9) {
|
||||
int fmt = inputData[idx + 10] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 10) {
|
||||
compression = inputData[idx + 11] & 0xFF;
|
||||
}
|
||||
}
|
||||
beginImage(x, y, w, h, bitDepth, compression);
|
||||
}
|
||||
|
||||
@@ -418,9 +418,27 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Character cell dimensions matching Query Reply presentation space
|
||||
private int charWidth = 9;
|
||||
private int charHeight = 16;
|
||||
|
||||
public synchronized void setCharDimensions(int width, int height) {
|
||||
if (width > 0) this.charWidth = width;
|
||||
if (height > 0) this.charHeight = height;
|
||||
this.transform.setDefaultCharMetrics(this.charWidth, this.charHeight);
|
||||
}
|
||||
|
||||
public int getCharWidth() {
|
||||
return charWidth;
|
||||
}
|
||||
|
||||
public int getCharHeight() {
|
||||
return charHeight;
|
||||
}
|
||||
|
||||
public int getTotalWidth() {
|
||||
int cols = screenCols > 0 ? screenCols : 80;
|
||||
return cols * 9;
|
||||
return cols * charWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,7 +475,7 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
*/
|
||||
public int getTotalHeight() {
|
||||
int rows = screenRows > 0 ? screenRows : 24;
|
||||
return rows * 16;
|
||||
return rows * charHeight;
|
||||
}
|
||||
|
||||
public int getXMax() {
|
||||
|
||||
@@ -47,7 +47,7 @@ public class TelnetFSM {
|
||||
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
|
||||
|
||||
// 3270 input buffer (accumulated between telnet framing)
|
||||
private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768);
|
||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
|
||||
|
||||
// Sub-negotiation buffer
|
||||
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
|
||||
@@ -70,8 +70,22 @@ public class TelnetFSM {
|
||||
|
||||
private List<String> getCandidateTerminalTypes() {
|
||||
List<String> list = new ArrayList<>();
|
||||
String currentLu = null;
|
||||
List<String> lus = config.getLuNames();
|
||||
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
|
||||
currentLu = lus.get(luIndex);
|
||||
} else if (config.getLuName() != null && !config.getLuName().trim().isEmpty()) {
|
||||
currentLu = config.getLuName().trim();
|
||||
}
|
||||
|
||||
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
|
||||
list.add(config.getTerminalName().trim());
|
||||
String tName = config.getTerminalName().trim();
|
||||
if (currentLu != null && !currentLu.isEmpty() && !tName.contains("@")) {
|
||||
list.add(tName + "@" + currentLu);
|
||||
list.add(tName);
|
||||
} else {
|
||||
list.add(tName);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
if (config.isDynamicModel()) {
|
||||
@@ -82,24 +96,41 @@ public class TelnetFSM {
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
list.add("UNKNOWN");
|
||||
return list;
|
||||
} else {
|
||||
TerminalModel model = config.getModel();
|
||||
list.add(model.getTerminalType());
|
||||
list.add(model.getBaseTerminalType());
|
||||
if (model.isColor()) {
|
||||
try {
|
||||
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
||||
list.add(mono.getTerminalType());
|
||||
list.add(mono.getBaseTerminalType());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
if (model.getModelNumber() != 2) {
|
||||
list.add("IBM-3279-2-E");
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
}
|
||||
list.add("UNKNOWN");
|
||||
}
|
||||
TerminalModel model = config.getModel();
|
||||
list.add(model.getTerminalType());
|
||||
list.add(model.getBaseTerminalType());
|
||||
if (model.isColor()) {
|
||||
try {
|
||||
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
||||
list.add(mono.getTerminalType());
|
||||
list.add(mono.getBaseTerminalType());
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
if (currentLu != null && !currentLu.isEmpty()) {
|
||||
List<String> luCandidates = new ArrayList<>();
|
||||
for (String item : list) {
|
||||
if (!"UNKNOWN".equalsIgnoreCase(item) && !item.contains("@")) {
|
||||
luCandidates.add(item + "@" + currentLu);
|
||||
}
|
||||
}
|
||||
for (String item : list) {
|
||||
if (!"UNKNOWN".equalsIgnoreCase(item)) {
|
||||
luCandidates.add(item);
|
||||
}
|
||||
}
|
||||
luCandidates.add("UNKNOWN");
|
||||
return luCandidates;
|
||||
}
|
||||
if (model.getModelNumber() != 2) {
|
||||
list.add("IBM-3279-2-E");
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
}
|
||||
list.add("UNKNOWN");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -589,6 +620,9 @@ public class TelnetFSM {
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
if (termType.contains("@")) {
|
||||
connectedLu = termType.substring(termType.indexOf('@') + 1).trim();
|
||||
}
|
||||
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
|
||||
}
|
||||
}
|
||||
@@ -990,9 +1024,9 @@ public class TelnetFSM {
|
||||
// ========== End of Record processing ==========
|
||||
|
||||
private void processEndOfRecord() {
|
||||
byte[] data = ibuf.toByteArray();
|
||||
ibuf.reset();
|
||||
if (data.length == 0) return;
|
||||
int dataLen = ibuf.size();
|
||||
if (dataLen == 0) return;
|
||||
byte[] data = ibuf.buffer();
|
||||
|
||||
if ((connectionState == ConnectionState.TELNET_PENDING ||
|
||||
connectionState == ConnectionState.CONNECTED_NVT ||
|
||||
@@ -1003,14 +1037,15 @@ public class TelnetFSM {
|
||||
|
||||
if (tn3270eNegotiated) {
|
||||
// TN3270E mode: data starts with 5-byte header
|
||||
processTN3270ERecord(data);
|
||||
processTN3270ERecord(data, 0, dataLen);
|
||||
} else {
|
||||
// Plain TN3270 mode: data is raw 3270 data stream
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.processRecord(data, 0, data.length, false);
|
||||
dsProcessor.processRecord(data, 0, dataLen, false);
|
||||
}
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
ibuf.reset();
|
||||
|
||||
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
|
||||
if (dsProcessor != null) {
|
||||
@@ -1062,19 +1097,23 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
public void processTn3270eHeader(byte[] data) {
|
||||
processTN3270ERecord(data);
|
||||
processTN3270ERecord(data, 0, data != null ? data.length : 0);
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data) {
|
||||
if (data.length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + data.length);
|
||||
processTN3270ERecord(data, 0, data != null ? data.length : 0);
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data, int offset, int length) {
|
||||
if (data == null || length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + length);
|
||||
return;
|
||||
}
|
||||
|
||||
int dataType = data[0] & 0xFF;
|
||||
int requestFlag = data[1] & 0xFF;
|
||||
int responseFlag = data[2] & 0xFF;
|
||||
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
|
||||
int dataType = data[offset] & 0xFF;
|
||||
int requestFlag = data[offset + 1] & 0xFF;
|
||||
int responseFlag = data[offset + 2] & 0xFF;
|
||||
int seqNumber = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
|
||||
|
||||
this.sdi_flag = (requestFlag & 0x01) != 0;
|
||||
this.kri_flag = (requestFlag & 0x02) != 0;
|
||||
@@ -1089,7 +1128,7 @@ public class TelnetFSM {
|
||||
|
||||
switch (dataType) {
|
||||
case DT_3270_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
// Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
|
||||
if (connectionState != ConnectionState.CONNECTED_TN3270E) {
|
||||
// Clear screen on transition to 3270 mode from unbound/SSCP/NVT
|
||||
@@ -1099,7 +1138,7 @@ public class TelnetFSM {
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
}
|
||||
try {
|
||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false);
|
||||
dsProcessor.processRecord(data, offset + EH_SIZE, length - EH_SIZE, false);
|
||||
notifyScreenUpdate();
|
||||
// Send positive response if required
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
@@ -1119,9 +1158,9 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case DT_SCS_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
try {
|
||||
processSCSInbound(data, EH_SIZE, data.length - EH_SIZE);
|
||||
processSCSInbound(data, offset + EH_SIZE, length - EH_SIZE);
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
@@ -1148,9 +1187,9 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_SSCP);
|
||||
tn3270eSubmode = TN3270ESubmode.E_SSCP;
|
||||
}
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
try {
|
||||
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
dsProcessor.processSscpLuData(data, offset + EH_SIZE, length - EH_SIZE);
|
||||
notifyScreenUpdate();
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
@@ -1169,11 +1208,15 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case DT_BIND_IMAGE:
|
||||
process_bind(data, responseFlag, seqNumber);
|
||||
{
|
||||
byte[] bindData = new byte[length];
|
||||
System.arraycopy(data, offset, bindData, 0, length);
|
||||
process_bind(bindData, responseFlag, seqNumber);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_UNBIND:
|
||||
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL;
|
||||
int unbindReason = (length > EH_SIZE) ? (data[offset + EH_SIZE] & 0xFF) : UNBIND_NORMAL;
|
||||
process_unbind(unbindReason, responseFlag, seqNumber);
|
||||
break;
|
||||
|
||||
@@ -1186,9 +1229,9 @@ public class TelnetFSM {
|
||||
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
try {
|
||||
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
processNVTData(data, offset + EH_SIZE, length - EH_SIZE);
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
@@ -1243,12 +1286,12 @@ public class TelnetFSM {
|
||||
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
|
||||
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
|
||||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
|
||||
dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
dataType == 0x0D || (length >= 2 && (data[offset] & 0xFF) == 0x11)) {
|
||||
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
|
||||
") in TN3270E mode — automatically switching to plain TN3270 mode");
|
||||
tn3270eNegotiated = false;
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
dsProcessor.processRecord(data, 0, data.length, true);
|
||||
dsProcessor.processRecord(data, offset, length, true);
|
||||
notifyScreenUpdate();
|
||||
} else {
|
||||
log.info("Unhandled TN3270E data type: " + dataType);
|
||||
@@ -1257,6 +1300,7 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void processSCSInbound(byte[] data) {
|
||||
if (data == null) return;
|
||||
processSCSInbound(data, 0, data.length);
|
||||
@@ -1289,7 +1333,7 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
/**
|
||||
* HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E).
|
||||
* HoD 5-byte send_response compatible signature.
|
||||
*/
|
||||
public void send_response(short s, short s2, int n) {
|
||||
byte[] byArray = new byte[5];
|
||||
|
||||
@@ -26,8 +26,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Core 3270 File Transfer Controller conforming to IBM Host On-Demand
|
||||
* (com.ibm.eNetwork.ECL.xfer3270.Xfer3270).
|
||||
* Core 3270 File Transfer Controller conforming to Host On-Demand specifications.
|
||||
*
|
||||
* Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options,
|
||||
* dynamic MTU buffering, host/local dataset name mappings, directory queries,
|
||||
@@ -37,6 +36,13 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
|
||||
private static final Logger log = Logger.getLogger(Xfer3270.class.getName());
|
||||
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
|
||||
public static final String UNICODE_UCS2_STR = "UCS2";
|
||||
public static final String UNICODE_UTF8_STR = "UTF8";
|
||||
public static final String UNICODE_UTF_8_STR = "UTF-8";
|
||||
@@ -175,20 +181,20 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
}
|
||||
|
||||
// Mainframe dataset parameters
|
||||
Matcher recfmMatcher = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?").matcher(options);
|
||||
Matcher recfmMatcher = RECFM_PATTERN.matcher(options);
|
||||
if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase();
|
||||
|
||||
Matcher lreclMatcher = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher lreclMatcher = LRECL_PATTERN.matcher(options);
|
||||
if (lreclMatcher.find()) {
|
||||
try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher blkMatcher = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher blkMatcher = BLK_PATTERN.matcher(options);
|
||||
if (blkMatcher.find()) {
|
||||
try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher spaceMatcher = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?").matcher(options);
|
||||
Matcher spaceMatcher = SPACE_PATTERN.matcher(options);
|
||||
if (spaceMatcher.find()) {
|
||||
try {
|
||||
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
|
||||
@@ -198,7 +204,7 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher avbMatcher = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher avbMatcher = AVB_PATTERN.matcher(options);
|
||||
if (avbMatcher.find()) {
|
||||
try {
|
||||
this.avblock = Integer.parseInt(avbMatcher.group(1));
|
||||
@@ -212,7 +218,7 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
this.spaceUnits = "CYLINDERS";
|
||||
}
|
||||
|
||||
Matcher mtuMatcher = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher mtuMatcher = MTU_PATTERN.matcher(options);
|
||||
if (mtuMatcher.find()) {
|
||||
try {
|
||||
SetMTUSize(Integer.parseInt(mtuMatcher.group(1)));
|
||||
|
||||
+116
@@ -82,4 +82,120 @@ public class DataStreamProcessorTest {
|
||||
assertNotNull(sentData.get());
|
||||
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAttributesPersistAcrossStartField() throws java.io.IOException {
|
||||
// User's exact ISPF Option 0 sequence:
|
||||
// SBA(7, 1) -> SF(prot,skip) -> SA(yellow) -> ' 4 ' -> SBA(7, 7) -> SF(prot,skip) -> 'DISPLAY'
|
||||
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||
stream.write(CMD_EW);
|
||||
stream.write(0xC3); // WCC
|
||||
|
||||
// SBA(7, 1) -> address 7*80 + 1 = 561
|
||||
byte[] addr1 = encodeAddress(561, 24, 80);
|
||||
stream.write(ORDER_SBA);
|
||||
stream.write(addr1);
|
||||
|
||||
// SF(prot, skip)
|
||||
stream.write(ORDER_SF);
|
||||
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
|
||||
|
||||
// SA(yellow) -> XA_FOREGROUND, COLOR_YELLOW (0xF6)
|
||||
stream.write(ORDER_SA);
|
||||
stream.write(XA_FOREGROUND);
|
||||
stream.write(0xF6);
|
||||
|
||||
// Data: ' 4 '
|
||||
stream.write(translator.stringToEbcdic(" 4 "));
|
||||
|
||||
// SBA(7, 7) -> address 7*80 + 7 = 567
|
||||
byte[] addr2 = encodeAddress(567, 24, 80);
|
||||
stream.write(ORDER_SBA);
|
||||
stream.write(addr2);
|
||||
|
||||
// SF(prot, skip)
|
||||
stream.write(ORDER_SF);
|
||||
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
|
||||
|
||||
// Data: 'DISPLAY'
|
||||
stream.write(translator.stringToEbcdic("DISPLAY"));
|
||||
|
||||
byte[] record = stream.toByteArray();
|
||||
processor.processRecord(record, 0, record.length, true);
|
||||
|
||||
// Positions 562..566 (' 4 ') must be yellow (0xF6)
|
||||
for (int i = 562; i <= 566; i++) {
|
||||
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should have yellow foreground (0xF6)");
|
||||
}
|
||||
|
||||
// Positions 568..574 ('DISPLAY') across the second SF must also retain yellow (0xF6)
|
||||
for (int i = 568; i <= 574; i++) {
|
||||
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " ('DISPLAY') should retain yellow foreground (0xF6)");
|
||||
}
|
||||
|
||||
// Now test that SA with XA_ALL resets character attributes to default (0)
|
||||
java.io.ByteArrayOutputStream resetStream = new java.io.ByteArrayOutputStream();
|
||||
resetStream.write(CMD_W);
|
||||
resetStream.write(0xC3);
|
||||
resetStream.write(ORDER_SA);
|
||||
resetStream.write(XA_ALL);
|
||||
resetStream.write(0x00);
|
||||
resetStream.write(translator.stringToEbcdic("TEST"));
|
||||
byte[] resetRecord = resetStream.toByteArray();
|
||||
processor.processRecord(resetRecord, 0, resetRecord.length, true);
|
||||
|
||||
for (int i = 575; i < 579; i++) {
|
||||
assertEquals((byte) 0, screen.getCell(i).fg, "Cell at " + i + " should have default foreground (0) after SA(XA_ALL)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAttributesPersistAcrossStartFieldExtended() throws java.io.IOException {
|
||||
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||
stream.write(CMD_EW);
|
||||
stream.write(0xC3);
|
||||
|
||||
// SA(yellow)
|
||||
stream.write(ORDER_SA);
|
||||
stream.write(XA_FOREGROUND);
|
||||
stream.write(0xF6);
|
||||
|
||||
// SFE with 1 pair (3270 FA)
|
||||
stream.write(ORDER_SFE);
|
||||
stream.write(0x01); // 1 pair
|
||||
stream.write(XA_3270);
|
||||
stream.write(FA_PRINTABLE);
|
||||
|
||||
// Data 'HELLO'
|
||||
stream.write(translator.stringToEbcdic("HELLO"));
|
||||
|
||||
byte[] record = stream.toByteArray();
|
||||
processor.processRecord(record, 0, record.length, true);
|
||||
|
||||
// Check cells of HELLO (positions 1..5) have fg == 0xF6
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should retain yellow foreground across SFE");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAttributesResetOnErase() throws java.io.IOException {
|
||||
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||
stream.write(CMD_EW);
|
||||
stream.write(0xC3);
|
||||
stream.write(ORDER_SA);
|
||||
stream.write(XA_FOREGROUND);
|
||||
stream.write(0xF6);
|
||||
stream.write(translator.stringToEbcdic("A"));
|
||||
processor.processRecord(stream.toByteArray(), 0, stream.size(), true);
|
||||
assertEquals((byte) 0xF6, screen.getCell(0).fg);
|
||||
|
||||
// New EW command resets attributes
|
||||
java.io.ByteArrayOutputStream ewStream = new java.io.ByteArrayOutputStream();
|
||||
ewStream.write(CMD_EW);
|
||||
ewStream.write(0xC3);
|
||||
ewStream.write(translator.stringToEbcdic("B"));
|
||||
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
|
||||
assertEquals((byte) 0, screen.getCell(0).fg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for high-throughput zero-allocation buffer primitives.
|
||||
*/
|
||||
public class FastByteBufferTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
ReusableByteBufferPool.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FastByteBuffer operations: write, grow, reset, slice, and direct array access")
|
||||
public void testFastByteBufferBasicOperations() {
|
||||
FastByteBuffer buf = new FastByteBuffer(16);
|
||||
assertEquals(0, buf.size());
|
||||
assertTrue(buf.buffer().length >= 16);
|
||||
|
||||
buf.write(0x11);
|
||||
buf.write(0x22);
|
||||
assertEquals(2, buf.size());
|
||||
assertEquals((byte) 0x11, buf.buffer()[0]);
|
||||
assertEquals((byte) 0x22, buf.buffer()[1]);
|
||||
|
||||
byte[] payload = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05};
|
||||
buf.write(payload, 0, payload.length);
|
||||
assertEquals(7, buf.size());
|
||||
|
||||
buf.write(payload, 1, 3); // write 0x02, 0x03, 0x04
|
||||
assertEquals(10, buf.size());
|
||||
|
||||
// Test auto-growth
|
||||
byte[] largeData = new byte[100];
|
||||
for (int i = 0; i < largeData.length; i++) {
|
||||
largeData[i] = (byte) (i & 0xFF);
|
||||
}
|
||||
buf.write(largeData, 0, largeData.length);
|
||||
assertEquals(110, buf.size());
|
||||
assertTrue(buf.buffer().length >= 110);
|
||||
|
||||
// Verify direct array access
|
||||
byte[] raw = buf.buffer();
|
||||
assertNotNull(raw);
|
||||
assertEquals((byte) 0x11, raw[0]);
|
||||
assertEquals((byte) 0x22, raw[1]);
|
||||
|
||||
// Verify ByteBuffer view
|
||||
ByteBuffer readOnly = buf.asByteBuffer();
|
||||
assertEquals(110, readOnly.remaining());
|
||||
assertEquals((byte) 0x11, readOnly.get());
|
||||
|
||||
// Verify ByteBuffer slice
|
||||
ByteBuffer slice = buf.slice(2, 5);
|
||||
assertEquals(5, slice.remaining());
|
||||
assertEquals((byte) 0x01, slice.get(0));
|
||||
assertEquals((byte) 0x05, slice.get(4));
|
||||
|
||||
// Verify copied array
|
||||
byte[] copied = buf.toByteArray();
|
||||
assertEquals(110, copied.length);
|
||||
assertEquals((byte) 0x11, copied[0]);
|
||||
|
||||
// Test reset
|
||||
buf.reset();
|
||||
assertEquals(0, buf.size());
|
||||
// Buffer retained for zero-allocation reuse
|
||||
assertTrue(buf.buffer().length >= 110);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ReusableByteBufferPool acquires, releases, and recycles tiered buffers")
|
||||
public void testByteBufferPoolRecycling() {
|
||||
byte[] small1 = ReusableByteBufferPool.acquire(256);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_SMALL, small1.length);
|
||||
|
||||
byte[] medium1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_MEDIUM);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_MEDIUM, medium1.length);
|
||||
|
||||
byte[] large1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_LARGE);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_LARGE, large1.length);
|
||||
|
||||
// Non-standard oversized buffer
|
||||
byte[] huge = ReusableByteBufferPool.acquire(65536);
|
||||
assertEquals(65536, huge.length);
|
||||
|
||||
// Release back to pool
|
||||
ReusableByteBufferPool.release(small1);
|
||||
ReusableByteBufferPool.release(medium1);
|
||||
ReusableByteBufferPool.release(large1);
|
||||
ReusableByteBufferPool.release(huge);
|
||||
|
||||
// Next acquire should reuse the released instances
|
||||
byte[] small2 = ReusableByteBufferPool.acquire(128);
|
||||
assertSame(small1, small2, "Small buffer should be recycled from pool");
|
||||
|
||||
byte[] medium2 = ReusableByteBufferPool.acquire(2048);
|
||||
assertSame(medium1, medium2, "Medium buffer should be recycled from pool");
|
||||
|
||||
byte[] large2 = ReusableByteBufferPool.acquire(30000);
|
||||
assertSame(large1, large2, "Large buffer should be recycled from pool");
|
||||
|
||||
ByteBuffer bb = ReusableByteBufferPool.acquireByteBuffer(ReusableByteBufferPool.SIZE_SMALL);
|
||||
assertNotNull(bb);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_SMALL, bb.capacity());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ReusableByteBufferPool is safe under concurrent acquisition and release")
|
||||
public void testConcurrentPoolAccess() throws InterruptedException {
|
||||
int threads = 8;
|
||||
int iterations = 1000;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||
CountDownLatch latch = new CountDownLatch(threads);
|
||||
AtomicInteger failures = new AtomicInteger(0);
|
||||
|
||||
for (int t = 0; t < threads; t++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
byte[] buf = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_SMALL);
|
||||
if (buf == null || buf.length != ReusableByteBufferPool.SIZE_SMALL) {
|
||||
failures.incrementAndGet();
|
||||
}
|
||||
buf[0] = (byte) 0xAA;
|
||||
buf[1] = (byte) 0xBB;
|
||||
ReusableByteBufferPool.release(buf);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failures.incrementAndGet();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
executor.shutdown();
|
||||
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class EpiDataStream3270Test {
|
||||
|
||||
private Screen screen;
|
||||
private DataStream3270 dataStream;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
screen = new Screen(80, 24);
|
||||
dataStream = new DataStream3270(screen);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeAndDecodeAddress() {
|
||||
int[] testAddresses = {0, 1, 79, 80, 1919, 2000, 4095};
|
||||
byte[] target = new byte[2];
|
||||
|
||||
for (int addr : testAddresses) {
|
||||
dataStream.encodeAddress(target, 0, addr);
|
||||
int decoded = dataStream.decodeAddress(target[0], target[1]);
|
||||
assertEquals(addr, decoded, "Address round-trip mismatch for " + addr);
|
||||
}
|
||||
|
||||
// Test invalid decode
|
||||
assertEquals(-1, dataStream.decodeAddress((byte) 0x00, (byte) 0x00));
|
||||
assertEquals(-1, dataStream.decodeAddress((byte) 0xFF, (byte) 0xFF));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterTranslations() {
|
||||
char ebcdicChar = dataStream.toEbcdic('A'); // ASCII 65 -> EBCDIC 0xC1
|
||||
assertEquals('\u00c1', ebcdicChar);
|
||||
byte asciiByte = dataStream.toAscii(ebcdicChar); // EBCDIC 0xC1 -> ASCII 65
|
||||
assertEquals('A', (char) asciiByte);
|
||||
|
||||
// Boundary cases
|
||||
char low = dataStream.toEbcdic(0x10);
|
||||
assertEquals((char) 0x10, low);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnalyzeWriteAndFormat() throws Exception {
|
||||
// Build 3270 Write buffer:
|
||||
// CMD_W (49 / 0x31), WCC (0xC3), SBA (17), Addr(0, 0), SF (29), Attr (0xC1 = MDT set), Text "TEST"
|
||||
byte[] stream = new byte[11];
|
||||
stream[0] = 49; // Write
|
||||
stream[1] = (byte) 0xC3; // WCC
|
||||
stream[2] = 17; // SBA
|
||||
dataStream.encodeAddress(stream, 3, 0);
|
||||
stream[5] = 29; // SF
|
||||
stream[6] = (byte) 0xC1; // Attribute (MDT=1)
|
||||
stream[7] = (byte) 'T';
|
||||
stream[8] = (byte) 'E';
|
||||
stream[9] = (byte) 'S';
|
||||
stream[10] = (byte) 'T';
|
||||
|
||||
dataStream.analyze(stream, stream.length);
|
||||
|
||||
assertEquals(1, screen.fieldCount());
|
||||
Field f = screen.field(1);
|
||||
assertNotNull(f);
|
||||
assertTrue(f.hasAttribute());
|
||||
assertEquals(1, f.dataTag());
|
||||
|
||||
// Now format outbound buffer
|
||||
screen.setAID(AID.enter);
|
||||
screen.setCursor(1, 1);
|
||||
byte[] outBuf = new byte[100];
|
||||
int outLen = dataStream.format(outBuf);
|
||||
|
||||
assertTrue(outLen > 0);
|
||||
assertEquals(AID.enter.translate(), outBuf[0]);
|
||||
// Cursor addr at [1, 2]
|
||||
// SBA (17) at [3]
|
||||
assertEquals(17, outBuf[3]);
|
||||
// Data text starting at [6]
|
||||
assertEquals((byte) 'T', outBuf[6]);
|
||||
assertEquals((byte) 'E', outBuf[7]);
|
||||
assertEquals((byte) 'S', outBuf[8]);
|
||||
assertEquals((byte) 'T', outBuf[9]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferSerialization() throws Exception {
|
||||
// Erase/Write (53), WCC (0), SBA (17), Addr(0), SF (29), Attr(0xC0), Text "OK"
|
||||
byte[] stream = new byte[9];
|
||||
stream[0] = 53; // Erase / Write
|
||||
stream[1] = 0; // WCC
|
||||
stream[2] = 17; // SBA
|
||||
dataStream.encodeAddress(stream, 3, 0);
|
||||
stream[5] = 29; // SF
|
||||
stream[6] = (byte) 0xC0; // Unmodified attribute
|
||||
stream[7] = (byte) 'O';
|
||||
stream[8] = (byte) 'K';
|
||||
|
||||
dataStream.analyze(stream, stream.length);
|
||||
assertEquals(1, screen.fieldCount());
|
||||
|
||||
byte[] outBuf = new byte[100];
|
||||
int len = dataStream.readBuffer(outBuf);
|
||||
assertTrue(len >= 8);
|
||||
assertEquals(screen.getAID().translate(), outBuf[0]);
|
||||
assertEquals(17, outBuf[3]); // SBA
|
||||
assertEquals(29, outBuf[6]); // SF
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEpiScreenBufferBridge() {
|
||||
screen.setWidth(80);
|
||||
screen.setDepth(24);
|
||||
screen.setCursor(2, 5);
|
||||
|
||||
Field f = new Field(screen, 80);
|
||||
f.setAttribute(true);
|
||||
f.setBaseAttribute((char) 0xC8);
|
||||
f.setBytes(0, new byte[]{(byte) 0xC1, (byte) 0xC2}, 2); // 'A', 'B'
|
||||
screen.insertField(f);
|
||||
|
||||
ScreenBuffer sb = new ScreenBuffer();
|
||||
EpiScreenBufferBridge.copyToScreenBuffer(screen, sb);
|
||||
|
||||
assertEquals(80, sb.getCols());
|
||||
assertEquals(24, sb.getRows());
|
||||
assertEquals(84, sb.getCursorAddress()); // row 2 (index 1) * 80 + col 5 (index 4) = 84
|
||||
|
||||
// Reverse copy
|
||||
Screen backScreen = new Screen();
|
||||
EpiScreenBufferBridge.copyFromScreenBuffer(sb, backScreen);
|
||||
assertEquals(80, backScreen.getWidth());
|
||||
assertEquals(24, backScreen.getDepth());
|
||||
assertEquals(2, backScreen.getCursorRow());
|
||||
assertEquals(5, backScreen.getCursorColumn());
|
||||
}
|
||||
}
|
||||
@@ -285,6 +285,79 @@ public class GocaDecoderTest {
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImonGddmImageRendering() throws Exception {
|
||||
GraphicsPlane plane = new GraphicsPlane(1040, 1118);
|
||||
plane.setCharDimensions(13, 26);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
assertEquals(1040, plane.getTotalWidth());
|
||||
assertEquals(1118, plane.getTotalHeight());
|
||||
assertEquals(520, plane.getXMax());
|
||||
assertEquals(558, plane.getYMax());
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// GSCP: Set Current Position (377, 494)
|
||||
out.write(GocaConstants.G_GSCP);
|
||||
out.write(0x04);
|
||||
out.write(0x01); out.write(0x79); // X = 377
|
||||
out.write(0x01); out.write(0xEE); // Y = 494
|
||||
|
||||
// GSCOL: Color 1 = Blue
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x01);
|
||||
|
||||
// G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80
|
||||
out.write(GocaConstants.G_GBIMGC);
|
||||
out.write(0x06);
|
||||
out.write(0x00); out.write(0x00); // flags/format
|
||||
out.write(0x00); out.write(0x50); // w = 80
|
||||
out.write(0x00); out.write(0x50); // h = 80
|
||||
|
||||
// G_GIMD (0x92): 10 bytes scanline
|
||||
out.write(GocaConstants.G_GIMD);
|
||||
out.write(0x0A);
|
||||
out.write(new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF });
|
||||
|
||||
// G_GEIMG (0x93): End Image with 2-byte operand (02 00 00 as sent by IMON)
|
||||
out.write(GocaConstants.G_GEIMG);
|
||||
out.write(0x02);
|
||||
out.write(0x00); out.write(0x00);
|
||||
|
||||
// Subsequent order to verify stream synchronization: GSCOL Color 2 = Red
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x02);
|
||||
|
||||
// G_GBIMGC (0x91): Second image tile
|
||||
out.write(GocaConstants.G_GBIMGC);
|
||||
out.write(0x06);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x50);
|
||||
out.write(0x00); out.write(0x50);
|
||||
|
||||
out.write(GocaConstants.G_GIMD);
|
||||
out.write(0x0A);
|
||||
out.write(new byte[] { (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA });
|
||||
|
||||
out.write(GocaConstants.G_GEIMG);
|
||||
out.write(0x02);
|
||||
out.write(0x00); out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after IMON GOCA decoding");
|
||||
int mappedX = plane.mapX(377);
|
||||
int mappedY = plane.mapY(494);
|
||||
assertEquals(897, mappedX);
|
||||
assertEquals(64, mappedY);
|
||||
|
||||
int[] rgb = plane.getRgbBuffer();
|
||||
int pixelAtImg = rgb[mappedY * 1040 + mappedX];
|
||||
assertNotEquals(0, pixelAtImg, "Pixel at image location should be rendered");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectObjectControlSf24() {
|
||||
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
|
||||
|
||||
@@ -84,6 +84,41 @@ public class TelnetFSMTest {
|
||||
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c4.isTn3270eEnabled());
|
||||
assertEquals("vm.ibm.com", c4.getHost());
|
||||
|
||||
ConnectionConfig c5 = ConnectionConfig.parseHostString("00C2@mvs.hugfreevikings.wtf:1023", 23, TerminalModel.IBM_3279_4);
|
||||
assertEquals("mvs.hugfreevikings.wtf", c5.getHost());
|
||||
assertEquals(1023, c5.getPort());
|
||||
assertEquals("00C2", c5.getLuName());
|
||||
|
||||
ConnectionConfig c6 = ConnectionConfig.parseHostString("L:TSO01@secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c6.isUseTls());
|
||||
assertEquals("secure.mvs.com", c6.getHost());
|
||||
assertEquals(992, c6.getPort());
|
||||
assertEquals("TSO01", c6.getLuName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainTn3270TTypeWithLuName() {
|
||||
config.setTn3270eEnabled(false);
|
||||
config.setLuName("00C2");
|
||||
fsm.onConnected();
|
||||
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
|
||||
// Host sends SB TTYPE SEND
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||
TelnetConstants.TELQUAL_SEND,
|
||||
TelnetConstants.IAC, TelnetConstants.SE);
|
||||
|
||||
boolean foundTtypeLu = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
String s = new String(pkt, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
if (s.contains("IBM-3279-4-E@00C2")) {
|
||||
foundTtypeLu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(foundTtypeLu, "Plain TN3270 TTYPE IS must append @<luName> when configured");
|
||||
assertEquals("00C2", fsm.getConnectedLu());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user