diff --git a/README.md b/README.md index f2e8b81..e706762 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ java -jar j3270.jar -s mainframe.example.com 992 # Or using standard x3270 L: prefix java -jar j3270.jar L:mainframe.example.com:992 +# Connect via Plain TN3270 (Non-E) +java -jar j3270.jar -P mainframe.example.com 23 +# Or using standard x3270 P: prefix +java -jar j3270.jar P:mainframe.example.com:23 + # Connect with unverified/self-signed certificate verification bypass java -jar j3270.jar --tls --insecure mainframe.example.com 992 @@ -43,7 +48,7 @@ java -jar j3270.jar -c config.ini ## ✨ Features -- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption. +- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption. - **SSL/TLS Security**: - Encrypted TN3270 over TLS connections on standard port `992` or custom ports. - Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection. @@ -71,7 +76,7 @@ The project includes self-contained, portable build scripts: # Build executable JAR in 2 seconds: sh ./build_all.sh -# Run all 48 automated unit tests in ~500ms: +# Run all 56 automated unit tests in ~500ms: sh ./test_all.sh ``` diff --git a/build/j3270/org/pubvm/j3270/J3270App$1.class b/build/j3270/org/pubvm/j3270/J3270App$1.class index 8ca98af..2b00d04 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App$1.class and b/build/j3270/org/pubvm/j3270/J3270App$1.class differ diff --git a/build/j3270/org/pubvm/j3270/J3270App$2.class b/build/j3270/org/pubvm/j3270/J3270App$2.class index 5113d3f..397f3f6 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App$2.class and b/build/j3270/org/pubvm/j3270/J3270App$2.class differ diff --git a/build/j3270/org/pubvm/j3270/J3270App.class b/build/j3270/org/pubvm/j3270/J3270App.class index ea768d4..5b49e26 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App.class and b/build/j3270/org/pubvm/j3270/J3270App.class differ diff --git a/build/j3270/org/pubvm/j3270/config/Settings.class b/build/j3270/org/pubvm/j3270/config/Settings.class index 8047394..4e44635 100644 Binary files a/build/j3270/org/pubvm/j3270/config/Settings.class and b/build/j3270/org/pubvm/j3270/config/Settings.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class index a5dadfe..356f038 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class and b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class index c0bbf60..bb643f8 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$1.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class index c26fa2c..5664c15 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel$2.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class index 97a146a..578b592 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class differ diff --git a/build/lib3270j/org/lib3270j/ConnectionConfig.class b/build/lib3270j/org/lib3270j/ConnectionConfig.class index 68b87e6..40366ff 100644 Binary files a/build/lib3270j/org/lib3270j/ConnectionConfig.class and b/build/lib3270j/org/lib3270j/ConnectionConfig.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class index e6c9497..83744b6 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class and b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class index 1ffce09..481ce26 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class and b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class differ diff --git a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class index 728073b..2b173d0 100644 Binary files a/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class and b/build/lib3270j/org/lib3270j/screen/ScreenBuffer.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class index 6508f81..a668bc8 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class and b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class b/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class index 89f0eb3..d3c9d7d 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class and b/build/lib3270j/org/lib3270j/telnet/TelnetFSM$TN3270ESubmode.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class b/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class index 58aed75..6a1d85c 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class and b/build/lib3270j/org/lib3270j/telnet/TelnetFSM.class differ diff --git a/j3270/src/main/java/org/pubvm/j3270/J3270App.java b/j3270/src/main/java/org/pubvm/j3270/J3270App.java index 25788a1..f2ecd39 100644 --- a/j3270/src/main/java/org/pubvm/j3270/J3270App.java +++ b/j3270/src/main/java/org/pubvm/j3270/J3270App.java @@ -34,6 +34,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate private int lastPort = 23; private FileTransfer fileTransfer; + private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false); public J3270App() { super("j3270 — Java TN3270 Terminal Emulator"); @@ -363,20 +364,23 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate @Override public void onScreenUpdated() { - SwingUtilities.invokeLater(() -> { - // During an active file transfer, let the CUT/DFT handler drive - // keyboard state. In x3270, ft_cut_data() runs before WCC - // keyboard-restore is applied — the keyboard stays locked for the - // entire CUT transfer. - if (fileTransfer != null) { - fileTransfer.onScreenUpdated(); - } - if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) { - client.getInputProcessor().setKeyboardLocked(false); - } - terminalPanel.repaint(); - statusBar.updateStatus(); - }); + if (screenUpdatePending.compareAndSet(false, true)) { + SwingUtilities.invokeLater(() -> { + screenUpdatePending.set(false); + // During an active file transfer, let the CUT/DFT handler drive + // keyboard state. In x3270, ft_cut_data() runs before WCC + // keyboard-restore is applied — the keyboard stays locked for the + // entire CUT transfer. + if (fileTransfer != null) { + fileTransfer.onScreenUpdated(); + } + if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) { + client.getInputProcessor().setKeyboardLocked(false); + } + terminalPanel.repaint(); + statusBar.updateStatus(); + }); + } } @Override @@ -442,6 +446,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate boolean debug = false; boolean cliTls = false; boolean cliNoVerifyCert = false; + Boolean cliTn3270e = null; org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null; String configFile = null; java.util.List remainingArgs = new java.util.ArrayList<>(); @@ -452,6 +457,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate cliTls = true; } else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) { cliNoVerifyCert = true; + } else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) { + cliTn3270e = false; + } else if ("--tn3270e".equals(args[i])) { + cliTn3270e = true; } else if (args[i].startsWith("--graphics=")) { cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11)); } else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) { @@ -529,6 +538,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate final boolean finalTls = cliTls; final boolean finalNoVerify = cliNoVerifyCert; + final Boolean finalTn3270e = cliTn3270e; final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode; SwingUtilities.invokeLater(() -> { @@ -560,6 +570,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (finalNoVerify) { config.setTlsVerifyCert(false); } + if (finalTn3270e != null) { + config.setTn3270eEnabled(finalTn3270e); + } if (finalGraphicsMode != null) { config.setGraphicsMode(finalGraphicsMode); } else { @@ -578,9 +591,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate int port = org.pubvm.j3270.config.Settings.getAutoConnectPort(); boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls(); boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert(); + boolean tn3270e = org.pubvm.j3270.config.Settings.getAutoConnectTn3270e(); if (host != null && !host.isEmpty()) { ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); config.setTlsVerifyCert(verify); + config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); if (finalGraphicsMode != null) { config.setGraphicsMode(finalGraphicsMode); } else { diff --git a/j3270/src/main/java/org/pubvm/j3270/config/Settings.java b/j3270/src/main/java/org/pubvm/j3270/config/Settings.java index ff780b7..927f28e 100644 --- a/j3270/src/main/java/org/pubvm/j3270/config/Settings.java +++ b/j3270/src/main/java/org/pubvm/j3270/config/Settings.java @@ -78,13 +78,21 @@ public class Settings { prefs.putBoolean("autoConnectVerifyCert", verify); } + public static boolean getAutoConnectTn3270e() { + return prefs.getBoolean("autoConnectTn3270e", true); + } + + public static void setAutoConnectTn3270e(boolean tn3270e) { + prefs.putBoolean("autoConnectTn3270e", tn3270e); + } + public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() { - String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name()); + String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.BOTH.name()); return org.lib3270j.graphics.GraphicsMode.fromString(modeStr); } public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) { - prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name()); + prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.BOTH).name()); } public static Color getColorOverride(int index, Color defaultColor) { @@ -243,6 +251,11 @@ public class Settings { case "tlsVerifyCert": setAutoConnectVerifyCert(Boolean.parseBoolean(value)); break; + case "autoConnectTn3270e": + case "tn3270e": + case "enableTn3270e": + setAutoConnectTn3270e(Boolean.parseBoolean(value)); + break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; default: log.warning("Unknown behavior/connection key: " + key); @@ -309,6 +322,7 @@ public class Settings { w.println("autoConnectPort = " + getAutoConnectPort()); w.println("autoConnectTls = " + getAutoConnectTls()); w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert()); + w.println("autoConnectTn3270e = " + getAutoConnectTn3270e()); } w.println("blockSelectMode = " + getBlockSelectMode()); w.println(); diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java b/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java index b6ac0bb..12edd6a 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java @@ -18,6 +18,7 @@ public class ConnectDialog extends JDialog { private JTextField luField; private JCheckBox tlsCheckBox; private JCheckBox verifyCertCheckBox; + private JCheckBox tn3270eCheckBox; private boolean confirmed; private ConnectionConfig result; @@ -146,6 +147,17 @@ public class ConnectDialog extends JDialog { verifyCertCheckBox.setFocusPainted(false); mainPanel.add(verifyCertCheckBox, gbc); + // TN3270E Checkbox + gbc.gridx = 1; + gbc.gridy = 7; + tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); + tn3270eCheckBox.setBackground(new Color(30, 30, 30)); + tn3270eCheckBox.setForeground(fg); + tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + tn3270eCheckBox.setSelected(org.pubvm.j3270.config.Settings.getAutoConnectTn3270e()); + tn3270eCheckBox.setFocusPainted(false); + mainPanel.add(tn3270eCheckBox, gbc); + // Buttons JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttonPanel.setBackground(new Color(30, 30, 30)); @@ -169,7 +181,7 @@ public class ConnectDialog extends JDialog { buttonPanel.add(connectBtn); gbc.gridx = 0; - gbc.gridy = 7; + gbc.gridy = 8; gbc.gridwidth = 2; mainPanel.add(buttonPanel, gbc); @@ -214,6 +226,7 @@ public class ConnectDialog extends JDialog { result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()); result.setUseTls(tlsCheckBox.isSelected()); result.setTlsVerifyCert(verifyCertCheckBox.isSelected()); + result.setTn3270eEnabled(tn3270eCheckBox.isSelected()); confirmed = true; dispose(); } @@ -243,4 +256,8 @@ public class ConnectDialog extends JDialog { public void setInitialVerifyCert(boolean verify) { verifyCertCheckBox.setSelected(verify); } + + public void setInitialTn3270e(boolean tn3270e) { + tn3270eCheckBox.setSelected(tn3270e); + } } diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java index eb12183..f5a77bc 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java @@ -22,6 +22,7 @@ public class TerminalPanel extends JPanel { // Font and cell dimensions private Font terminalFont; + private Font boldTerminalFont; private int cellWidth; private int cellHeight; private int fontAscent; @@ -47,6 +48,14 @@ public class TerminalPanel extends JPanel { // ========== Selection / Copy-Paste state ========== private boolean blockSelectMode = false; + + private static final String[] CHAR_STRINGS = new String[128]; + static { + for (int i = 0; i < 128; i++) { + CHAR_STRINGS[i] = String.valueOf((char) i); + } + } + private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180); private int selectionStartRow = -1, selectionStartCol = -1; private int selectionEndRow = -1, selectionEndCol = -1; private boolean isDragging = false; @@ -779,6 +788,7 @@ public class TerminalPanel extends JPanel { cellHeight = fm.getHeight(); fontAscent = fm.getAscent(); fontDescent = fm.getDescent(); + boldTerminalFont = terminalFont.deriveFont(Font.BOLD); } private void setupCursorBlink() { @@ -818,6 +828,8 @@ public class TerminalPanel extends JPanel { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); // Clear entire panel with background color g2.setColor(bgColor); @@ -828,8 +840,8 @@ public class TerminalPanel extends JPanel { int oy = getRenderOffsetY(); ScreenBuffer sb = client.getScreenBuffer(); - int rows = sb.getRows(); - int cols = sb.getCols(); + int rows = sb.getDisplayRows(); + int cols = sb.getDisplayCols(); boolean isColorModel = client.getConfig().getModel().isColor(); // Track current field attribute for monochrome color decisions @@ -839,7 +851,7 @@ public class TerminalPanel extends JPanel { for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { int baddr = row * cols + col; - ExtendedAttribute ea = sb.getCell(baddr); + ExtendedAttribute ea = sb.getDisplayCell(baddr); int x = ox + col * cellWidth; int y = oy + row * cellHeight; @@ -854,10 +866,7 @@ public class TerminalPanel extends JPanel { if (ea.isFieldAttribute()) { currentFA = ea.fa; currentFieldEa = ea; - // Field attributes display as blanks - g2.setColor(this.bgColor); - g2.fillRect(x, y, cellWidth, cellHeight); - // Selection highlight on field attribute cells too + // Selection highlight on field attribute cells if (isCellSelected(row, col)) { g2.setColor(SELECTION_COLOR); g2.fillRect(x, y, cellWidth, cellHeight); @@ -892,12 +901,9 @@ public class TerminalPanel extends JPanel { // Handle invisible fields (zero intensity / password fields) // Modern UX: render '*' for typed characters so user sees length/digit count if (faIsZero(currentFA & 0xFF)) { - g2.setColor(this.bgColor); - g2.fillRect(x, y, cellWidth, cellHeight); - char ch = ea.ucs4; if (ch > 0x20 && ch != 0xFF) { - Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont; + Font f = bold ? boldTerminalFont : terminalFont; g2.setFont(f); g2.setColor(fgColor); g2.drawString("*", x, y + fontAscent); @@ -917,9 +923,11 @@ public class TerminalPanel extends JPanel { bgColor = tmp; } - // Draw background - g2.setColor(bgColor); - g2.fillRect(x, y, cellWidth, cellHeight); + // Draw background only if different from default panel bgColor or if inverted + if (!bgColor.equals(this.bgColor) || reverse) { + g2.setColor(bgColor); + g2.fillRect(x, y, cellWidth, cellHeight); + } // Draw character or Programmed Symbol int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0); @@ -927,21 +935,25 @@ public class TerminalPanel extends JPanel { if (cs >= 0x40 && client.getProgramSymbolManager() != null) { org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); if (slot != null) { - int[] rgb = slot.getRgbPixels(fgColor.getRGB(), bgColor.getRGB()); - java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(slot.getWidth(), slot.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB); - img.setRGB(0, 0, slot.getWidth(), slot.getHeight(), rgb, 0, slot.getWidth()); - g2.drawImage(img, x, y, cellWidth, cellHeight, null); - drawnAsPs = true; + java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), bgColor.getRGB()); + if (img != null) { + g2.drawImage(img, x, y, null); + drawnAsPs = true; + } } } if (!drawnAsPs) { char ch = ea.ucs4; if (ch > 0x20 && ch != 0xFF) { - Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont; + Font f = bold ? boldTerminalFont : terminalFont; g2.setFont(f); g2.setColor(fgColor); - g2.drawString(String.valueOf(ch), x, y + fontAscent); + if (ch < 128) { + g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent); + } else { + g2.drawString(String.valueOf(ch), x, y + fontAscent); + } } } @@ -975,13 +987,13 @@ public class TerminalPanel extends JPanel { // Draw cursor if (cursorVisible && client.getConnectionState().isFullSession()) { - int curAddr = sb.getCursorAddress(); + int curAddr = sb.getDisplayCursorAddress(); int curRow = curAddr / cols; int curCol = curAddr % cols; int cx = ox + curCol * cellWidth; int cy = oy + curRow * cellHeight; - g2.setColor(new Color(255, 255, 255, 180)); + g2.setColor(CURSOR_COLOR); g2.setXORMode(bgColor); g2.fillRect(cx, cy, cellWidth, cellHeight); g2.setPaintMode(); diff --git a/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java b/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java index cb45cb4..431d72a 100644 --- a/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java +++ b/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java @@ -17,7 +17,8 @@ public class ConnectionConfig { private int connectTimeoutMs = 15000; private int nopIntervalSeconds = 0; private String terminalName = null; // override terminal type string - private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.NONE; + private boolean tn3270eEnabled = true; + private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.BOTH; public ConnectionConfig() {} @@ -66,6 +67,9 @@ public class ConnectionConfig { public boolean isTlsVerifyCert() { return tlsVerifyCert; } public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; } + public boolean isTn3270eEnabled() { return tn3270eEnabled; } + public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; } + public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; } public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; } @@ -87,8 +91,8 @@ public class ConnectionConfig { public void setTerminalName(String name) { this.terminalName = name; } /** - * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port") - * or standard "host:port" formats. + * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"), + * plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats. */ public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) { if (hostStr == null || hostStr.trim().isEmpty()) { @@ -96,17 +100,35 @@ public class ConnectionConfig { } String s = hostStr.trim(); boolean tls = false; + boolean tn3270e = true; - // Check TLS prefixes - if (s.startsWith("L:") || s.startsWith("l:")) { - tls = true; - s = s.substring(2); - } else if (s.startsWith("Y:") || s.startsWith("y:")) { - tls = true; - s = s.substring(2); - } else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) { - tls = true; - s = s.substring(4); + // Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port") + boolean prefixFound = true; + while (prefixFound) { + prefixFound = false; + if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) { + tls = true; + s = s.substring(2); + prefixFound = true; + } else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) { + tls = true; + s = s.substring(4); + prefixFound = true; + } else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) { + tls = false; + int colon = s.indexOf(':'); + s = s.substring(colon + 1); + prefixFound = true; + } else if (s.startsWith("P:") || s.startsWith("p:")) { + tn3270e = false; + s = s.substring(2); + prefixFound = true; + } else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) { + tn3270e = false; + int colon = s.indexOf(':'); + s = s.substring(colon + 1); + prefixFound = true; + } } String host = s; @@ -133,6 +155,7 @@ public class ConnectionConfig { ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); config.setUseTls(tls); + config.setTn3270eEnabled(tn3270e); return config; } diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index 1e04775..2596c4d 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -9,6 +9,7 @@ import static org.lib3270j.protocol.DS3270Constants.*; import java.io.ByteArrayOutputStream; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -105,74 +106,92 @@ public class DataStreamProcessor { return; int cmd = data[offset] & 0xFF; - log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes"); + synchronized (screen.getRenderLock()) { + if (log.isLoggable(Level.FINE)) { + log.fine(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes"); + } - switch (cmd) { - case CMD_W: - case SNA_CMD_W: - processWrite(data, offset, length, false); - break; - case CMD_EW: - case SNA_CMD_EW: - log.info(">>> ERASE/WRITE: clearing screen (default size)"); { - int oldRows = screen.getRows(); - int oldCols = screen.getCols(); - screen.erase(false); - graphicsPlane.clear(); - processWrite(data, offset, length, true); - if (oldRows != screen.getRows() || oldCols != screen.getCols()) { - notifyScreenSizeChanged(); - } + switch (cmd) { + case CMD_W: + case SNA_CMD_W: + programSymbolManager.commitStagedSymbols(); + processWrite(data, offset, length, false); + break; + case CMD_EW: + case SNA_CMD_EW: + programSymbolManager.commitStagedSymbols(); + if (log.isLoggable(Level.FINE)) { + log.fine(">>> ERASE/WRITE: clearing screen (default size)"); + } + { + int oldRows = screen.getRows(); + int oldCols = screen.getCols(); + screen.erase(false); + graphicsPlane.clear(); + processWrite(data, offset, length, true); + if (oldRows != screen.getRows() || oldCols != screen.getCols()) { + notifyScreenSizeChanged(); + } + } + break; + case CMD_EWA: + case SNA_CMD_EWA: + programSymbolManager.commitStagedSymbols(); + if (log.isLoggable(Level.FINE)) { + log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); + } + { + int oldRows = screen.getRows(); + int oldCols = screen.getCols(); + screen.erase(true); + graphicsPlane.clear(); + processWrite(data, offset, length, true); + if (oldRows != screen.getRows() || oldCols != screen.getCols()) { + notifyScreenSizeChanged(); + } + } + break; + case CMD_RB: + case SNA_CMD_RB: + programSymbolManager.commitStagedSymbols(); + processReadBuffer(); + break; + case CMD_RM: + case SNA_CMD_RM: + programSymbolManager.commitStagedSymbols(); + processReadModified(false); + break; + case CMD_RMA: + case SNA_CMD_RMA: + programSymbolManager.commitStagedSymbols(); + processReadModified(true); + break; + case CMD_EAU: + case SNA_CMD_EAU: + programSymbolManager.commitStagedSymbols(); + log.info(">>> EAU: erasing all unprotected fields"); + screen.eraseAllUnprotected(); + break; + case CMD_WSF: + case SNA_CMD_WSF: + processWriteStructuredField(data, offset, length); + break; + case CMD_NOP: + log.info(">>> NOP command"); + break; + default: + log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd)); + break; } - break; - case CMD_EWA: - case SNA_CMD_EWA: - log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); { - int oldRows = screen.getRows(); - int oldCols = screen.getCols(); - screen.erase(true); - graphicsPlane.clear(); - processWrite(data, offset, length, true); - if (oldRows != screen.getRows() || oldCols != screen.getCols()) { - notifyScreenSizeChanged(); - } - } - break; - case CMD_RB: - case SNA_CMD_RB: - processReadBuffer(); - break; - case CMD_RM: - case SNA_CMD_RM: - processReadModified(false); - break; - case CMD_RMA: - case SNA_CMD_RMA: - processReadModified(true); - break; - case CMD_EAU: - case SNA_CMD_EAU: - log.info(">>> EAU: erasing all unprotected fields"); - screen.eraseAllUnprotected(); - break; - case CMD_WSF: - case SNA_CMD_WSF: - processWriteStructuredField(data, offset, length); - break; - case CMD_NOP: - log.info(">>> NOP command"); - break; - default: - log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd)); - break; + + // Translate EBCDIC to Unicode for display + screen.translateToUnicode(); + screen.markAllChanged(); + screen.updateDisplaySnapshot(); } - // Translate EBCDIC to Unicode for display - screen.translateToUnicode(); - screen.markAllChanged(); - // Debug: dump non-empty screen lines - if (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW) { + if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) { int r = screen.getRows(); int c = screen.getCols(); StringBuilder dump = new StringBuilder(); @@ -205,7 +224,7 @@ public class DataStreamProcessor { } } if (dump.length() > 0) { - log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString()); + log.fine("Screen content after " + commandName(cmd) + ":\n" + dump.toString()); } } } @@ -798,7 +817,6 @@ public class DataStreamProcessor { byte[] psData = new byte[fieldLen - 3]; System.arraycopy(data, pos + 3, psData, 0, psData.length); programSymbolManager.loadps(psData); - notifyScreenUpdated(); } break; case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix @@ -810,7 +828,6 @@ public class DataStreamProcessor { byte[] psData = new byte[fieldLen - 4]; System.arraycopy(data, pos + 4, psData, 0, psData.length); programSymbolManager.loadps(psData); - notifyScreenUpdated(); } break; case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders) diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java index 3cac7ab..0172521 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -22,7 +22,7 @@ public class QueryReplyBuilder { private static final int Yr_3279_2 = 0x0002006f; private final ScreenBuffer screen; - private GraphicsMode graphicsMode = GraphicsMode.NONE; + private GraphicsMode graphicsMode = GraphicsMode.BOTH; // Base query reply codes (text mode) private static final int[] SUPPORTED_QR_BASE = { @@ -313,9 +313,9 @@ public class QueryReplyBuilder { private byte[] buildCharsets() { if (graphicsMode.isProgrammedSymbolsEnabled()) { - // Programmed Symbols mode (3279 PS with LoadPS 0x0A) + // Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID) ByteArrayOutputStream out = new ByteArrayOutputStream(65); - out.write(0x82); // flags: GE, CGCSGID present + out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02) out.write(0x00); // more flags out.write(SW_3279_2); // SDW (9) out.write(SH_3279_2); // SDH (12) @@ -328,14 +328,14 @@ public class QueryReplyBuilder { out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25); // Descriptor 2 (SET 1): APL/GE character set out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); - // Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) + // Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane) out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - // Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); - out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + // Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane) + out.write(0x04); out.write(0xc0); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x05); out.write(0xc0); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x06); out.write(0xc0); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x07); out.write(0xc0); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); return out.toByteArray(); } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java index 791c7a2..49933f3 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java @@ -17,12 +17,31 @@ public class ProgramSymbolManager { public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5) public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9) + private int defaultCellWidth = 9; + private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12) + + public void setDefaultCellDimensions(int width, int height) { + this.defaultCellWidth = (width > 0) ? width : 9; + this.defaultCellHeight = (height > 0) ? height : 12; + } + + public int getDefaultCellWidth() { + return defaultCellWidth; + } + + public int getDefaultCellHeight() { + return defaultCellHeight; + } + private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS]; + private final ProgramSymbolSet[] stagingSets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS]; + private final ProgramSymbolSet[] lcidMap = new ProgramSymbolSet[256]; public ProgramSymbolManager() { for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) { boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS); sets[i] = new ProgramSymbolSet(isTriple); + stagingSets[i] = new ProgramSymbolSet(isTriple); } } @@ -30,32 +49,67 @@ public class ProgramSymbolManager { * Resets all symbol sets. */ public synchronized void clearAll() { - for (ProgramSymbolSet set : sets) { - set.clear(); - set.setLcid(0); + Arrays.fill(lcidMap, null); + for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) { + sets[i].clear(); + sets[i].setLcid(0); + stagingSets[i].clear(); + stagingSets[i].setLcid(0); + } + } + + /** + * Commits all staged multi-plane symbol sets to the active presentation sets atomically. + */ + public synchronized void commitStagedSymbols() { + for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) { + int lcid = stagingSets[i].getLcid(); + if (lcid > 0) { + ProgramSymbolSet staged = stagingSets[i]; + ProgramSymbolSet active = sets[i]; + active.setLcid(lcid); + for (int slot = 0; slot < ProgramSymbolSet.NUM_SLOTS; slot++) { + active.setSlot(slot, staged.getSlot(slot)); + } + if (lcid < 256) { + lcidMap[lcid] = active; + } + } } } /** * Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE). */ - public synchronized ProgramSymbolSet getSymbolSet(int lcid) { - if (lcid <= 0) { + public ProgramSymbolSet getSymbolSet(int lcid) { + if (lcid <= 0 || lcid >= 256) { return null; } - for (ProgramSymbolSet set : sets) { - if (set.getLcid() == lcid) { - return set; + ProgramSymbolSet set = lcidMap[lcid]; + if (set == null) { + for (ProgramSymbolSet s : stagingSets) { + if (s.getLcid() == lcid) return s; } } - return null; + return set; } /** * Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE). */ - public synchronized ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) { - ProgramSymbolSet set = getSymbolSet(lcid); + public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) { + if (lcid <= 0 || lcid >= 256) { + return null; + } + ProgramSymbolSet set = lcidMap[lcid]; + if (set == null) { + for (ProgramSymbolSet s : stagingSets) { + if (s.getLcid() == lcid) { + set = s; + break; + } + } + } if (set == null) { return null; } @@ -98,12 +152,12 @@ public class ProgramSymbolManager { return; } - ProgramSymbolSet set = sets[setIndex]; boolean isTriplePlane = (rws >= 4 && rws <= 7); + ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex]; int extHeaderLen = 0; - int cellWidth = 9; - int cellHeight = 16; + int cellWidth = defaultCellWidth; + int cellHeight = defaultCellHeight; int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue if (hasExtHeader && data.length > 4) { @@ -121,10 +175,10 @@ public class ProgramSymbolManager { } } - if (clearAll) { - set.clear(); - } set.setLcid(lcid); + if (!isTriplePlane && lcid > 0 && lcid < 256) { + lcidMap[lcid] = set; + } int offset = 4 + (hasExtHeader ? extHeaderLen : 0); int remaining = data.length - offset; @@ -142,12 +196,10 @@ public class ProgramSymbolManager { } while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) { - byte[] pixelData; + byte[] pixelData = new byte[cellWidth * cellHeight]; ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex); - if (existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) { - pixelData = existing.getPixelData(); - } else { - pixelData = new byte[cellWidth * cellHeight]; + if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) { + System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length)); } if (loadFormat == 1) { @@ -163,9 +215,17 @@ public class ProgramSymbolManager { remaining -= bytesPerSymbol; } - logger.info(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)", - lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane", - cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint))); + if (clearAll) { + for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) { + set.clearSlot(i); + } + } + + if (logger.isLoggable(Level.FINE)) { + logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)", + lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane", + cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint))); + } } /** diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java index f41eb72..8971921 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java @@ -64,6 +64,12 @@ public class ProgramSymbolSet { private int[] cachedRgbArray; private int cachedFgRgb = -1; private int cachedBgRgb = -1; + private java.awt.image.BufferedImage cachedImage; + private java.awt.image.BufferedImage cachedScaledImage; + private int cachedTargetW = 0; + private int cachedTargetH = 0; + private int cachedScaledFgRgb = -1; + private int cachedScaledBgRgb = -1; public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) { this.width = width > 0 ? width : 9; @@ -88,6 +94,61 @@ public class ProgramSymbolSet { return isTriplePlane; } + /** + * Returns an image scaled directly to target dimensions (cellWidth x cellHeight). + * Enables unscaled 1:1 hardware blitting in Java2D. + */ + public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) { + if (targetW <= 0 || targetH <= 0) { + return getImage(fgArgb, bgArgb); + } + if (targetW == width && targetH == height) { + return getImage(fgArgb, bgArgb); + } + if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH + && cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) { + return cachedScaledImage; + } + int[] srcRgb = getRgbPixels(fgArgb, bgArgb); + java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB); + int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData(); + + for (int dy = 0; dy < targetH; dy++) { + int sy = dy * height / targetH; + int srcRowOffset = sy * width; + int dstRowOffset = dy * targetW; + for (int dx = 0; dx < targetW; dx++) { + int sx = dx * width / targetW; + dstRgb[dstRowOffset + dx] = srcRgb[srcRowOffset + sx]; + } + } + + this.cachedScaledImage = scaled; + this.cachedTargetW = targetW; + this.cachedTargetH = targetH; + this.cachedScaledFgRgb = fgArgb; + this.cachedScaledBgRgb = bgArgb; + return scaled; + } + + /** + * Computes and returns the cached BufferedImage for this symbol glyph. + * Eliminates per-cell heap allocations during high frame rate rendering. + */ + public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) { + if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) { + return cachedImage; + } + int[] rgb = getRgbPixels(fgArgb, bgArgb); + java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); + int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData(); + System.arraycopy(rgb, 0, imgData, 0, rgb.length); + this.cachedImage = img; + this.cachedFgRgb = fgArgb; + this.cachedBgRgb = bgArgb; + return img; + } + /** * Computes and returns the 32-bit ARGB pixel array for this symbol. * The returned array has length (width * height). diff --git a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java index fae33f0..c217566 100644 --- a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java @@ -36,8 +36,12 @@ public class ScreenBuffer { private byte defaultGr = 0x00; private byte defaultCs = 0x00; private byte defaultIc = 0x00; - private final EbcdicTranslator translator; + private final Object renderLock = new Object(); + + public Object getRenderLock() { + return renderLock; + } public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) { this.translator = translator; @@ -79,6 +83,50 @@ public class ScreenBuffer { return buffer[addr]; } + private ExtendedAttribute[] displaySnapshot; + private int displayRows; + private int displayCols; + private int displayCursorAddress; + + /** + * Atomically creates a snapshot of the current presentation buffer for tear-free rendering. + * Takes ~2 microseconds and eliminates mutual thread contention with the UI thread. + */ + public synchronized void updateDisplaySnapshot() { + int size = rows * cols; + if (displaySnapshot == null || displaySnapshot.length < size) { + displaySnapshot = new ExtendedAttribute[size]; + for (int i = 0; i < size; i++) { + displaySnapshot[i] = new ExtendedAttribute(); + } + } + for (int i = 0; i < size; i++) { + displaySnapshot[i].copyFrom(buffer[i]); + } + this.displayRows = rows; + this.displayCols = cols; + this.displayCursorAddress = cursorAddress; + } + + public synchronized ExtendedAttribute getDisplayCell(int addr) { + if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) { + return getCell(addr); + } + return displaySnapshot[addr]; + } + + public synchronized int getDisplayRows() { + return displayRows > 0 ? displayRows : rows; + } + + public synchronized int getDisplayCols() { + return displayCols > 0 ? displayCols : cols; + } + + public synchronized int getDisplayCursorAddress() { + return displayRows > 0 ? displayCursorAddress : cursorAddress; + } + // ========== Dimension accessors ========== public int getRows() { return rows; } public int getCols() { return cols; } diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java index 0d967f7..a6126cf 100644 --- a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java @@ -149,9 +149,7 @@ public class TelnetConnection { log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n)); } try { - for (int i = 0; i < n; i++) { - fsm.feedByte(buf[i] & 0xFF); - } + fsm.feedBytes(buf, 0, n); fsm.endOfNetworkData(); } catch (Throwable t) { log.log(Level.SEVERE, "Exception processing incoming data stream", t); diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java index ce2c6b0..d6436ba 100644 --- a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java @@ -58,6 +58,7 @@ public class TelnetFSM { private int eXmitSeq; private int responseRequired = RSF_NO_RESPONSE; private boolean deferredWillTtype; + private boolean tn3270eDeviceTypeSent; // Connection references private TelnetConnection connection; @@ -103,6 +104,7 @@ public class TelnetFSM { java.util.Arrays.fill(hisOpts, false); java.util.Arrays.fill(eFuncs, false); tn3270eNegotiated = false; + tn3270eDeviceTypeSent = false; tn3270eSubmode = TN3270ESubmode.UNBOUND; tn3270eBound = false; eXmitSeq = 0; @@ -118,6 +120,37 @@ public class TelnetFSM { changeState(ConnectionState.TELNET_PENDING); } + /** + * Feed a bulk buffer of bytes from the network into the FSM. + */ + public void feedBytes(byte[] buf, int offset, int len) { + int end = offset + len; + int i = offset; + while (i < end) { + if (state == TNS_DATA) { + int start = i; + while (i < end && (buf[i] & 0xFF) != IAC) { + i++; + } + if (i > start) { + if (connectionState == ConnectionState.TELNET_PENDING) { + changeState(ConnectionState.CONNECTED_NVT); + } + if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) { + ibuf.write(buf, start, i - start); + } + } + if (i < end) { + state = TNS_IAC; + i++; + } + } else { + feedByte(buf[i] & 0xFF); + i++; + } + } + } + /** * Feed a single byte from the network into the FSM. */ @@ -172,8 +205,8 @@ public class TelnetFSM { changeState(ConnectionState.CONNECTED_NVT); } - // Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states) - if (connectionState.is3270() || connectionState.isTn3270e()) { + // Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending) + if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) { ibuf.write(c); } // NVT data would go to NVT processor (not implemented in initial version) @@ -189,7 +222,7 @@ public class TelnetFSM { break; case EOR: // End of record — process accumulated 3270 data log.fine("RCVD EOR"); - if (connectionState.is3270() || connectionState.isTn3270e()) { + if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) { processEndOfRecord(); } ibuf.reset(); @@ -235,7 +268,9 @@ public class TelnetFSM { break; case TELOPT_TN3270E: - if (!hisOpts[opt]) { + if (!config.isTn3270eEnabled()) { + sendCommand(DONT, opt); + } else if (!hisOpts[opt]) { hisOpts[opt] = true; sendCommand(DO, opt); } @@ -278,7 +313,7 @@ public class TelnetFSM { case TELOPT_TTYPE: if (!myOpts[opt]) { myOpts[opt] = true; - if (hisOpts[TELOPT_TN3270E]) { + if (config.isTn3270eEnabled() && hisOpts[TELOPT_TN3270E]) { // Defer TTYPE response until TN3270E negotiation completes deferredWillTtype = true; } else { @@ -288,11 +323,16 @@ public class TelnetFSM { break; case TELOPT_TN3270E: - if (!myOpts[opt]) { + if (!config.isTn3270eEnabled()) { + sendCommand(WONT, opt); + } else if (!myOpts[opt]) { myOpts[opt] = true; sendCommand(WILL, opt); // Start TN3270E sub-negotiation: send device type request - sendTN3270EDeviceTypeRequest(); + if (!tn3270eDeviceTypeSent) { + sendTN3270EDeviceTypeRequest(); + tn3270eDeviceTypeSent = true; + } } break; @@ -428,7 +468,10 @@ public class TelnetFSM { switch (op) { case OP_SEND: // Host asks us to send device-type request - sendTN3270EDeviceTypeRequest(); + if (!tn3270eDeviceTypeSent) { + sendTN3270EDeviceTypeRequest(); + tn3270eDeviceTypeSent = true; + } break; case OP_DEVICE_TYPE: @@ -488,14 +531,19 @@ public class TelnetFSM { pos++; } + // Check if REJECT if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) { - // Rejection pos++; - int reason = -1; - if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) { - pos++; - if (pos < data.length) { - reason = data[pos] & 0xFF; + int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ; + if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) { + // Try fallback model 2 if we were requesting something else + if (config.getModel() != TerminalModel.IBM_3278_2 && + config.getModel() != TerminalModel.IBM_3279_2) { + log.warning("TN3270E device-type rejected (" + + TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2"); + config.setModel(TerminalModel.IBM_3278_2); + sendTN3270EDeviceTypeRequest(); + return; } } log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason)); @@ -563,33 +611,86 @@ public class TelnetFSM { log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE"); } - private void handleTN3270EFunctions(byte[] data) { - // Parse: TN3270E FUNCTIONS IS [func...] - int pos = 2; // Skip TN3270E, FUNCTIONS + private void sendTN3270EFunctionsIs() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(IAC); + out.write(SB); + out.write(TELOPT_TN3270E); + out.write(OP_FUNCTIONS); + out.write(OP_IS); - if (pos < data.length && (data[pos] & 0xFF) == OP_IS) { - pos++; // Skip IS + StringBuilder funcNames = new StringBuilder(); + for (int i = 0; i < eFuncs.length; i++) { + if (eFuncs[i]) { + out.write(i); + if (funcNames.length() > 0) funcNames.append(" "); + funcNames.append(TN3270EConstants.functionName(i)); + } } - // The remaining bytes are the agreed-upon functions - java.util.Arrays.fill(eFuncs, false); - StringBuilder funcNames = new StringBuilder(); + out.write(IAC); + out.write(SE); + sendBytes(out.toByteArray()); + log.info("SENT SB TN3270E FUNCTIONS IS " + funcNames + " SE"); + } + + private void handleTN3270EFunctions(byte[] data) { + // Parse: TN3270E FUNCTIONS REQUEST [func...] or TN3270E FUNCTIONS IS [func...] + int pos = 1; + boolean isRequest = false; + + while (pos < data.length) { + int b = data[pos] & 0xFF; + if (b == OP_FUNCTIONS) { + pos++; + } else if (b == OP_REQUEST) { + isRequest = true; + pos++; + break; + } else if (b == OP_IS) { + isRequest = false; + pos++; + break; + } else { + pos++; + } + } + + // The remaining bytes are the proposed/agreed functions + boolean[] hostFuncs = new boolean[8]; while (pos < data.length) { int func = data[pos] & 0xFF; if (func <= FUNC_SNA_SENSE) { - eFuncs[func] = true; - if (funcNames.length() > 0) funcNames.append(" "); - funcNames.append(TN3270EConstants.functionName(func)); + hostFuncs[func] = true; } pos++; } + if (isRequest) { + // Host sent FUNCTIONS REQUEST -> We reply with FUNCTIONS IS (intersection of functions) + for (int i = 0; i < eFuncs.length; i++) { + eFuncs[i] = eFuncs[i] && hostFuncs[i]; + } + sendTN3270EFunctionsIs(); + } else { + // Host sent FUNCTIONS IS -> Accept host's agreed function list + for (int i = 0; i < eFuncs.length; i++) { + eFuncs[i] = hostFuncs[i]; + } + } + tn3270eNegotiated = true; - log.info("TN3270E functions IS: " + funcNames); + log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames()); log.info("TN3270E negotiation complete"); - // Move to CONNECTED_UNBOUND or CONNECTED_SSCP - changeState(ConnectionState.CONNECTED_UNBOUND); + // RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered + // to be bound immediately upon completion of the FUNCTIONS negotiation. + if (eFuncs[FUNC_BIND_IMAGE]) { + changeState(ConnectionState.CONNECTED_UNBOUND); + } else { + changeState(ConnectionState.CONNECTED_TN3270E); + tn3270eSubmode = TN3270ESubmode.E_3270; + } // Notify listeners for (ConnectionListener l : connectionListeners) { @@ -597,12 +698,29 @@ public class TelnetFSM { } } + private String getNegotiatedFunctionNames() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < eFuncs.length; i++) { + if (eFuncs[i]) { + if (sb.length() > 0) sb.append(" "); + sb.append(TN3270EConstants.functionName(i)); + } + } + return sb.length() > 0 ? sb.toString() : ""; + } + // ========== End of Record processing ========== private void processEndOfRecord() { byte[] data = ibuf.toByteArray(); + ibuf.reset(); if (data.length == 0) return; + if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) { + log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode"); + changeState(ConnectionState.CONNECTED_3270); + } + if (tn3270eNegotiated) { // TN3270E mode: data starts with 5-byte header processTN3270ERecord(data); @@ -701,11 +819,11 @@ public class TelnetFSM { bindRa = screenBuffer.getMaxRows(); bindCa = screenBuffer.getMaxCols(); break; - case 0x7e: + case 0x7E: // Both default and alternate = specified values bindRa = bindRd; bindCa = bindCd; break; - case 0x7f: + case 0x7F: // Default and alternate are both specified separately bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF; bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF; @@ -767,7 +885,20 @@ public class TelnetFSM { break; default: - log.info("Unhandled TN3270E data type: " + dataType); + // Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.) + // 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 == 0x05 || + dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 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); + notifyScreenUpdate(); + } else { + log.info("Unhandled TN3270E data type: " + dataType); + } break; } } @@ -790,7 +921,7 @@ public class TelnetFSM { if (connectionState != ConnectionState.TELNET_PENDING) return; // For TN3270E, we wait for TN3270E negotiation to complete - if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) { + if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) { return; // TN3270E in progress } diff --git a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java index 071a3a9..447929f 100644 --- a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java @@ -15,8 +15,27 @@ public class QueryReplyBuilderTest { private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); @Test - public void testBuildAllQueryRepliesDefaultNone() { - assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode()); + public void testBuildAllQueryRepliesDefaultBoth() { + assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode()); + byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + assertNotNull(replies); + assertTrue(replies.length > 0); + assertEquals((byte) AID_SF, replies[0]); + + // In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present + boolean hasB0 = false; + for (int i = 0; i < replies.length - 3; i++) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) { + hasB0 = true; + break; + } + } + assertTrue(hasB0); + } + + @Test + public void testBuildAllQueryRepliesExplicitNone() { + qrBuilder.setGraphicsMode(GraphicsMode.NONE); byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); assertNotNull(replies); assertTrue(replies.length > 0); @@ -156,4 +175,61 @@ public class QueryReplyBuilderTest { assertEquals(80, altCols); assertEquals(43, altRows); } + + @Test + public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() { + qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS); + byte[] requested = new byte[] { (byte) QR_CHARSETS }; + byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); + + assertNotNull(replies); + int offset = -1; + for (int i = 0; i < replies.length - 3; i++) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) { + offset = i + 2; + break; + } + } + assertTrue(offset >= 0, "QR_CHARSETS must be present"); + + // QR_CHARSETS payload structure: + // Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte) + // DL is at offset + 8, and is 7 bytes per descriptor. + int dl = replies[offset + 8] & 0xFF; + assertEquals(7, dl); + + int descOffset = offset + 9; + + // Descriptor 1: SET 0 (Base) -> flags 0x10 + assertEquals(0x00, replies[descOffset] & 0xFF); + assertEquals(0x10, replies[descOffset + 1] & 0xFF); + + // Descriptor 2: SET 1 (APL) -> flags 0x00 + assertEquals(0x01, replies[descOffset + 7] & 0xFF); + assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF); + + // Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane) + assertEquals(0x02, replies[descOffset + 14] & 0xFF); + assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF); + + // Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane) + assertEquals(0x03, replies[descOffset + 21] & 0xFF); + assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF); + + // Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) + assertEquals(0x04, replies[descOffset + 28] & 0xFF); + assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF); + + // Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) + assertEquals(0x05, replies[descOffset + 35] & 0xFF); + assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF); + + // Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) + assertEquals(0x06, replies[descOffset + 42] & 0xFF); + assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF); + + // Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) + assertEquals(0x07, replies[descOffset + 49] & 0xFF); + assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF); + } } diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java index e1bcaa2..e7afd15 100644 --- a/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java +++ b/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java @@ -46,11 +46,11 @@ public class GraphicsModeTest { @Test public void testConnectionConfigDefault() { ConnectionConfig config = new ConnectionConfig(); - assertEquals(GraphicsMode.NONE, config.getGraphicsMode()); - - config.setGraphicsMode(GraphicsMode.BOTH); assertEquals(GraphicsMode.BOTH, config.getGraphicsMode()); + config.setGraphicsMode(GraphicsMode.NONE); + assertEquals(GraphicsMode.NONE, config.getGraphicsMode()); + config.setGraphicsMode(null); assertEquals(GraphicsMode.NONE, config.getGraphicsMode()); } diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java index 0568190..efb22a4 100644 --- a/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java +++ b/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java @@ -51,7 +51,7 @@ public class ProgramSymbolManagerTest { ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41); assertNotNull(slot); assertEquals(9, slot.getWidth()); - assertEquals(16, slot.getHeight()); + assertEquals(12, slot.getHeight()); assertFalse(slot.isTriplePlane()); // Verify pixel data (Row 0 Col 0 is 1) @@ -64,10 +64,33 @@ public class ProgramSymbolManagerTest { int bg = 0xFF000000; // Black int[] rgb = slot.getRgbPixels(fg, bg); assertNotNull(rgb); - assertEquals(9 * 16, rgb.length); + assertEquals(9 * 12, rgb.length); assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green } + @Test + public void testExplicitCellDimensions() { + ProgramSymbolManager manager = new ProgramSymbolManager(); + manager.setDefaultCellDimensions(9, 16); + + byte[] payload = new byte[4 + 18]; + payload[0] = 0x01; // Format 1 + payload[1] = 0x40; // LCID 0x40 + payload[2] = 0x41; // Code point 0x41 + payload[3] = 0x02; // RWS 2 + payload[4] = (byte) 0x80; + payload[5] = (byte) 0x00; + for (int r = 0; r < 16; r++) { + payload[6 + r] = (byte) 0xFF; + } + + manager.loadps(payload); + ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41); + assertNotNull(slot); + assertEquals(9, slot.getWidth()); + assertEquals(16, slot.getHeight()); + } + @Test public void testTriplePlaneMultiColorComposite() { ProgramSymbolManager manager = new ProgramSymbolManager();